Files
fxhnt/tests/integration/test_execute_multistrat_b2a.py
jgrusewski bcf58e703d fix(exec): entry-floor threshold on the sizing envelope, not the raw NLV
The IGLN gold-sleeve 'priced but no order' bug. scaled_weights sizes orders to
the paper envelope (envelope*within_weight), but _plan's entry-floor/hysteresis
threshold used entry_floor*nlv (the full $1M account NLV) — so the effective
floor was ~entry_floor*(nlv/envelope) of the envelope (~5%), silently dropping
every sleeve below it. A 3% gold sleeve at a $100k envelope on a $1M account
skipped. FIX: rebalance_weights takes sizing_capital (the envelope); the floor
is entry_floor*min(sizing_capital,nlv). Sizing + recording unchanged. Regression
test: a 3% sleeve survives WITH the envelope basis, drops without it. 2005 green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 16:14:06 +02:00

80 lines
3.7 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, contract_specs=None, sizing_capital=None):
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())