VIOLET V3.4: integrate full 5-factor VioletSizer into VioletDecisionEngine

Additive (non-breaking): decide(factors=None) keeps the V3a base-only path (existing
11 tests unchanged); decide(factors=SizingFactors(...)) produces BLUE-complete
conviction via VioletSizer (base_max=8 + dc/regime(ACB)/ob/esof, capped@9) with the
full factor breakdown on ShadowDecision (base_leverage/dc_lev_mult/regime_size_mult/
market_ob_mult/esof_size_mult, None on the base path). SizingFactors value object =
the live-plane inputs the launcher will source (V3.4b). 6 new tests incl. consistency
vs VioletSizer, STALKER cap, EsoF-stale haircut. 17 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-15 23:35:00 +02:00
parent f1ee1368d2
commit a97bb90bf6
2 changed files with 137 additions and 4 deletions

View File

@@ -13,8 +13,9 @@ from pathlib import Path
from prod.clean_arch.violet.cadence import Action, CadenceControlPlane, INSTA_Q_NS, SCAN_Q_NS
from prod.clean_arch.violet.decision_engine import (
STABLECOIN_SYMBOLS, ShadowDecision, VioletDecisionEngine,
STABLECOIN_SYMBOLS, ShadowDecision, SizingFactors, VioletDecisionEngine,
)
from prod.clean_arch.violet.sizing import VioletSizer
LOOKBACK = 5
@@ -140,3 +141,74 @@ def test_determinism_same_inputs_same_decision():
assert (d1 is None) == (d2 is None)
if d1 is not None:
assert d1.model_dump() == d2.model_dump()
# ── V3.4: full 5-factor sizing path (SizingFactors → VioletSizer) ──────────────
def _full_factors(**kw):
base = dict(boost=1.3, beta=0.8, mc_scale=1.0, esof_score=0.3,
ob_median_imbalance=0.5, ob_agreement_pct=0.90,
dc_status="NONE", posture="APEX")
base.update(kw)
return SizingFactors(**base)
def test_sizing_factors_neutral_defaults():
f = SizingFactors()
assert f.boost == 1.0 and f.beta == 0.0 and f.mc_scale == 1.0
assert f.esof_score is None and f.dc_status == "NONE" and f.posture == "APEX"
def test_base_path_leaves_breakdown_none():
e = _engine(); _warm(e)
d = e.decide(now_ns=10**12, scan_number=99, capital=69_000.0, vel_div=-0.20)
if d is not None:
assert d.regime_size_mult is None and d.market_ob_mult is None
assert d.base_leverage is None and d.dc_lev_mult is None and d.esof_size_mult is None
def test_full_path_populates_breakdown_and_caps():
e = _engine(); _warm(e)
d = e.decide(now_ns=10**12, scan_number=99, capital=69_000.0, vel_div=-0.20,
factors=_full_factors())
if d is not None:
for v in (d.base_leverage, d.dc_lev_mult, d.regime_size_mult,
d.market_ob_mult, d.esof_size_mult):
assert v is not None
assert d.base_leverage <= 8.0 + 1e-9 # VioletSizer base_max=8
assert 0.0 <= d.conviction_leverage <= 9.0 + 1e-9 # capped @ abs_max
def test_full_conviction_matches_violet_sizer_directly():
# engine's full conviction == VioletSizer.size() on the same inputs (consistency).
e = _engine(); _warm(e)
f = _full_factors()
d = e.decide(now_ns=10**12, scan_number=99, capital=69_000.0, vel_div=-0.20, factors=f)
if d is not None:
vs = VioletSizer(base_fraction=0.20, min_leverage=0.5, base_max_leverage=8.0,
abs_max_leverage=9.0, vel_div_threshold=-0.02)
direct = vs.size(capital=69_000.0, vel_div=-0.20, boost=f.boost, beta=f.beta,
mc_scale=f.mc_scale, esof_score=f.esof_score,
ob_median_imbalance=f.ob_median_imbalance,
ob_agreement_pct=f.ob_agreement_pct, dc_status=f.dc_status,
posture=f.posture, trade_direction=-1)
assert d.conviction_leverage == direct.decision.conviction_leverage
def test_stalker_posture_caps_full_conviction_at_2():
e = _engine(); _warm(e)
d = e.decide(now_ns=10**12, scan_number=99, capital=69_000.0, vel_div=-0.20,
factors=_full_factors(posture="STALKER"))
if d is not None:
assert d.conviction_leverage <= 2.0 + 1e-9
def test_full_path_esof_stale_haircuts_below_base():
# esof_score=None -> stale fallback (<1) -> conviction at/below base (min-floored).
e = _engine(); _warm(e)
d = e.decide(now_ns=10**12, scan_number=99, capital=69_000.0, vel_div=-0.025,
factors=_full_factors(esof_score=None, boost=1.0, beta=0.0,
ob_median_imbalance=None, ob_agreement_pct=None))
if d is not None:
assert d.esof_size_mult < 1.0
assert d.conviction_leverage <= d.base_leverage + 1e-9