feat(sp20): Phase 3 Task 3.3 — target_hold_pct behavioral spot-checks

Adds an integration-style 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 = 0.05 → target_hold_pct = 0.725
  - Confident: aux_conf_p50 = 0.40 → target_hold_pct = 0.20

Both exercise `clamp(0.8 - aux_p50 × 1.5, 0.1, 0.8)` at non-
saturating points. The existing
sp20_controllers_compute_test::target_hold_pct_inverse_relation unit
test covers endpoints (aux_p50 ∈ {0.0, 0.5, 0.2}); this test asserts
the formula holds end-to-end through all 4 kernels.

Engineering aux_conf:
  K=2 logits [a, b] ⇒ peak_softmax = sigmoid(b-a) ⇒ aux_conf = peak-0.5
  aux_conf=0.05 ⇒ Δlogit = ln(0.55/0.45) ≈ 0.20067
  aux_conf=0.40 ⇒ Δlogit = ln(0.90/0.10) ≈ 2.19722
All envs given identical Δlogit so aux_conf_p50 lands exactly.

Per `pearl_tests_must_prove_not_lock_observations`: spec §4.2 names
these operating points as self-stabilizing-property invariants, so
the assertion is invariant-style not observed-value-style.

NO production code changes. TARGET_HOLD_PCT controller was wired in
Phase 1.3 (sp20_controllers_compute_kernel.cu:164-173) atomically
with the other 5 controllers. This commit is test-only, satisfying
Task 3.3's "verify the wire path is complete and add a behavioral
test" requirement.

See plan errata Gap 8 for the plan-vs-reality decision rationale.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
jgrusewski
2026-05-10 01:24:55 +02:00
parent a5ee49e8ab
commit b6f6d4fd02
2 changed files with 176 additions and 0 deletions

View File

@@ -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."
);
}
}

View File

@@ -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::