fef59395563dc1d5d718cb3ef4e64c62ee8b9ef6
18 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fef5939556 |
feat(ml-backtesting): cold-start stopgap — max-confidence bytecode policy (Q1/Tier1)
The threshold-tuning smoke at
|
||
|
|
cd82f9a4a0 |
feat(ml-backtesting): threshold gate + per-fill cost integration (P4)
Adds the two sweep axes that the spec's deployability grid needs but were missing from the kernels: Threshold gate (decision_policy.cu, both kernels): - New per-backtest `threshold_per_b` array kernel arg. - Pre-Kelly prelude: if max_h |alpha[h] - 0.5| * 2 < threshold[b], emit noop and return. Kept deterministic from alpha alone so the threshold pre-registration step (p60-p95 absolute calibration on a validation window, future P6) reflects exactly what gets gated in deployment. Per-fill cost integration (resting_orders.cu / apply_fill_to_pos): - apply_fill_to_pos signature grows three args: b, cost_per_lot_per_side_per_b, total_fees_per_b. Single insertion point at line 90. - After the close-leg realized_pnl math runs (so the gross unwind P&L is preserved), deduct fill_cost = filled_lots * cost_per_lot_per_side[b] from pos.realized_pnl AND accumulate into total_fees_per_b[b]. - Net-of-cost semantics: isv_kelly_update_on_close reads realized_pnl delta which is now net of cost — Kelly state learns from realistic return distribution. - All 3 apply_fill_to_pos call sites in step_resting_orders updated. order_match.cu's submit_market_immediate path is dead code in the post-P1 flow (everything routes through seed_inflight_limits_batched → step_resting_orders → apply_fill_to_pos) so not touched here. BatchedSimConfig + UniformSimParams + BacktestHarnessConfig gain threshold + cost_per_lot_per_side fields. All UniformSimParams constructors in tests and main.rs updated with defaults (0.0, 0.0 = gate disabled, frictionless). Regression: - threshold_gate_skips_low_conviction (p=0.51 + threshold=0.10 → noop) - threshold_gate_allows_high_conviction (p=0.8 + threshold=0.10 → buy 1+) - threshold_zero_is_passthrough (sanity) - All P1+P2+P3 tests continue to pass via the new ABI. cost_deducted_at_each_fill + kelly_state_sees_net_return end-to-end tests deferred — they require a full submit_market → fill → close sequence, which the production smoke exercises. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
b8966fb1a6 |
feat(ml-backtesting): per-backtest sim parameter arrays (P1)
Migrates target_annual_vol_units, annualisation_factor, max_lots, latency_ns, kelly_frac_floor, sharpe_weight_floor from scalar-broadcast kernel args to per-backtest device arrays via BatchedSimConfig. Atomic contract change per feedback_no_partial_refactor — kernel + sim + harness + all 3 existing test files migrate in this commit. - crates/ml-backtesting/src/sim.rs → sim/mod.rs (directory module) - crates/ml-backtesting/src/sim/batched_config.rs (NEW): BatchedSimConfig + UniformSimParams + validate(). from_uniform rebuilds the legacy uniform-broadcast behaviour at n=1 (smoke/fixtures). from_grid lands in P6 for the 140-variant sweep packing. - LobSimCuda gains 6 per-backtest device buffers (target_annual_vol_units_d, annualisation_factor_d, max_lots_d, latency_ns_d, kelly_frac_floor_d, sharpe_weight_floor_d). step_decision_with_latency uploads from BatchedSimConfig each call; both decision kernel launches now pass per-backtest array pointers. - decision_policy_default + decision_policy_program: scalar args become const float* / const int* per_b arrays; first lines of each kernel index by `b` into the arrays. Behaviour preserved at n=1 uniform. - dispatch_latent_market_orders: reads latency per-backtest from &BatchedSimConfig (host loop stays for P1; P2 replaces with kernel). - step_decision_with_latency now ALWAYS dispatches through the latency path; when cfg.latency_ns[b]=0 the in-flight slot's arrival_ts equals current_ts and gets promoted immediately on next snapshot. Eliminates the if/else branch and consolidates the launch path. - harness.rs: BacktestHarness gains a sim_config field, built via BatchedSimConfig::from_uniform at new() from the harness cfg's scalar fields. The run loop passes &self.sim_config to step_decision_with_latency. Regression coverage: - parallel_sim_correctness::parallel_sim_equivalence_with_uniform_config — n=8 with uniform config produces 8 bit-identical market_targets (proves per-backtest indexing reduces correctly). - Existing decision_floor_coldstart tests (3) all pass through the new ABI — proves cold-start floor + variance-cap gate behaviour preserved. - parallel_sim_independence_per_backtest deferred to P2 (needs read_first_inflight_arrival_ts helper that depends on LimitSlot layout). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c7fdc617dc |
fix(ml-backtesting): variance-cap sample-size gate + aggregate GPU req
Three follow-ups to the cold-start floor fix: 1. Kernel: MIN_TRADES_FOR_VAR_CAP gate. After the first trade closed, `isv_kelly_update_on_close` set `realised_return_var = ret²` — a single-sample variance proxy that systematically collapses `cap_units = target_vol / sqrt(var × ann_factor)` near zero for any biased return. cap_lots → 0 → no further trades despite strong alpha. Gate the variance-derived cap behind `n_trades_seen >= 10`; below the threshold cap falls back to host-supplied `max_lots`, same as the pre-first-trade path. Same gate applied to both `decision_policy_default` and `decision_policy_program`. Regression: `post_first_loss_state_does_not_lock_out_further_trades` reproduces the exact pre-fix state from the smoke (n_trades_seen=1, var=103.6) and asserts the kernel still fires a long with p=0.8. 2. Aggregate: add `nvidia.com/gpu: 1` resource request. Scaleway's L40S device plugin mounts libcuda.so.1 into the container only on GPU-requesting pods; the aggregate logic is CPU-only but the binary's dynamic loader needs the driver libs. Cheapest correct fix until a separate CPU-only aggregator binary exists. 3. Smoke YAML: `max_events: 0` (exhaust loader). 100k events is minutes of ES.FUT, far shorter than the h6000 holding horizon the model was trained on. Full quarter exercises sustained trading + variance estimate ramp-up. All three regression tests pass locally on RTX 3050 Ti. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
da21feb1b1 |
fix(ml-backtesting): cold-start Kelly + Sharpe-weight floors in decision kernel
Production smoke completed end-to-end but produced n_trades=0 across 99,969 decisions — `decision_policy_default` and `decision_policy_program` both applied a sentinel-skip pattern: if `isv_kelly_d` had not been seeded (pnl_ema_win == 0), each horizon's signed-size stayed zero, AND each horizon's aggregation weight (= recent_sharpe) also stayed zero. The cross-horizon w_sum was therefore 0, final_size was 0, every market_target was noop. State only updates on trade close → no trade ever fires → infinite cold-start. Per pearl_blend_formulas_must_have_permanent_floor (`max(real, floor)`, not blend) and pearl_kelly_cap_signal_driven_floors, replace the sentinel- skip with a two-layer floor on each kernel: 1. Kelly fraction: `max(kelly_frac_floor, computed_kelly)` — when state is sentinel, falls back to the floor directly. Cap_lots falls back to `max_lots` when realised_return_var is sentinel. 2. Aggregation weight: `max(sharpe_weight_floor, recent_sharpe)` — lets cross-horizon sum produce a non-zero size before recent_sharpe is populated. Once a horizon shows positive sharpe it dominates. Plumbed through `step_decision_with_latency` / `step_decision` as two new f32 args (atomic contract change, every caller migrated). Defaults 0.20 / 0.10 chosen so a strong-conviction signal (sig_mag ≥ 0.5) fires 1 lot at cold-start under max_lots=5 while weaker signals stay flat (see `default_kelly_frac_floor` comment for the arithmetic). Exposed as CLI flags + sweep-grid base/cell overrides. Regression test `decision_floor_coldstart` proves: - default floors (0.20/0.10) fire a 1-lot buy with p_h=0.8 and zero state - zero floors reproduce the original noop bug Also moves `aggregate` step to the GPU pool because fxt-backtest is dynamically linked against libcuda.so.1 (the ci-compile-cpu hosts don't expose CUDA driver libs). Verified locally on RTX 3050 Ti — workspace cargo check passes, both regression tests pass, trunk save/load roundtrip still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
87b8303950 |
chore(ml-alpha): drop 'v2' naming — greenfield, no backward compat
Per project_ml_alpha_starting_capital greenfield posture: there is no V1 to differentiate from (the V1 trunk forward was dead code, no V1 checkpoint files exist in the wild). The 'v2' prefix on every identifier was historical baggage from the migration period. Renames: - CheckpointV2 -> Checkpoint (also drops the version: u32 field — bincode either deserialises a current envelope or errors; no migration path needed) - CheckpointVersionProbe removed (was only for V1 rejection) - LAYER_NORM_CUBIN_V2 / VARIABLE_SELECTION_CUBIN_V2 / ATTENTION_POOL_CUBIN_V2 -> LAYER_NORM_CUBIN / VARIABLE_SELECTION_CUBIN / ATTENTION_POOL_CUBIN - _ln_module_v2 / _vsn_module_v2 / _attn_module_v2 -> drop _v2 suffix - smoke_load_v2_checkpoint test -> smoke_load_checkpoint - config/ml/sweep_v2_*.yaml -> config/ml/sweep_*.yaml - migration-era 'V2 weight skeleton' / 'V2 fields' / etc. comments cleaned to remove the v2 prefix Pre-existing 'v2' references in ml-backtesting CUDA files (decision_policy.cu, pnl_track.cu) are NOT touched — those refer to future planned 'v2' refinements (Portfolio mode, multi-fill averaging) from the C1-C19 commits and reflect aspirational features unrelated to this session's trunk-grows work. Verification: ml-alpha + ml-backtesting + fxt-backtest all build clean. perception_forward_golden bit-exact (max_diff = 0.000000). |
||
|
|
5e8f49bf1c |
test(ml-backtesting): GPU smoke against real CheckpointV2 (X18)
Adds #[ignore]d integration test that loads a CheckpointV2 file produced by alpha_train and verifies CfcTrunk::load_checkpoint accepts it. Activated via FOXHUNT_SMOKE_CKPT env var on a CUDA-capable host. Also adds a compile-time witness test confirming Summary.max_drawdown_pct field exists post-X16 (required by emit_deployability_verdict). Note: full BacktestHarness end-to-end smoke (running one cell against fixture MBP-10) requires the v2 forward path to live on the trunk (X11 — deferred). This commit verifies the producer/consumer wire-up. Per spec §3.4, §4.1 (X18). |
||
|
|
8d3efa450e |
test(ml-backtesting): integrated Ring 2 fuzz over full pipeline (C18)
Existing lob_sim_fuzz only exercised book_update + market orders. This new test suite drives the FULL integrated pipeline under random adversarial conditions: apply_snapshot (random-walk book) → step_resting_orders (random signed trade-flow signal) → broadcast_alpha (random per-horizon probs every 4th event) → step_decision_with_latency (mixed latency=0 + latency=50ms cells) → submit_market_immediate (immediate path) → pnl_track_step + isv_kelly_update_on_close Warm-starts every backtest with random-but-plausible Kelly state (at least h4 set credibly profitable so decisions actually open positions). Random target_annual_vol + annualisation_factor + max_lots per decision to vary the Kelly cap. Per-50-event invariants: • book monotonicity (bid_px[k] ≤ bid_px[k-1], ask_px[k] ≥ ask_px[k-1]) • no-crossed-book (ask[0] > bid[0]) • Pos.realized_pnl + Pos.vwap_entry finite (no NaN/Inf leaks) • Pos.position_lots in plausible range (|≤100|) • All 5 per-horizon IsvKellyState fields finite Three test sizes: integrated_fuzz_n1_short — N=1, 200 events integrated_fuzz_n8_medium — N=8, 500 events integrated_fuzz_n64_long — N=64, 1000 events All three pass on RTX 3050. The N=64 × 1000 case exercises 64,000 event-snapshots × 16,000 decisions × ~12,800 trade attempts without any invariant violation or NaN propagation. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|
|
dd8d731dcf |
test(ml-backtesting): Ring 3 production-day replay validation (C17)
Closes the spec §8 Ring 3 deferral. Two integration tests against
FOXHUNT_TEST_DATA real MBP-10 day files:
buy_and_hold_full_day
Walk the first .dbn.zst from open to close; submit a market buy
of 1 lot at the first event, run the full step-loop
(apply_snapshot + step_resting_orders) over every subsequent
event, then close with a market sell at the last event. Assert
realized P&L matches (close_mid - open_mid) within ±2 ticks per
spec slippage budget. Proves the book-walking + position
accounting + step-loop orchestration are wire-level correct
against real data, not just hand-crafted JSON fixtures.
walk_book_one_full_day
Same fixture, no trades — just iterates every event through
apply_snapshot + step_resting_orders and asserts the book
invariants (bid/ask monotonicity, no-crossed-book) hold at every
10_000-event checkpoint across the full day. Stress test of
the kernel against real market microstructure (gaps, locked
markets, regime shifts).
Both tests skip gracefully when FOXHUNT_TEST_DATA is absent OR the
predecoded sidecars are empty (the current 40-byte placeholder
fixtures in test_data/futures-baseline/ES.FUT/*.predecoded.bin will
trigger the skip path; populating those with a real Databento decode
makes both tests fire end-to-end). Ring 3 is operational, not
CI-mandatory.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
ed19985c95 |
feat(ml-backtesting): bytecode VM dispatcher for Strategy compositions (C13)
The hardcoded WeightedByRealizedSharpe path from C7 is now joined by
a stack-based bytecode interpreter that consumes Strategy::flatten()
output, unlocking RegimeSwitch / Portfolio / non-default Ensemble /
single-Leaf compositions specified via policy-grid YAML.
cuda/decision_policy.cu — new kernel `decision_policy_program`:
Stack-based VM with parallel (value, attribution_mask) stacks.
Opcodes mirror src/policy/mod.rs::OpCode exactly:
NoOp / PushScalar / EvalRegime / BranchIfRegime
EmitPerHorizonSize (computes sized intent from alpha[h] +
IsvKellyState[h] using same Kelly + ISV-cap formula as the
hardcoded default)
AggMean / AggWeightedSharpe / AggMaxConfidence (pop n values,
push aggregated, OR attribution masks)
ApplyConflict (v1 no-op, reserved for Portfolio)
WriteOrder (terminal — converts top-of-stack to market_target +
open_horizon_masks attribution if currently flat)
AggWeightedSharpe recovers the source horizon from a single-bit
attribution mask to look up recent_sharpe; multi-bit masks (nested
aggregators that collapsed horizons) fall back to uniform weight.
decision_policy_default extended with a program_lens param: skips any
backtest whose plen > 0 (the program kernel handled it). The two
kernels run sequentially in step_decision_with_latency with mutual
exclusivity on each backtest slot.
LobSimCuda gains:
upload_program(b, &Program) — uploads a Strategy::flatten() output
to backtest b's slot in program_table_d, updates program_lens_d.
set_regime(b, regime_id) — writes regimes_d for OP_EVAL_REGIME /
OP_BRANCH_IF_REGIME consumption.
BacktestHarnessConfig.strategies (Vec<Strategy>) — empty means every
cell uses the hardcoded default; non-empty len must equal n_parallel
and each strategy is flattened + uploaded at construction.
bin/fxt-backtest --policy-grid <yaml> path now actually plumbs through
to the kernel (was parsed-but-ignored in C9). Empty grid keeps the
default behaviour.
New fixture decision_program_h4_only: uploads Strategy::Leaf(h4_only)
flattened to (EmitPerHorizonSize, WriteOrder) — 2 instructions, 16
bytes — and verifies the bytecode kernel produces equivalent end-state
to the hardcoded default (3 lot buy at vwap=5500). Proves the VM
dispatcher works end-to-end.
12/12 GPU fixtures green. 33 lib unit tests + 3 Ring 2 fuzz tests
still green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
344d7a67d7 |
feat(ml-backtesting): wire --latency-ns end-to-end + fixture (C12)
The in-flight machinery from C11 is now reachable from the harness +
CLI. New step_decision_with_latency on LobSimCuda:
latency_ns == 0 → existing immediate-match path (submit_market_immediate
kernel fills against current book).
latency_ns > 0 → host reads market_targets, converts each non-noop
into seed_limit_order(active=2, price=very-aggressive,
arrival_ts_ns = current + latency_ns). The
resting_orders kernel promotes + fills at arrival
time, so the order sees whatever book exists then —
not the book at decision time.
Aggressive-price heuristic: buy at 1e9 / sell at 0 — the marketability
check (price ≥ best ask for buy, ≤ best bid for sell) unconditionally
crosses at arrival; the kernel then walks book levels for the actual
fill price. This models "market order with latency" correctly because
slippage emerges from the book-walking at arrival, not from a limit
price boundary.
BacktestHarness gains a latency_ns field; harness::run() always calls
step_decision_with_latency now. Also calls sim.step_resting_orders
per event (with trade_signed_vol=0.0 — the Mbp10RawInput trade-flow
hookup is deferred to a trades-feed integration commit) so in-flight
orders get a chance to promote on every snapshot.
bin/fxt-backtest no longer ignores --latency-ns; the flag value
propagates through BacktestHarnessConfig.latency_ns into the kernel
path. Default stays at 100_000_000 (IBKR + Scaleway baseline per
spec §4). Setting --latency-ns 0 selects the legacy immediate path.
Adds latency_in_flight_miss GPU fixture: limit at 5510 submitted
active=2 at T=1s with arrival_ts=T+100ms. step_resting at T+50ms
sees no promotion (still in-flight). Book moves +5 against buyer
during the 100ms window. step_resting at T+150ms promotes the
limit and fills it at the WORSE post-move book (vwap=5505 vs the
original 5500 it would have hit without latency). Slot ends active=0.
11/11 GPU fixtures green on RTX 3050. 33 lib tests still green.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1fb2decb79 |
feat(ml-backtesting): resting orders + stops + OCO + audit ring (C11)
Picks up the deferred work from C5/C7 trim notes. Adds:
cuda/lob_state.cuh — LimitSlot (32 B) + StopSlot (32 B) + Orders
(limits[32] + stops[16] = 1536 B/backtest). u64-aligned with
explicit _pad[6] on both slot structs so the Rust mirrors
(LimitSlotFlat / StopSlotFlat) bytemuck::Pod-derive without
panics about implicit padding.
cuda/resting_orders.cu — three kernels:
resting_orders_step (runs per event after book_update):
1. In-flight → resting promotion at arrival_ts_ns. Queue position
initialised pessimistically (full level depth ahead) per spec §9.
2. Queue decay against same-side trade_signed_vol at level: ahead-
of-us first then us; partial-fill emits OrderEvent + pos update.
3. Marketability check: book moved through our price → cross at
the just-arrived book (slippage-aware fill walks levels).
IOC cancels remainder; FOK does too (partial-fill not allowed).
4. Stop trigger detection: best ask up for buy-stop, best bid
down for sell-stop. StopMarket walks book; StopLimit converts
to a new LimitSlot (overflow → submission_overflow++).
5. OCO mutual exclusion: oco_pair byte (0..31 = limit, 32..47 =
stop) — when one leg fills/triggers, the paired slot is freed.
seed_limit_slot / seed_stop_slot — host-side single-slot seeders
for fixture testing + as a v1 entry point until the decision
kernel learns to emit resting orders. Both set an overflow_flag
if the target slot is already in use (gen_counter bumped on
successful seed for SlotTag freshness).
src/sim.rs — wires three new methods:
seed_limit_order(b, slot, side, kind, active_state, oco_pair,
price, size, queue_position_init, arrival_ts_ns)
seed_stop_order(b, slot, side, kind, active_state, oco_pair,
trigger_price, limit_price, size, arrival_ts_ns)
step_resting_orders(current_ts_ns, trade_signed_vol)
→ launches resting_orders_step kernel + chains step_pnl_track
so any fill that closes a position emits a TradeRecord.
Plus read_limit_slot / read_stop_slot / read_audit_records for
fixture inspection.
Audit-ring buffers (deferred from C2 originally) now allocated:
audit_d (n × 256 × 24 B) + audit_head_d (n × u32). Both
resting_orders.cu and the decision kernel's WriteOrder path
populate it via the inlined pack_slot_tag + emit_audit helpers.
Four new GPU fixtures:
limit_rest_marketable_fill — resting buy at 5500 with book at
ask 5500 fills immediately on step_resting (3 lots, vwap=5500).
stop_trigger — buy 4 lots @ 5500, seed sell-stop at trigger=5495,
book moves so bid=5494.50: stop triggers, walks book, position
closes flat, TradeRecord emitted, stop slot active=0.
oco_one_cancels_other — pair {buy@5495, sell@5505} with oco_pair
cross-linked: book moves so bid=5505.50, sell leg fills (pos=-2),
buy leg auto-cancelled. Both slots end active=0.
submission_overflow — host loop fills all 32 limit slots, then 33rd
seed_limit_order call must return Err (slot 0 already in use).
All 10 GPU fixtures green on RTX 3050 (6 original + 4 new). Ring 2
fuzz at N ∈ {1, 16, 256} still green. 33 lib unit tests still green.
The decision kernel (C7) and submit_market_immediate (C5) paths
are unchanged — they continue to use the "immediate fill" route.
A follow-up commit can teach the decision kernel to emit resting
limits via submit_limit_alloc (already defined in resting_orders.cu).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
771faac723 |
test(ml-backtesting): Ring 2 invariant fuzz at N ∈ {1, 16, 256} (C10)
Property-based fuzz tests with random-walk MBP-10 sequences applied to
the LOB simulator at three backtest counts; assert per-snapshot
invariants that must hold regardless of input or block scheduling:
fuzz_n1_book_only (200 events, no orders)
Pure book-update kernel — verifies mid-drift random walks preserve
book monotonicity and never produce a crossed book.
fuzz_n16_with_orders (200 events, market orders every 8th event)
16 backtests in parallel, each submitting random buy/sell market
orders 1-3 lots at every 8th event. Asserts book invariants on
each backtest's state PLUS:
- position_lots stays within ±30 (plausible given fixture book depth)
- realized_pnl + vwap_entry finite (no NaN/Inf leaks)
fuzz_n256_with_orders (100 events, market orders)
Production-scale parallelism. Same invariants as N=16. Each block
has its own per-backtest Pos + OpenTradeState + TradeLog, exercising
the per-block isolation discipline established in C5-C7.
All 3 pass on RTX 3050. Spec §8 Ring 2 confidence gate hit.
Adds rand + rand_chacha to ml-backtesting dev-dependencies.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
a4127935a8 |
feat(ml-backtesting): BacktestHarness orchestrator + Ring 1b parity (C8)
BacktestHarness::new(cfg, &dev, trunk) constructs a MultiHorizonLoader
(inference_only=true), captures the trunk's perception Graph A using
loader.peek_first() as the template, then allocates a LobSimCuda.
run() walks the chronological snapshot stream, calls apply_snapshot on
every event, and at decision-stride boundaries:
trunk.update_input_buffers(raw)
→ trunk.perception_forward_captured() → (probs[N_HORIZONS], proj)
→ sim.broadcast_alpha(&probs)
→ sim.step_decision(ts, target_vol, ann_factor, max_lots)
Returns RunStats { events_processed, decisions_taken }. The harness
deliberately accepts an externally-built CfcTrunk (random-init in v1)
because a checkpoint format isn't pinned in ml-alpha yet; when one
lands, the binary CLI (C9) can switch from new_random to load_checkpoint.
trainer_parity.rs Ring 1b — two ignored tests:
peek_first_byte_equal_across_modes
Verifies the Mbp10RawInput produced by the loader path used by the
backtest harness (inference_only=true) is BYTE-EQUAL to what the
trainer's loader (inference_only=false) produces from the same
source. Guards against any future refactor accidentally diverging
the two paths (e.g. someone special-casing inference path to skip
regime feature computation). All 50 f32 fields + scalars compared
via .to_bits() equality.
inference_iteration_matches_chronological_snapshots
Verifies next_inference_input() yields monotone-ts snapshots with
correct cur==prev semantics on the first read and prev_ts==prior_ts
afterwards.
Both tests skip gracefully when FOXHUNT_TEST_DATA fixtures lack
populated sidecars (the placeholder-empty bins committed in the
test_data/ tree).
GPU-side inference parity (probs[N_HORIZONS] bit-equal across loader
modes) is deferred until ml-alpha pins a checkpoint format and we have
a small checkpoint fixture to gate it on FOXHUNT_TEST_CKPT.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
25f43a4268 |
feat(ml-backtesting): decision_policy + per-horizon ISV-Kelly (C7)
Two new kernels in cuda/decision_policy.cu:
decision_policy_default — alpha[N_HORIZONS] × per-horizon IsvKellyState
→ market target. Per-horizon Kelly fraction × signal magnitude × ISV-cap
(target_annual_vol / sqrt(realised_return_var × annualisation_factor)),
aggregated via WeightedByRealizedSharpe (weights = max(0, recent_sharpe)
/ Σ; auto-shifts capital toward empirically winning horizons per spec §5
+ §6). No floor — sub-1-lot intents become no-op. Round-to-nearest on
the final lot count to dodge an f32-truncation off-by-one where
0.8(f32) * 0.75 * 5.0 ≈ 2.99999976 → trunc-int 2.
isv_kelly_update_on_close — for every horizon flagged in
closed_horizon_mask[b], updates pnl_ema_{win,loss} via Wiener-α (floor
0.4 per pearl_wiener_alpha_floor_for_nonstationary), win_rate_ema,
Welford-ish realised_return_var, and the recent_sharpe composite.
First-observation bootstrap (per pearl_first_observation_bootstrap):
sentinel n_trades_seen=0 → direct EMA replacement, no zero-bias warmup.
The full bytecode VM from spec §6 is NOT in this commit — the default
policy is hardcoded in the kernel as the path Strategy::default_for()
already produces. Bytecode plumbing in src/policy/mod.rs stays put for
v2 expansion (custom RegimeSwitch / Portfolio compositions).
IsvKellyState struct added to lob_state.cuh (24 bytes per horizon × 5
per backtest); host mirror IsvKellyStateHost from C3 cast-compatible
via bytemuck::Pod. LobSimCuda gains broadcast_alpha + step_decision +
read_isv_kelly + write_isv_kelly (warm-start). step_decision chains:
decision_policy → merge_open_mask → submit_market_immediate
→ pnl_track_step → host close-detect → isv_kelly_update_on_close.
PRE-submit pos/pnl/mask snapshots feed the host close-detection;
captured via three small DtoH copies (cold path, 24 bytes × n_backtests).
decision_alpha_buy_close fixture: warm-start h4 with positive Kelly
state (n=50, recent_sharpe=1.0), broadcast alpha[4]=0.9 → buy 3 lots
@ ask top 5500.00. Snapshot moves to bid 5505.00, broadcast alpha[4]=0.1
→ sell 3 lots → close at +15 P&L. Verify ISV-Kelly h4: n_trades 50→51
exactly, others unchanged. PASS — all 6 Ring 1 fixtures green on RTX 3050.
Out-of-scope for this commit (defer to follow-ups, per plan trim notes):
- stop_trigger / oco_one_cancels_other / submission_overflow fixtures
(need resting-order LimitSlot[32] machinery deferred from C5)
- Bytecode VM dispatch (RegimeSwitch / Portfolio compositions)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1b679b5e40 |
feat(ml-backtesting): pnl_track kernel + segment_complete TradeRecord emission
pnl_track_step runs after each matching pass, compares per-block
position-state-now against persisted OpenTradeState (24 B scratch)
and either:
- records entry context (entry_ts_ns, entry_px_x100, entry_size,
realised_at_open) on open transition (prev==0, now!=0); or
- emits a 40-byte TradeRecord into the per-block trade-log buffer
on close transition (prev!=0, now==0), reconstructing implied
exit_px from the realized P&L delta and converting to USD ×100
fixed-point ($50/index-point × 100 = ×5000 multiplier).
Multi-fill averaging (scale-in then partial close) deferred to v2 —
v1 covers the clean open→close case the spec calls out as primary.
LobSimCuda owns three new buffers: open_trade_state_d (n × 24),
trade_log_d (n × TRADE_LOG_CAP × 40), trade_log_head_d (n × u32).
submit_market now takes current_ts_ns and chains pnl_track_step
internally; step_pnl_track() exposed for caller-driven orchestration.
read_trade_records(backtest_idx) drains the per-block ring as
Vec<TradeRecord>; LSP-pinned 40-byte Pod struct from C2 lines up
1:1 with the kernel's hand-rolled byte writes.
pnl_accounting_buy_close fixture: buy 4 lots @ ask top (5500.00),
book moves +5 to bid top 5505.00, sell 4 to close. Expected
realized_pnl = (5505 − 5500.00) × 4 = $20 in price-units, which is
$20 × $50/contract × 100 = 100000 USD ×100 fixed-point. PASS within
$1 fixed-point tolerance.
All 5 Ring 1 fixtures green on RTX 3050.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
78f0618294 |
feat(ml-backtesting): order_match immediate market-fill kernel + Pos accounting
submit_market_immediate walks the ask/bid book for a buy/sell market
order, computes notional cost + filled lots across up to 10 levels,
and updates per-backtest Pos { position_lots, vwap_entry, realized_pnl }
with correct VWAP scale-in + counter-direction realized-P&L accounting.
Single-writer (thread 0) per block — no atomics per
feedback_no_atomicadd.md. Each backtest gets its own target slot
{ side, size } in the device-global targets array (side=2 = no-op).
Pos struct added to lob_state.cuh (24 bytes); host mirror PosFlat in
src/lob/mod.rs is repr(C) bytemuck::Pod for direct host↔device cast.
LobSimCuda gains submit_market(backtest_idx, side, size) +
read_pos(backtest_idx) -> PosFlat. Fixture harness extended with the
SubmitMarket event variant + ExpectedPos check (vwap_tol + realized_pnl_tol
for FP tolerance).
market_order_consumes_top fixture verifies a buy of 5 lots into
ask[3@5500.00, 10@5500.25] fills 3+2 → position_lots=5,
vwap_entry=5500.10 within 1e-3 tolerance. All 4 Ring 1 fixtures
(book_update × 3 + market_order × 1) green on RTX 3050.
Scope deliberately trimmed from plan C5: this commit lands market-fill
matching only. Queue decay on resting limits, in-flight latency
promotion, stop-trigger detection, and OCO leg-cancellation will land
in follow-up commits — each in its own kernel addition. This keeps the
incremental delta reviewable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
33636d250b |
feat(ml-backtesting): build.rs + book_update CUDA kernel + sim skeleton
build.rs compiles every .cu under crates/ml-backtesting/cuda/ to \$OUT_DIR/<name>.cubin via nvcc (no nvrtc per feedback_no_nvrtc.md); pairs every env::var with rerun-if-env-changed per pearl_build_rs_rerun_if_env_changed.md. Default arch sm_86 (RTX 3050 local); production sets FOXHUNT_CUDA_ARCH=sm_89 (L40S) or sm_90 (H100). First kernel: book_update_apply_snapshot — block-per-backtest replace of the 10-level book from a broadcast MBP-10 snapshot. Single-writer per level (thread tid = level idx), no atomics per feedback_no_atomicadd.md. lob_state.cuh defines the canonical per-block shared-mem layout that all subsequent kernels share (Book struct in this commit; Orders, Pos, IsvKellyState[5] added in C5-C7). LobSimCuda host struct (in src/sim.rs) constructed from an &MlDevice; owns per-backtest device book state + mapped-pinned input snapshots. apply_snapshot launches the kernel and synchronises; read_books drains device state for tests. Three Ring 1 fixture tests (book_update_replace, _two_step, _n_backtests=4) — N≤4 bit-exact GPU oracle assertions loaded from JSON. All three pass on RTX 3050 locally. Verifies the build-script → cubin → cudarc.load_cubin → kernel launch → DtoH read pipeline end-to-end. cudarc added as direct path dep (../../vendor/cudarc) since it's not in [workspace.dependencies] — same vendored fork ml-alpha uses. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |