Files
fxhnt/tests/integration/test_execute_multistrat_b2a.py
jgrusewski 02317a14ab fix(exec): skip recording on blocked rebalance; alias exec track's reconciliation ref to modeled multistrat (C1)
A blocked RebalancePlan places zero orders but the account keeps its prior positions, so
plan_and_record must not advance the multistrat_exec_pos basis or book a forward_record in that
case — otherwise every subsequent executed return is computed against a phantom target book that
was never placed. Also wires CockpitBacktestRefProvider to alias multistrat_exec -> multistrat so
the reconciliation gate actually reconciles the executed track against the modeled book's
basis-matched backtest reference, as the registry comment already (incorrectly) claimed. Plus a
doc comment clarifying positions_after is the intended target basis, and type-annotated locals in
compute_exec_return to keep the inferred return type float instead of Any.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 00:12:17 +02:00

80 lines
3.6 KiB
Python

import datetime as dt
from fxhnt.adapters.persistence.cockpit_models import StrategyAllocationRow
from fxhnt.adapters.persistence.forward_nav import ForwardNavRepo
from fxhnt.application.multistrat_exec_record import plan_and_record
class _FakePlan:
def __init__(self):
self.orders = []
self.notes = []
self.nlv = 100_000.0
self.blocked = False
class _FakeExecSvc:
def __init__(self, blocked=False):
self.last = None
self._blocked = blocked
def rebalance_weights(self, name, weights, prices, *, max_gross, execute):
self.last = dict(weights)
plan = _FakePlan()
plan.blocked = self._blocked
return plan
def _seed_alloc(repo, dollars, at):
repo.replace_allocations([StrategyAllocationRow(
strategy_id="multistrat", target_weight=0.5, target_dollars=dollars, policy_version=1,
policy_hash="h", as_of="2026-07-10", computed_at=at)], at)
def test_envelope_scales_weights_and_records_on_execute(tmp_path):
repo = ForwardNavRepo(f"sqlite:///{tmp_path}/e.db")
repo.migrate()
at = dt.datetime(2026, 7, 10, 23, 30)
_seed_alloc(repo, 10_000.0, at)
svc = _FakeExecSvc()
plan, env = plan_and_record(repo=repo, exec_svc=svc, within_weights={"SPY": 0.6, "IEF": 0.4},
prices={"SPY": 500.0, "IEF": 100.0}, nlv=100_000.0,
today="2026-07-10", at=at, max_gross=2.0, execute=True)
assert env == 10_000.0
assert abs(svc.last["SPY"] - 0.06) < 1e-12 # 0.6 * 10k/100k
assert repo.get_book_state("multistrat_exec_pos")["envelope"] == 10_000.0 # recorded
# positions_after is the TARGET book we rebalanced to: SPY 0.06*100k/500=12, IEF 0.04*100k/100=40
pos_after = repo.get_book_state("multistrat_exec_pos")["positions"]
assert pos_after.keys() == {"SPY", "IEF"}
assert abs(pos_after["SPY"] - 12.0) < 1e-9
assert abs(pos_after["IEF"] - 40.0) < 1e-9
def test_zero_allocation_flattens(tmp_path):
repo = ForwardNavRepo(f"sqlite:///{tmp_path}/z.db")
repo.migrate()
at = dt.datetime(2026, 7, 10, 23, 30) # no allocation row for multistrat
svc = _FakeExecSvc()
plan, env = plan_and_record(repo=repo, exec_svc=svc, within_weights={"SPY": 0.6, "IEF": 0.4},
prices={"SPY": 500.0, "IEF": 100.0}, nlv=100_000.0,
today="2026-07-10", at=at, max_gross=2.0, execute=False)
assert env == 0.0 and svc.last == {"SPY": 0.0, "IEF": 0.0} # $0/absent → flatten
def test_blocked_plan_records_nothing(tmp_path):
"""A safety-gate-tripped plan (blocked=True, zero orders placed) must NOT be recorded: no forward_record
row for multistrat_exec, and the multistrat_exec_pos book-state must stay untouched (still empty on a
first run) — the account did not change, so the basis must not either."""
repo = ForwardNavRepo(f"sqlite:///{tmp_path}/b.db")
repo.migrate()
at = dt.datetime(2026, 7, 10, 23, 30)
_seed_alloc(repo, 10_000.0, at)
svc = _FakeExecSvc(blocked=True)
plan, env = plan_and_record(repo=repo, exec_svc=svc, within_weights={"SPY": 0.6, "IEF": 0.4},
prices={"SPY": 500.0, "IEF": 100.0}, nlv=100_000.0,
today="2026-07-10", at=at, max_gross=2.0, execute=True)
assert plan.blocked is True
assert repo.get_book_state("multistrat_exec_pos") == {} # unchanged — still empty
assert repo.nav_history("multistrat_exec") == [] # no forward_record row
assert not any(s.strategy_id == "multistrat_exec" for s in repo.all_summaries())