diff --git a/crates/ml/tests/sp20_phase1_4_wireup_test.rs b/crates/ml/tests/sp20_phase1_4_wireup_test.rs index 699ddf2f4..8d8238c41 100644 --- a/crates/ml/tests/sp20_phase1_4_wireup_test.rs +++ b/crates/ml/tests/sp20_phase1_4_wireup_test.rs @@ -355,4 +355,102 @@ mod gpu { placeholder. Aggregation kernel writes 0.0 until \ Component 2 lands the per-bar producer."); } + + /// SP20 Phase 3 Task 3.3 (2026-05-10) — `target_hold_pct` + /// behavioral spot-checks through the full Path C chain. + /// + /// Spec §4.2 self-stabilizing-property invariants: + /// + /// 1. Aux untrained warmup: `aux_conf_p50_ema ≈ 0.05` → + /// `target_hold_pct ≈ 0.725` (high-Hold target → controller + /// drives `cost_scale` toward floor → per-bar Hold cost + /// effectively zero, no explicit warmup gate needed). + /// + /// 2. Aux confident: `aux_conf_p50_ema ≈ 0.40` → + /// `target_hold_pct ≈ 0.20` (low-Hold target → controller + /// pushes Hold% down to free up trade-eligible bars when the + /// aux head has a real edge). + /// + /// Both points exercise the formula + /// `clamp(0.8 - aux_p50_ema × 1.5, 0.1, 0.8)` at non-saturating + /// operating points — the unit-test + /// `sp20_controllers_compute_test::target_hold_pct_inverse_relation` + /// covers ENDPOINTS (aux_p50 ∈ {0.0, 0.5, 0.2}); this test covers + /// SPEC-CALLED-OUT mid-range operating points end-to-end through + /// Stats → Aggregate → EMAs → Controllers (proving the formula + /// holds across the full chain, not just the controllers kernel + /// in isolation). + /// + /// Per `pearl_tests_must_prove_not_lock_observations` — the spec + /// EXPLICITLY states these operating points as + /// self-stabilizing-property invariants, so the assertion is + /// invariant-style, not observed-value-style. + /// + /// Engineering aux_conf to land exactly at 0.05 / 0.40: + /// K=2 logits [a, b] ⇒ peak_softmax = sigmoid(b - a) + /// aux_conf = peak_softmax - 0.5 + /// ⇒ b - a = ln(peak / (1 - peak)) + /// For aux_conf = 0.05 ⇒ peak = 0.55 ⇒ Δlogit = ln(0.55/0.45) ≈ 0.20067 + /// For aux_conf = 0.40 ⇒ peak = 0.90 ⇒ Δlogit = ln(0.90/0.10) ≈ 2.19722 + /// All envs given identical Δlogit so `aux_conf_p50` lands at the + /// target value exactly (and `aux_conf_std = 0`). + /// + /// First-observation REPLACE bootstrap: a single chain launch + /// promotes the synthetic per-step `aux_conf_p50` directly to + /// `aux_conf_p50_ema` (no Wiener-α blending required since + /// `obs_count = 0` ⇒ replace path). + #[test] + #[ignore = "requires GPU"] + fn target_hold_pct_behavioral_spot_checks() { + let n_envs = 8; + + // ── Spot-check 1: aux_conf_p50 = 0.05 → target_hold_pct = 0.725 ── + let delta_low = (0.55_f32 / 0.45_f32).ln(); // ≈ 0.20067 + let mut aux_logits_low = Vec::with_capacity(n_envs * AUX_K_CLASSES); + for _ in 0..n_envs { + aux_logits_low.push(0.0_f32); // logit_down + aux_logits_low.push(delta_low); // logit_up — clear peak + } + // No closes, no Hold majority (avoid contaminating other ISV + // slots with side-effects; we're testing TARGET_HOLD_PCT only). + let trade_close = vec![0_i32; n_envs]; + let step_ret = vec![0.0_f32; n_envs]; + let hold_at_exit = vec![0.0_f32; n_envs]; + let actions = vec![encode_dir(2); n_envs]; // all Long + let isv = run_chain_once( + n_envs, &aux_logits_low, &trade_close, &step_ret, + &hold_at_exit, &actions, /* aux_dir_acc = */ 0.5, + ); + let target_low = isv[TARGET_HOLD_PCT_INDEX]; + let expected_low = 0.725_f32; // 0.8 - 0.05 * 1.5 + // Tolerance: aux_conf softmax + first-obs replace are + // bit-identical f32 paths; allow 1e-3 for cumulative softmax + // round-off across 8 envs' median computation. + assert!( + (target_low - expected_low).abs() < 1e-3, + "Spot-check 1 (warmup): aux_conf_p50≈0.05 → target_hold_pct \ + expected {expected_low}, got {target_low}. Spec §4.2 \ + self-stabilizing-property warmup gate violated." + ); + + // ── Spot-check 2: aux_conf_p50 = 0.40 → target_hold_pct = 0.20 ── + let delta_high = (0.90_f32 / 0.10_f32).ln(); // ≈ 2.19722 + let mut aux_logits_high = Vec::with_capacity(n_envs * AUX_K_CLASSES); + for _ in 0..n_envs { + aux_logits_high.push(0.0_f32); + aux_logits_high.push(delta_high); + } + let isv = run_chain_once( + n_envs, &aux_logits_high, &trade_close, &step_ret, + &hold_at_exit, &actions, /* aux_dir_acc = */ 0.5, + ); + let target_high = isv[TARGET_HOLD_PCT_INDEX]; + let expected_high = 0.20_f32; // 0.8 - 0.40 * 1.5 + assert!( + (target_high - expected_high).abs() < 1e-3, + "Spot-check 2 (confident): aux_conf_p50≈0.40 → \ + target_hold_pct expected {expected_high}, got {target_high}. \ + Spec §4.2 confident-state operating point violated." + ); + } } diff --git a/docs/dqn-wire-up-audit.md b/docs/dqn-wire-up-audit.md index 67c92e12e..29c3531a5 100644 --- a/docs/dqn-wire-up-audit.md +++ b/docs/dqn-wire-up-audit.md @@ -2,6 +2,84 @@ **Status:** Populated during Plan 1 Task 6 (A.5 orphan audit). Updated on every commit per Invariant 7. +## 2026-05-10 — SP20 Phase 3 Task 3.3: target_hold_pct behavioral spot-checks (test-only) + +Adds an integration-style behavioral test exercising the +`TARGET_HOLD_PCT_INDEX = 514` controller through the full Phase 1.4 +Path C chain (Stats → Aggregate → EMAs → Controllers) at the two +spec-§4.2-called-out operating points: + + - **Warmup**: `aux_conf_p50_ema = 0.05` → `target_hold_pct = 0.725` + (high-Hold target → controller drives `cost_scale` toward the + floor, no explicit warmup gate needed). + - **Confident**: `aux_conf_p50_ema = 0.40` → `target_hold_pct = 0.20` + (low-Hold target → controller frees up trade-eligible bars when + aux head has a real edge). + +Both points exercise the formula `clamp(0.8 - aux_p50_ema × 1.5, 0.1, +0.8)` at non-saturating operating points — the +`sp20_controllers_compute_test::target_hold_pct_inverse_relation` +unit test covers ENDPOINTS (aux_p50 ∈ {0.0, 0.5, 0.2}); this test +covers SPEC-CALLED-OUT mid-range operating points end-to-end through +the full chain (proving the formula holds across all 4 kernels, not +just the controllers kernel in isolation). + +### Engineering aux_conf to land at exact target values + +K=2 logits `[a, b]` ⇒ `peak_softmax = sigmoid(b - a)`, +`aux_conf = peak_softmax - 0.5`. + + - For `aux_conf = 0.05` ⇒ `peak = 0.55` ⇒ `Δlogit = ln(0.55/0.45) ≈ + 0.20067`. + - For `aux_conf = 0.40` ⇒ `peak = 0.90` ⇒ `Δlogit = ln(0.90/0.10) ≈ + 2.19722`. + +All envs given identical `Δlogit` so `aux_conf_p50` lands at the +target value exactly (and `aux_conf_std = 0`). First-observation +REPLACE bootstrap promotes the synthetic per-step `aux_conf_p50` +directly to `aux_conf_p50_ema` (no Wiener-α blending — `obs_count = 0` +⇒ replace path). + +### Pearls + invariants honoured + + - `pearl_tests_must_prove_not_lock_observations` — the spec + EXPLICITLY states these operating points as + self-stabilizing-property invariants (warmup gate, confident + state). The assertion is invariant-style ("the formula holds at + these named operating points"), not observed-value-style + ("matches the value seen in some run"). + - `pearl_first_observation_bootstrap` — single chain launch is + sufficient because `aux_conf_p50_ema` cold-starts at 0.0 + (sentinel) and gets REPLACED by the synthetic per-step value on + the first observation. + +### Files modified + +| File | Status | Purpose | +|------|--------|---------| +| `crates/ml/tests/sp20_phase1_4_wireup_test.rs` | +1 test | `target_hold_pct_behavioral_spot_checks` | +| `docs/dqn-wire-up-audit.md` | This entry | Audit log | + +### Verification + +``` +SQLX_OFFLINE=true cargo check -p ml --tests +SQLX_OFFLINE=true CUDA_COMPUTE_CAP=86 cargo test -p ml \ + --test sp20_phase1_4_wireup_test --features cuda \ + -- --ignored --nocapture target_hold_pct_behavioral_spot_checks +``` + +NO production code changes — `TARGET_HOLD_PCT` controller was wired +into `sp20_controllers_compute_kernel.cu` in Phase 1.3 (lines 164- +173) atomically with the other 5 controllers. This commit is +test-only, satisfying the plan's Task 3.3 requirement ("verify the +wire path is complete and add a behavioral test"). + +### Plan accuracy errata + +See `docs/superpowers/plans/2026-05-09-sp19-20-wr-first.md` Phase 3 +Errata Gap 8 for the plan-vs-reality decision rationale. + ## 2026-05-09 — SP20 Phase 2 Task 2.2: SP12 v3 reward block replacement + per-env alpha plumbing (atomic) Lands the SP20 4-quadrant reward at the `experience_env_step::