feat(sp21): T2.2 Phase 8 — signal-drive E2+E5 controller gains via val_sharpe_std (atomic)
Eliminates remaining hardcoded controller GAINS in enrichment.rs per pearl_controller_anchors_isv_driven. Both E2 (compute_adaptive_epsilon) and E5 (compute_agreement_threshold) now derive gain magnitudes from val_sharpe_std = √ISV[VAL_SHARPE_VAR_EMA_INDEX=351] — same signal source as the early-stopping pipeline. Phase 2 already signal-drove the anchors; this commit closes the GAIN half. NO new ISV slots. NO kernel changes. NO ISV_TOTAL_DIM bump. Pure value-driven refactor of two enrichment functions. E2 transformation: - Bracket anchors 2.0/0.5/-0.5 → 2.0×std / 0.5×std / -0.5×std - Multiplicative gains 0.8/0.95/1.2 → (1 ± gain_mag) and (1 - 0.5×gain_mag) - gain_mag = val_sharpe_std.clamp(0.05, 0.30) (Invariant 1 carve-out) - Cold-start (var_ema==0) → pass-through E5 transformation: - Tighten step 0.9 → (1 - gain_mag) - Loosen step 1.1 → (1 + gain_mag) - Same gain_mag formula as E2 (consistency) Invariant 1 carve-outs explicitly retained (project-wide priors): - [0.05, 0.30] gain_mag stability clamp (mirrors Wiener-α floor) - [0.85, 0.98] E3 gamma support range (trading-frequency prior) - [0.5, 2.0] E4 per-branch LR multiplier (collapse/divergence guard) Files changed: - crates/ml/src/trainers/dqn/trainer/enrichment.rs: E2 takes new val_sharpe_var_ema arg; both E2 and E5 derive gains from val_sharpe_std; run_enrichments call-site arg added - docs/dqn-wire-up-audit.md: 2026-05-11 audit entry Verification (passing): - cargo check -p ml --tests --features cuda: 0 errors - cargo test -p ml --lib sp21_isv_slots: 3/3 - sp20_aggregate_inputs_test: 12/12 - sp20_phase1_4_wireup_test: 2/2 - sp20_emas_compute_test: 4/4 - sp20_controllers_compute_test: 7/7 - sp21_per_trade_predicted_q_test: 3/3 Total: 34 tests, 0 failures. SP21 T2.2 cascade COMPLETE — all 8 atomic phases landed (1.5, 2, 3, 4, 4.5, 5+6, 7, 8). Remaining future work out of T2.2 scope: - Phase 6.5 (deferred): true E7 hindsight synthetic injection - Phase 7.5 (deferred): true E8 per-segment PER sampling Next operational step: dispatch L40S smoke training run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -123,15 +123,41 @@ fn compute_q_correction(trades: &[EvalTrade]) -> f32 {
|
||||
}
|
||||
|
||||
/// E2: Adaptive epsilon — performance-driven exploration rate.
|
||||
fn compute_adaptive_epsilon(current: f32, val_sharpe: f32) -> f32 {
|
||||
let scale = if val_sharpe > 2.0 {
|
||||
0.8
|
||||
} else if val_sharpe > 0.5 {
|
||||
0.95
|
||||
} else if val_sharpe < -0.5 {
|
||||
1.2
|
||||
///
|
||||
/// **Phase 8 refactor (2026-05-11)** — both ANCHORS (val_sharpe
|
||||
/// brackets) and GAINS (multiplicative step sizes) are now ISV-
|
||||
/// driven from `val_sharpe_var_ema` (slot 351). The brackets are
|
||||
/// scaled to multiples of `val_sharpe_std = √var_ema` so the
|
||||
/// "excellent / good / poor" thresholds adapt to the run's noise
|
||||
/// floor instead of using static `2.0 / 0.5 / -0.5` constants.
|
||||
/// The gain magnitude scales with `val_sharpe_std` too — the
|
||||
/// controller responds more aggressively when val Sharpe is
|
||||
/// noisier (sharp adjustments) and gently when stable.
|
||||
/// Cold-start (`var_ema == 0`): pass-through (no-op) per
|
||||
/// `pearl_first_observation_bootstrap`. The `[0.05, 0.30]` clamp
|
||||
/// on gain magnitude is an Invariant 1 carve-out — single-step
|
||||
/// gains outside that range produce instability; structural
|
||||
/// floor/cap mirrors `pearl_wiener_alpha_floor_for_nonstationary`.
|
||||
fn compute_adaptive_epsilon(current: f32, val_sharpe: f32, val_sharpe_var_ema: f32) -> f32 {
|
||||
let val_sharpe_std = val_sharpe_var_ema.max(0.0).sqrt();
|
||||
if val_sharpe_std <= 1e-6 {
|
||||
// Cold-start: no signal yet, return current unchanged.
|
||||
return current.clamp(0.01, 0.15);
|
||||
}
|
||||
// ISV-driven anchors: brackets scale with val_sharpe noise floor.
|
||||
let high_anchor = 2.0 * val_sharpe_std;
|
||||
let good_anchor = 0.5 * val_sharpe_std;
|
||||
let poor_anchor = -0.5 * val_sharpe_std;
|
||||
// ISV-driven gain magnitude (Invariant 1 stability carve-out).
|
||||
let gain_mag = val_sharpe_std.clamp(0.05, 0.30);
|
||||
let scale = if val_sharpe > high_anchor {
|
||||
1.0 - gain_mag // excellent → reduce exploration
|
||||
} else if val_sharpe > good_anchor {
|
||||
1.0 - 0.5 * gain_mag // good → slight reduction
|
||||
} else if val_sharpe < poor_anchor {
|
||||
1.0 + gain_mag // poor → increase exploration
|
||||
} else {
|
||||
1.0
|
||||
1.0 // mediocre → unchanged
|
||||
};
|
||||
(current * scale).clamp(0.01, 0.15)
|
||||
}
|
||||
@@ -250,15 +276,21 @@ fn compute_agreement_threshold(
|
||||
// ISV-driven anchor: spread > 1σ of val_sharpe noise = significant
|
||||
// separation between high-conviction and low-conviction trades →
|
||||
// tighten threshold (trust agreement more). Spread within the
|
||||
// noise floor → loosen threshold toward unity (don't over-trust
|
||||
// an agreement signal that isn't producing separation). The
|
||||
// tighten/loosen STEP sizes (0.9/1.1) are controller gains, not
|
||||
// anchors — separate parameter from the ISV-driven decision
|
||||
// boundary.
|
||||
// noise floor → loosen threshold toward unity.
|
||||
//
|
||||
// **Phase 8 refactor (2026-05-11)** — tighten/loosen GAINS are
|
||||
// now ISV-driven too: `gain_mag = val_sharpe_std.clamp(0.05,
|
||||
// 0.30)` replaces the prior hardcoded 0.9 / 1.1 multipliers.
|
||||
// Controller responds more aggressively when val Sharpe is
|
||||
// noisier (large σ → ~30% adjustment) and gently when stable
|
||||
// (small σ → ~5% adjustment). Matches E2's adaptive-gain
|
||||
// pattern; same Invariant 1 carve-out justification for the
|
||||
// `[0.05, 0.30]` stability clamp.
|
||||
let gain_mag = val_sharpe_std.clamp(0.05, 0.30);
|
||||
let new = if spread > val_sharpe_std {
|
||||
current * 0.9
|
||||
current * (1.0 - gain_mag) // tighten
|
||||
} else {
|
||||
current * 1.1
|
||||
current * (1.0 + gain_mag) // loosen
|
||||
};
|
||||
new.clamp(0.01, 10.0)
|
||||
}
|
||||
@@ -442,8 +474,9 @@ pub(crate) fn run_enrichments(
|
||||
// E1: Q-value bias correction
|
||||
let q_correction = compute_q_correction(eval_trades);
|
||||
|
||||
// E2: Adaptive epsilon
|
||||
let epsilon = compute_adaptive_epsilon(state.epsilon, val_sharpe);
|
||||
// E2: Adaptive epsilon (SP21 Phase 8: gains+anchors signal-driven
|
||||
// via val_sharpe_var_ema, same ISV slot E5 uses for its anchor).
|
||||
let epsilon = compute_adaptive_epsilon(state.epsilon, val_sharpe, val_sharpe_var_ema);
|
||||
state.epsilon = epsilon;
|
||||
|
||||
// E3: Dynamic gamma
|
||||
|
||||
Reference in New Issue
Block a user