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>
bybit_sleeve_ret (the cockpit sim's precompute + the recon gate's reconciliation
reference) was refreshed only WEEKLY, via the separate fxhnt-backtest-refs CronJob
(0 2 * * 1). The live forward book updates DAILY, so it ran up to a week ahead of
its reconciliation reference — the recent forward days couldn't be reconciled and
the recon gate silently skips its divergence band without a fresh ref (false-PASS
risk on the real-capital book).
Fold `backtest-refs --all` into the existing DAILY fxhnt-compare-measured-precompute
CronJob (already own-memory 6Gi/8Gi, pinned to the 16GB ci-compile-cpu autoscale
pool, identical egress). Now one daily job runs, in order: bybit-spread-refresh,
compare-measured-precompute (compare_measured_cache), backtest-refs --all
(bybit_sleeve_ret + backtest_summary refs). Both writers run independently (a
compare failure must not starve the recon ref); the Job still fails if either does.
NOT compute-once: the per-coin (compare) and per-sleeve (refs) paths stay separate
computations — co-scheduled only, so the numbers are unchanged. Retires the weekly
fxhnt-backtest-refs.yaml (its generic report-ref dispatch is covered by the same
`backtest-refs --all`; no live tooling references its name).
Apply is manual (deploy skips CronJob manifests):
kubectl apply -f infra/k8s/jobs/fxhnt-compare-measured-precompute-cronjob.yaml
kubectl delete cronjob fxhnt-backtest-refs -n foxhunt
kubectl delete networkpolicy fxhnt-backtest-refs -n foxhunt
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>
Applies the strategy-page design language (h1 + plain-language orientation
verdict banner, consistent with base.html's .verdict trust-anchor) to the
three remaining cockpit pages so every view leads with a clear "what am I
looking at" moment instead of a cramped muted subtitle + tooltip:
- paper.html: h1 "Paper books" + neutral verdict framing the two paper
accounts (Bybit deploy venue vs not-yet-connected IBKR leg).
- sim.html: h1 "Backtest" + verdict — "a what-if, not the live record",
keeps the test-anchored honest-cost phrases verbatim.
- replay.html: h1 "Replay" + track-aware verdict — "the real forward record,
what actually happened" (naive) / levered-shadow note (levered).
Data/behaviour unchanged; all functional hooks (HTMX, forms, switchers,
scrubbers) preserved. Verified on desktop (1280) + mobile (390) against the
real prod DB. 248 web tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cockpit read like a dev console; it must be the trust source for real money.
base.html: one design system every page inherits — sans prose + tabular-mono
figures (the shift away from all-monospace), CSS-var palette, sticky header,
polished cards/stats/badges, and a VERDICT banner (the 'is this working?' anchor).
strategy.html: redesigned to a trader summary — a plain-language verdict +
three big colour-coded cards (Backtested edge = the real signal / Live paper so
far = noisy sample, don't react / In the fund = capital), curve, then the dense
audit sections demoted under a collapsed Details area. Verified on desktop +
mobile. Restored the guarded exec-twin 'real trades vs plan' card.
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>
Validated 2026-07-16: under yahoo the EU equity book drops (Yahoo has no CBU0.L);
under ibkr it sizes 441d / $1M ceiling / Sharpe 1.30 on authoritative on-exchange
volume of the exact traded lines. CronJob keeps ibkr_pit_volume fresh; empty ->
coverage guard -> nav fallback (graceful).
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>
Runs in the multistrat-rebalancer network context (git-sync + pip ib_async +
ib-gateway:4004 ingress-allowed), writes real on-exchange volume to the DB; the
daemon reads it when FXHNT_ALLOCATION_UCITS_VOLUME_SOURCE=ibkr. 08:30 weekdays.
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>
Probe-verified: IBKR historical TRADES volume works (no subscription); bid/ask
blocked (Error 354, subscription needed -> spread stays estimated). Fetch in
rebalancer context (netpol) -> ibkr_pit_volume DB -> daemon reads. Source flag
ucits_volume_source yahoo|ibkr. CronJob for the fetch.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Whole-branch review note: the docstring listed the GBP lines while the map
correctly uses the USD lines. Also noted deferred: M1 regime in policy hash,
M2 assert FUND_INSTRUMENTS<=ucits.map, M3 pydantic regime validator.
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>
Ingest UCITS LSE volume (CSPX.L/IDTM.L/IGLN.L/ICOM.L/DBMF.L), per-line UCITS
spreads, and a regime flag (ucits default | us). equity_allocation_inputs
sources each US sleeve's ADV+spread from the regime's instrument; returns stay
US. Switch to us regime after electing MiFID-II professional (~EUR500k).
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>
Equity honest-reference analog for the IBKR US-ETF multistrat book: ingest real
Yahoo volume (PIT) for a data-driven dollar-ADV capacity model, charge real
costs (sqrt-impact on ADV + per-ETF spread + IBKR commission, not the 15bp
stub), produce a cost-netted book_series sizing series (>60d so it sizes),
merge into record_allocation. The growth vehicle above the crypto ~350k break.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The book deploy target recomputes 3 sleeves per AUM; the 5-point grid to $10M
OOM-killed the nightly (3 sleeves x 6 points > 4Gi daemon). The crypto book
breaks ~$350k (xsfunding capacity) so higher points are wasted compute. Load-
once/reprice provider refactor (follow-on) would allow a wider grid cheaply.
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>
Whole-branch review Minor: the book is no longer an allocation target (per-sleeve
allocation, 2026-07-16 spec §4d) — the sim switcher badge + comments said 'deploy'.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>