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>
The IEF->CBU0 UCITS remap (515117d, 2026-07-16 12:28) left the paper account
holding IDTM (the old thin Dist line, bought on the 07-15/16 09:00 runs under the
prior IEF->IDTM config). The rebalancer only ever traded its TARGET symbols
(_plan iterates targets.items()), so it never sold the orphaned IDTM — it lingered
in the account NLV and (correctly) tripped the stray-holdings warning.
Fix — scoped, safe self-cleaning:
- superseded_holdings(): the symbols multistrat ITSELF previously traded (per its
own exec_fills) that are still held but no longer a target — priced at the last
fill. Scoped to multistrat's own fills, so a position ANOTHER strategy holds on
the SHARED IBKR account is never touched.
- plan_and_record: adds them at target weight 0 (+ LSEETF/USD spec so a UCITS line
resolves by symbol to SELL) before the rebalance. Best-effort: a scan failure
never blocks a real rebalance.
- _plan: a full EXIT (weight 0 on a held line) ALWAYS trades — hysteresis damps
churn, it must not strand a position we're deliberately closing.
Next UCITS rebalance (09:00) closes IDTM and buys CBU0; the warning clears. Also
prevents future mapping-change legacy from silently accreting in the NLV.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The multistrat ETF book is an UNDER-levered risk-parity book: it runs at ~5.7%
vol vs its own 10% target because leverage is capped at 1x (MAXLEV=1.0), leaving
return on the table. Add a return-defined observe-only shadow (cf.
bybit_4edge_levered) that levers the SAME series to the 10% vol target minus the
HONEST financing drag on the borrowed leg.
- LeveredShadowStrategy (paper_strategies.py): L = min(max_lev, target_vol/vol),
floored at 1x; levered_ret = L·r − (L−1)·financing/252. Never executed.
- multistrat_levered_nav asset (deps=multistrat_nav) publishes its forward track
+ basis-matched backtest ref like the base; registered in the nightly job.
- multistrat_levered registry entry + SIM_BOOKS (Backtest switcher).
- financing_bps=530 GROUNDED in IBKR's real USD schedule (verified 2026-07-18:
Fed funds 3.63% + Pro tiered spread → 5.83% <$100k / ~5.3% $100k-1M).
- leverage_financing_sensitivity() + a cockpit BAND on the levered book's
Backtest view: levered return/vol/Sharpe across 1..6.83% + the break-even
(= the book's 7.7% return), so the verdict is never hostage to one rate.
Honest finding it surfaces: at any realistic IBKR retail rate (~5-7%) levering
this modest-return book LOWERS risk-adjusted return (Sharpe 1.35 -> ~0.9); it
only pays at institutional financing (~1-3%). The financing drag is what makes
the shadow honest (without it, free leverage would look like 13.5%/Sharpe 1.35).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two inconsistencies found verifying the IBKR /strategy/multistrat page:
1. A FAILED backtest with a high Sharpe (multistrat: Sharpe 1.7, gate FAIL)
rendered a GREEN "Validated edge ... backtest passed" banner directly above
the FAIL badge — the Sharpe>=1.0 fallback overrode the explicit FAIL. The
verdict now respects the badge: an explicit FAIL is an amber "Backtest
didn't clear the gate ... treat it as unproven", never a green "passed".
The backtest Sharpe figure also goes neutral-grey (not alarming red) when
unvalidated — the number itself (1.7) isn't bad, it just didn't validate.
2. The UCITS lines the EU DU account actually holds (CSPX/CBU0/IDTM/IGLN/ICOM)
weren't in ASSET_DESCRIPTIONS, so the holdings "what it is" column read
"other". Mapped each to its plain description (US stocks / US Treasuries /
Gold / Commodities), same as its US cousin.
Regression tests: a passed=False + Sharpe 1.7 detail renders amber
"didn't clear the gate" and never "backtest passed"/"Validated edge"; UCITS
tickers resolve to plain descriptions, never "other". Verified desktop+mobile.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Root cause of the 'half' inconsistency: two name paths disagreed — the fleet
(dashboard_service) read the raw DB strategy_registry.display_name (seeded from
the registry), while other views used display_name()'s _OVERRIDES map (checked
first). So multistrat showed 'Adaptive multi-strat book (SPY/IEF/GLD/PDBC/DBMF)'
in the fleet and 'Multi-asset ETF portfolio' elsewhere. Fix: fold the overrides
INTO the registry display_name (multistrat->'ETF Portfolio', vrp->'Options income
(put spreads)'), delete _OVERRIDES, and route the fleet/detail through the ONE
display_name() resolver (registry SSOT, always fresh — no seed-lag). Every
surface now shows the same name. Locally verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The UCITS volume history (~90-309d) is far shorter than the multistrat return
series (~441d). The per-day-ADV cost model zeroed the pre-volume-history days
(scale_hit) -> degenerate series -> partial-sizing guard dropped the whole book.
Capacity is a CURRENT-liquidity concept: dollar_adv now carries the nearest
trailing-median ADV across every close date (forward-fill + back-fill), so old
return days are sized at the nearest available liquidity, not zeroed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 1 of the IBKR UCITS volume source plan: adds ibkr_pit_volume (first-seen-
frozen, mirrors yahoo_pit_volume), IbkrBroker.historical_daily_volume
(reqHistoricalData TRADES, integration-only, never raises), and the
ucits_volume_source config flag (default yahoo, additive ibkr path). Task 2
(CLI + provider read) and Task 3 (CronJob) are separate follow-ons.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
equity_allocation_inputs now sources each US sleeve's dollar-ADV + half-spread from the
instrument the EU account actually trades: "us" uses the US ticker's own volume +
ETF_HALF_SPREAD_BPS (byte-identical to before this task); "ucits" (the settings default)
uses the UCITS-mapped ticker's volume (US close as price proxy) + that line's
half_spread_bps. Returns (book_series/target_weights) stay US in both regimes. Added an
optional spread_bps override to equity_honest_returns/equity_capacity_curve so the regime
spread dict can override the module-constant lookup without mutating it. record_allocation
threads regime from settings.allocation.equity_execution_regime.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 1 of the UCITS re-base plan: UcitsListing gains yahoo_ticker (defaults
to f"{ticker}.L") and half_spread_bps (CSPX 1.5, IDTM 2.0, IGLN 2.0, ICOM 8.0,
DBMF 30.0); AllocationSettings gains equity_execution_regime ("ucits" default
| "us" post-MiFID-II professional). backfill-etf-volume and the multistrat_nav
nightly asset now also snapshot each UCITS line's Yahoo LSE volume, isolated
per-symbol so a thin/absent line (DBMF.L) can't break ingestion.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Whole-branch review: the equity ceiling used per-period (raw daily) Sharpe vs
the annualized capacity_min_sharpe=1.0 -> ~19x too small -> ceiling 0 ->
multistrat sized to $0 under the default policy (a silent no-op; tests masked
it with min_sharpe=-10). Now uses the crypto path's annualized sharpe_of. Also
guard against an empty/partial ADV store (pre-backfill) emitting a garbage
zero-capacity series instead of falling back to the nav record.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Whole-branch review: promoted_strategy_ids() is a never-deleted persisted set;
removing promote_on_pass from book constituents (xsfunding/unlock) must also
retract them from the deploy allocation, else the book + its constituents are
allocated twice. Enforce the invariant in code, not the runbook.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Phase-0b venue re-home reset the crypto sleeves' forward OOS clock to 2026-07-14
(definition-hash change → auto-re-inception). Since these were faithful re-homes (same
edge + PIT data, record_mode=recompute), the pre-re-home days are still valid OOS; on
2026-07-15 they were recovered by backdating the active anchors' t0 in the DB (unlock
06-20 → recon-gate PASS +4.57%; xsfunding/positioning/bybit_4edge/levered → 8d).
That backdate lived ONLY in the DB — a full operational-Postgres rebuild would let the
anchors re-incept to `today` and silently lose the recovered window. This makes it
reproducible from git (the SSOT):
- forward_definition.inception_override(sid): returns a definition's declared
`inception_t0.date`, VERSION-PINNED (honored only when its `version` == the
definition's current version — a genuine re-tune bumps `version`, auto-ignoring a
stale pin and correctly getting a fresh today-clock). Lives OUTSIDE `params` so it is
NOT hashed — declaring it never itself re-inceptions (verified: the five sleeves'
definition_hash is byte-identical to their live DB anchors).
- run_track: at inception, t0 = inception_override(sid) if present AND <= today, else
today (never incept in the future — clock-skew/not-yet-reached pins fall back to today).
- registry: xsfunding/unlock/positioning/bybit_4edge/bybit_4edge_levered carry their
pinned pre-re-home t0 (07-07/06-20/07-06/07-07/07-07, version 2).
Tests: inception_override version match/stale/absent + not-hashed + a regression pin on
the recovered dates; run_track honors the pin, ignores a version-stale pin, and falls
back to today for a future pin. 262 forward/registry tests pass.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Live probe on DU9600528 (2026-07-15): SMART+ISIN returns 0 results for all
five UCITS lines (SMART grabs the wrong US grey-market listing or nothing);
LSEETF+ISIN+USD resolves each cleanly, and SMART stays in every valid-exchange
set so orders still route. Two fixes: (1) UcitsListing.exchange default
SMART->LSEETF; (2) _resolve_contract qualifies the ISIN WITHOUT a symbol (the
map ticker is cosmetic and differs from the USD line's real symbol: IEF->IDTM,
GLD->IGLN). Map tickers corrected to the USD lines. Rebalancer envelope
1,000,000 -> 100,000. 2004 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
VrpStrategy.advance replayed ~1600 trading days issuing a fresh Session +
DB round-trip per day (chain_asof + bars_for x N) — thousands of queries,
minutes-long. Add an opt-in in-memory preload: one SELECT mirrors the
covered slice, and chain_asof/bars_for/trading_days serve from memory,
falling back to the DB for any date outside the preloaded range (never
under-returns). The live single-day exec path is untouched (no preload).
Guarded by a cache==DB equivalence test across every query shape. 2004 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live execute-vrp crashed at 07:42 UTC: plan_and_record_vrp froze
_freeze_day(as_of=today), but OPRA publishes on a ~1-session lag, so
today's slice is never available intraday -> DatabentoRangeUnavailable
before the run ever reached the IBKR combo (the scheduled Monday 14:40
UTC run would crash identically).
Fix: freeze the last COMPLETE OPRA session via _freeze_latest_session
(the same robust bounded step-back the nightly vrp_nav uses) and key
every bar read to that freeze_date (T-1) -- _mark_spread,
vrp_marking.model_spread_value, _select_spread, and the new-rung price
lookup -- while keeping `today` (calendar) for all date-logic: DTE,
entry-weekday, the last_run_date idempotency key, run_track booking and
the pos-state stamp. Only the OPRA data date lags; the booking date does
not.
Test: test_freeze_uses_last_complete_session_not_today drives the real
plan_and_record_vrp composition with a dbn whose freeze for today raises
DatabentoRangeUnavailable but succeeds for T-1; asserts no crash, marks
frozen at T-1 (absent at today), exec return still recorded under today.
uv run pytest -q -> 1998 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review follow-ups on the VRP model-mark fix.
MINOR-1 (reconcile the live exec twin): vrp_exec_record still marked the
spread as raw short-long for its 50%-profit-take signal — the same
single-leg mark noise removed from the backtest — so a noisy far-OTM
long-leg spike could spuriously trip a live close on the paper account.
Extract the model-mark valuation into a shared single-source module
vrp_marking.py (forward_and_atm_iv, model_spread_value, settlement_value);
both vrp_book.VrpStrategy and vrp_exec_record now call it. plan_vrp_ladder
takes exec_marks (actual fill basis) AND signal_marks (model); the
profit-take decision uses only the model signal, while the close order
net limit and realized P&L stay mark-to-actual.
Also fixes a latent bug: the freeze band stores near-ATM calls only for
DTE[20,45], so a spread aged below 20 DTE has no calls at its own expiry.
The prior forward required calls at the spread's expiry -> would have
prematurely closed every spread ~20 DTE before expiry. The shared forward
now falls back to the globally cleanest ATM pair across expiries (parity
forward is spot, common across expiries under r=0), markable at any age.
MINOR-2 (efficiency): memoize (F, IV) per (day, expiry) — advance threads
a per-day day_cache; plan_and_record_vrp threads a signal_cache — so
rungs sharing an expiry don't re-query the chain.
MINOR-3 (doc): the absolute defined-risk bound [-width*100, +width*100]
holds unconditionally; the tight [-(width-credit)*100, +credit*100] is the
clean-BS-surface case (entry model value == credit).
Tests: uv run pytest -q -> 1997 passed. New: noisy-actual-mark doesn't
trip the exec profit-take; backtest and shared marking are single-source.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daily mark was `short_close - long_close` — the difference of two
individually-noisy raw OPRA close marks. On a ~$1-2 20Δ 5-wide spread,
~$1 of quote noise in either leg swung the value 50-100%, and ÷ the $500
defined-risk margin turned mark noise into ±50% "daily returns"
(prod: ann_vol 68.5%, maxDD -92.3%, Sharpe -0.02, 14 days |ret|>15%).
It was a mark-noise artifact, not real P&L.
Rebuild the daily value from a smooth model instead:
- extract a reusable per-day (forward F, ATM IV) helper: parity forward
at the clean ATM strike + implied_vol_put inverted from that ATM mark,
queried over a wide DTE window so an open spread is markable at any age;
- _spread_value = clip(bs_put(F,K_short,iv,tau) - bs_put(F,K_long,iv,tau),
0, width) — smooth (spot+vol, not a noisy quote difference) and hard-
clipped to the defined-risk bound [0, width]; carry last good IV on a
degenerate day; close (no zombie) only when the expiry chain is gone;
- realize clip(K_short - F, 0, width) settlement intrinsic at DTE<=1;
- baseline P&L at the entry-day model value so every return is a change
in the smooth model value (no raw far-OTM leg noise on day 1).
Per-spread cumulative P&L is bounded to the defined-risk envelope
[-(width-credit)*100, +credit*100]. Entry credit / strike selection
unchanged.
Synthetic BEFORE/AFTER: ann_vol 211% -> 59%, |ret|>15% 65 -> 1 day,
extremes +62%/-36% -> +13%/-17%, Sharpe (noise) 3.8 / prod -0.02 -> +1.9.
The residual vol is real defined-risk P&L (inherent per-margin exposure,
sized by the allocation engine), not mark noise.
Tests: 11 vrp_book unit cases (bounded+smooth value, defined-risk
cumulative bound, clean-path daily bound, clipped settlement, dollar
basis); full VRP unit+integration green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review round 2 (F1 Important + two Minor):
F1 — per-sleeve exception isolation for the UCITS price fetch. A raise (not
empty return) from the IB API previously propagated out of route_ucits, past
_plan's per-order isolation, aborting the WHOLE rebalance. Now IbkrBroker.
market_price wraps the IB round-trip in try/except -> log + None, and
route_ucits wraps price_fn(listing) in try/except -> log + skip. One thin/broken
sleeve (e.g. the new DBMF LU2951555585 line) can no longer wedge the book.
F2 — a non-injective UCITS map raised inside the broker block and was
misreported as "paper-envelope refused". The CLI now validates the map with
apply_ucits_map BEFORE connecting, printing a clear "UCITS map error: collision".
F3 — first UCITS run compared US-keyed prior positions against UCITS-keyed
prices, booking a spurious multistrat_exec return. Extracted
exec_return_for_run(pos_prior, prices, venue): a venue switch (prior book-state
venue tag != current venue) is booked as INCEPTION (no return); same-venue runs
delegate to compute_exec_return as before.
Tests: +5 (price_fn raises -> skip others route; market_price IB-raise -> None;
venue switch -> inception; same-venue -> normal return; no-prior -> inception).
uv run pytest -q: 1991 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
execute-multistrat --venue ucits was unrunnable and mis-sized:
- place_order hardcoded Stock(symbol, SMART, USD) so LSE-listed UCITS ETFs
(CSPX/IBTM/SGLN/ICOM/DBMF) failed IBKR resolution (Error 200);
- apply_ucits_map reused the US ticker's Yahoo price for the UCITS ticker, so
shares = weight*envelope/price was wrong;
- any single unmapped sleeve refused the whole (PRIIPs-legal) rebalance.
Fix:
- Order carries optional exchange/currency/sec_id_type/sec_id (US path keeps
SMART/USD defaults, byte-unchanged). IbkrBroker resolves UCITS orders via
Contract(secIdType=ISIN, secId, currency=USD) + qualifyContracts; an
unresolved contract logs loudly and is SKIPPED (no crash, no wrong order).
- IbkrBroker.market_price fetches the REAL UCITS USD price via delayed market
data (reqMarketDataType(3) + reqMktData snapshot); the UCITS route sizes from
it, not the US Yahoo price. Modeled US weights unchanged.
- route_ucits routes sleeves that map+resolve+price and loudly logs+skips the
rest; survivors keep their own weight (book NOT rescaled), skipped $ reported.
- UcitsSettings.map now carries verified ISINs (CSPX IE00B5BMR087, IBTM
IE00B1FZS798, SGLN IE00B4ND3602, ICOM IE00BDFL4P12) and DBMF is mapped to the
real iMGP DBi Managed Futures UCITS ETF (LU2951555585, USD line) with a
liquidity caveat comment.
TDD mocks the IB client (no live broker). Full suite: 1986 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review fixes (2 Major + 2 Minor):
- MAJOR: reword the pre-existing "Executed reality" card (same detail page) off the forbidden
"divergence"/"vs sim"/"decay"/"bp" jargon -> "Real trades vs the plan", "N% behind/ahead of plan",
"cost to trade %". Extend the web forbidden-strings test to assert (on VISIBLE text, stripping
CSS/markup) no divergence/decay/vs-sim/bare-sim/bps anywhere on the rendered page.
- MAJOR: %-of-book uses GROSS exposure (sum of abs) as the denominator, so a mixed-sign defined-risk
book (vrp put-credit-spread: short put + long wing) no longer renders >100%/negative/non-summing
shares. Each row keeps its signed value. New mixed-sign holdings test.
- MINOR: holdings value is marked at each holding's last trade price (no live mark) — surface a per-row
"as of <date>" and a column caveat so a stale mark isn't over-trusted.
- MINOR: an unmapped asset symbol (future vrp option contracts) falls back to a plain generic "other"
instead of echoing a raw contract string.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>