Files
fxhnt/tests/integration/test_execute_multistrat_b2a.py
Jeroen Grusewski 0ed2cfdff3 fix(exec): UCITS live-run 478/10349 + false-EXECUTED (Mon 2026-07-20)
The first live UCITS paper run on bizworx reported EXECUTED/exit-0 but was
degraded: the IEF->CBU0 leg got 0 fills and DBMF hung Submitted.

- Error 478 (contract conflict): the ISIN-qualified UCITS contract kept a
  localSymbol/secId conflicting with the pinned conId (requested CBU0 vs
  canonical CSBGU0). Strip secId/secIdType/localSymbol after resolve and route
  by conId + SMART.
- Error 10349 (TIF preset): the bare MarketOrder had no TIF so the account
  preset forced DAY + cancel/reconfirm. Set explicit tif="DAY".
- False-success (the dangerous one): plan.executed counted Cancelled/Submitted
  legs as placed, and exec-record booked on any non-blocked run. Now executed
  requires EVERY leg filled; a degraded run prints DEGRADED, exits 1, and
  skips exec-track booking so the reconciliation gate stays WAIT (never books a
  phantom rebalance) — real money errs false-WAIT.

Tests: new test_ucits_exec_fill_status.py + degraded cases in test_execution.py
and test_execute_multistrat_b2a.py (TDD failing-first). 523 exec/forward/gate
tests green, mypy clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 09:20:47 +00:00

104 lines
5.2 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
self.executed = True
class _FakeExecSvc:
def __init__(self, blocked=False, degraded=False):
self.last = None
self._blocked = blocked
self._degraded = degraded # orders attempted but a leg didn't fill -> executed False
def rebalance_weights(self, name, weights, prices, *, max_gross, execute, contract_specs=None, sizing_capital=None):
self.last = dict(weights)
plan = _FakePlan()
plan.blocked = self._blocked
if self._degraded:
plan.orders = [object()] # an order WAS placed
plan.executed = False # ...but it did not fill
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())
def test_degraded_plan_records_nothing(tmp_path):
"""A DEGRADED plan (orders placed but a leg did NOT fill — Error 478/10349 live 2026-07-20) must NOT be
recorded: recording would advance the basis to the INTENDED target book, which the account does not hold
(the un-filled leg), permanently diverging the executed track. Same skip as a blocked plan — err
false-WAIT."""
repo = ForwardNavRepo(f"sqlite:///{tmp_path}/d.db")
repo.migrate()
at = dt.datetime(2026, 7, 10, 23, 30)
_seed_alloc(repo, 10_000.0, at)
svc = _FakeExecSvc(degraded=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.executed is False and plan.orders # degraded: placed but unfilled
assert repo.get_book_state("multistrat_exec_pos") == {} # NOT advanced
assert repo.nav_history("multistrat_exec") == [] # no phantom forward_record
assert not any(s.strategy_id == "multistrat_exec" for s in repo.all_summaries())