fix(aux): three bugs causing systematic anti-correlation (F1+F2)
F-series investigations triangulated TWO real bugs that together explained: aux BCE > ln(2) chance baseline, sub-chance dir_acc with balanced labels, and bit-identical training across vastly different POS_WEIGHT_MAX values. BUG #1 (F2) — One-sided book contamination in mid_price_f32: - crates/ml-alpha/src/data/loader.rs::mid_price_f32 blindly averaged bid_px[0] + ask_px[0] without checking validity. MBP-10 snapshots at session boundaries / halts / stale level-0 vacancies have bid_px=0 or ask_px=0; result was mid = real_price/2 (~2750 for ES) or mid = 0 (both sides empty). - generate_outcome_labels_ab:218-220 only guarded is_finite() (NOT > 0). Sister fn generate_labels:75 does check both — asymmetry between parallel generators. - Transitions like mid=0 → mid=5500 produced ΔP=5500, instantly satisfying delta > 2×cost → spuriously triggering y_prof=1. Each contaminated snapshot tainted up to 2K downstream labels (appears as p_t for K positions and as p_kt for K positions). - With ~10-20% one-sided books in real ES, ~35-45% of K=10 labels spuriously positive — exactly matching the observed pos_fraction (0.35, 0.39, 0.46 for long). Fix (broader): mid_price_f32 returns NaN for one-sided/empty books at the source. Cascades to ALL downstream consumers (regime features, snap_features encoder, labels). Defensive guard also added in multi_horizon_labels.rs:218 for direct-test callers bypassing the loader. BUG #2 (F1+F3) — dir_acc metric structural bias: - perception.rs:3647 match condition for flat-true bucket required float-EXACT equality on raw logits (`pred_diff == 0.0`) — essentially unreachable for continuous-valued logits. - On real ES, ~30-50% of samples are flat (neither direction profitable at 2×cost threshold). ALL counted as misses → random baseline depressed to ~0.35 (matching observed 0.32-0.45). BCE can DROP while dir_acc DROPS — decoupled metrics. - aux_lift_dir_acc_threshold=0.85 was structurally UNREACHABLE under this metric regardless of model quality. Fix: skip flat-true samples (`if true_diff == 0 { continue; }`). Converts dir_acc into "given a directional outcome, did we get the direction right?" with proper random baseline 0.5. F3 + F4 confirmed: aux head wiring (8 head Adam optimizers, fwd/bwd kernel arg order, BCE+sigmoid gradient sign, reduce_axis0 reduction) is correct. The synthetic perception_overfit test stays in chance regime because it constructs constant prof_long=1, prof_short=0 → no zero-priced snapshots, no flat-true bucket. Production-data path divergence is the canonical pattern in pearl_canary_input_freshness_launch_order. cargo check --workspace --all-targets: clean. cargo test -p ml-alpha --lib multi_horizon_labels: 15 passed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -507,11 +507,26 @@ impl MultiHorizonLoader {
|
||||
}
|
||||
}
|
||||
|
||||
/// Mid price at level 0; returns NaN for one-sided / empty books per
|
||||
/// the F2 finding (2026-05-22). MBP-10 snapshots at session boundaries,
|
||||
/// halts, or stale level-0 vacancies have `bid_px = 0` or `ask_px = 0`;
|
||||
/// the previous blind average produced `mid = real_price / 2 ≈ 2750`
|
||||
/// (single side) or `mid = 0` (both sides empty). Downstream label
|
||||
/// generation in `multi_horizon_labels::generate_outcome_labels_ab`
|
||||
/// only guarded `is_finite()` (not `> 0`), so transitions from mid=0
|
||||
/// to mid≈5500 produced `ΔP ≈ 5500`, spuriously satisfying the cost
|
||||
/// threshold and inflating `pos_fraction` from the expected ~5% to
|
||||
/// the observed ~40%. NaN return cascades through label generation
|
||||
/// and the BCE NaN-mask handles it cleanly.
|
||||
fn mid_price_f32(s: &Mbp10Snapshot) -> f32 {
|
||||
let l = s.levels.first().copied().unwrap_or_else(BidAskPair::empty);
|
||||
let bid = BidAskPair::price_to_f64(l.bid_px);
|
||||
let ask = BidAskPair::price_to_f64(l.ask_px);
|
||||
(0.5 * (bid + ask)) as f32
|
||||
if bid > 0.0 && ask > 0.0 {
|
||||
(0.5 * (bid + ask)) as f32
|
||||
} else {
|
||||
f32::NAN
|
||||
}
|
||||
}
|
||||
|
||||
fn convert(
|
||||
|
||||
@@ -215,7 +215,13 @@ pub fn generate_outcome_labels_ab(
|
||||
for t in 0..n - k {
|
||||
let p_t = prices[t];
|
||||
let p_kt = prices[t + k];
|
||||
if !p_t.is_finite() || !p_kt.is_finite() {
|
||||
// F2 fix (2026-05-22): also reject p <= 0 to match `generate_labels:75`.
|
||||
// mid_price_f32 in loader.rs now returns NaN for one-sided books, so
|
||||
// the is_finite guard alone would suffice — this is belt+suspenders
|
||||
// for callers that bypass the loader (e.g. direct unit tests passing
|
||||
// raw price arrays). Without the `> 0` guard a transition from
|
||||
// mid=0 to mid≈5500 produces ΔP=5500, spuriously triggering y_prof=1.
|
||||
if !p_t.is_finite() || !p_kt.is_finite() || p_t <= 0.0 || p_kt <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let delta = p_kt - p_t;
|
||||
|
||||
@@ -3641,10 +3641,24 @@ impl PerceptionTrainer {
|
||||
continue;
|
||||
}
|
||||
let true_diff = lt - st;
|
||||
// F1 fix (2026-05-22): skip flat-true samples
|
||||
// (`y_prof_long == y_prof_short`). On real ES data
|
||||
// ~30-50% of samples are flat (neither direction
|
||||
// profitable). The prior `true_diff == 0 &&
|
||||
// pred_diff == 0.0` match condition required
|
||||
// float-exact equality on raw logits — unreachable
|
||||
// for continuous-valued logits, so all flat samples
|
||||
// counted as misses and depressed dir_acc to ~0.35
|
||||
// (matching observed 0.32-0.45). Skipping them
|
||||
// converts dir_acc into "given a directional
|
||||
// outcome, did we get the direction right?" with
|
||||
// proper random baseline 0.5.
|
||||
if true_diff == 0.0 {
|
||||
continue;
|
||||
}
|
||||
let pred_diff = prof_long[idx] - prof_short[idx];
|
||||
if (true_diff > 0.0 && pred_diff > 0.0)
|
||||
|| (true_diff < 0.0 && pred_diff < 0.0)
|
||||
|| (true_diff == 0.0 && pred_diff == 0.0)
|
||||
{
|
||||
per_h_match[h] += 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user