Move the recompute-replay sim_curve_ret persist to after the `if sm is None: return`
guard so it is gated on the SAME condition as the reconciliation-gate backtest_summary
write. A too-short replay (< 2 rows, no gate ref) now writes NEITHER, never a degenerate
sim curve with no matching gate ref. Adds test_insufficient_replay_writes_neither_ref_nor_sim_curve.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Generalizes the Backtest page beyond the Bybit-only venue: bybit_4edge/bybit_4edge_levered
keep reading the precomputed bybit_sleeve_ret table; the IBKR paper strategies (multistrat,
vrp) now read their own precomputed recompute-replay curve from a new sim_curve_ret table,
written nightly by _persist_track_backtest_ref from the SAME rows the reconciliation-gate
ref is derived from. New sim_curves.sim_returns_for dispatches on registry_backtest_kind;
app.py wires an _ibkr_measured_context render path (reusing the existing pure
simulate_naive_eqwt engine) alongside the unchanged Bybit measured-cost path, with
plain-language pill labels for the two IBKR books.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer noted drift 0.001/vol 0.008 (Sharpe ~2) still failed the OOS-decay
check on ~5% of seeds (seed 11 happened to pass). Bump to 0.0015/0.007
(Sharpe ~3.4), verified 200/200 seeds pass — robust, not seed-lucky.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
crypto_tstrend's standalone STRATEGY_REGISTRY entry was retired in Phase 0b
Task 4, but the still-live crypto_tstrend book SLEEVE in the bybit_4edge
per-edge breakdown (Overview + /paper) shared the same string id, so its
display-name lookup blanked to the raw id and linked to a dead
/strategy/crypto_tstrend (404).
Add _SLEEVE_DISPLAY_NAMES next to _DEFAULT_BYBIT_SLEEVES in
bybit_book_eval.py — the sleeve breakdown's own display-name source,
independent of STRATEGY_REGISTRY, so a future forward-track retirement can
never blank a live sleeve again. _deploy_individual_edges() now sources
display_name from that map and adds a `linkable` field (true only when the
sleeve id is also a real registry entry); deploy_constituent_row renders the
/strategy link only when linkable, plain text otherwise.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test_keeps_real_premium was RED on master: at the shared module RNG's state it
drew an in-sample half with ~zero drift (is_sharpe 0.02), so the gauntlet
CORRECTLY rejected it (no in-sample evidence) — the test's premise, not the
gauntlet, was wrong. Now uses a local seeded RNG (order-independent) and a
genuinely strong premium (drift 0.001/vol 0.008, in-sample Sharpe ~2, t~4.8);
verified 30/30 seeds pass. The gauntlet keeps genuine premia (is_sharpe 0.82-1.23
-> dsr 0.978-0.999); it was never broken.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The vrp health axis flagged vrp=STALE because the OPRA freeze
(xsp_option_bars) was stuck: vrp_nav froze as_of=today, but today's
session is only partially available intraday, so the freeze failed with
`422 data_schema_not_fully_available` (distinct from the C1a clamp's
`data_end_after_available_end`). That made the freeze timing-dependent —
it could only succeed in a narrow post-settlement window and otherwise
silently degraded to recompute-off-stale-frozen.
Make it robust and timing-independent:
- databento: normalize the `data_schema_not_fully_available` 422 into the
single catchable DatabentoRangeUnavailable (surgical — only that 422;
any other client error propagates raw). Expose
last_available_option_date() as the step-back's starting candidate.
- assets: new _freeze_latest_session entry point walks back one TRADING
day (skip Sat/Sun) from OPRA's available-end until a COMPLETE session
freezes, bounded to 5 steps; returns/logs the frozen session date.
Never freezes a partial session's marks.
- No silent degradation: if no complete session is found in the bound it
raises DatabentoRangeUnavailable; the existing C1b wrapper in vrp_nav
catches it → recompute off frozen, and the health axis keeps vrp STALE
(loud), never a quiet no-op.
TDD: step-back-past-partial, weekend-skip, bound-exhausted-raises, 422
normalization (both chain + bars paths), unrelated-error-propagates,
last_available_option_date caching. Full suite green (one pre-existing,
unrelated seed-dependent gauntlet flake).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "no forward data yet" carve-out in compute_health let a reconciliation-gated
recompute track that never produced data since inception (empty freeze table / no
forward_summary, old anchor t0 or no active anchor at all) read HEALTHY the same
way a genuinely brand-new track does. Thread the active forward_anchor.t0 through
evaluate_health so compute_health can tell "just inceptioned" (anchor within the
stale threshold -> HEALTHY) apart from "broken from inception" or "never anchored"
(-> STALE), closing the silent-degradation hole without false-alarming new tracks.
Also corrects the module docstring: BROKEN only covers per-track compute failures,
not the migrate/all_summaries/all_backtest_summaries/xsp_freeze_max_date reads
evaluate_health hoists outside the per-track try (those fail the whole
cockpit_forward Dagster asset instead).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vrp rotted for 2 weeks because a data failure degraded QUIETLY (recompute off a
frozen-but-stale table, only a log line) and the gate can't tell a stale track
from a healthy WAIT. Add a HEALTH axis SEPARATE from the gate so ANY live/gated
track that stops updating surfaces LOUDLY on day 1.
forward_health.py (new): compute_health (pure state machine) + evaluate_health,
called by cockpit_forward AFTER ingest. States {HEALTHY, STALE(age), NO_REF,
BROKEN}, precedence BROKEN>STALE>NO_REF>HEALTHY:
- STALE: a `recompute` track whose source as_of OR its freeze table's max date
(vrp → xsp_option_bars) is > Settings.stale_after_business_days (default 3)
business days behind. The freeze axis catches the exact vrp mode: forward
"updates" yet the frozen input is stale.
- NO_REF: a reconciliation-gated recompute track with no resolvable backtest
reference (the gate can never reconcile) — the M6 static invariant made a real
runtime check.
- record_mode carve-out: `observed` twins (vrp_exec/…) are externally-driven and
legitimately DORMANT when execution is suspended — never staleness/no-ref
alarmed (won't false-alarm the intentionally-dormant reference_only tracks).
- BROKEN: a per-track health-compute error is surfaced, never swallowed.
Storage: new strategy_health table (create_all — idempotent, Postgres-safe, no
ALTER), replace-all each run so a recovered track clears its stale row.
Rendering: FleetRow/StrategyDetail gain health/health_stale_days/health_reason;
a LOUD red health_badge (STALE 14d / NO REF / BROKEN) on Overview, the all-tracks
table, and the detail header — visually impossible to confuse with a WAIT.
cockpit_forward + evaluate_health log at ERROR with the unhealthy list.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The nightly OPRA freeze parsed metadata.get_dataset_range with a defensive
guess that only read a top-level "end" key and required a dict — a VALID
per-schema-nested response ({schema: {"start","end"}}) or a DatasetRange-like
object spuriously raised DatabentoRangeUnavailable, which vrp_nav's C1b wrapper
absorbs → silent recompute off a stale frozen table (the exact 2-week vrp rot).
- _dataset_range_ends(): parse ALL real 0.79 shapes (return type
dict[str, str | dict[str, str]]): top-level {"start","end"}, per-schema
nesting (max end across schemas), and a .end/.start attribute object. Only
return [] (→ catchable DatabentoRangeUnavailable) for a genuinely empty /
malformed response, never on a shape that carries the range.
- _to_utc_ts(): single tzinfo guard (localize if naive, convert if aware).
Fixes _clamp_option_end's C1b gap where pd.Timestamp(x).tz_localize("UTC")
raised TypeError on a tz-AWARE start/end.
- document the _option_available_end cache lifetime (one nightly run).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
multistrat._build dropped dates[-1] whenever it equalled UTC-today (to shed an
incomplete intraday bar). Correct for the exec path (execute-multistrat, 14:35
UTC, market OPEN) but WRONG for the nightly multistrat_nav (23:30 UTC, after the
US close): it first freezes today's FINAL close, then _build threw that day away
every night, leaving forward_summary.multistrat.as_of stuck one trading day
behind forever.
Thread a drop_incomplete_today flag (default True) through _build / book_series
/ target_weights / MultiStratStrategy. The nightly multistrat_nav constructs
MultiStratStrategy(..., drop_incomplete_today=False) — and since the SAME
build_strategy lambda backs both the forward track and its backtest ref, both
sides use the identical complete-today basis (no reconciliation divergence). The
exec target_weights path and growth_discipline keep the default True.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
cockpit_forward reads purely from the DB (forward_summary) and its deps are
ordering-only, so a skipped upstream stalls the whole gate/promotion/allocation
pipeline. Close the two pre-tracker offenders so no *_nav asset raises on a
data-provider failure:
- multistrat_nav: the snapshot_yahoo_closes loop runs OUTSIDE _run_paper_tracker;
a Yahoo outage now logs ERROR and degrades to a no-op {forward_days:0,...}
return (record unchanged) instead of raising and skipping the gate.
- _run_paper_tracker: broaden the caught set to also isolate the databento
provider error base (BentoError/BentoClientError, imported lazily since
databento is an optional extra). Bare Exception is NOT caught — genuine
strategy-math bugs (KeyError/ValueError) still propagate loudly.
With every expected data-provider failure mode isolated, cockpit_forward always
runs (deps kept for ordering). See report for the decouple-approach rationale.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
vrp_nav hard-failed every night: _freeze_day runs OUTSIDE _run_paper_tracker
and databento 422'd (data_end_after_available_end) because end=as_of+1day
exceeded OPRA's ~22:30-UTC available end at the 23:30 freeze. The raised
BentoClientError killed the asset before run_track/set_anchor, zeroing vrp's
forward anchor AND skipping the downstream cockpit_forward gate.
C1a: DatabentoDataProvider now fetches the OPRA available end via
metadata.get_dataset_range (cached once per instance) and clamps the exclusive
query `end` down to it in fetch_option_chain/fetch_option_chain_range/
fetch_option_bars, so no query requests past what OPRA has published. An empty
window (start >= clamped end) raises catchable DatabentoRangeUnavailable, never
a raw 422.
C1b: vrp_nav wraps _freeze_day so any freeze failure logs a WARNING and
continues to _run_paper_tracker, which recomputes VrpStrategy off the FROZEN
xsp_option_bars table — guarantees a forward row/anchor/ref even on a day the
freeze fails, and never skips the gate.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
bybit is fxhnt's sole paper book (Task 7f hardcoded every PaperRepo read/write
to it); the prod purge left every paper_* table bybit-only. Remove the now-constant
`venue` column at the SCHEMA level so the paper book is single-venue by construction.
- cockpit_models.py: drop `venue` from all 7 paper models; re-key the 5 composite-PK
tables on their remaining columns; paper_nav_summary swaps its sole `venue` PK for a
singleton surrogate `id` (=1); paper_trades drops the non-PK venue index+column.
- paper_book_migration.py (new): Postgres-only, idempotent reverse migration invoked by
PaperRepo.migrate(). Guarded on the live `venue` column so it is a pure no-op after the
first run (heavy DDL on every cockpit startup would crashloop). Sole home of the
column-drop DDL, keeping paper_repo free of any discriminator logic.
- paper_repo.py: delete `_BYBIT_VENUE`/`_VENUE_TABLES`; strip every venue kwarg/predicate;
PK lookups use (1) / (key). `grep -n venue paper_repo.py` → 0.
- test_no_binance_data.py: 7f venue-default guard → schema guard (no paper model has a
`venue` column).
Full suite green: `uv run pytest -q` → 1853 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 7e only renamed venue="binance" -> "legacy" to pass the substring guard;
the dual-venue repo machinery was left in place even though bybit is the ONLY
live venue. PaperRepo now hardcodes every discriminated read/write to a
_BYBIT_VENUE = "bybit" module constant instead of a constructor venue param
(removed) -- reads/writes ONLY the bybit rows, so the orphaned legacy/binance
rows from the retired combined-crypto book (Task 7d) are never touched.
nav_summary's venue arg is dropped the same way (every caller always passed
"bybit"). cockpit_models keeps the venue column (dropping it from the PK is a
separate prod schema migration, documented in the report for the merge
runbook) with reworded comments reflecting the single-venue reality.
Test fallout: deleted test_paper_repo_venue.py (tested the now-removed
dual-venue isolation) and 3 "does-not-touch-binance" tests that constructed
PaperRepo(venue="binance"); mechanically dropped the venue kwarg everywhere
else. Added a guard assertion that PaperRepo.__init__ has no venue param.
Full suite green (1853 passed); binance substring guard still 0 hits.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Step-1 finding: migration_builders.py and the migrate-forward-anchors CLI
(cli.py:427) already carry their own local strategy_id -> state_file basename
map (track_builders()) from an earlier Task-8 pass -- neither reads
entry["state_file"] off STRATEGY_REGISTRY, so removing the field doesn't
touch them. Only three direct consumers needed changes:
- registry.py: dropped "state_file" from all 11 entries + the header comment.
- assets.py: _run_paper_tracker's state_file param was unused at steady
state (its own docstring said so) -- dropped it + updated its 3 call
sites (sixtyforty_nav, multistrat_nav, vrp_nav).
- a handful of tests asserted on the registry's state_file key; updated to
drop those assertions, and added tests/unit/test_no_registry_state_file.py
as a permanent guard.
The JSON ForwardTracker + its .bak crash-recovery, and migrate-forward-anchors
itself, are untouched -- still the one-time JSON->DB seed / deployment tool.
- pytest-xdist added to dev deps; addopts='-n auto' runs the ~1940-test suite across all
cores (~6-9min -> ~1.5min); registered a 'slow' marker for a serial '-m "not slow"' loop.
- equity_backtest_runner liquidity fixture: trimmed the 3000-bar (8yr) LIVELONG/ILLIQUID
histories to 800 bars (still >> min_history=300, multi-year for peak-yearly ranking) —
the peak-yearly aggregation over 8yr was the cost: 50s/30s/19s tests -> ~5.8s each,
assertions unchanged.
- paper_sim perf guards: made them parallel-robust (the wall-clock <1.6s bound flaked under
-n auto CPU contention). full-history guard relaxed to <10s (it targets an O(D²) blowup =
orders of magnitude, not a constant factor); cheap-reapply made RELATIVE (< full/10, ratio
is contention-invariant). Both marked slow.
Full suite 1940 passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes the cockpit's (web-layer) use of Binance: the /paper venue toggle + Binance paper book view, the /paper/replay venue toggle, and the sim's binance_combined book + Binance-vs-Bybit compare view (_sim_compare.html deleted). Bybit is now the cockpit's only paper-book venue. Backend Binance data/research modules (compare_measured_cost.py, data adapters) are untouched -- Task 7b deletes those.
Binance is deprecated as a broker; kept only as a DATA source (Task 7 removes
that). Deletes binance_ccxt.py, crypto_execution.py (no non-Binance consumer),
the crypto-rebalance/crypto-positions/crypto-flatten CLI commands, and the
execution-only BinanceSettings class + field. Adds a narrow guard test.
Alpaca's rebalancer CronJob has been suspended with 401'd keys since Task 4's
venue consolidation; IBKR paper is already the live forward-track venue for
the multi-strat book. This removes the dead code/config/manifests instead of
leaving them to rot, and relabels the multistrat/multistrat_exec registry
venues from the vestigial "alpaca-paper" to "ibkr-equity" (they execute on
IBKR paper, not Alpaca — exec_venue itself is still computed dynamically at
runtime).
- Delete src/fxhnt/adapters/broker/alpaca.py, AlpacaSettings + the `alpaca`
config field; move the one real setting it carried (min_order_notional,
a generic "skip sub-min-notional dust" floor for any future
fractional-capable broker) onto ExecutionSettings.
- Remove the `--broker` option and its alpaca branches from `execute` and
`execute-multistrat` (cli.py) — IBKR is now implicit; IbkrBroker was
already the module-level import, so the branch was pure dead code once
Alpaca is gone.
- Delete infra/k8s/jobs/fxhnt-alpaca-rebalancer.yaml and
infra/k8s/secrets/alpaca-credentials.yaml; delete the superseded
infra/k8s/services/multistrat-rebalancer.yaml (PVC + NetworkPolicy +
CronJob), replaced by fxhnt-ibkr-rebalancer. Drop the now-invalid
`--broker ibkr` argument from fxhnt-ibkr-rebalancer.yaml and
fxhnt-ucits-rebalancer.yaml (the flag no longer exists).
- Delete tests/integration/test_alpaca_broker.py (tested the deleted
adapter directly); add tests/unit/test_no_alpaca.py as a standing guard
(no "alpaca" substring anywhere in src/fxhnt + IbkrBroker still imports).
- Update test_registry_multistrat_exec.py + test_cockpit_ssot.py (venue
label) and test_cli_execute_helpers.py (drop the broker_name kwarg and
AlpacaBroker fake, use IbkrBroker instead) to match.
MANUAL cluster ops (not run by CI, record only):
kubectl delete cronjob/fxhnt-alpaca-rebalancer secret/alpaca-credentials -n foxhunt
kubectl delete cronjob/multistrat-rebalancer networkpolicy/multistrat-rebalancer pvc/multistrat-state -n foxhunt
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Neither is tradeable on Bybit: crypto_tstrend as a standalone Binance-perp forward
track was a -0.21 marginal-Sharpe drag/crash-amplifier, and stablecoin_rotation's
FDUSD/USDP pairs aren't listed on Bybit. Removes the two *_nav assets, their
registry entries, the orphaned migration_builders builders, and the
Binance-only StableRotationForward wrapper (stablecoin_runner.py's
StableReversionRunner stays -- it's still live via the Bybit stablecoin eval
paths). The crypto_tstrend SLEEVE inside the bybit_4edge deploy book
(_DEFAULT_BYBIT_SLEEVES, Bybit data) is untouched.
Updates dependent tests: deletes 5 whose subject (the retired asset/registry
entry/module) no longer exists, and swaps the retired sid for a still-registered
one (unlock/xsfunding/sixtyforty) in tests that only used crypto_tstrend as a
generic example sid.
The standalone Bybit sleeve tracks (positioning/xsfunding/unlock) AND the two 4-edge
book tracks (bybit_4edge/bybit_4edge_levered) charged cost_bps_per_turnover=10.0 in
their forward nav assets, while their reconciliation ref (_persist_bybit_book) computed
the backtest at 5.5 — a forward-charged-more-than-ref mismatch that biases the gate
toward WAIT on a cost artifact (safe direction, but could permanently WAIT a real edge).
New Settings.bybit_cost_bps=5.5 (the measured liquid-book cost — the mirage finding) is
the SINGLE source for every Bybit forward track + its ref; registry cost_bps 10->5.5 +
definition_version 1->2 (visible deterministic re-inception). Equity/tradfi tracks keep
cost_bps_per_turnover (different venue). Invariant test locks all 5 Bybit sids. Full
suite 2058 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reuses build_bybit_sleeve_forward_nav (Task 1). unlock_nav drops the standalone
Binance UnlockShortForward(UnlockCalendarLive) path, runs the single unlock sleeve
on bybit_features through the deterministic engine, loading the unlock calendar via
load_unlock_events (the same loader the Bybit book uses). unlock joins _BYBIT_BOOK_SIDS
+ bybit_report_sid_kwargs (ref via _persist_bybit_book); registry venue=bybit-perp,
backtest.kind=report. _DEFAULT_BYBIT_SLEEVES/deploy book untouched. Regression lock
now requires the unlock ref too. Full suite 2057 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a generic build_bybit_sleeve_forward_nav(repo, store, sleeve, ...) builder in
assets.py, refactors build_positioning_forward_nav to delegate to it, and moves
xsfunding_nav off the standalone Binance XsFundingForward(BinanceXsFundingLive())
construction onto its own Bybit single-sleeve forward track (mirrors positioning
exactly). xsfunding still runs unchanged as a sleeve inside the bybit_4edge deploy
book (_DEFAULT_BYBIT_SLEEVES untouched).
- bybit_forward_track.py: bybit_report_sid_kwargs() gains an xsfunding case.
- cli.py: _BYBIT_BOOK_SIDS += "xsfunding" (routing only, series already computed).
- registry.py: xsfunding -> venue bybit-perp, backtest.kind sleeve->report, params
mirror positioning's shape, record_mode observed->recompute (required for the
deterministic-engine recompute path; confirmed by the exhaustive observed-mode
pin in test_forward_definition.py, updated accordingly).
- migration_builders.py: replaced the xsfunding lambda:None stub (valid only under
the old observed record_mode) with a real single-sleeve Bybit builder, mirroring
positioning's, to avoid a latent crash if the one-time forward migration is ever
re-run.
Tests: new tests/unit/test_bybit_sleeve_forward_builder.py (TDD, brief-verbatim);
full tests/unit + tests/integration sweep: 2056 passed, 0 failed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
End-to-end test at the real production seam CockpitBacktestRefProvider(repo).reference(sid, days)
-> evaluate_reconciliation_gate: proves the reconciliation gate is permanently stuck in the
"no backtest reference to reconcile against" WAIT with no backtest_summary row, and un-sticks
(reconciles to PASS) once one is persisted (Tasks 2/6's fix for bug A).
Step 1 investigation (verified, no code migration needed): the live paper
path (paper_book.py, paper_sleeves.py, paper_backfill.py) has ZERO
DuckDbFeatureStore/.duckdb/read_parquet usage — it only consumes a store via
get_feature_store(). The only non-test DuckDbFeatureStore constructor is
get_feature_store() itself (adapters/warehouse/__init__.py), gated behind the
explicit feature_store == "duckdb" opt-in; the default feature_store="postgres"
(config.py) returns TimescaleFeatureStore. DuckDB is dev/test/opt-in-only on
the live path — no migration required.
- Add tests/unit/test_no_live_duckdb_files.py: asserts Settings().feature_store
defaults to "postgres" and get_feature_store() returns TimescaleFeatureStore,
not DuckDbFeatureStore, locking the safety invariant noted in config.py (a
Job that forgets FXHNT_FEATURE_STORE can't silently write a throwaway DuckDB
file). Uses a sqlite DSN (matching every other TimescaleFeatureStore test in
this repo) so the unit test needs no live Postgres server.
- Retire the orphaned B3 platform Jobs/PVC (no kustomization/script referenced
them): infra/k8s/jobs/fxhnt-backtest-ingest-job.yaml,
fxhnt-backtest-verdict-job.yaml, infra/k8s/storage/fxhnt-backtest-pvc.yaml.
- Mark equity_backtest_runner.py PARKED in its module docstring (no live
consumer; revivable — plugs into backtest_refs when an equity strat is
added to the registry). Logic unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Refactor _persist_track_backtest_ref (assets.py) to delegate to the unified backtest_ref dispatcher
instead of re-implementing the recompute-replay path inline, and wire xsfunding's missing sleeve-kind
ref through it (bug A's asset half) using the Binance-scoped feature store. Replace the manual
fxhnt-bybit-sleeve-ret Job with a weekly-scheduled fxhnt-backtest-refs CronJob running
`fxhnt backtest-refs --all` (bug B) so the report-kind refs (bybit_4edge/bybit_4edge_levered/positioning)
no longer depend on a human remembering to apply a one-off Job.
report_backtest_summary dispatches the 3 report-kind sids (bybit_4edge,
bybit_4edge_levered, positioning) to the existing bybit_4edge_backtest_summary
book engine with the right per-sid kwargs, reconstructing liquid universe /
unlock calendar / positioning haircuts from the store + settings. Replaces
the stale test stub (_build_report_for, list-shaped constructions) with the
dispatch-verification tests from the brief. Adds `backtest-refs --all` CLI
command: iterates STRATEGY_REGISTRY's report-kind sids through the unified
backtest_ref dispatcher and upserts backtest_summary, per-strategy failure
logs and continues.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The model-ref branch in backtest_ref() delegated to backtest_ref(model_id, ...),
which is dead today but would silently overwrite the real model's backtest_summary
row under a mislabeled sid if Task 6 generalizes the caller to pass the exec twin's
own builder. Executed tracks reconcile via CockpitBacktestRefProvider._REF_ALIAS at
the gate layer, not by writing their own ref here, so return None instead.
Also add the missing vrp_exec -> vrp alias entry so vrp_exec's model-ref label is
functionally true (otherwise its gate would WAIT forever once armed).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-day backfill did ~6-8 OPRA API round-trips/day (~26k over 2013->), all latency
-> ~45h. Rewrote it batched: fetch_option_chain_range (one range definition/month, deduped)
+ one range ohlcv fetch/month (band-bounded via a coarse median-strike forward, wide enough
for intra-month moves incl. COVID), then every trading day processed LOCALLY (parity forward
+ band + upsert). Extracted the forward/band logic into pure helpers (_forward_from_marks,
_band_contracts, _band_rows_from_marks) shared by the per-day path (_xsp_forward_estimate,
_freeze_xsp_slice — refactored, behaviour unchanged, tests green) and _backfill_month. CLI
iterates per-month. ~1h, ~$70-100 (bulk pulls a wider slice than the tight daily band).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_xsp_forward_estimate and _freeze_xsp_slice each independently called fetch_option_chain
(definition) per day, doubling OPRA definition cost on every nightly vrp_nav run AND the
2013-> backfill (~$75 -> ~$37 definition). New _freeze_day fetches the chain ONCE, derives
the parity forward from it, and freezes the band from the SAME chain. Both helpers now accept
an optional pre-fetched chain (default-fetch preserved, so existing callers/tests unaffected).
All 3 callers (vrp_nav, plan_and_record_vrp, backfill-xsp-opra) use _freeze_day. Cuts the
2013-> backfill from ~$90 to ~$55.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The wide freeze band (DTE[1,45], put moneyness[0.70,1.20] + ATM calls) has >2000
XSP contracts; a single fetch_option_bars 422'd 'data_exceeded_maximum_number_of_symbols'.
Now chunks at Databento's 2000-symbol limit, sums cost across ALL chunks BEFORE any
download (estimate-then-refuse preserved), merges results. Caught by the live paper
dry-run after the inclusive-end fix. Affects vrp_nav asset + execute-vrp + backfill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Databento's range end is exclusive and rejects start>=end. The freeze path
(_freeze_xsp_slice / _xsp_forward_estimate) fetches a single day (start==end),
which 422'd 'data_time_range_start_on_or_after_end'. Now queries [start, end+1day),
mirroring fetch_option_chain. Caught by the live paper dry-run (unit tests used a
fake provider). Affects the nightly vrp_nav asset + execute-vrp + the OPRA backfill.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The paper-envelope-refusal test calls execute_multistrat() directly; the new --venue
validation guard receives the typer.OptionInfo default unless venue_kind is passed
explicitly, so the direct call now passes venue_kind='us' like its other options.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Robustness guards for the SUSPENDED UCITS routing: reject an unknown --venue
value instead of silently treating a typo as 'us'; refuse --venue ucits
without --broker ibkr (UCITS ETFs are LSE/Euronext-listed, IBKR-only);
apply_ucits_map now raises on a non-injective map (two sleeves mapping to
the same ticker would otherwise silently drop one via dict-key collision).
Adds coverage that the shipped default map maps every FUND_INSTRUMENTS
sleeve, so a future sleeve added without a UCITS mapping fails loudly.