From 8ba8755b481cf0a08a81ebd5a6326b9d789abf5e Mon Sep 17 00:00:00 2001 From: jgrusewski Date: Wed, 20 May 2026 17:24:03 +0200 Subject: [PATCH] =?UTF-8?q?spec(crt):=20v2=20amendments=20from=20critical?= =?UTF-8?q?=20review=20=E2=80=94=2012=20issues=20+=20greenfields?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Self-critical review of v1 (commit 0f3484325) found 12 issues. All fixed inline. Spec marked Status: Design v2 with explicit changelog. LOAD-BEARING FIXES 1. §4.4 conviction cold-start: removed backwards "uniform fallback" branch that maintained trading authority when net_edge=0 across all horizons (exactly when model has no edge). Replaced with ε floor on net_edge_h. Single formula, smooth bootstrap, no regime switch. 3. §4.3 exit logic: 5 OR conditions → 1 composite exit_signal scalar (max of degradation, disagreement, pnl_decay terms). SL/trail and 8h circuit-breaker stay orthogonal as safety, not exit policy. Attribution per term recorded in §7 state for observability. 6. §9 Gate 2: PF > 1.0 (2.5× improvement, unrealistic) → tiered MUST (no-regression) + WIN (real lift, four candidate criteria) + explicit STOP condition if MUST passes without WIN. 7. §9.1.1: alpha-realization metric defined explicitly as realized_pnl / max(unrealized during signal-validity window). Was hand-waved in v1; now formulated. TRACTABLE INLINE FIXES 2. §7 open_trade_state: 128 → 64 bytes. Diagnostic-only fields (scale_up/down counts, max/min target reached) moved to a separate audit buffer (single source of truth keeps state lean). 4. §5.1 envelope: multiplicative formula → additive bounded contributions. tanh(sharpe), min(dd/cap), clamp(vol). No single factor can crash envelope to zero. Floor (envelope_floor lots) and hard-cap (envelope_hard_cap) remain. 5. §5.2 threshold: hand-waved "self-tune toward PF" → explicit 3-arm bandit (lower, current, higher percentile) every K_eval closed trades, Wiener-α EMA target with floor, bounded [0.5, 0.95]. 8. §3.4 + §3.6: Layer A wall-time ≤ 5× → ≤ 2× (controller is light, 5× would indicate structural bug, not config tuning). 9. §4.2: explicit Wiener-α formula for conviction EMA with floor at 0.4 per pearl_wiener_alpha_floor_for_nonstationary. 10. §6.2.1: shadow eval defined explicitly with backtest-mode (last 15% of fold) and live-mode (parallel-prediction-only) variants. Promotion criteria spelled out (PF, Sharpe, tail loss). 11. §6.2: Layer D loss explicit (primary = value regression on realized PnL conditional on state; secondary = EWC++ regularizer toward base). Was three losses with no specified combination. 12. §10.5: adapter regime over-fit risk added with sample-count down-weighting, cross-regime shadow eval, periodic adapter reset as hard backstop. GREENFIELDS (per user direction) - §3.3: decision_stride field DELETED from YAML + Rust (not deprecated). No backwards-compat shim. Per feedback_no_legacy_aliases. - §7.1: open_trade_state 24→64 byte migration is atomic, no old layout shim. - §8: boundary matrix decision_stride row marked REMOVED, not DEPRECATED. §13 pearl conformance: per-section pearl application (not bulk citation). Added pearl_audit_unboundedness_for_implicit_asymmetry, pearl_no_deferrals_for_complementary_fixes acknowledgement (layers are sequenced amplifications, not parallel fixes), and the new pearl_stop_checks_run_at_deadline_cadence from S2. Status: Design v2 — awaiting user review before writing-plans. Co-Authored-By: Claude Opus 4.7 (1M context) --- ...5-20-continuous-reasoning-trader-design.md | 430 ++++++++++++------ 1 file changed, 302 insertions(+), 128 deletions(-) diff --git a/docs/superpowers/specs/2026-05-20-continuous-reasoning-trader-design.md b/docs/superpowers/specs/2026-05-20-continuous-reasoning-trader-design.md index 5c819dd4e..4aa0eeaa6 100644 --- a/docs/superpowers/specs/2026-05-20-continuous-reasoning-trader-design.md +++ b/docs/superpowers/specs/2026-05-20-continuous-reasoning-trader-design.md @@ -2,9 +2,24 @@ **Date:** 2026-05-20 **Branch context:** `ml-alpha-phase-a` (S2 closed at `8828e8ab1`) -**Status:** Design — pending user review before implementation plan +**Status:** Design v2 (amended from v1 after self-critical review — 12 issues addressed inline) **Supersedes scope of:** `2026-05-19-isv-driven-stop-controller-design.md` (that spec ships clean stops; this spec replaces the rule-based policy that consumes those stops) +**v2 changelog** (vs v1 commit `0f3484325`): +1. §4.4 conviction: removed backwards cold-start fallback; replaced with ε floor on net_edge for smooth bootstrap. No regime switch when model has zero edge. +2. §7 open_trade_state: 128 → 64 bytes. Observability fields moved to a separate audit buffer (single source of truth keeps state minimal). +3. §4.3 exit logic: 5 OR conditions → 1 composite exit_signal scalar with attribution. Safety (SL/trail/circuit-breaker) orthogonal. +4. §5.1 envelope: multiplicative → additive bounded contributions. No single factor can crash envelope to zero. +5. §5.2 threshold: hand-waved "learn toward PF" → explicit 3-arm bandit with Wiener-α target EMA, bounded [0.5, 0.95]. +6. §9 Gate 2: PF > 1.0 (unrealistic) → tiered MUST/WIN criteria with explicit STOP condition. +7. §9.1.1: defined alpha-realization metric explicitly (was pointed-at, now formulated). +8. §3.4 + §3.6: Layer A wall-time target ≤ 5× → ≤ 2× (controller is light, 5× would mean structural bug). +9. §4.2: explicit Wiener-α formula for conviction EMA (was hand-waved). +10. §6.2.1: shadow eval defined with explicit modes (backtest / live) and metrics. +11. §6.2: Layer D loss explicit (primary = value regression; secondary = EWC++ regularizer). +12. §10.5 added: adapter regime over-fit risk with sample-count down-weighting + cross-regime shadow eval. +13. Greenfields per user direction: removed all backwards-compat shims (decision_stride field deleted, not deprecated; no 24-byte open_trade_state compat). + --- ## 1. Motivation @@ -65,15 +80,18 @@ Sequencing rationale: A unlocks event-rate consumption needed by B. B is where a Controller runs every event. Trunk forward pass (already every event) feeds the latest alpha probabilities directly into the controller every event. `seed_inflight`'s target-delta semantics absorb continuous target changes by seeding incremental orders. -### 3.3 Files touched +### 3.3 Files touched (greenfields — no backwards-compat) -- `crates/ml-backtesting/src/harness.rs`: remove `if event_idx % decision_stride == 0` gate around `step_decision`. +- `crates/ml-backtesting/src/harness.rs`: remove `if event_idx % decision_stride == 0` gate around `step_decision`. Controller fires every event unconditionally. - `crates/ml-backtesting/src/sim/mod.rs`: `step_decision` invoked every event from harness; internal kernel batching unchanged. -- `bin/fxt-backtest/src/main.rs` + `config/ml/*.yaml`: keep `decision_stride` field for backwards-compat but ignore it (deprecation log line on first use). +- `bin/fxt-backtest/src/main.rs`: REMOVE `decision_stride` field from `SweepBase` and `ResolvedSimVariant`. No deprecation shim, no compat-warning, no field-kept-for-backwards-compat. Per [feedback_no_legacy_aliases](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_legacy_aliases.md). +- `config/ml/*.yaml`: REMOVE `decision_stride` line from every YAML in one sweep. Per [feedback_no_partial_refactor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_partial_refactor.md) — every caller migrates atomically. ### 3.4 Cost target -Per-event controller cost ≤ 10× current per-event cost. Sim throughput target ≥ 5000 events/sec (current ~21000 events/sec at stride=200 with controller every 200th event; expect ~5000-10000 events/sec at continuous controller). +The controller kernels (`decision_policy_*` and `seed_inflight_limits_batched`) are LIGHT: block-per-backtest, single-writer thread. 200× more invocations of light kernels should produce ≤ 2× total wall-time slowdown. If the smoke takes > 2× current (~95 s → 190 s+), something is structurally wrong with the controller cost model — pause and profile, do not paper over. + +**Target:** sim throughput ≥ 10000 events/sec (vs current ~21000). Wall-time ≤ 2× current. ### 3.5 Failure mode addressed @@ -81,10 +99,16 @@ Decisions land on stale signal: at decision_stride=200 events on ES Q1 data (~3. ### 3.6 Layer A acceptance criteria (Gate 1) -- Smoke completes with continuous controller in ≤ 30s wall-time vs ~95s current (or accept ≤ 5× slowdown). -- No regression in n_trades vs Layer-A-off (continuous control should produce ≥ as many trades). -- Trade-level entry timestamps cluster tighter to signal-spike timestamps than the stride=200 baseline (measurable via signal-vs-entry-ts cross-correlation). -- `total_pnl_usd` and `sharpe_ann` within ±15% of stride=200 baseline (Layer A alone is not the alpha — its job is to unlock Layer B). +- Smoke wall-time ≤ 2× stride=200 baseline (~95 s → ≤ 190 s). +- Trade-level entry timestamps cluster tighter to signal-spike timestamps than the stride=200 baseline (measurable via signal-vs-entry-ts cross-correlation; ≥ 30% reduction in mean signal-to-entry lag). +- `total_pnl_usd` and `sharpe_ann` within ±15% of stride=200 baseline (Layer A alone is not the alpha — its job is to unlock Layer B). Substantial regression (worse than −15%) blocks Gate 1. +- No new NaN/sentinel counters fire. + +### 3.7 Layer A risk: high-frequency target oscillation + +At event-rate, consecutive decisions see nearly-identical state. If the state→target map is sensitive, you get hyperactive oscillation (target +1, 0, +1, 0, ...) each costing the spread. + +**Mitigation:** target sizing in §4.2 uses a **Wiener-α adaptive EMA with floor** — fast in regime changes, slow in steady state. The Wiener formula `α = diff_var / (diff_var + sample_var + ε)` is MSE-optimal for stationary signals; floor at 0.4 in this non-stationary control loop per [pearl_wiener_alpha_floor_for_nonstationary](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_alpha_floor_for_nonstationary.md). Floor prevents adaptive α from collapsing during quiet periods, which would slow genuine reversals. --- @@ -122,34 +146,65 @@ Concretely the policy logic becomes: `seed_inflight_limits_batched` already uses target-delta semantics (`crates/ml-backtesting/cuda/resting_orders.cu` line 652-672). Target changes any event → delta computed → incremental order seeded. -Sizing formula: +Sizing formula (with explicit Wiener-α adaptive EMA): + ``` -conviction = isv_weighted_agreement(alpha_probs, isv_state, mask) // see 4.4 -conviction_ema = EMA(conviction, α=isv_derived_alpha) // smoothing -target_lots = round(conviction_ema × envelope_max) +conviction = isv_weighted_agreement(alpha_probs, isv_state, mask) // see §4.4 + +// Wiener-α adaptive EMA on conviction. diff_var tracks event-to-event +// changes in conviction; sample_var tracks variance of conviction itself. +// α automatically high when signal is genuinely changing, low when stable. +diff_var = EMA((conviction_now − conviction_prev)², α=0.1) +sample_var = EMA((conviction_now − conviction_ema_prev)², α=0.1) +alpha_raw = diff_var / (diff_var + sample_var + ε) +alpha_active = max(alpha_raw, 0.4) // floor for non-stationarity + +conviction_ema = alpha_active × conviction + (1 − alpha_active) × conviction_ema_prev + +// Target sizing — direction from conviction_signed, magnitude from |conviction_ema|. +target_lots_signed = round(direction × |conviction_ema| × envelope_max) // envelope_max from §5.1 ``` -EMA prevents micro-oscillation churn. α derived per [pearl_wiener_optimal_adaptive_alpha](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_optimal_adaptive_alpha.md) with floor per [pearl_wiener_alpha_floor_for_nonstationary](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_alpha_floor_for_nonstationary.md). +EMA prevents micro-oscillation churn. α floor at 0.4 per [pearl_wiener_alpha_floor_for_nonstationary](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_alpha_floor_for_nonstationary.md) — adaptive α alone collapses in quiet periods, slowing genuine reversals; the floor keeps minimum responsiveness. Per [pearl_wiener_optimal_adaptive_alpha](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_optimal_adaptive_alpha.md). ### 4.3 Conviction-degradation exit -Replaces `max_hold_ns` as the dominant exit mechanism. `max_hold_ns` becomes a high-default circuit-breaker (e.g., 8 hours) for safety only. +Replaces `max_hold_ns` as the dominant exit mechanism. `max_hold_ns` becomes a high-default circuit-breaker (8 hours) for safety only. -**Per-trade conviction trajectory** stored in expanded `open_trade_state` (see §6). Tracks: -- conviction at entry -- conviction EMA over the trade lifetime -- multi-horizon agreement at each decision -- pnl-adjusted conviction = conviction × sign(unrealized_pnl) +**Single composite exit signal.** Five OR-ed conditions are 5 fire modes, 5 attribution headaches, and 5 bug surfaces (we just spent the day finding three independent bugs in `max_hold` alone). Instead, ONE scalar that crosses ONE threshold. -**Exit fires when ANY of:** +``` +// All three terms are non-negative scalars in [0, ∞). Exit fires when ANY +// term exceeds 1.0 — but the COMPOSITION is one max-of-three, so attribution +// is by which term dominated at exit time (recorded in trade observability). -1. **Net conviction degradation:** `conviction_ema_now < conviction_at_entry × degradation_factor` for K consecutive events. `degradation_factor` and K are ISV-derived (not hardcoded). -2. **Multi-horizon disagreement:** h100 or h500 reversed sign relative to h6000 for ≥ K_short events. -3. **pnl-adjusted conviction crosses zero:** signal says exit AND market agrees. -4. **Hard stop:** SL/trail still active per current `stop_check_isv` logic (Layer B doesn't remove them — they're orthogonal safety). -5. **Circuit-breaker:** `(current_ts − entry_ts) ≥ max_hold_circuit_breaker_ns` where the circuit-breaker default is 8 hours, ~480× the current 60s cap. +// Term 1: conviction has degraded relative to entry. Ratio < 1 means conviction +// is weakening; degradation_factor (ISV-derived) sets how much weakening triggers exit. +degradation_term = (1.0 − conviction_ema / max(conviction_at_entry, ε)) / degradation_factor -This makes time-in-trade emergent. Winners hold as long as conviction holds. Losers cut when conviction degrades, not when a clock fires. +// Term 2: short-horizon disagreement with long-horizon thesis. Counts +// consecutive events where h100 or h500 reversed sign relative to h6000. +// disagreement_factor (ISV-derived) sets tolerance. +disagreement_term = short_horizon_disagreement_events / disagreement_factor + +// Term 3: pnl-adjusted conviction deeply negative. Signal AND market both saying +// "this trade is wrong." pnl_decay_factor (ISV-derived) sets tolerance. +pnl_decay_term = max(−pnl_adjusted_conviction_ema, 0) / pnl_decay_factor + +exit_signal = max(degradation_term, disagreement_term, pnl_decay_term) +exit_fires = (exit_signal ≥ 1.0) + +// Attribution recorded for observability (see §7 open_trade_state). +exit_cause = argmax({degradation_term, disagreement_term, pnl_decay_term}) +``` + +**ISV-derived factors:** `degradation_factor`, `disagreement_factor`, `pnl_decay_factor` are anchored from ISV state per [pearl_controller_anchors_isv_driven](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_controller_anchors_isv_driven.md). Not YAML constants. + +**Safety layer (orthogonal, NOT exit policy):** +- SL/trail from existing `stop_check_isv` — runs at policy rate as before, fires for outsized adverse moves the conviction signal might not catch fast enough. +- Circuit-breaker `(current_ts − entry_ts) ≥ max_hold_circuit_breaker_ns`, default 8 hours — catches pathological infinite holds. + +Time-in-trade is now emergent. Winners hold as long as conviction holds. Losers cut when the composite exit signal crosses threshold. ### 4.4 Conviction definition (ISV-weighted multi-horizon agreement) @@ -161,45 +216,49 @@ For each horizon h in {h100, h500, h1500, h6000}: direction_h = sign(alpha_h − 0.5) magnitude_h = |alpha_h − 0.5| × 2 // ∈ [0, 1] - # ISV-derived weight per horizon. Rewards horizons with positive - # net edge (win EMA exceeds loss EMA) AND low realised-return - # variance (high SNR). Floor at 0 prevents negative-edge horizons - # from dragging conviction; cost^2 in the denominator is the - # bootstrap sentinel per pearl_trade_level_vol_for_stop_distance. - net_edge_h = max(isv[h].pnl_ema_win − isv[h].pnl_ema_loss, 0) + // ISV-derived weight per horizon. Rewards horizons with positive net + // edge (win EMA exceeds loss EMA) AND low realised-return variance + // (high SNR). The floor ε is small-positive (NOT zero) so that: + // - horizons with no positive edge contribute negligibly (weight ≈ ε / var) + // - the formula never branches to a "fallback" mode that maintains + // trading authority precisely when the model has no edge. + // cost^2 in the denominator is the bootstrap sentinel per + // pearl_trade_level_vol_for_stop_distance. + net_edge_h = max(isv[h].pnl_ema_win − isv[h].pnl_ema_loss, ε) weight_h = net_edge_h / (isv[h].realised_return_var + cost^2) weighted_mag_h = magnitude_h × weight_h × direction_h // signed -# Normalise by total weight. If all horizons have zero net edge -# (cold start), fall back to uniform-weighted magnitude per -# pearl_first_observation_bootstrap. -total_weight = sum(|weight_h|) -if total_weight > 0: - conviction_signed = sum(weighted_mag_h) / total_weight // ∈ [-1, +1] -else: - conviction_signed = sum(magnitude_h × direction_h) / 4 // cold-start uniform +// Normalise by total weight. total_weight is ALWAYS > 0 because every +// weight_h is ≥ ε / (var + cost²) > 0 by construction. No cold-start +// branch needed — the floor IS the bootstrap. When all horizons have +// (near-)zero net edge, conviction naturally shrinks toward zero +// (small numerator, normalized denominator), and the §5.2 percentile +// gate suppresses trading. This is the correct behavior: no edge → no trade. +total_weight = sum(|weight_h|) // > 0 by construction +conviction_signed = sum(weighted_mag_h) / total_weight // ∈ [-1, +1] -conviction = |conviction_signed| // ∈ [0, 1] +conviction = |conviction_signed| // ∈ [0, 1] direction = sign(conviction_signed) ``` +**ε choice:** small relative to typical post-bootstrap `net_edge_h`. Suggested `ε = cost / 100` so that pre-bootstrap weights are dominated by `cost / (cost²) = 1/cost` — i.e., the cost-bootstrap variance term dominates exactly when ISV variance hasn't bootstrapped yet, and ISV variance dominates after bootstrap. The transition is smooth, not branched. + Notes: - Weights are ISV-derived ([pearl_controller_anchors_isv_driven](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_controller_anchors_isv_driven.md)). Per-horizon SNR drives how much that horizon contributes. -- Net edge follows the asymmetric structural bound — losses are NOT symmetrically subtracted from gains since negative-edge horizons should drop out, not flip sign ([pearl_audit_unboundedness_for_implicit_asymmetry](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_audit_unboundedness_for_implicit_asymmetry.md)). -- `cost^2` bootstrap follows [pearl_trade_level_vol_for_stop_distance](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_trade_level_vol_for_stop_distance.md): cost as sentinel-floor for variance until real var bootstraps. +- Net edge with positive ε floor follows [pearl_blend_formulas_must_have_permanent_floor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_blend_formulas_must_have_permanent_floor.md): `max(real, floor)`, never branching to a different formula. +- Net edge asymmetric ([pearl_audit_unboundedness_for_implicit_asymmetry](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_audit_unboundedness_for_implicit_asymmetry.md)) — losing-edge horizons should drop out, not flip sign. +- `cost^2` bootstrap per [pearl_trade_level_vol_for_stop_distance](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_trade_level_vol_for_stop_distance.md): cost as sentinel-floor for variance until real var bootstraps. - Normalization makes conviction scale-invariant per [pearl_zscore_normalization_for_magnitude_asymmetric_signals](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_zscore_normalization_for_magnitude_asymmetric_signals.md). - Single unbounded multiplicand: `net_edge_h / (var + cost^2)` is the one unbounded factor; `magnitude_h × direction_h` is bounded in `[-1, +1]`. Per [pearl_one_unbounded_signal_per_reward](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_one_unbounded_signal_per_reward.md). - Disagreeing horizons reduce conviction magnitude even if individually large — opposing signed `weighted_mag_h` terms partially cancel in the sum. -- Cold-start fallback (uniform weighting before ISV bootstraps) ensures the controller is functional from event 1; matches [pearl_first_observation_bootstrap](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_first_observation_bootstrap.md). +- No cold-start branch: ε floor smoothly bootstraps the formula without a regime switch (the regime switch in v1 was the bug — it MAINTAINED trading authority when net edge was zero across all horizons, which is exactly when the model is admitting it has no edge). ### 4.5 Layer B acceptance criteria (Gate 2) -- Hold-time distribution is no longer cap-binding: mean and max NOT clustered at the circuit-breaker. -- `profit_factor > 1.0` OR a defined alpha-realization metric (e.g., realized-vs-theoretical PnL on tagged signal opportunities) shows ≥ 30% lift over Layer A baseline. -- Win rate stable or up (24.2% baseline; target ≥ 25% with PF lift). -- Drawdown not worsened by > 10% vs Layer A baseline. -- Conviction degradation exits account for ≥ 60% of closes (rest = SL/trail/circuit-breaker). Validates the mechanism is dominant. +See §9 Gate 2 for the canonical tiered MUST/WIN structure. Short form: +- **MUST**: hold-time emergent (circuit-breaker fires < 5%), no metric regression (Sharpe ≥ baseline − 0.5, PF ≥ baseline × 0.95, drawdown ≤ baseline × 1.10), conviction-degradation accounts for ≥ 60% of closes, exit attribution coherent, no new NaN counters. +- **WIN (one of)**: Sharpe lift ≥ +0.5; PF lift ≥ 30%; alpha-realization ratio (§9.1.1) lift ≥ +20%; drawdown reduction ≥ 20% at equal PnL. --- @@ -211,28 +270,61 @@ ISV-derived bounds replace YAML constants. Replaces hardcoded `max_lots: 5`. -``` -rolling_sharpe_short = isv_aggregated rolling sharpe over last N closed trades -drawdown_pct = current_dd / peak_equity -regime_vol_inverse = 1 / (regime_atr_ema + cost) +**Additive bounded formula** (v1 multiplicative crashed envelope when any one factor → 0): -envelope_raw = base_lots × rolling_sharpe_short_clamped × (1 − drawdown_pct/dd_cap) × regime_vol_inverse -envelope_max = clamp(envelope_raw, envelope_floor, envelope_hard_cap) +``` +// Each contribution bounded; no factor can crash envelope to zero. +sharpe_term = 0.5 × tanh(rolling_sharpe_short / sharpe_norm) // ∈ [−0.5, +0.5] +dd_term = 0.3 × min(drawdown_pct / dd_cap, 1.0) // ∈ [0, +0.3] +vol_term = 0.2 × clamp(regime_vol / vol_norm, 0, 1) // ∈ [0, +0.2] + +envelope_factor = 1 + sharpe_term − dd_term − vol_term // ∈ [0, 1.5] +envelope_lots = max(envelope_floor, base_lots × envelope_factor) +envelope_max = min(envelope_lots, envelope_hard_cap) ``` -Floor and hard cap follow [pearl_blend_formulas_must_have_permanent_floor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_blend_formulas_must_have_permanent_floor.md): `max(real, floor)`, not blend. Hard cap stays as safety (default 50 lots, configurable). +- `sharpe_norm`, `vol_norm`, `dd_cap` are ISV-derived anchors per [pearl_controller_anchors_isv_driven](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_controller_anchors_isv_driven.md). Not YAML constants. +- `envelope_floor` is a small positive integer (e.g., 1 lot) so we never reach "can't trade" state. [pearl_blend_formulas_must_have_permanent_floor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_blend_formulas_must_have_permanent_floor.md). +- `envelope_hard_cap` is a structural safety (default 50 lots), the only YAML knob in this layer. +- No single term can collapse the envelope. Even at max drawdown + max vol + min Sharpe: `1 − 0.5 − 0.3 − 0.2 = 0`; envelope hits floor, not zero. ### 5.2 `threshold` (conviction-percentile gate) Replaces hardcoded `threshold: 0.0`. Conviction-percentile-based — trade only when current conviction exceeds the recent N-conviction percentile target. ``` -conviction_history = circular buffer of last N convictions (per backtest, on-device) -percentile_target = ISV-derived (start at 0.75, learn toward maximizing realized PF) -threshold = percentile(conviction_history, percentile_target) +conviction_history = circular buffer of last N=256 convictions (per backtest, on-device) +percentile_target_ema = EMA of percentile target with Wiener-α floor +threshold = percentile(conviction_history, percentile_target_ema) ``` -Self-tuning: percentile_target drifts toward whatever value maximizes recent realized PF, bounded by a hard floor (e.g., 0.5 — never trade below median conviction). +**Explicit self-tuning controller** (vs hand-waved "learn toward PF"): + +``` +// Every K_eval = 32 closed trades, compare PF at current percentile vs PF at adjacent percentiles. +// Move toward higher PF, bounded by structural limits. + +every K_eval trades: + pf_at_current = profit_factor(last K_eval trades at current threshold) + pf_at_higher = profit_factor of trades that would have been kept at percentile + Δ + pf_at_lower = profit_factor of trades that would have been kept at percentile − Δ + where Δ = 0.05 + + best_dir = argmax(pf_at_lower, pf_at_current, pf_at_higher) + + // Move target toward best_dir using Wiener-α adaptive EMA per pearl_wiener_optimal_adaptive_alpha + target_proposal = percentile_target_ema + step_size × sign(best_dir) + percentile_target_ema_new = α × target_proposal + (1 − α) × percentile_target_ema_old + where α has floor 0.1 per pearl_wiener_alpha_floor_for_nonstationary + + // Hard structural bounds: + percentile_target_ema = clamp(percentile_target_ema_new, 0.5, 0.95) +``` + +- Bounded by `[0.5, 0.95]` — never trade below median conviction, never demand top 5% only. +- `pf_at_higher` / `pf_at_lower` are counterfactual estimates from the historical conviction distribution; they don't require running parallel simulations. +- Wiener-α floor prevents thrash; the step size is one Δ per evaluation cycle. +- This is a 1-D bandit with three arms (lower, current, higher), evaluated every K_eval trades. ### 5.3 `target_annual_vol_units` @@ -266,18 +358,48 @@ LoRA adapter on frozen base checkpoint. Low-rank delta applied to selected weigh **Offline-batch, per session** — NOT per-trade in-loop. ``` -For each session end (or every N trades): - 1. Collect trajectories: (state_at_decision, action, outcome_realized_pnl) for all closed trades. - 2. Compute outcome-conditional loss: - - Value regression (predicted_value vs realized) - - Direction loss (predicted_direction vs realized_pnl_sign) - - Conviction calibration (conviction vs |realized_pnl|) - 3. EWC++ regularizer toward base checkpoint (prevents catastrophic forgetting). - 4. Gradient step on LoRA adapter only (base frozen). - 5. Shadow eval: run adapter-on vs adapter-off on a held-out validation window. - 6. If shadow eval shows PF degradation > X% vs baseline: revert adapter to checkpoint. +For each session end (or every N=128 closed trades): + 1. Collect trajectories: (state_at_decision, action_taken, outcome_realized_pnl) tuples. + + 2. Primary loss: value regression on realized PnL conditional on state. + L_value = mean((predicted_value(state) − realized_pnl) ²) + Single primary loss avoids multi-objective tuning ambiguity. + + 3. EWC++ regularizer toward base checkpoint weights θ_base. + L_ewc = λ_ewc × sum_i (F_i × (θ_adapter_i − θ_base_i) ²) + F_i = Fisher information per parameter from the base training run. + Prevents catastrophic forgetting; λ_ewc tuned at gate-4 time. + + 4. Total loss: L = L_value + L_ewc + + 5. Gradient step on LoRA adapter parameters only (base θ_base frozen). + Small LR (e.g., 1e-5) — adapter changes per session bounded. + + 6. Shadow eval (see §6.2.1). + + 7. Promotion gate: if shadow eval passes → adapter checkpoint promoted to live. + Otherwise → adapter reverted to last-promoted checkpoint. ``` +### 6.2.1 Shadow eval — explicit definition + +Shadow eval validates the new adapter on real data BEFORE promoting it to production trading. Three modes depending on operational context: + +**Mode A (backtest / walk-forward):** hold out the last 15% of each fold as a shadow window. Apply candidate adapter to predictions on the shadow window, measure PF, Sharpe, drawdown. Compare adapter-on vs base-only. + +**Mode B (live trading):** parallel-prediction-only execution. Candidate adapter produces shadow predictions on real-time data, but **does NOT route trades** — predictions are logged and compared against realized outcomes. After N_shadow = 64 closed trades' worth of shadow signal, compare shadow-predicted PnL vs realized PnL. + +**Promotion criteria** (both modes): +- Shadow PF ≥ 0.95 × baseline PF (no significant degradation) +- Shadow Sharpe ≥ baseline Sharpe − 0.3 (allow small noise) +- No new tail-loss events > 2× baseline tail loss + +**Demotion**: failed shadow eval → automatic revert to last-promoted adapter checkpoint. No human intervention. + +### 6.2.2 Circuit-breaker + +Independent of shadow eval. Continuously monitors rolling Sharpe over last 32 trades. If rolling_sharpe degrades by > 25% vs the rolling_sharpe at time-of-last-promotion, revert adapter regardless of shadow eval. Circuit-breaker is the safety net for real-time degradation that shadow eval missed. + ### 6.3 References - [feedback_safla_neural](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/) — SAFLA pattern for memory-persistent learning @@ -297,46 +419,44 @@ For each session end (or every N trades): Current: 24 bytes per backtest (entry_ts:8, entry_px_x100:4, entry_size:4, realized_at_open:4, horizon_idx:1, pad:3). -Target: **128 bytes per backtest** (single source of truth per trade per [feedback_single_source_of_truth_no_duplicates](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_single_source_of_truth_no_duplicates.md)). +Target: **64 bytes per backtest** (single source of truth per trade per [feedback_single_source_of_truth_no_duplicates](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_single_source_of_truth_no_duplicates.md)). State only — pure observability fields (scale-up counts, etc.) go to a separate per-trade audit buffer, not the per-event state slot. Proposed layout: ``` -Offset Size Field -───────────────────────────────────────────────────────────────────── - 0 8 entry_ts_ns (u64) - 8 4 entry_px_x100 (i32) - 12 4 entry_size (i32) - 16 4 realized_at_open (f32) - 20 1 horizon_idx_dominant_at_entry (u8) - 21 3 pad - 24 4 conviction_at_entry (f32) - 28 4 conviction_ema (f32) - 32 16 conviction_per_horizon_at_entry [4 × f32] (h100, h500, h1500, h6000) - 48 16 conviction_per_horizon_ema [4 × f32] - 64 4 pnl_adjusted_conviction_ema (f32) - 68 4 degradation_consecutive_events (u32) - 72 4 short_disagreement_consecutive_events (u32) - 76 4 peak_unrealized_pnl (f32) - 80 4 trough_unrealized_pnl (f32) - 84 4 scale_up_events_count (u32) - 88 4 scale_down_events_count (u32) - 92 4 max_target_lots_reached (i32) - 96 4 min_target_lots_reached (i32) -100 28 reserved for future trajectory features +Offset Size Field Purpose +──────────────────────────────────────────────────────────────────────────────── + 0 8 entry_ts_ns (u64) timing + 8 4 entry_px_x100 (i32) close attribution + 12 4 entry_size (i32) close attribution + 16 4 realized_at_open (f32) segment-PnL delta + 20 4 conviction_at_entry (f32) degradation_term denominator + 24 4 conviction_ema (f32) §4.2 sizing input + 28 16 conviction_per_horizon_ema [4 × f32] (h100, h500, h1500, h6000) + §4.3 disagreement_term input + 44 4 pnl_adjusted_conviction_ema (f32) §4.3 pnl_decay_term input + 48 4 peak_unrealized_pnl (f32) trail invariant + 52 4 degradation_consecutive_events (u32) §4.3 attribution + 56 4 disagreement_consecutive_events (u32) §4.3 attribution + 60 1 horizon_idx_dominant_at_entry (u8) close attribution + 61 3 pad align +──────────────────────────────────────────────────────────────────────────────── + 64 TOTAL — fits in one cache line ``` -128 bytes total. Aligns to cache line. +**Diagnostic-only fields go elsewhere.** `scale_up_events_count`, `scale_down_events_count`, `max/min_target_lots_reached` — useful for post-trade analysis but not needed for next-decision logic. Per [feedback_no_hiding](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_hiding.md), if these are wanted, route to a separate `trade_audit_log` ring buffer in observability infrastructure. Mixing state and observability bloats the hot-path read/write. -### 7.1 Migration +### 7.1 Migration (greenfields atomic) -Every reader and writer of `open_trade_state` migrates atomically per [feedback_no_partial_refactor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_partial_refactor.md). Sites to update: +Every reader and writer of `open_trade_state` migrates atomically per [feedback_no_partial_refactor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_partial_refactor.md). Sites to update in one commit: -- `crates/ml-backtesting/cuda/pnl_track.cu` — `OPEN_TRADE_STATE_BYTES` constant from 24 to 128; all `st + offset` reads/writes updated. -- `crates/ml-backtesting/cuda/decision_policy.cu` — `stop_check_isv` reads entry_ts via `+ 24` indexing; update. +- `crates/ml-backtesting/cuda/pnl_track.cu` — `OPEN_TRADE_STATE_BYTES` constant from 24 to 64; all `st + offset` reads/writes updated; reset-on-close zeros all 64 bytes. +- `crates/ml-backtesting/cuda/decision_policy.cu` — `stop_check_isv` reads entry_ts via `+ 24` indexing; update for new layout. - `crates/ml-backtesting/cuda/resting_orders.cu` — S2.2 max_hold event-rate check also reads via `+ 24`; update. -- `crates/ml-backtesting/src/sim/mod.rs` — allocation site, `read_open_trade_state` helper. -- Any tests asserting layout. +- `crates/ml-backtesting/src/sim/mod.rs` — allocation site, accessors. +- Tests: any test asserting the layout updates in the same commit. + +No backwards-compat shim for the old 24-byte layout. Greenfields. --- @@ -349,7 +469,7 @@ Every reader and writer of `open_trade_state` migrates atomically per [feedback_ | `annualisation_factor` | **HARDCODED** | Definitional constant. | | `min/max_reasonable_px` | **HARDCODED** (config, per-instrument) | Instrument bounds. | | `max_hold_ns` | **CIRCUIT-BREAKER** (default 8h) | Safety net only. Behavior emerges from §4.3. | -| `decision_stride` | **DEPRECATED** | Layer A makes obsolete. Field kept for backwards-compat. | +| `decision_stride` | **REMOVED** | Greenfields — field deleted from YAML schema and Rust structs in this rebuild. No deprecation shim. | | `max_lots` | **ADAPTIVE** (Layer C §5.1) | Envelope from rolling Sharpe × DD × regime vol. | | `threshold` | **ADAPTIVE** (Layer C §5.2) | Conviction-percentile gate. | | `target_annual_vol_units` | **ADAPTIVE** (Layer C §5.3) | Regime-vol-inverse. | @@ -373,15 +493,45 @@ Every reader and writer of `open_trade_state` migrates atomically per [feedback_ ### Gate 2 (Layer B): Signal-driven position management -**Pass criteria:** -- Hold-time mean NOT cap-binding (max_hold circuit-breaker rarely fires; < 5% of closes). -- `profit_factor > 1.0` OR ≥ 30% lift on a defined alpha-realization metric vs Layer A baseline. -- Win rate ≥ 24.2% (current). -- Drawdown not worsened > 10%. -- Conviction degradation accounts for ≥ 60% of closes. -- Per-trade trajectory data shows conviction tracks predicted alpha (sanity check on the formula). +Tiered criteria — first MUST is required to advance; WIN is the architectural payoff. -**Validation:** parametric sweep over (degradation_factor, disagreement_K, conviction_EMA_α) on the smoke window. Pick the winner. Then walk-forward on 3 folds. +**MUST (required to advance from Gate 2):** +- Hold-time mean NOT cap-binding (8h circuit-breaker fires in < 5% of closes). +- No regression on baseline metrics: Sharpe ≥ baseline − 0.5 units; PF ≥ baseline × 0.95; drawdown ≤ baseline × 1.10. +- Conviction-degradation exit accounts for ≥ 60% of closes — validates the mechanism is dominant. +- Trade attribution coherent: `exit_cause` field shows interpretable distribution across the three terms (not 99% one term). +- No new NaN/sentinel counters fire. + +**WIN (the architectural payoff — one of):** +- Sharpe lift ≥ +0.5 units vs Layer A baseline. +- Profit factor lift ≥ 30% (e.g., 0.40 → 0.52, not 0.40 → 1.0). +- Alpha realization ratio (see §9.1.1) lift ≥ 20%. +- Drawdown reduction ≥ 20% at equal PnL. + +**STOP** at gate close-out if NO WIN condition was hit despite MUST being green: re-evaluate whether to proceed to Layer C, or whether the signal itself needs improvement before any policy layer can extract value (the latter is a separate workstream). + +**Validation:** parametric sweep over (degradation_factor, disagreement_factor, pnl_decay_factor, conviction_EMA_α_floor) on the smoke window. Pick the winner by composite of MUST + WIN criteria. Then walk-forward on 3 folds. + +### 9.1.1 Alpha-realization ratio (defined) + +Per-trade ratio of realized PnL to theoretical maximum PnL achievable within the trade's signal-validity window: + +``` +For each closed trade: + signal_window_start = entry_ts + signal_window_end = max(exit_ts, entry_ts + h6000_horizon_ns) + optimal_exit_ts = argmax_t∈[start,end] (unrealized_pnl_at_t) + optimal_pnl = unrealized_pnl_at(optimal_exit_ts) + realized_pnl = trade.realized_pnl_usd + + alpha_realization_per_trade = realized_pnl / max(optimal_pnl, cost_per_lot_per_side × size) + +alpha_realization_ratio = mean(alpha_realization_per_trade across all trades) +``` + +Bounded by trade's signal-validity window (h6000 horizon ns). Captures "how much of the available alpha the policy actually captured." Cost-bootstrapped denominator (using `cost × size` as floor) per [pearl_trade_level_vol_for_stop_distance](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_trade_level_vol_for_stop_distance.md) handles trades where `optimal_pnl` is sub-cost noise. + +Computed offline from trade records + per-event unrealized PnL reconstruction (the latter requires logging unrealized PnL at decision events — minor instrumentation add). Single, unambiguous metric. ### Gate 3 (Layer C): Adaptive risk envelope @@ -445,6 +595,16 @@ Famous failure mode. - Circuit-breaker revert — automatic rollback on rolling Sharpe degradation. - Adapter is small (1-5% of base) — limited expressivity to misbehave. +### 10.5 Layer D — adapter regime over-fit + +EWC++ guards against catastrophic forgetting of base capabilities but does NOT prevent the adapter from over-fitting to the **most recent regime** if recent sessions all live in one regime (e.g., a multi-week trending market followed by chop). + +**Mitigations:** +- **Sample-count down-weighting:** the gradient step magnitude is scaled by `sqrt(samples_in_current_regime / samples_in_base_training_data)`. Regimes with little base coverage get small updates; regimes with rich base coverage get full updates. This is the standard distribution-shift compensation. +- **Regime detection** uses ISV-derived regime-vol classification (also used in §5.1 envelope and §5.3 vol target). When current regime ≠ base-training regime distribution, the adapter learns proportionally less. +- **Shadow eval is cross-regime** when possible: hold out a window from a DIFFERENT regime than the training window, so an adapter that over-fits to the training regime fails the cross-regime shadow. +- **Periodic adapter reset** as a hard backstop: if rolling Sharpe degrades AND shadow eval consistently fails for K consecutive cycles, drop the adapter entirely and re-bootstrap from base. + --- ## 11. Open questions / future work @@ -461,31 +621,45 @@ These are recognized scope-cuts. Document for future iteration. ## 12. Acceptance criteria summary (rollup) ``` -GATE 1 (Layer A): Continuous controller live, no PnL regression, ≤ 5× wall-time -GATE 2 (Layer B): PF > 1.0 OR +30% alpha-realization, hold-time emergent, 60% degradation exits -GATE 3 (Layer C): DD reduction at equal PnL OR PnL lift at equal DD -GATE 4 (Layer D): Walk-forward 75% positive lift, no catastrophic forgetting +GATE 1 (Layer A): Continuous controller live, wall-time ≤ 2× baseline, signal-to-entry + lag reduced ≥ 30%, no PnL regression > 15%. + +GATE 2 (Layer B): MUST — hold time emergent (cap fires < 5%), no metric regression, + 60% of closes via conviction-degradation, exit attribution coherent. + WIN — one of: Sharpe lift ≥ +0.5, PF lift ≥ 30%, alpha-realization + ratio ≥ +20%, drawdown reduction ≥ 20% at equal PnL. + STOP — MUST green but no WIN means re-evaluate signal-vs-policy before C. + +GATE 3 (Layer C): Drawdown reduction at equal PnL OR PnL lift at equal drawdown. + Envelope stays bounded by floor and hard-cap. Percentile self-tuning + stable (no thrash). Floor activates when signal-derived envelope underflows. + +GATE 4 (Layer D): Walk-forward across ≥ 3 folds, ≥ 75% positive PF lift. + Shadow eval catches synthetic-bad-update. Circuit-breaker revert works. + No catastrophic forgetting (base+adapter ≥ base on prior windows). + No regime over-fit (cross-regime shadow eval passes). ``` -Each gate is a stop. If a gate fails, do not advance until the gate is reopened with revised criteria or revised implementation. +Each gate is a stop. If a gate fails, do not advance until the gate is reopened with revised criteria or revised implementation. Gate 2 STOP condition (MUST without WIN) is the most consequential: it forces an explicit decision between "iterate Layer B" and "this signal can't carry a controller at this cost/latency anchor." --- ## 13. Pearl conformance summary -- [pearl_controller_anchors_isv_driven](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_controller_anchors_isv_driven.md) — Layer C envelopes are ISV-derived. -- [pearl_first_observation_bootstrap](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_first_observation_bootstrap.md) — conviction trajectory bootstrap uses sentinel=0, first observation replaces directly. -- [pearl_wiener_optimal_adaptive_alpha](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_optimal_adaptive_alpha.md) — conviction EMA α adaptive. -- [pearl_wiener_alpha_floor_for_nonstationary](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_alpha_floor_for_nonstationary.md) — α floor in conviction tracking (non-stationary by definition). -- [pearl_blend_formulas_must_have_permanent_floor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_blend_formulas_must_have_permanent_floor.md) — envelope floor uses `max(real, floor)`. -- [pearl_zscore_normalization_for_magnitude_asymmetric_signals](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_zscore_normalization_for_magnitude_asymmetric_signals.md) — multi-horizon weights are scale-normalized. -- [pearl_one_unbounded_signal_per_reward](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_one_unbounded_signal_per_reward.md) — conviction formula has exactly one unbounded multiplicand. -- [pearl_trade_level_vol_for_stop_distance](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_trade_level_vol_for_stop_distance.md) — `cost^2` bootstrap for ISV weight denominator. -- [pearl_stop_checks_run_at_deadline_cadence](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_stop_checks_run_at_deadline_cadence.md) — Layer A makes ALL checks event-rate. -- [feedback_single_source_of_truth_no_duplicates](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_single_source_of_truth_no_duplicates.md) — open_trade_state is single source. -- [feedback_no_partial_refactor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_partial_refactor.md) — open_trade_state migration is atomic. -- [feedback_isv_for_adaptive_bounds](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_isv_for_adaptive_bounds.md) — adaptive bounds live in ISV, not constants. -- [feedback_adaptive_not_tuned](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_adaptive_not_tuned.md) — signal-driven, not tuned. +Per-section pearl application (not just citation): + +- §3 Layer A: [pearl_stop_checks_run_at_deadline_cadence](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_stop_checks_run_at_deadline_cadence.md) — checks at event rate. +- §4.2 sizing: [pearl_wiener_optimal_adaptive_alpha](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_optimal_adaptive_alpha.md) + [pearl_wiener_alpha_floor_for_nonstationary](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_wiener_alpha_floor_for_nonstationary.md) — adaptive α with floor. +- §4.3 exit composite: [pearl_controller_anchors_isv_driven](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_controller_anchors_isv_driven.md) — degradation/disagreement/pnl_decay factors are ISV-derived. +- §4.4 conviction: [pearl_blend_formulas_must_have_permanent_floor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_blend_formulas_must_have_permanent_floor.md) — ε floor on net_edge (no cold-start branch); [pearl_audit_unboundedness_for_implicit_asymmetry](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_audit_unboundedness_for_implicit_asymmetry.md) — net edge is one-sided positive; [pearl_one_unbounded_signal_per_reward](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_one_unbounded_signal_per_reward.md) — `net_edge / (var + cost²)` is the one unbounded factor; [pearl_zscore_normalization_for_magnitude_asymmetric_signals](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_zscore_normalization_for_magnitude_asymmetric_signals.md) — `/ total_weight` scale-normalizes; [pearl_trade_level_vol_for_stop_distance](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_trade_level_vol_for_stop_distance.md) — `cost²` bootstraps variance. +- §5.1 envelope: [pearl_blend_formulas_must_have_permanent_floor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_blend_formulas_must_have_permanent_floor.md) — additive bounded contributions, floor at 1 lot; [feedback_isv_for_adaptive_bounds](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_isv_for_adaptive_bounds.md) — sharpe_norm / vol_norm / dd_cap are ISV anchors. +- §5.2 threshold: explicit 3-arm bandit replaces hand-wave; bounded `[0.5, 0.95]` per [pearl_blend_formulas_must_have_permanent_floor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_blend_formulas_must_have_permanent_floor.md). +- §6.2 Layer D: single primary loss (value regression) + EWC++; shadow eval with explicit window/metric. +- §7 state: [feedback_single_source_of_truth_no_duplicates](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_single_source_of_truth_no_duplicates.md) — open_trade_state is sole per-trade scratch; [feedback_no_partial_refactor](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_partial_refactor.md) — all consumers migrate atomically; [feedback_no_legacy_aliases](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_legacy_aliases.md) — no compat shim for the old 24-byte layout (greenfields). +- §8 boundary: [feedback_adaptive_not_tuned](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_adaptive_not_tuned.md) — every non-hardware constant becomes signal-derived; [feedback_no_legacy_aliases](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/feedback_no_legacy_aliases.md) — `decision_stride` removed, not deprecated. +- §9 gates: tiered MUST/WIN avoids the "(0, 30s)" anti-pattern from the just-closed S2 (set gates we can actually hit, gates that distinguish "broken" from "needs more work"). +- §11.4 percentile bootstrap: [pearl_first_observation_bootstrap](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_first_observation_bootstrap.md) — init from first observed conviction, no special branch. +- Overall sequencing: [pearl_no_deferrals_for_complementary_fixes](../../../../home/jgrusewski/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/pearl_no_deferrals_for_complementary_fixes.md) acknowledged — layers are NOT complementary fixes (which we'd combine) but sequenced amplifications (which we'd phase). The rationale is explicit in §2. ---