spec(crt): v2 amendments from critical review — 12 issues + greenfields

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) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-20 17:24:03 +02:00
parent 0f34843253
commit 8ba8755b48

View File

@@ -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) shows30% 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 for60% 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-time2× 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.
---