VIOLET V3d: base-sizer parity harness + gate vs recorded BLUE
parity_harness.py: median-curve parity of V3a VioletBetSizer vs recorded dolphin.trade_events (vel_div->leverage), restricted to short-signal domain. GATE PASSES on prod host: pearson 0.9998, max_abs_err 0.238 (budget 1.0) over 23 bins -> base conviction sizer reproduces BLUE's central tendency. Per-trade scatter is the deferred SC/ACB/OB/gold modulation layer (separate finding doc). 3 unit + 1 gate green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
80
prod/clean_arch/violet/test_violet_parity.py
Normal file
80
prod/clean_arch/violet/test_violet_parity.py
Normal file
@@ -0,0 +1,80 @@
|
||||
"""V3d: base-sizer median-curve parity vs recorded BLUE (unit + @gate)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
||||
|
||||
import pytest
|
||||
|
||||
from prod.clean_arch.violet.parity_harness import (
|
||||
ParityReport, base_curve_parity, live_blue_sizer, load_recorded_samples_from_ch,
|
||||
)
|
||||
|
||||
REPORTS_DIR = Path("/mnt/dolphinng5_predict/prod/VIOLET_dev/reports")
|
||||
|
||||
|
||||
def _base_samples(per_bin: int = 12):
|
||||
"""Synthetic samples drawn exactly from the base curve (perfect parity)."""
|
||||
s = live_blue_sizer()
|
||||
out = []
|
||||
for k in range(2, 13): # vd = -0.02 .. -0.12, all on 0.01 bin centers
|
||||
vd = -k / 100.0
|
||||
base = s.calculate(capital=1.0, vel_div=vd, trade_direction=-1).conviction_leverage
|
||||
out.extend([(vd, base)] * per_bin)
|
||||
return out
|
||||
|
||||
|
||||
def test_perfect_base_samples_pass():
|
||||
rep = base_curve_parity(_base_samples(), live_blue_sizer())
|
||||
assert rep.passed
|
||||
assert rep.max_abs_err == pytest.approx(0.0, abs=1e-6)
|
||||
assert rep.pearson_r == pytest.approx(1.0, abs=1e-6)
|
||||
|
||||
|
||||
def test_median_tracks_base_under_minority_haircuts():
|
||||
# majority at base, minority haircut down -> median still tracks base -> passes.
|
||||
s = live_blue_sizer()
|
||||
samples = []
|
||||
for k in range(2, 13): # vd on 0.01 bin centers
|
||||
vd = -k / 100.0
|
||||
base = s.calculate(capital=1.0, vel_div=vd, trade_direction=-1).conviction_leverage
|
||||
samples.extend([(vd, base)] * 8) # 8 at base
|
||||
samples.extend([(vd, base * 0.3)] * 3) # 3 haircut (minority)
|
||||
rep = base_curve_parity(samples, s)
|
||||
assert rep.passed
|
||||
|
||||
|
||||
def test_decorrelated_noise_fails_gate():
|
||||
# leverage independent of vel_div -> low correlation -> gate fails (guard works).
|
||||
import random
|
||||
rng = random.Random(7)
|
||||
samples = [(vd / 1000.0, rng.uniform(0.5, 9.0))
|
||||
for vd in range(-60, -1) for _ in range(12)]
|
||||
rep = base_curve_parity(samples, live_blue_sizer())
|
||||
assert not rep.passed
|
||||
|
||||
|
||||
@pytest.mark.gate
|
||||
def test_base_curve_parity_vs_recorded_blue():
|
||||
"""GATE (prod host): the V3a base sizer reproduces BLUE's recorded median curve."""
|
||||
samples = load_recorded_samples_from_ch(limit=5000)
|
||||
assert len(samples) >= 500, f"too few recorded samples: {len(samples)}"
|
||||
rep = base_curve_parity(samples, live_blue_sizer())
|
||||
|
||||
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
||||
(REPORTS_DIR / f"violet_v3_parity_{ts}.json").write_text(
|
||||
json.dumps(rep.model_dump(), indent=2, default=str))
|
||||
|
||||
assert isinstance(rep, ParityReport)
|
||||
assert rep.n_bins >= 5
|
||||
assert rep.passed, (
|
||||
f"base-curve parity FAILED: max_abs_err={rep.max_abs_err:.3f} "
|
||||
f"(budget {rep.max_abs_err_budget}), pearson={rep.pearson_r:.3f} "
|
||||
f"(budget {rep.pearson_budget})"
|
||||
)
|
||||
Reference in New Issue
Block a user