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>
Backtest-only gate (no forward wiring) for the China/India diversifier question,
decided from ~7y of real Yahoo data. Reuses the SSOT marginal_sharpe primitive +
multistrat.book_series verbatim; cost model = max(vol-norm re-sizing turnover,
monthly-rebalance floor) so a quiet co-crasher can't survive at ~0 modeled cost.
Result (10bp UCITS, 2019-08..2026-07): CNYA.L marginal Sharpe -0.099, NDIA.L
-0.048 — both co-crash the book (+0.23 corr) and worsen with cost. KILL both:
the accessible UCITS wrapper carries EM beta, not the mainland inefficiency
(direct A-shares/NSE closed to the EU-retail entity). Same co-crash death as VRP
and unlock. Do NOT wire the em_asia forward sleeve.
Gate is generic — reusable for any single-instrument diversifier candidate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move _persist_bybit_book + _BYBIT_BOOK_SIDS to application/bybit_book_persist.py
(single source of truth; breaks the cli<->assets coupling). Add a new nightly
asset bybit_book_precompute (deps=[bybit_warehouse_refresh]) that wraps the
same helper backtest-refs/bybit-persist-sleeve-ret run — writing bybit_sleeve_ret
+ the report-kind gate refs in one compute-once pass. Tagged
dagster-k8s/config 6Gi req / 8Gi limit so K8sRunLauncher runs it in its own
right-sized Job (never the 4Gi daemon). Wired into combined_book_job + defs;
tests updated (13->14 assets + presence + wiring). CLI commands left intact
(deleted at the cron layer later). 60 tests pass.
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>
Following the /paper blurb removal, sweep the remaining pages:
- Backtest (sim.html): drop the paragraph-long verdict banner; keep a one-line
plain subtitle ("a what-if on capital & period at real trading cost").
- Measured-cost caption (_sim_result.html): visible text now plain
("real trading cost, per coin — fee + spread · avg drag X%"); the technical
detail (L1 quoted spread / current-snapshot caveat / "not a slider") moves to
the hover tooltip. IBKR book caption likewise plain.
- Hard-coded "vol-targeted @ 20%" label (Overview, paper_crypto, strategy) ->
"each shown on its own" (drops the hard-coded 20% + the jargon).
Tests updated to the new plain copy; technical strings that stay in tooltips
(real L1 quoted spread, the snapshot caveat) are still asserted there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Paper-books landing led with a paragraph-long verdict banner ("Two paper
accounts, one job..."). The two book cards below already convey account value,
holdings and status, so the blurb was redundant text. Keep just the h1 + a
one-line subtitle. Removes the two tests that asserted the banner copy.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifying the IBKR side surfaced an IBIT (bitcoin ETF) position in the DU paper
account on 2026-07-14 — a legacy leftover from the pre-UCITS US book (exec_fills
has zero IBIT fills; the current multistrat only ever traded the 5 UCITS lines).
It was liquidated in the 07-15 UCITS transition, but until then it sat silently
in the account NLV, unattributed to any book.
Add a stray-holdings guard: `_expected_book_symbols()` derives the allowed set
from the SAME two SSOTs the executor uses (multistrat.FUND_INSTRUMENTS + the
UCITS map) — never a hardcoded duplicate. Each holding gets an `in_book` flag;
StrategyDetail carries `stray_holdings` (held, nonzero-qty symbols outside the
set). The holdings view now renders a red "Position outside the book: <syms>"
banner + a ⚠ on the offending rows, so legacy/stray ballast can't inflate the
NLV unnoticed. [] = clean (no warning).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifying the Backtest page: the scrub state panel labelled n_active "active
sleeves", but n_active counts the instruments HELD that day — the Bybit book's
~64-coin liquid universe (compare_measured_cost sets n_active=[n_coins]*len),
the ETF book's ~5 ETFs — NOT the book's sleeves (its 4 edges). So a 4-edge book
read as having "64 sleeves". Relabelled "active positions", accurate for both
the coin book and the ETF book.
Regression test pins "active positions" (and asserts "active sleeves" absent).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verifying the Overview: the IBKR "↳ real trades" sub-row put the real account's
cumulative return (e.g. -0.01%) bare in the borrowed "allocated" column — on
desktop it read as an allocation, and on mobile the card reflow labelled it
"allocated: -0.01%" (actively wrong; it's a return, not capital allocated).
The data was correct and reconciled (real -0.01% vs plan +0.56% => "0.6% behind
plan"); only the labelling was off. Mirrored the exec-twin row's self-labelling:
the return now renders "real -0.01%" inline, and every sub-row cell carries an
accurate data-label (real return / trades / vs plan / cost) so the mobile card
reflow shows the true meaning, not the borrowed column name.
Test pins the labelled form ("real +3.10%"), so the label can't silently drop.
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>
The IBKR paper account (DU9600528) IS connected and trading: the armed
fxhnt-ucits-rebalancer (0 9 * * 1-5) books the multi-strat ETF leg through
UCITS lines (EU KID/PRIIPs blocks the US-ETF leg, whose rebalancer is
correctly suspended). Prod already rendered it ($1,029,621, 5 holdings).
Two self-inflicted "not connected" artifacts fixed:
- scripts/dev_cockpit_local.py did NOT wire ibkr_account_repo, so the LOCAL
dev cockpit always showed the IBKR card empty regardless of real data —
a misleading local-verification artifact. Now wires every repo prod wires.
- paper.html's new orientation banner hardcoded "not connected yet" (written
from that stale local view). Made it data-aware (keyed on ibkr_view) so it
states the live NLV + holdings when trading, "not connected yet" only when
genuinely empty — it can't drift from the card beneath it.
Adds a regression assertion: connected account => banner says "live now,
trading through UCITS" and never "not connected yet".
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>
Trim the registry display_name (drops the (naive eq-wt, forward) parenthetical;
exec/levered keep their qualifiers), rename the /paper/crypto h2 (was 'Bybit
paper book') and the sim pill (was 'Bybit 4-edge'). Overview/paper/sim/strategy
all render 'Bybit 4-edge book' now (locally verified); old names gone.
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>