Files
foxhunt/crates/ml-backtesting/tests
jgrusewski 1e656948b3 arch(crt-1): composite exit_signal safety circuit-breaker (C1.4)
Per spec §4.3 + plan C1.4. Safety backup for the primary exit path
(target → 0 from collapsed multi-horizon conviction, C1.2). Catches
the edge case where conviction stays bounded above zero but the trade
is deep in unrealized loss, or short/long horizons disagree for many
events while threshold-gate logic still permits sizing.

Three terms, max-of-three composite:
  degradation_term  = (1 − conv_ema_now / conviction_at_entry) / 2.0
  disagreement_term = disagree_consecutive_events / 32.0
  pnl_decay_term    = max(−pnl_adjusted_conv_ema, 0) / 1.0
  exit_signal       = max(deg, dis, pnl_decay)

Fires force-flat (market_targets = (3, 0)) when exit_signal >= 1.0.

State-update flow:
  - pnl_track open branch: write conviction_at_entry (offset 20),
    conviction_ema (offset 24, initialised to entry value),
    pnl_adjusted_conviction_ema (offset 44, = 0),
    peak_unrealized_pnl (offset 48, = 0). Takes new conviction_ema_per_b
    read-only arg to snapshot at open.
  - decision kernel intra-event (when pos open): update offsets
    24/44/48/56 (disagreement counter) via update_open_trade_trajectory.
  - composite_exit_check device helper: reads offsets 20/24/44/56,
    writes (3, 0) on fire. Sibling of stop_check_isv with same return
    semantics; called after trajectory update so all four reads see
    the freshly-written values.

Fields not read in CRT.1 (offsets 28-43 per-horizon EMA, 52 degradation
counter, 60 horizon_idx_dominant) are NOT written by the open branch —
alloc_zeros covers init. Per feedback_no_hiding: don't populate unused
fields. Also fixes a latent C1.1 bug: pnl_track close branch read
horizon_idx from offset 20 (now conviction_at_entry, f32); moved the
read to offset 60 per the documented layout.

Threshold-bootstrap defaults (will be ISV-derived in CRT.2 alongside
the rest of the controller anchors per pearl_controller_anchors_isv_driven):
  degradation_factor  = 2.0  (50% conviction decay vs entry → ≤0.5 term)
  disagreement_factor = 32.0 (32 events of short/long disagreement)
  pnl_decay_factor    = 1.0  (pnl_adj_conv structurally ∈ [-1, +1])

Under these defaults disagreement_term is the only term that can fire
on its own — degradation_term caps at 0.5 (conv_ema/conviction_at_entry
≥ 0) and pnl_decay_term caps at 1.0 in the limit. The three terms still
add evidence in superposition; CRT.2 will retune factors against ISV
percentiles. Unit-test coverage validates the disagreement path
(composite_exit_signal_fires_on_disagreement).

Per pearl_no_host_branches_in_captured_graph — all composite logic
lives inside the decision kernel; no host roundtrip in the per-event
hot path. Per feedback_no_htod_htoh_only_mapped_pinned — no new
memcpy or stream.synchronize() in step_pnl_track or
dispatch_latent_market_orders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 21:15:40 +02:00
..