# SP15: Trader Discipline and Recovery — Design

> **Status**: spec
> **Date**: 2026-05-06
> **Predecessors**: SP11/12/13/14 chain merged into main at `0275a25d9` (2026-05-06)
> **Implementation branch**: `sp15-trader-discipline-recovery` (forks from main post-spec-commit)
> **Authoritative path**: `docs/superpowers/specs/2026-05-06-trader-discipline-and-recovery-design.md`

## TL;DR

Take the system from "stable, gradient-flowing, but trades like a panicked toddler" to "disciplined trader with measurable competence on a sealed quarter." Six phases:

1. **Phase 0** — Retune SP14 EGF (gate1=closed forever) so its anchors are ISV-driven, not hardcoded.
2. **Phase 1** — Honest numbers: unified sharpe kernel, cost-net sharpe (commission + spread + OFI-impact, dev/prod parity), drawdown reporting, 8 counterfactual baselines, `dd_pct` as foundational state input, `--holdout-quarters` + `--dev-quarters` CLI flags, consume abandoned walk-forward test slice.
3. **Phase 2** — 22-test behavioral suite on dev RTX 3050 Ti. Pre-commit hook blocks `argo-train.sh` if any test fails. CI gate before L40S.
4. **Phase 3** — 5 trader teachings (reward + action selection): r_quality/r_discipline split, explicit cost, quadratic DD penalty, regret signal, confidence-aware Hold floor.
5. **Phase 3.5** — 4 recovery mechanisms (3.5.1 already in Phase 1): asymmetric reward under DD, cooldown gate, plasticity injection, recovery curriculum in PER.
6. **Phase 4** — L40S walk-forward on Q1-Q7, Q8 final dev, sealed Q9 OOS — produces report card with deltas vs all 8 baselines.

**Cross-cutting discipline rule**: no new pearl, controller, kernel, ISV slot, or reward term ships without a Phase 2 behavioral test that proves the intended trader behavior. Hard rule alongside `feedback_no_partial_refactor`.

**Estimated scope**: ~4900 LOC, 13-21 days serial / 10-15 days with parallel dispatch (Approach B).

---

## 1. Premise

> "we know it's stable, we know it learns, but it has behavioural issues as well as mechanical issues. The model doesn't understand its role as trader. Drives itself into a downward spiral, very hard to recover from."
>
> — User, 2026-05-06, post-`train-dd4xl` termination

The architecture works. Gradients flow. Smoke tests pass. But:

- Each iteration we identify a gaming behaviour and add an adaptive controller. Controllers control controllers. We end up with a 50-controller toddler, not a trader.
- The model has no architectural mechanism to escape its own losses. Once it starts losing, it learns "trade less" — which reduces gradient signal — which slows recovery — which compounds.
- We measure on the same window we tune controllers against. "Out-of-sample" sharpe on val is a fantasy: val IS the selection set.

This spec is the alternative path: fix measurement, validate behaviour locally, teach role explicitly.

---

## 2. Failure evidence

### 2.1 The spiral curve (`train-dd4xl`, 8 epochs, terminated 2026-05-06)

Commit `c0fc28e45` (full SP14 Layer A+B chain, bus fix, warmup gate delete) on L40S walk-forward:

```
        trades   active   sharpe   pf      dir_ent
ep1:    131,805   0.482   79.45   1.195   1.03
ep2:    127,463   0.446   70.46   1.197   1.00
ep3:    116,817   0.381   65.87   1.214   0.93
ep4:     98,835   0.296   69.52   1.252   0.81
ep5:     98,781   0.295   63.92   1.279   0.81
ep6:     91,883   0.268   60.40   1.293   0.76
ep8:     63,837   0.171   43.63   1.225   0.57   ← terminated
```

Monotone descent across `trade_count` (−51%), `active_frac` (−65%), `dir_entropy` (−45%). Profit factor briefly **rises** (1.195 → 1.293) — fake win, surviving trades are higher-confidence — then drops as the policy collapses to non-trading.

### 2.2 HEALTH_DIAG[8] terminal state

```
val_dir_dist     [short=0.07 hold=0.69 long=0.10 flat=0.14]   ← 69% Hold
mag_stats wr     q=0.429  h=0.412  f=0.414                     ← all ~ random
mag_stats var    q=4.6e-10  h=4.2e-9  f=1.1e-8                 ← magnitudes flat
eval_dist        eq=0.885  eh=0.115  ef=0.000                  ← 88.5% Quarter only
c51_budget mag   0.0004                                        ← magnitude over-suppressed
aux_dir_acc      short=0.518  long=0.487                       ← 50/50 = no signal
aux_moe util     one expert at 1.000, rest at 0                ← MoE collapse
improvement_z   -0.850                                         ← controller says "worse"
gate1            closed                                        ← EGF never opens
sp9_kelly        floor=0.0000                                  ← Kelly literally cannot trade
```

The model isn't learning to trade. It's learning to look stable on the window it tunes against, while collapsing to "Hold most bars, pick Quarter mag, ignore direction."

### 2.3 Walk-forward audit finding

`crates/ml/src/cuda_pipeline/gpu_walk_forward.rs:122-165` defines proper expanding-window walk-forward with `train_start..train_end / val_start..val_end / test_start..test_end` per fold.

`crates/ml/src/trainers/dqn/trainer/mod.rs:1155` consumes only `train` and `val`:
```rust
self.set_training_range(fold.train_start, fold.train_end, fold.val_start, fold.val_end);
self.set_val_data_from_slices(&features[fold.val_start..fold.val_end], ...)
```

`grep -rn "fold\.test_start"` outside the generator returns **zero hits**. The reported `sharpe_annualised=64-79` is on the **val window** — same window the early-stop callback, Kelly warmup cap, and controller anchors all read.

What this means in practice:

| Signal | Sees val window? |
|---|---|
| Parameter gradients | No (train only) ✅ |
| Early-stop callback | **Yes** ❌ |
| Kelly warmup cap | **Yes** ❌ |
| Controller anchors / EMAs | **Yes** ❌ |
| Reported "OOS" sharpe | **Yes** (it's the same window) ❌ |

Val is temporally clean for parameter learning, but it's the selection set, not a sealed test set. Sharpe=80 reported means "best on selection set we tuned against," not "robust on unseen quarter."

---

## 3. Decisions reference (clarifying questions, 2026-05-06)

| # | Question | Decision |
|---|---|---|
| Q1 | DD-state architectural placement | **Foundational (Phase 1)**: ISV slot + producer kernel + trunk concat. Layout fingerprint break OK (greenfield). Eval-time policy SEES dd context. |
| Q2 | SP14 EGF cut vs retune | **Keep, retune to ISV-driven**. No hardcoded thresholds; Schmitt thresholds, variance gate refs, steepness anchors all become ISV-tracked. |
| Q3 | Cost model dev/prod parity | **commission + spread + OFI-impact, everywhere**. Dev synthetic markets generate synthetic spread + OFI. Prod reads from MBP10 fxcache. Same kernel. |
| Q4 | Counterfactual baselines | **All 8**: buyhold, hold-only, random_dir + same Kelly, naive momentum, aux-only, mag-Quarter-fixed, trail-only, naive reversion. |
| Q5 | Phase 2 test set | **22 tests**: 7 trading behavior + 10 state/arch grounding + 5 recovery dynamics (added 2.22 plasticity_cooldown_interlock per critical-review fix). |
| Q6 | OOS framework | **Train/dev/test split**: Q1-Q7 walk-forward train, Q8 final dev, Q9 sealed final test. |
| Q7 | Phase 3.5.4 plasticity injection | **Land all 4 recovery mechanisms** including plasticity. No defer. |

---

## 4. Architecture overview

### 4.1 Phase chain

```
Phase 0: SP14 EGF ISV-driven retune          ─┐
Phase 1: Honest numbers + dd_pct foundation  ─┼─ parallel sub-worktrees (Approach B)
Phase 2A: behavioral test scaffolding         ─┘
                       │
                       ▼  (Phase 2B individual tests land as Phase 1 capabilities ship)
                Phase 2B: Groups 1+2 (17 tests, gates Phase 3 entry)
                       │
                       ▼
                Phase 3: 5 trader teachings (atomic per teaching, each gated on anchor 2.X test)
                       │
                       ▼
              Phase 3.5: 4 recovery mechanisms (3.5.1 already in Phase 1.5)
                  │            ↑ Phase 2C (Group 3 tests) interleaved with each teaching
                  ▼
                Phase 4: L40S walk-forward (Q1-Q7) + Q8 final dev + sealed Q9 OOS
```

### 4.2 Branch and worktree structure

- **Branch**: `sp15-trader-discipline-recovery` forks from main (`0275a25d9` post-SP11-merge)
- **Sub-worktrees** for parallel dispatch:
  - `sp15-phase0-egf-retune` — SP14 EGF retune (Phase 0)
  - `sp15-phase1-honest-numbers` — Phase 1 sub-fixes (parallel to Phase 0)
  - `sp15-phase2a-test-scaffold` — synthetic market generators + harness (parallel to Phase 0+1)
- **Merge model**: each phase merges back to `sp15-trader-discipline-recovery` atomically. SP15 branch merges to main only after Phase 4 production-track gate passes.

### 4.3 ISV slot allocation map (resolves parallel-dispatch ISV-table conflict)

Approach B parallel dispatch requires that Phase 0 / Phase 1 / Phase 2A sub-worktrees not collide on ISV slot indices. The spec pre-allocates disjoint slot ranges per phase. SP14 occupied slots 383-395 (`ISV_TOTAL_DIM = 396` post-merge `0275a25d9`). SP15 starts at 397.

| Range | Owner | Purpose | Count |
|---|---|---|---|
| `[397..401)` | Phase 0.B (EGF retune) | `EGF_SCHMITT_HI`, `EGF_SCHMITT_LO`, `EGF_VAR_AUX_REF`, `EGF_VAR_Q_REF` | 4 |
| `[401..407)` | Phase 1.3 (drawdown) | `DD_CURRENT`, `DD_MAX`, `DD_RECOVERY_BARS`, `DD_PERSISTENCE`, `CALMAR`, `DD_PCT` | 6 |
| `[407..409)` | Phase 1.2 (cost kernel) | `OFI_IMPACT_LAMBDA` (ISV-tracked), `COST_PER_BAR_AVG` | 2 |
| `[409..417)` | Phase 1.4 (8 baselines) | one `BASELINE_SHARPE_NET_*` per baseline | 8 |
| `[417..420)` | Phase 3.1 (α split) | `ALPHA_SPLIT` (ISV-driven from grad ratio), `GRAD_NORM_QUALITY`, `GRAD_NORM_DISCIPLINE` | 3 |
| `[420..423)` | Phase 3.3 (DD penalty) | `LAMBDA_DD`, `DD_THRESHOLD`, `DD_PENALTY_GRAD_NORM` | 3 |
| `[423..426)` | Phase 3.4 (regret) | `REGRET_EMA`, `LAMBDA_REGRET`, `REGRET_GRAD_NORM` | 3 |
| `[426..430)` | Phase 3.5 (Hold floor) | `HOLD_FLOOR_ALPHA`, `HOLD_FLOOR_K`, `HOLD_FLOOR_EPS0`, `ENTROPY_DIST_REF` | 4 |
| `[430..433)` | Phase 3.5.2 (DD asymmetric reward) | `DD_ASYMMETRY_LAMBDA` (ISV-tracked from DD distribution), `R_GAIN_DD_BOOST`, `DD_DIST_VAR` | 3 |
| `[433..436)` | Phase 3.5.3 (cooldown) | `COOLDOWN_K_THRESHOLD` (ISV from running mean of per-trade PnL), `COOLDOWN_M_BARS`, `COOLDOWN_BARS_REMAINING` | 3 |
| `[436..439)` | Phase 3.5.4 (plasticity) | `PLASTICITY_FIRED_THIS_FOLD`, `PLASTICITY_PERSISTENCE_THRESHOLD`, `PLASTICITY_WARM_BARS_REMAINING` (force cooldown engagement post-fire) | 3 |
| `[439..441)` | Phase 3.5.5 (recovery curriculum) | `DD_TRAJECTORY_DECREASING` (per-step proxy for "in recovery"), `RECOVERY_OVERSAMPLE_WEIGHT` | 2 |
| `[441..443)` | Phase 3.5 deferred ISV anchors (resolves second-review #5, #7) | `DD_TRAJECTORY_FLOOR` (25th percentile of `dd_pct` distribution — replaces hardcoded 0.02), `MEDIAN_STREAK_LENGTH` (running median of consecutive-loss streak lengths — used by 3.5.3 cooldown K formula) | 2 |
| **Total** | | **`ISV_TOTAL_DIM = 443` post-SP15** | 46 |

Each phase's first commit lands `sp15_isv_slots.rs` extension covering its allocated range. Layout fingerprint seed extended atomically per phase. Sub-worktrees check the allocation map before assigning indices.

### 4.4 Phase 2A ↔ Phase 1.2 ABI contract (resolves parallelism dependency)

The synthetic-LOB triple `(price_t, spread_t, ofi_t)` is the canonical ABI between Phase 2A test markets and Phase 1.2 cost kernel. Phase 2A defines this contract first; Phase 1.2 kernel signature must conform.

```rust
// Canonical ABI defined by Phase 2A scaffolding, consumed by Phase 1.2:
struct LobBar {
    price: f32,           // mid-price, units = MES futures dollar
    spread: f32,          // bid-ask spread, units = same as price (e.g., 0.25 = quarter-tick)
    ofi: f32,             // order flow imbalance signed magnitude, normalized to ~[-3, 3]
}
// Synthetic markets emit Vec<LobBar>; fxcache loader emits Vec<LobBar>; cost kernel reads &[LobBar]
```

Phase 2A lands the `LobBar` type + synthetic generators FIRST. Phase 1.2 cost kernel reads `&[LobBar]`. Phase 0 + Phase 1 (other subs) + Phase 2A scaffold can then dispatch in parallel because:
- Phase 0 doesn't touch LobBar (EGF kernels)
- Phase 1.1, 1.3-1.7 don't touch LobBar (sharpe kernel, DD reporting, baselines, dd_pct, CLI, test slice consumption)
- Phase 1.2 conforms to Phase 2A's pre-defined ABI

This restores Approach B parallelism with explicit contract precedence.

### 4.5 Cross-cutting discipline rule

**No new pearl, controller, kernel, ISV slot, or reward term ships without:**
1. **Phase 2 behavioral test** that proves the intended trader behavior (the test commit and the implementation commit ship together).
2. **Wire-up audit entry** in `docs/dqn-wire-up-audit.md`.
3. **Memory pearl** in `~/.claude/projects/-home-jgrusewski-Work-foxhunt/memory/` if the technique generalises beyond this spec.

This adds to `feedback_no_partial_refactor`, `feedback_no_stubs`, `feedback_isv_for_adaptive_bounds`, `feedback_wire_everything_up` as a hard discipline rule. Violations mean the change is reverted, not merged-with-followup.

---

## 5. Phase 0 — SP14 EGF ISV-Driven Retune (~1-2 days, atomic per fix)

### 5.1 Goal

Make EGF actually fire under target conditions. Eliminate every hardcoded threshold per `feedback_isv_for_adaptive_bounds`.

### 5.2 Phase 0.A — Diagnose

`HEALTH_DIAG[7]/[8]` from `train-dd4xl` showed:
- `var_aux=0.00021` against threshold `0.01` → 50× below trip point
- `var_q=0.00879` against threshold `0.05` → 6× below trip point
- `α_smoothed=0.0008`, `α_raw=0.0006` → mathematically consistent with closed gate

Either the thresholds are wrong (signal magnitudes are inherently smaller than expected when calibrated on synthetic-distribution training) or the signals being read into `var_aux`/`var_q` aren't the right ones (e.g., reading post-clipped values that have been collapsed). Diagnostic step:

1. Add temporary HEALTH_DIAG emission of the raw aux/Q variance signals **before** EGF reads them.
2. Run 5-epoch L40S smoke against post-Phase-0.A HEAD.
3. Compare emitted variances to the EGF-internal `var_aux`/`var_q` values. Mismatch → upstream signal corruption. Match → thresholds need ISV-driven dynamic anchoring.

### 5.3 Phase 0.B — Retune

Every hardcoded anchor becomes ISV-driven:

| Anchor (current value) | ISV slot (per allocation map §4.3) | Producer |
|---|---|---|
| Schmitt thresholds (±0.03 hardcoded) | `EGF_SCHMITT_HI` (slot 397), `EGF_SCHMITT_LO` (slot 398) | rolling p99/p1 of α_raw over fold |
| Variance gate ref aux (0.01) | `EGF_VAR_AUX_REF` (slot 399) | running mean of var_aux over fold |
| Variance gate ref Q (0.05) | `EGF_VAR_Q_REF` (slot 400) | running mean of var_q over fold |
| Steepness normaliser (k_aux=20/(1+var_aux/0.01) hardcoded `0.01`) | reuses `EGF_VAR_AUX_REF` (slot 399) | (above) |
| Steepness normaliser k_q (0.05 hardcoded) | reuses `EGF_VAR_Q_REF` (slot 400) | (above) |

Total: 4 new ISV slots (`[397..401)` per §4.3) + 2 new producer kernels. EGF kernels read these instead of constants.

### 5.4 Phase 0 anchor test (Phase 2.21 — `egf_gate_opens_under_disagreement`)

Synthetic batch with deliberately disagreeing aux head (output = +1.0) vs Q head (output = −1.0) → gate1 must open within K=100 steps. If it doesn't, EGF is broken regardless of how clean the kernels look.

This test is authored alongside Phase 0.B and gates the commit. EGF retune cannot land without test 2.21 going green.

---

## 6. Phase 1 — Honest Numbers (~2-3 days, 7 atomic sub-fixes)

### 6.1 Sub 1.1 — Unified sharpe kernel

Single GPU kernel `compute_sharpe_per_bar_kernel.cu`. Inputs: `pnl_array[T]`. Output: `(mean_pnl, std_pnl, sharpe_per_bar)`. Train and val use identical formula and identical window definition.

Eliminates `sharpe_ema` (per-batch EMA with smoothing constant divergence) vs `sharpe_annualised` (val × sqrt(525,600)) confusion. Reports both `sharpe_per_bar` and `sharpe_annualised = sharpe_per_bar × sqrt(N_bars_per_year)` from the same kernel.

### 6.2 Sub 1.2 — Cost-net sharpe

```
cost_t = commission_per_rt × rt_indicator(t)
       + half_spread_t × |position_size_t| × side_indicator(t)
       + ofi_impact_t
```

**Per-side semantics (resolves round-trip ambiguity)**:
- `commission_per_rt`: $1.50 default per round-trip, applied entirely on exit (not split). `--commission` CLI overridable.
- `rt_indicator(t)`: 1.0 on close-trade bars, 0 otherwise.
- `half_spread_t = spread_t / 2`: half-spread cost per side. **Charged BOTH at entry AND at exit.** `side_indicator(t)`: 1.0 on entry bars, 1.0 on exit bars, 0 otherwise. Net: a complete round-trip pays `commission_per_rt + half_spread_at_entry × position + half_spread_at_exit × position` — i.e., one full spread cost per RT plus one commission.
- `ofi_impact_t = λ × |position_size_t| × |ofi_t| × side_indicator(t)`: applied per-side (entry and exit).
  - Initial `λ = 2e-4` (rough MES literature fit; impact ≈ 0.2 ticks per contract per σ-unit OFI).
  - ISV-tracked at slot `OFI_IMPACT_LAMBDA` (slot 407 per §4.3) — refit per fold by minimizing prediction error of next-tick mid-price shift vs OFI quartile on fold's training window.
- `position_size`: contracts traded (1 for Quarter, 2 for Half, 4 for Full at base Kelly cap).

**Sharpe net**:
```
sharpe_net = (mean_pnl − mean_cost) / std_pnl_with_costs
```
where `mean_cost = sum(cost_t) / N_bars` is emitted to ISV slot `COST_PER_BAR_AVG` (slot 408).

**Dev/prod parity**: the cost kernel reads `spread_t`, `ofi_t` as input arrays. Phase 2A synthetic markets generate these via the §4.4 LobBar ABI. Prod fxcache produces the same LobBar type. One kernel, two environments — `feedback_isv_for_adaptive_bounds` extends to costs.

### 6.3 Sub 1.3 — Drawdown reporting

New ISV slots:
- `DD_CURRENT_INDEX`: peak-to-trough percentage of running cumulative PnL
- `DD_MAX_INDEX`: maximum DD seen this fold
- `DD_RECOVERY_BARS_INDEX`: bars since last DD peak (0 = currently at ATH)
- `DD_PERSISTENCE_INDEX`: bars in current drawdown ≥ threshold (used by 3.5.4)

HEALTH_DIAG emits `max_dd_pct`, `current_dd`, `dd_recovery_bars`, `Calmar = mean_pnl / max(max_dd_pct, 1e-4)`. Denominator floor (1e-4) replaces the current Calmar=100 saturation artifact.

### 6.4 Sub 1.4 — 8 counterfactual baselines (with shared trunk forward)

Each baseline replays the val/test window with a different action policy and computes sharpe_net + max_dd + trade_count. **Critical implementation detail (resolves Important #9)**: baselines that need policy-trained heads (aux_only, mag_quarter_fixed, trail_only) MUST share the trunk forward pass with the main policy evaluation. Without trunk fusion, 8 separate full-policy kernel launches per fold per epoch would inflate eval cost ~4-8×.

**Implementation**: a single fused trunk forward computes shared state representation `h_s2` for the val/test window. Then 8 lightweight per-baseline kernels read `h_s2` (where applicable) plus other inputs, output baseline action sequences, run cost-net sharpe kernel.

| Baseline | Inputs | Trunk forward needed? |
|---|---|---|
| `buyhold` | price series only | No |
| `hold_only` | none (constant Hold) | No |
| `random_dir_kelly` | seeded RNG, policy Kelly cap output | trunk YES (for Kelly cap) |
| `naive_momentum` | last-bar return only | No |
| `aux_only` | trunk h_s2 + aux head only | trunk YES (subset head) |
| `mag_quarter_fixed` | trunk h_s2 + direction Q head only | trunk YES (subset head) |
| `trail_only` | seeded RNG, policy trail-distance ISV | trunk NO (just trail logic) |
| `naive_reversion` | last-bar return only | No |

Of 8 baselines, 3 need the shared trunk (already computed once for main policy eval). Remaining 5 are constant or last-bar-only — pure kernels with negligible cost.

**Realistic overhead estimate**: ~15-25% of full eval pass (not 5-10% as initially claimed). Acceptable for the validation rigor benefit. Documented honestly so we don't underestimate L40S timing.

HEALTH_DIAG emits per-fold deltas: `vs_buyhold = sharpe_net − buyhold_sharpe_net`, etc. Each baseline's sharpe_net writes to its allocated ISV slot (`BASELINE_SHARPE_NET_*` slots 409-416 per §4.3).

### 6.5 Sub 1.5 — `dd_pct` as foundational state input

This is the layout fingerprint break.

- New ISV slot: `DD_PCT_INDEX = current_dd / max(dd_budget, 1e-4)`. `dd_budget` is config (e.g., 0.20 = 20% drawdown limit). Producer kernel reads `DD_CURRENT_INDEX` from sub 1.3.
- **Trunk concat extension**: trunk forward input grows by 1 dim. Layout fingerprint seed extended with `DD_PCT_TRUNK_INPUT`. All trunk-input consumers migrate atomically (per `feedback_no_partial_refactor`): batched_forward.rs, batched_backward.rs, gpu_dqn_trainer scratch buffer sizing, eval-mode forward path.

Eval-time policy now SEES dd_pct on every forward. Phase 3 teachings condition on it naturally.

### 6.6 Sub 1.6 — `--holdout-quarters` and `--dev-quarters` CLI flags

In `bin/fxt/` and `services/ml-training/`:
- `--holdout-quarters N` (default 1): slice last N quarters off `training_data` BEFORE walk-forward generation. This is the sealed final test set.
- `--dev-quarters M` (default 1): slice next-N-from-tail quarters off, after holdout. This is the final dev set (used for hyperparameter tuning, controller anchor calibration).
- Walk-forward generates folds on `Q1..Q(9-N-M)`. With defaults: walk-forward on Q1-Q7 (3 folds at 0.5/0.125/0.125/0.125), Q8 = final dev, Q9 = sealed test.

### 6.7 Sub 1.7 — Consume the abandoned test slice

`set_test_data_from_slices` method on trainer + post-fold `test_sharpe_net` emit. The 12.5% `test_start..test_end` slice generated by walk-forward is now actually used: per-fold test slice metric reported separately from val. Sharpe gap between val and per-fold-test surfaces the selection-vs-test difference quantitatively for each fold.

### 6.8 Phase 1 sequencing

Sub 1.1 → 1.2 → 1.3 → 1.4 → 1.5 → 1.6 → 1.7. Each is one atomic commit. 1.5 (dd_pct) is the layout-fingerprint-break commit; explicitly tagged in the commit message so checkpoints from before this point are known-incompatible.

---

## 7. Phase 2 — Behavioral Test Suite (~3-5 days, ~2000 LOC)

### 7.1 Goal

Every behavior the policy must exhibit gets a deterministic test on dev RTX 3050 Ti. Suite runs <10 min total. Pre-commit hook blocks `argo-train.sh` if any test fails. CI gate before L40S.

### 7.2 Phase 2A — Test infrastructure (parallel to Phase 0+1)

- **Synthetic market generators** in `crates/ml/tests/behavioral/synthetic_markets.rs`:
  - `flat_market(noise_σ)` — flat price + Gaussian noise + parameterized bid-ask spread
  - `drift_market(μ, σ, spread_pattern)` — geometric drift + spread parameterization
  - `OU_market(θ, σ_eq, μ)` — Ornstein-Uhlenbeck process for mean-reversion
  - `regime_switch_market(N_regimes, transition_matrix)` — multi-regime Markov chain
  - All generators produce `(price_t, spread_t, ofi_t)` triples — feed unified cost kernel from Phase 1.2
- **Oracle policies** — known-correct action sequences per market type. Used both as reference for test assertions and as initial-weight seeds for "policy starts informed" tests.
- **Evaluator harness** in `crates/ml/tests/behavioral/harness.rs`: runs policy on synthetic for K bars, captures action distribution + sharpe_net + max_dd + behavioral metrics, compares against test assertions.
- **Pre-commit hook** in `.claude/helpers/pre_commit_behavioral_suite.sh`: runs `cargo test -p ml --test behavioral_suite -- --nocapture`. Fail → block commit.
- **CI integration**: `argo-train.sh` invokes the same hook before submitting workflow. Local fail = no L40S deploy.

### 7.3 22 tests by group

#### Group 1: Trading behavior (7 tests, lands as Phase 1 cost + market gen ships)

| # | Test | Pass criterion |
|---|---|---|
| 2.1 | `flat_market_holds` | ≥80% Hold over last 5k bars on flat synthetic |
| 2.2 | `trend_market_directional` | drift>0 → ≥70% Long; drift<0 → ≥70% Short |
| 2.3 | `mean_revert_reverses` | OU process → reverses at ±2σ within 5 bars |
| 2.4 | `cost_sensitivity` | trade freq drops ≥40% at high spread vs low spread |
| 2.6 | `regime_silences` | high-noise no-edge regime → trade freq ≤20% |
| 2.7 | `stop_discipline` | adverse excursion past trail dist → close within 1 bar |
| 2.18 | `action_latency_test` | step-function +5σ signal → policy switches Hold→Long within 5 bars |

#### Group 2: State/architecture grounding (10 tests, lands as Phase 1 state + Phase 0 EGF ships)

| # | Test | Pass criterion |
|---|---|---|
| 2.8 | `hold_vs_flat_semantics` | Hold preserves pre_pos; Flat zeros it |
| 2.9 | `eval_fold_isolation` | KL(shuffled-future, unshuffled-future) < 0.01 — no temporal leak in policy state |
| 2.13 | `eval_train_consistency` | KL(train-mode, eval-mode action dists) < 0.5 |
| 2.14 | `magnitude_uses_all_buckets` | each mag bucket > 5% of trade events |
| 2.15 | `fold_transition_stability` | sharpe collapse at fold boundary < 50% |
| 2.16 | `q_value_bounded_under_replay` | 1k replay updates → Q-values stay in [V_MIN, V_MAX] |
| 2.17 | `gradient_flow_to_all_heads` | non-zero grad reaches every head (dir/mag/ord/urg/aux/CQL/IQN/C51/ens) |
| 2.19 | `kelly_calibration_test` | kelly_f monotonically decreasing in Q-variance |
| 2.20 | `state_input_grounding_test` | KL(uptrend-context, downtrend-context action dists) > 0.5 |
| 2.21 | `egf_gate_opens` (Phase 0 anchor) | aux/Q deliberate disagreement → gate1 opens within 100 steps |

#### Group 3: Recovery dynamics (5 tests, anchors Phase 3.5 teachings)

| # | Test | Pass criterion |
|---|---|---|
| 2.5 | `drawdown_de_risks` | inject 10-bar loss streak → Kelly cap ≤0.5×base over next 100 bars |
| 2.10 | `recovery_after_streak` | inject 10R loss streak → recover ≥5R over next 200 bars (NOT collapse to all-Hold) |
| 2.11 | `dd_state_aware_sizing` | KL(0% DD, 50% dd_budget action dists) > 0.1 — policy conditions sizing on DD |
| 2.12 | `cooldown_engagement` | K=5 losses → cooldown fires within 1 bar; disengages cleanly after M=20 bars |
| 2.22 | `plasticity_cooldown_interlock` | trigger plasticity fire (synthetic persistent DD) → action selection forces Hold for full M_warm bars; zero trades during warm-up; no second plasticity trigger within fold |

### 7.4 Sequencing within Phase 2

Three atomic commit batches:
- **2A: scaffolding** (synthetic gens + oracle + harness) — first
- **2B: Groups 1+2** (17 tests) — once Phase 0+1 ships; gates Phase 3 entry
- **2C: Group 3** (5 tests) — written alongside Phase 3.5 teachings; each test commit paired with its teaching commit per the discipline rule

### 7.5 Discipline gate enforcement

Every Phase 3 / 3.5 commit cites which Group 1/2/3 test it makes green. If teaching X lands but `2.X` doesn't go green, teaching reverts. No "we'll fix the test later" exceptions.

---

## 8. Phase 3 — Trader Teaching (~3-5 days, atomic per teaching, sequential)

### 8.1 Goal

Reward shape and action selection teach the trader role, not just optimize a number.

**SP12 v3 (per-trade event-driven reward) stays.** Phase 3 builds ON SP12, doesn't replace it. SP12's per-event asymmetric pos/neg cap (5.0 / -10.0) and zero per-bar dense shaping are preserved.

### 8.2 The 5 teachings

#### 3.1 — r_quality + r_discipline split

Architectural framing:

```
r_total = α × r_quality_per_event + (1 − α) × r_discipline_per_bar
```

- `r_quality_per_event` = SP12 per-trade event-driven reward (UNCHANGED)
- `r_discipline_per_bar` = 3.4 regret signal (NEW)
- **α is ISV-driven** at slot `ALPHA_SPLIT` (slot 417 per §4.3). Producer reads `GRAD_NORM_QUALITY` (slot 418, EMA of |∇r_quality|) and `GRAD_NORM_DISCIPLINE` (slot 419, EMA of |∇r_discipline|).
- **Cold-start init (resolves second-review #2)**: `ALPHA_SPLIT` slot is initialized **directly to 0.5 in the trainer constructor** (NOT derived from formula at boot). The formula `α = grad_norm_q / (grad_norm_q + grad_norm_d + ε)` only takes over once BOTH grad-norm slots have accumulated `≥ N_warm` non-zero observations (Welford warm-up; `N_warm = 100` initial sentinel, ISV-tracked from per-fold convergence pattern). Until then, the slot remains at the constructor-written 0.5.
- The `pearl_first_observation_bootstrap` semantics (sentinel = 0; first observation replaces directly) apply to the grad-norm EMAs themselves — first observation of `|∇r_quality|` becomes `GRAD_NORM_QUALITY[0]`, etc. But the dependent slot `ALPHA_SPLIT` is NOT computed from these until both EMAs are warmed up.
- Per `feedback_isv_for_adaptive_bounds` — once warm, α tracks the actual gradient distribution rather than being a hardcoded 0.7 guess.

**Anchor tests**: 2.4 (cost_sensitivity), 2.6 (regime_silences). Both should be made simultaneously achievable by the split.

#### 3.2 — Explicit cost in r_quality

```
r_quality_per_event −= cost_t × trade_attempt_indicator
```

`cost_t` is the same Phase 1.2 unified kernel (commission + spread + OFI-impact). Model SEES the bill in the gradient signal during training, not just in eval-time metrics.

**Anchor test**: 2.4 (cost_sensitivity). Policy must trade less under high-spread regime.

#### 3.3 — Quadratic drawdown penalty

```
r −= λ_dd × max(0, current_dd − dd_threshold)²
```

- Asymmetric quadratic: zero penalty until dd crosses threshold, then quadratic growth. Encodes loss aversion per `pearl_audit_unboundedness_for_implicit_asymmetry`.
- `dd_threshold`: ISV-driven from running variance of dd (initial: 0.05 = 5% of capital, retunes to per-fold scale).
- `λ_dd`: ISV-driven from grad-balance ratio between r_total and r_quality (so DD term doesn't dominate gradient).

**Anchor test**: 2.5 (drawdown_de_risks). Policy must reduce position size after loss streak.

#### 3.4 — Regret signal = r_discipline content

When the policy holds and a trade WOULD have been profitable past `cost_t + trail_dist`, log `regret_bar = ideal_pnl_missed − cost_t`.

```
r_discipline_per_bar = −λ_regret × regret_ema
```

- `regret_ema`: rolling EMA of `regret_bar` (Wiener-α adaptive smoothing per `pearl_wiener_optimal_adaptive_alpha`).
- `λ_regret`: ISV-driven from a target ratio between regret-derived gradient and r_quality gradient.

Teaches "Hold when no edge: fine. Hold when edge exists: bad."

**Anchor test**: 2.6 (regime_silences). Policy must trade in high-edge regimes, NOT silence in them.

#### 3.5 — Confidence-aware Hold floor

Action-selection level (NOT reward shaping). **Function form: bounded sigmoid** (specified to avoid implementer ambiguity):

```
hold_floor = α × σ(k × (entropy − ε₀))
```

Where:
- `entropy` = Shannon entropy of dir_Q_distribution (range `[0, log(K_dir)]`, K_dir=4 → max ≈ 1.386)
- `α` = scale factor in `HOLD_FLOOR_ALPHA` (slot 426, ISV-tracked from rolling 95th percentile of |Q_dir| over fold — NOT running max, which is one-way ratchet sensitive to outliers. 95th-percentile is robust to early-training noise spikes while still tracking actual Q magnitude scale)
- `k` = sigmoid steepness in `HOLD_FLOOR_K` (slot 427, ISV-tracked from running variance of entropy — wider entropy distribution → gentler slope)
- `ε₀` = entropy threshold in `HOLD_FLOOR_EPS0` (slot 428, ISV-tracked at 75th percentile of running entropy distribution `ENTROPY_DIST_REF` slot 429 — biases Hold only when policy is in upper quartile of uncertainty)
- `σ` = standard logistic sigmoid

Sigmoid (NOT linear, NOT exp) chosen because:
- Linear: doesn't saturate — at extreme entropy it can dominate Q values, breaking action selection at confident peaks too
- Exp: dominates too aggressively at full entropy, fails 2.2/2.3 (trend / mean-revert)
- Sigmoid: bounded `[0, α]`, smooth transition, parametrizable steepness — matches the qualitative "bias when uncertain, transparent when confident" behavior

When `entropy ≪ ε₀` (high confidence) → `hold_floor ≈ 0` → no bias.
When `entropy ≫ ε₀` (low confidence) → `hold_floor ≈ α` → strong Hold bias.

Hold-Q at action selection: `Q_hold_effective = Q_hold + hold_floor`.

**Anchor tests**: 2.1 (flat_market_holds), 2.6 (regime_silences). Hold must dominate when dir entropy is high.

### 8.3 Phase 3 sequencing

Sequential. Each teaching's commit asserts (a) its anchor test goes green AND (b) prior anchor tests still pass. If 3.3 breaks 2.4, 3.3 reverts.

---

## 9. Phase 3.5 — Recovery Dynamics (~2-3 days, atomic per mechanism, sequential)

### 9.1 Goal

Model can escape its own losses — break the spiral observed in train-dd4xl.

**3.5.1 (dd_pct as state input) already landed in Phase 1.5** per Q1. Phase 3.5 has 4 remaining mechanisms.

### 9.2 The 4 mechanisms

#### 3.5.2 — Asymmetric reward under DD

```
r_adjusted = r × (1 + λ × dd_pct)   for gains (r > 0)
r_adjusted = r                       for losses (r ≤ 0)
```

**Composition with SP12 caps (resolves Nit 12)**: multiplier applies BEFORE the SP12 pos-cap clamp.

```
intermediate = r_quality_pre_cap × (1 + λ × dd_pct)   [this teaching's multiplier]
r_quality_post_cap = clamp(intermediate, NEG_CAP, POS_CAP)  [SP12's cap stays final ceiling]
```

- For `dd_pct = 0` (at ATH): `(1 + 0) = 1` → no-op, SP12 cap unchanged.
- For `dd_pct > 0` AND `r_quality_pre_cap × (1 + λ × dd_pct) > POS_CAP`: the multiplier saturates the cap. Behaviorally correct — recovery trades hit ceiling sooner, encouraging frequent small recoveries over rare-large.
- SP12 NEG_CAP (loss side) still applies asymmetrically; this teaching doesn't touch loss multiplier.
- **Compound interaction with α split (resolves second-review #8)**: `r_quality_post_cap` then feeds `r_total = α × r_quality_post_cap + (1 − α) × r_discipline`. The compound multiplier × α weight of recovery-trade r_quality is **intentional and bounded by POS_CAP**. The POS_CAP IS the binding ceiling — implementers should NOT "fix" the apparent double-amplification by removing α-weighting or splitting the cap. The semantics: amplifier in DD → cap in DD → α-weighted contribution to total. Cap is the safety; α is the balance signal.

`λ` is ISV-tracked at `DD_ASYMMETRY_LAMBDA` (slot 430). Producer reads `DD_DIST_VAR` (slot 432, running variance of dd_pct over fold) — wider DD distribution → smaller λ (less aggressive multiplier when DD is volatile and unstable). Initial λ ≈ 0.5 sentinel.

Teaches "a winning trade out of DD is worth more than the same trade at ATH." Matches actual trading P&L psychology — recovering from drawdown is harder than maintaining ATH, so recovery gains deserve more reward signal.

**Anchor test**: 2.10 (recovery_after_streak).

#### 3.5.3 — Cooldown gate

After K consecutive losing trades, force Hold for M bars. Both K and M are ISV-driven:
- **K (loss-streak threshold)**: at slot `COOLDOWN_K_THRESHOLD` (slot 433). Producer reads running mean of per-trade PnL (NOT variance — variance is LOW during a streak when all losses cluster, which would delay cooldown precisely when needed). When running mean drops below a fold-percentile threshold (e.g., 25th percentile), K is reduced — cooldown trips sooner. Concretely: `K = max(2, floor(MEDIAN_STREAK_LENGTH × clamp(mean_pnl_recent / mean_pnl_baseline, 0.5, 2.0)))`.
- `MEDIAN_STREAK_LENGTH` (slot 442) is **ISV-driven** — running median of observed consecutive-loss streak lengths within fold. Producer kernel maintains a streak counter (incremented on losing trade, reset on winning trade), updates median via two-heap algorithm or running quantile sketch on each trade-close event. Per `feedback_isv_for_adaptive_bounds`: K threshold derives from observed streak distribution, not a guessed constant.
- **M (cooldown duration)**: at slot `COOLDOWN_M_BARS` (slot 434). Tracked from time-to-mean-reversion of vol_normalizer signal — cooldown lasts until regime stabilises.

**Cooldown counter as state**: ISV slot `COOLDOWN_BARS_REMAINING` (slot 435). Producer decrements per bar; consumer (action selection) forces Hold while > 0. Model SEES the counter on every forward — can reason about it, not work around it.

**Anchor test**: 2.12 (cooldown_engagement).

#### 3.5.4 — Plasticity injection during persistent drawdown

When `DD_PERSISTENCE` (slot 404 per §4.3) exceeds `PLASTICITY_PERSISTENCE_THRESHOLD` (slot 437), reset last 10% of advantage-head weights (initialization scheme specified below) to escape local minima.

- **Debounced**: fires at most once per fold via `PLASTICITY_FIRED_THIS_FOLD` (slot 436).
- **Threshold ISV-driven**: `PLASTICITY_PERSISTENCE_THRESHOLD` (slot 437) tracks running mean of `dd_persistence_bars` over training history. Set to mean + 2σ so only persistent (statistically unusual) drawdowns trigger.
- **Reset target**: last 10% of advantage-head weights (NOT shared trunk, NOT direction Q-head fully). Affects only the magnitude/ordinal/urgency advantage tails where local minima are most likely.
- **Reset initialization**: **Kaiming-He** (`mode=fan_in`, `gain` matched to activation). The trunk uses GRN blocks with GLU activations and the advantage heads chain through ReLU; Kaiming-He is calibrated for half-rectified activations and is the architecturally-appropriate default. Xavier (sometimes proposed) assumes symmetric linear activations and would underestimate variance for the actual activation chain. (Resolves second-review #10.)
- **Mandatory cooldown interlock (resolves Critical #3)**: when plasticity fires, the kernel performs a **two-step recovery sequence** (resolves second-review #3):
  1. **Flat all positions** at the fire bar (action = Flat, force-overrides policy output for that bar). The position that drove persistent drawdown is closed; the policy is no longer holding a known-bad position while its advantage tail is randomised.
  2. **Engage warmup cooldown** by writing `M_warm` to `PLASTICITY_WARM_BARS_REMAINING` (slot 438). This routes into the 3.5.3 cooldown consumer via OR-gate: `effective_cooldown = max(COOLDOWN_BARS_REMAINING, PLASTICITY_WARM_BARS_REMAINING)`. While `PLASTICITY_WARM_BARS_REMAINING > 0`, action selection forces Hold (now safe — no position to bleed since step 1 closed it).
- M_warm = 200 bars (initial sentinel; ISV-tracked post-fold from observed Adam re-equilibration time).
- Prevents the random-tail-output → loss-streak → re-trigger loop AND the open-losing-position-during-warmup leak. Symmetric: plasticity = "step away & restart" requires both closing current trade AND not re-entering during warmup, matching real-trader behavior.

Mimics real-trader "step away & restart" — the most aggressive recovery mechanism. Most research-y; gated strictly on 2.10 staying green AND 2.22 (NEW interlock test) green.

**Anchor tests**:
- 2.10 (recovery_after_streak) — plasticity must improve recovery, not break it.
- 2.16 (q_value_bounded_under_replay) — plasticity reset cannot create unbounded Q values.
- 2.22 (NEW: plasticity_cooldown_interlock) — when plasticity fires, action selection MUST force Hold for full M_warm bars; no trades during warm-up period.

#### 3.5.5 — Recovery curriculum in PER (per-transition proxy)

**Replaces episode-level metadata** (which doesn't exist in the transition-level PER) with a per-step proxy that's already computed.

PER stores transitions `(s_t, a_t, r_t, s_{t+1})` with TD-error priority. Adding episode-level "started in DD, recovered to flat" tags requires structural PER changes (episode boundaries, episode-level indices) which are out of scope.

**Per-transition proxy**:
- New per-step ISV signal `DD_TRAJECTORY_DECREASING` (slot 439): boolean (or smoothed scalar in `[0, 1]`) indicating `dd_pct(t) < dd_pct(t-1) AND dd_pct(t-1) > DD_TRAJECTORY_FLOOR`. True when the transition is part of a recovery (DD shrinking from a non-trivial drawdown).
- `DD_TRAJECTORY_FLOOR` (slot 441) is **ISV-driven** — tracks 25th percentile of running `dd_pct` distribution. Per `feedback_isv_for_adaptive_bounds`: "non-trivial drawdown" scales with the fold's own DD regime, not a guessed `0.02` constant.
- This signal is COMPUTED PER-BAR by the existing dd_pct producer (sub 1.5) — no PER restructure, no episode tracking, no replay buffer schema change.
- When PER samples a batch, weight transitions by `1.0 + RECOVERY_OVERSAMPLE_WEIGHT × DD_TRAJECTORY_DECREASING_t` (slot 440). Transitions on a recovery path get amplified sampling weight.
- `RECOVERY_OVERSAMPLE_WEIGHT` is ISV-driven from current `dd_pct`: more DD now → higher weight (current trajectory needs more recovery examples).

This still gives "transitions where recovery is happening" higher gradient signal — credit assignment for recovery patterns — without requiring episode metadata.

Currently PER oversamples loss states (high TD-error), which compounds the spiral. This counter-balances by giving "I survived a similar drawdown" examples more weight via per-step trajectory signal.

**Anchor test**: 2.10 (recovery_after_streak).

### 9.3 Phase 3.5 sequencing

Per Q7: **all 4 mechanisms land**. No defer of 3.5.4.

```
Phase 2B green → 3.5.2 → 3.5.3 → 3.5.4 → 3.5.5
                     ↓ each gated on anchor test
```

3.5.4's gate is strict: must not regress 2.10 OR 2.16. If it does, reverts and we ship 3.5 with only 3.5.2 + 3.5.3 + 3.5.5.

---

## 10. Phase 4 — Final Validation (~2-3 days)

### 10.1 Goal

Produce trustworthy "this trained system is/isn't a trader" signal. Phase 4 is the only phase that touches the sealed Q9.

### 10.2 Sub 4.1 — Pre-flight (~½ day)

- All 22 Phase 2 tests green on dev (RTX 3050 Ti). Pre-commit hook blocks if not.
- Per-pearl audit doc updated, MEMORY.md index extended for any new pearls landed.
- New worktree `sp15-final-validation` for the deployment artifacts.

### 10.3 Sub 4.2 — Walk-forward training (~1 day wall)

```
argo-train.sh \
  --branch sp15-final-validation \
  --commit <sp15-merged-sha> \
  --holdout-quarters 1 \
  --dev-quarters 1 \
  --seeds 3 \
  --epochs 30
```

- 3 seeds × 3 walk-forward folds on Q1-Q7. Q8 used as final dev (early-stop, Kelly warmup, controller anchors legitimately read from Q8 — it IS the dev set now, not the test set).
- HEALTH_DIAG emits `sharpe_net` + `Calmar` + 8 baseline deltas per fold per epoch.
- **Hard kill criterion** (per `feedback_kill_runs_on_anomaly_quickly`): if any fold sharpe_net trends down 3 epochs straight AND below all 8 baselines, terminate workflow + diagnose which Group 1/2/3 test failed in the wild.
- Best checkpoint per seed/fold saved to `trained-models-pvc` (task #30 dependency — needs landing first or as Phase 4 sub-step).

### 10.4 Sub 4.3 — Sealed Q9 OOS evaluation (~½ day)

- **Separate Argo workflow** `scripts/argo-eval-final.sh` — model-loaded eval-only, never touches training. Cannot accidentally use Q9 during a misconfigured training run.
- 3 seeds × Q9 evaluation. Report mean ± std for each metric.
- **Q9 burn policy**: each Q9 run generates `docs/runs/YYYY-MM-DD-sp15-q9-<commit>.md` audit file. Q9 is "burned" when run; subsequent runs against meaningfully-different architecture are allowed but each is logged. Clearly NOT to be used for hyperparameter tuning.

### 10.5 Sub 4.4 — Report card

Generated automatically by Phase 4.3 workflow:

```
SP15 Q9 Final OOS Report — commit <sha>
─────────────────────────────────────────
Per-fold (Q1-Q7 walk-forward, Q8 final dev):
  fold 0: sharpe_net=X  Calmar=Y  trades=N  dd_max=Z
  fold 1: ...
  fold 2: ...
  Q8 dev: ...

Q9 Sealed OOS (3-seed mean ± std):
  sharpe_net    = X.X ± σ
  Calmar        = Y.Y ± σ
  trade_count   = N ± σ
  max_dd_pct    = Z.Z ± σ
  recovery_bars = R ± σ

Deltas vs 8 baselines (Q9):
  vs buyhold        : +X.X (need > 0)
  vs hold-only      : +X.X (need > 0)
  vs random_dir     : +X.X (need > 0)
  vs naive_momentum : +X.X (need > 0)
  vs aux-only       : +X.X (need > 0)
  vs mag-Q-fixed    : +X.X (need > 0)
  vs trail-only     : +X.X (need > 0)
  vs naive_revert   : +X.X (need > 0)

Behavioral test status (final, on dev):
  22/22 passing

Pearls landed: <list>
Architectural changes: dd_pct as state, cost-net unified, 8 baselines, Q9 sealed
```

### 10.6 Sub 4.5 — Production-track gate

**Pass criteria** (thresholds are config-driven via `--gate-min-sharpe`, `--gate-min-calmar`, `--gate-baselines-must-beat`):

| Threshold | Default | Rationale |
|---|---|---|
| `min_sharpe_net` | 1.0 | Floor below which deploying is worse than passive holding (buyhold typical sharpe). To be tightened post-Phase-1 once we observe what baseline numbers actually look like on Q9. |
| `min_calmar` | 1.0 | Calmar = annualized_return / max_dd. 1.0 means recovering one max-drawdown's worth of capital per year — a survival-bar threshold. |
| `must_beat_all_baselines` | true | Every baseline delta > 0. Most rigorous; can be weakened to "beats N of 8" if the bar proves too tight in practice. |
| Phase 2 status | all 22 green | No regression in behavioral tests |

**Initial defaults are explicitly placeholders.** Phase 1 baseline runs will reveal actual baseline performance distribution. Phase 4.4 report card (first run) anchors realistic thresholds. If buyhold turns out to give sharpe_net = 0.5 on Q9, then "beats buyhold" is the floor and we tighten min_sharpe to e.g. 0.6. If buyhold gives 1.5, we tighten to 1.6+.

The thresholds are NOT hardcoded as architectural assumptions — they're config-driven AND empirically anchored.

**Fail mode → behavioral test mapping**:

| Failure | Targeted test | Action |
|---|---|---|
| sharpe_net low | 2.4, 2.6 | Cost-aware trading / regime appropriateness broken |
| Calmar low | 2.5, 2.10 | Drawdown management or recovery broken |
| loses to hold-only | 2.6 | Model overtrading garbage |
| loses to aux-only | architecture | Q-network not earning keep over aux head — consider simplification |
| loses to mag-Q-fixed | 2.14 | Magnitude head dead silicon |
| loses to trail-only | architecture | Entry logic not adding value over random+disciplined exit |

NOT a generic re-train. Each failure axis has a specific behavioral test to target.

---

## 11. Cumulative scope

| Phase | LOC | Days serial | Key artifacts |
|---|---|---|---|
| 0 | ~400 | 1-2 | EGF retune kernels, 4 ISV slots, test 2.21 |
| 1 | ~700 | 2-3 | sharpe kernel, cost kernel, 8 baselines, dd_pct trunk concat, CLI flags |
| 2 | ~2100 | 3-5 | Synthetic markets, harness, 22 tests, pre-commit hook |
| 3 | ~800 | 3-5 | 5 teachings (reward + Hold floor) |
| 3.5 | ~700 | 2-3 | 4 recovery mechanisms |
| 4 | ~300 | 2-3 | argo-eval-final.sh, report card automation |
| **Total** | **~4900** | **13-21** | |

With Approach B parallel dispatch on Phases 0+1+2A: ~10-15 days realistic.

---

## 12. Risks and open questions

### 12.1 3.5.4 plasticity injection is research-y

No published results in trading RL for "reset weights mid-training to escape local minima." Risk: mistuned trigger destroys good learning. Mitigation:
- Strict gate on 2.10 staying green
- Strict gate on 2.16 (q_value_bounded) NOT regressing — plasticity must not create unbounded values
- Debounced fire (at most once per fold)

### 12.2 8 baselines compute cost

Each baseline runs against the val/test window. 8 baselines × N_bars per fold per epoch. Mitigation per §6.4:
- Baselines that need policy-trained heads (3 of 8: aux_only, mag_quarter_fixed, random_dir_kelly) share the trunk forward pass with main policy eval — no redundant trunk computation
- 5 of 8 are constant or last-bar-only (buyhold, hold_only, naive_momentum, trail_only, naive_reversion) — pure kernels with negligible cost
- Run baselines only once per fold per epoch (not per step)
- **Realistic overhead estimate: ~15-25% of total eval cost** (per §6.4 — revised from earlier 5-10% which underestimated trunk-fusion-required baselines)

### 12.3 Q9 burn policy is honor-system

**Explicit acknowledgment**: this is honor-system, not enforcement. No tooling makes Q9 contamination impossible — it requires developer honesty.

A motivated developer could run Q9 eval N times and only commit the best. Even with tag requirements, lightweight tags are mutable; annotated tags can be deleted and recreated. The proposed mitigations reduce convenience of cheating but don't prevent it:

- `argo-eval-final.sh` rejects un-tagged or amended commits (raises bar slightly)
- Each Q9 run writes a counter + commit-hash + result-summary to `trained-models-pvc` infrastructure (chain-hash). Retroactive deletion would leave detectable gaps in the chain.
- Per-fold audit doc commits go to main as part of every Q9 run; log of "I ran Q9" is git-history-visible.

These are deterrents, not enforcement. Accepting the discipline-only nature of this is honest spec hygiene. The alternative (cryptographic infrastructure for unfakeable Q9 evaluation logs) is over-engineering for a single-developer project.

### 12.4 Synthetic LOB fidelity for Phase 2

Phase 2 synthetic markets generate synthetic spread + OFI to match the Phase 1.2 unified cost kernel's signature. Risk: synthetic LOB doesn't match real LOB statistics → Phase 2 tests pass but L40S behavior diverges.

Mitigation:
- Calibrate synthetic LOB to match MES historical first-moments (mean spread, OFI distribution) on Q1-Q7 training data
- Add Phase 2 test "cost_consistency_check": run policy on (synthetic LOB, real LOB) — KL divergence < 0.3
- This is the same kind of dev/prod parity discipline as the unified cost kernel itself

### 12.5 Discipline rule enforcement

"No new pearl without behavioral test" relies on commit-time review. Mitigation:
- Pre-commit hook checks new ISV slots against `docs/dqn-wire-up-audit.md` entries
- Plan reviews in PRs reference the discipline rule explicitly
- If violation slips through, it's reverted, not patched

### 12.6 Phase 3.1 "architectural split" is fuzzy

3.1 is described as "no separate code beyond the convex combination." Risk: it doesn't land as a distinguishable commit, gets merged into 3.4. Mitigation:
- 3.1 lands as a single explicit commit that introduces the `α × r_quality + (1-α) × r_discipline` combine point with `r_discipline = 0.0` default. 3.4 then populates `r_discipline` with regret content.
- If 3.1 ends up indistinguishable from 3.4 in implementation, accept that and merge into a single 3.1+3.4 atomic commit. The discipline rule still applies (anchor tests 2.4, 2.6 must go green).

### 12.7 Layout fingerprint break in Phase 1.5

`dd_pct` trunk concat invalidates pre-SP15 checkpoints. Per Q1 ("greenfield, checkpoint compatibility doesn't matter") this is acceptable — but it does mean any in-flight L40S work prior to SP15 cannot resume from those checkpoints.

Mitigation:
- Phase 1.5 commit message tagged with explicit warning
- `argo-train.sh` rejects loading pre-SP15 checkpoints (layout fingerprint mismatch hard error)

---

## 13. Pearl candidates pending validation

**Important**: Section 14's discipline rule says pearls emerge from validation, not pre-planning. The list below is **candidates for pearl authoring IF AND ONLY IF the technique demonstrably generalises** beyond this spec. Some, all, or none may be authored. Pre-naming creates risk of authoring-pressure regardless of generalisation; the candidate framing acknowledges this.

| Candidate | Authoring trigger | Origin |
|---|---|---|
| `pearl_dd_pct_foundational_state` | If 2.11 (dd_state_aware_sizing) shows policy reliably conditions on dd_pct | Phase 1.5 |
| `pearl_unified_cost_kernel_dev_prod_parity` | If Phase 4 Q9 net metrics match dev tests within tolerance (cost calibration verified) | Phase 1.2 |
| `pearl_behavioral_test_gates_l40s_deploy` | If pre-commit hook catches at least one regression that would have shipped | Phase 2 |
| `pearl_recovery_curriculum_per_step_proxy` | If 2.10 passes via 3.5.5 alone (without plasticity 3.5.4 dominating) | Phase 3.5.5 |
| `pearl_plasticity_injection_for_rl_local_minima` | If 3.5.4 fires AND 2.10 measurably improves AND no 2.16 regression | Phase 3.5.4 |
| `pearl_8_counterfactual_baselines_arch_specific` | If at least 2 of the architecture-specific baselines (aux-only, mag-Q-fixed, trail-only) reveal a real subsystem-not-earning-keep finding | Phase 1.4 |
| `pearl_egf_isv_driven_anchors` | If Phase 0.B retune makes gate1 actually open under target conditions (test 2.21 green AND non-trivial fire rate on real workloads) | Phase 0.B |
| `pearl_plasticity_cooldown_interlock` | If 2.22 verifies the OR-gate prevents re-trigger loops in real training | Phase 3.5.4 |

Any candidate that doesn't trigger after Phase 4 — drop, don't author. The discipline rule cuts both ways: pearls only get written when the technique generalised.

---

## 14. Discipline rule (re-stated for posterity)

**No new pearl, controller, kernel, ISV slot, or reward term ships without:**
1. A Phase 2 behavioral test that proves the intended trader behavior
2. A wire-up audit entry
3. A memory pearl if the technique generalises

Violations mean the change is reverted, not merged-with-followup.

This rule applies to every commit on the `sp15-trader-discipline-recovery` branch and every commit that merges back to main. It supersedes prior tolerance for "land it now, behavioral validation later." There is no later.
