Merge: multistrat self-cleans superseded lines (IDTM->CBU0)

This commit is contained in:
jgrusewski
2026-07-18 11:30:00 +02:00
4 changed files with 96 additions and 2 deletions

View File

@@ -124,8 +124,11 @@ class ExecutionService:
target_shares = nlv * weight / px
current = acct.positions.get(sym, 0.0)
delta = target_shares - current
# A full EXIT (target weight 0 on a held line — e.g. closing a superseded UCITS line) ALWAYS
# trades: hysteresis exists to damp churn, not to strand a position we're deliberately closing.
is_exit = weight == 0.0 and current != 0.0
threshold = (ex.entry_floor if current == 0 else ex.hysteresis) * floor_base
if abs(delta * px) <= threshold:
if not is_exit and abs(delta * px) <= threshold:
continue
if frac:
qty: float = round(abs(delta), 6) # fractional shares

View File

@@ -14,6 +14,20 @@ from typing import Any
log = logging.getLogger(__name__)
def superseded_holdings(*, fills: list[Any], current_targets: set[str],
held: dict[str, float]) -> dict[str, float]:
"""multistrat's OWN superseded lines to close: symbols it PREVIOUSLY traded (per its own `fills`) that are
STILL held but are NO LONGER in the current target set — e.g. IDTM after the IEF->CBU0 remap. Keyed to the
LAST recorded fill price (a real mark for the liquidating sell). SCOPED to multistrat's own fills, so a
position another strategy holds on the SHARED IBKR account is NEVER included (a symbol multistrat never
traded can't be here). Zero-qty holdings are ignored. Empty dict when the account is already clean."""
last_price: dict[str, float] = {}
for f in sorted(fills, key=lambda f: f.at): # ascending -> the LAST fill's price wins
last_price[f.symbol] = float(f.price)
return {sym: last_price[sym] for sym, qty in held.items()
if qty and sym in last_price and sym not in current_targets}
def scaled_weights(within_weights: dict[str, float], envelope: float, nlv: float) -> dict[str, float]:
if envelope <= 0.0 or nlv <= 0.0:
return {s: 0.0 for s in within_weights} # flatten to cash (B2a $0 / no NLV)
@@ -108,8 +122,30 @@ def plan_and_record(*, repo: Any, exec_svc: Any, within_weights: dict[str, float
else:
envelope = min(float(repo.current_allocation().get("multistrat", 0.0)), float(nlv))
weights = scaled_weights(within_weights, envelope, nlv)
# SELF-CLEANING: close multistrat's OWN superseded lines (held + previously traded by multistrat + no
# longer a target — e.g. IDTM after the IEF->CBU0 remap) so a mapping change doesn't strand legacy holdings
# in the account NLV. Scoped to multistrat's own fills (NEVER another strategy's position on the SHARED
# account) and best-effort (a scan failure must never block the rebalance). Added at target weight 0, so
# `_plan` closes the FULL holding (a weight-0 exit bypasses hysteresis). Priced at the last fill; UCITS
# orphans resolve on LSEETF/USD by symbol (no ISIN needed to SELL a line already held).
prices = dict(prices)
specs = dict(contract_specs or {})
if execute and broker is not None and account_repo is not None and account_is_paper:
try:
held = broker.account_state().positions
fills = account_repo.fills_for("multistrat", limit=10000)
superseded = superseded_holdings(fills=fills, current_targets=set(weights), held=held)
for sym, px in superseded.items():
weights[sym] = 0.0
prices.setdefault(sym, px)
if "ucits" in str(venue).lower():
specs.setdefault(sym, {"exchange": "LSEETF", "currency": "USD"})
if superseded:
log.info("multistrat self-clean: closing superseded line(s) %s", sorted(superseded))
except Exception: # noqa: BLE001 — the cleanup scan is best-effort; never block a real rebalance over it
log.warning("multistrat superseded-line scan failed (skipped)", exc_info=True)
plan = exec_svc.rebalance_weights("multistrat", weights, prices, max_gross=max_gross, execute=execute,
contract_specs=contract_specs, sizing_capital=envelope)
contract_specs=specs, sizing_capital=envelope)
if execute and not plan.blocked:
pos_prior = repo.get_book_state("multistrat_exec_pos")
exec_return = exec_return_for_run(pos_prior, prices, venue)

View File

@@ -166,3 +166,26 @@ def test_rebalance_weights_ucits_path_enriches_order_with_contract_spec() -> Non
assert o.currency == "USD"
# sized from the REAL UCITS price 620 (60% of 100k / 620 ≈ 96.77), NOT a US price
assert abs(o.quantity - (0.6 * 100_000 / 620.0)) < 1e-6
def test_weight_zero_exit_always_closes_a_held_line() -> None:
# A held line taken to weight 0 (a superseded UCITS line being closed) ALWAYS sells — even a tiny holding
# that hysteresis would otherwise strand. This is what makes the self-cleaning of legacy lines (IDTM after
# the IEF->CBU0 remap) actually liquidate instead of lingering.
broker = FakeBroker(AccountState(nlv=1_000_000, cash=1_000_000, positions={"IDTM": 1.0}, is_paper=True))
svc = ExecutionService(FakeData(), broker, _settings())
plan = svc.rebalance_weights("multistrat", {"IDTM": 0.0}, {"IDTM": 96.0}, execute=True,
sizing_capital=1_000_000)
assert not plan.blocked
sells = [o for o in broker.placed if o.symbol == "IDTM" and o.side == "SELL"]
assert len(sells) == 1 and sells[0].quantity == 1
def test_held_line_at_tiny_nonzero_weight_is_left_in_band() -> None:
# Contrast: the SAME tiny holding at a small NONZERO target weight is within hysteresis -> no order (only a
# deliberate weight-0 EXIT bypasses the band, not every small drift).
broker = FakeBroker(AccountState(nlv=1_000_000, cash=1_000_000, positions={"IDTM": 1.0}, is_paper=True))
svc = ExecutionService(FakeData(), broker, _settings())
plan = svc.rebalance_weights("multistrat", {"IDTM": 0.0000001}, {"IDTM": 96.0}, execute=True,
sizing_capital=1_000_000)
assert not broker.placed

View File

@@ -144,3 +144,35 @@ def test_plan_and_record_default_mode_reads_b2a():
nlv=500000.0, today="2026-07-11", at=dt.datetime(2026, 7, 11), max_gross=2.0,
execute=False, paper_envelope=0.0, account_is_paper=True)
assert envelope == 25000.0 # min($25k B2a, $500k nlv)
# ---- superseded_holdings: self-cleaning of multistrat's OWN legacy lines (e.g. IDTM after IEF->CBU0) --------
import datetime as _dt
from types import SimpleNamespace
from fxhnt.application.multistrat_exec_record import superseded_holdings
def _fill(sym, price, day):
return SimpleNamespace(symbol=sym, price=price, at=_dt.datetime(2026, 7, day, 9, 0))
def test_superseded_flags_own_legacy_line_at_last_fill_price():
# multistrat traded IDTM (the legacy IEF line) + CSPX; it now targets CSPX/CBU0/IGLN. Held: IDTM + CSPX.
fills = [_fill("IDTM", 95.0, 15), _fill("CSPX", 500.0, 15), _fill("IDTM", 96.0, 16)]
out = superseded_holdings(fills=fills, current_targets={"CSPX", "CBU0", "IGLN"},
held={"IDTM": 98.0, "CSPX": 24.0})
assert out == {"IDTM": 96.0} # IDTM: held + traded + not a target -> flagged @ LAST fill price
def test_superseded_never_touches_another_strategys_position():
# A held symbol multistrat NEVER traded (another strategy's line on the SHARED account) is never flagged.
out = superseded_holdings(fills=[_fill("CSPX", 500.0, 15)], current_targets={"CSPX"},
held={"CSPX": 24.0, "SOMEOTHER": 10.0})
assert out == {}
def test_superseded_ignores_zero_qty_and_current_targets():
out = superseded_holdings(fills=[_fill("IDTM", 95.0, 15), _fill("CSPX", 500.0, 15)],
current_targets={"CSPX", "CBU0"}, held={"IDTM": 0.0, "CSPX": 24.0})
assert out == {} # IDTM flat (0 qty); CSPX is a current target