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:
@@ -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."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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::
|
||||
|
||||
Reference in New Issue
Block a user