Commit Graph

317 Commits

Author SHA1 Message Date
0ed2cfdff3 fix(exec): UCITS live-run 478/10349 + false-EXECUTED (Mon 2026-07-20)
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>
2026-07-20 09:20:47 +00:00
jgrusewski
38b8ff26d2 feat(exec): multistrat self-cleans its OWN superseded lines (fixes lingering IDTM)
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>
2026-07-18 11:30:00 +02:00
jgrusewski
713424a89d feat(fund): multistrat_levered — observe-only levered shadow of the ETF book + financing sensitivity band
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>
2026-07-18 11:15:55 +02:00
jgrusewski
8bd87e8ab1 fix(cockpit): honest FAIL verdict + plain UCITS holdings on the strategy detail
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>
2026-07-16 16:31:27 +02:00
jgrusewski
a71737eeca refactor(cockpit): ONE name resolver — fold display-name overrides into the registry SSOT
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>
2026-07-16 15:05:09 +02:00
jgrusewski
70d584251f fix(equity): carry ADV across the return series (capacity = current-liquidity)
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>
2026-07-16 13:02:34 +02:00
jgrusewski
cd5203a33c feat(equity): backfill-ucits-volume-ibkr CLI + source-flag read in equity provider
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 12:49:30 +02:00
jgrusewski
ee8694dffb feat(equity): IBKR pit-volume store + historical_daily_volume + ucits_volume_source flag
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>
2026-07-16 12:42:59 +02:00
jgrusewski
515117de2c feat(equity): swap IEF->CBU0 (liquid treasury UCITS) + partial-sizing coverage (use as much as tradeable)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 12:28:25 +02:00
jgrusewski
0c4ffc11fc fix(equity): reseed UCITS volumes in equity integration test + fail loud on unknown regime
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 11:53:58 +02:00
jgrusewski
c2dd057ce1 feat(equity): regime-aware ADV/spread (ucits default | us) for the capacity+cost model
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>
2026-07-16 11:44:39 +02:00
jgrusewski
1ad41f5145 feat(equity): ingest UCITS LSE volume + regime flag + UCITS spread config
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>
2026-07-16 11:28:36 +02:00
jgrusewski
514f17df6a fix(equity): annualized Sharpe for the ADV ceiling + ADV-coverage guard
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>
2026-07-16 11:08:03 +02:00
jgrusewski
797462bdf7 feat(equity): equity_allocation_inputs provider merged into record_allocation
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:48:27 +02:00
jgrusewski
dde51b8641 feat(equity): real ETF cost model + ADV capacity curve (equity_honest.py)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:33:49 +02:00
jgrusewski
3f2c114241 feat(equity): ingest ETF volume (PIT) + backfill-etf-volume CLI
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 10:23:09 +02:00
jgrusewski
90cc809456 refactor(alloc): load-once/reprice honest inputs (panels loaded once, repriced per AUM)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 09:55:37 +02:00
jgrusewski
86e0035d46 fix(alloc): stale promotion of a de-declared constituent must not re-enter deploy_sids (double-count guard)
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>
2026-07-16 09:22:19 +02:00
jgrusewski
c4102c7319 feat(fund-config): max_venue_weight knob (fxhnt fund-config set --max-venue-weight)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 09:11:07 +02:00
jgrusewski
bc83fbdbe7 feat(alloc): max_venue_weight per-venue gross cap (default 1.0 = no-op)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 09:03:59 +02:00
jgrusewski
6ac1325d36 feat(alloc): switch crypto deploy target to the bybit_4edge book (75% deploy vs 18% per-sleeve)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:58:01 +02:00
jgrusewski
98dda4e835 feat(alloc): per-sleeve crypto allocation (promote xsfunding/unlock; bybit_4edge->reference)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 07:47:26 +02:00
jgrusewski
c1d1cb80fb feat(fund-config): nightly allocation reads fund_config for equity + policy (target_vol/kelly)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 07:30:34 +02:00
jgrusewski
d2353ced52 feat(fund-config): fxhnt fund-config show|set CLI + seed on migrate-cockpit
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 07:23:54 +02:00
jgrusewski
b5a2b48de0 feat(fund-config): single-row fund_config SSOT (read/write/fallback/validate/seed)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 07:20:17 +02:00
jgrusewski
6abf2a8289 fix(alloc): killswitch on forward record (not backtest), ISO-normalize honest keys, unviable-capacity=0 sentinel 2026-07-16 00:07:48 +02:00
jgrusewski
efc304e3f9 feat(alloc): capacity policy knobs (min_sharpe, equity_default_ceiling) + ceiling shown next to the curve (spec 6/8) 2026-07-15 23:45:42 +02:00
jgrusewski
242277db30 feat(alloc): record_allocation sizes from honest-reference + caps at capacity; graceful store injection (spec 4b/4c) 2026-07-15 23:41:10 +02:00
jgrusewski
5c5171efc5 feat(alloc): honest-reference series+ceiling provider per deploy strategy (spec 4c/5) 2026-07-15 23:32:54 +02:00
jgrusewski
a27fc717e6 feat(alloc): compute_allocation weights by inverse-vol (capped) not equal-weight 2026-07-15 23:24:01 +02:00
jgrusewski
41913da027 feat(alloc): capacity ceiling from an honest capacity curve 2026-07-15 23:18:28 +02:00
jgrusewski
74ae34a28b feat(alloc): inverse-vol (risk-parity) base weights 2026-07-15 23:16:39 +02:00
jgrusewski
b6e3f24ed2 fix(honest-ref): impact calibration (400bps + turnover-basis), edge-4 sign, book union-fill, wire G4, leverage-ceiling guard
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 22:35:57 +02:00
jgrusewski
dd55f331c3 feat(honest-ref): research report + CLI; consolidate spread reader to PaperRepo SSOT (spec 9) 2026-07-15 22:13:30 +02:00
jgrusewski
91f25215f3 feat(honest-ref): candidate edge #4 causal pump detector + 3-gate honest-cost eval (spec 8) 2026-07-15 22:03:33 +02:00
jgrusewski
d1cba19133 feat(honest-ref): leverage ceiling vs real DD + G4 anti-reactive gate (spec 7) 2026-07-15 21:58:46 +02:00
jgrusewski
255d5baa81 feat(honest-ref): gates G1 survives / G2 LOO / G3 beats-concentration (spec 6) 2026-07-15 21:54:56 +02:00
jgrusewski
65a9ae1f90 feat(honest-ref): risk-parity weights + book aggregation (spec Phase 2) 2026-07-15 21:49:36 +02:00
jgrusewski
5019e9ffbb feat(honest-ref): sqrt-impact/spread-floor lock + real-spread snapshot reader (spec 0c) 2026-07-15 21:42:51 +02:00
jgrusewski
595dc5da0e feat(forward): pin re-homed sleeves' recovered OOS inception in git (reproducible on DB rebuild)
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>
2026-07-15 19:31:16 +02:00
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
jgrusewski
c5a5a0ba24 fix(ucits): resolve UCITS by ISIN on LSEETF (not SMART); $100k envelope
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>
2026-07-15 11:36:17 +02:00
jgrusewski
09ebc4c4e0 perf(vrp): one-query preload for the replay (kill the per-day N+1)
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>
2026-07-15 10:32:30 +02:00
jgrusewski
8697e1c0ba fix(vrp): exec twin freezes last complete OPRA session, not today
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>
2026-07-15 09:55:55 +02:00
jgrusewski
38f5dfa784 fix(vrp): single-source model mark for backtest + live exec profit-take
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>
2026-07-15 09:49:45 +02:00
jgrusewski
768ce4a068 test: update UCITS/US rebalancer arm-state guard to the EU-hours armed config (33318ec)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 09:29:43 +02:00
jgrusewski
0a3308c3a6 fix(vrp): model-mark the defined-risk XSP spread daily signal
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>
2026-07-15 09:27:19 +02:00
jgrusewski
8af24f96c2 fix(ucits): isolate price-fetch failures, fix map-collision msg + venue-switch return
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>
2026-07-15 02:06:40 +02:00
jgrusewski
edf7a659f6 fix(ucits): resolve UCITS contracts by ISIN + size from real IBKR price
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>
2026-07-15 01:56:06 +02:00
jgrusewski
29f60a8584 fix(D2.2/2.3): plain-language exec card copy, gross %-of-book, stale-value caveat, generic asset fallback
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>
2026-07-15 00:12:55 +02:00