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>
104 lines
5.2 KiB
Python
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())
|