2026-06-13 18:51:27 +02:00
|
|
|
"""V3c: VioletDecisionEngine — muted-decision gating, cadence suppression, no exec."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
2026-06-14 21:51:09 +02:00
|
|
|
import re
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
2026-06-13 18:51:27 +02:00
|
|
|
from prod.clean_arch.violet.cadence import Action, CadenceControlPlane, INSTA_Q_NS, SCAN_Q_NS
|
2026-06-14 21:51:09 +02:00
|
|
|
from prod.clean_arch.violet.decision_engine import (
|
2026-06-15 23:35:00 +02:00
|
|
|
STABLECOIN_SYMBOLS, ShadowDecision, SizingFactors, VioletDecisionEngine,
|
2026-06-14 21:51:09 +02:00
|
|
|
)
|
2026-06-15 23:35:00 +02:00
|
|
|
from prod.clean_arch.violet.sizing import VioletSizer
|
2026-06-13 18:51:27 +02:00
|
|
|
|
|
|
|
|
LOOKBACK = 5
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _engine(cp=None, **kw):
|
|
|
|
|
return VioletDecisionEngine(control_plane=cp, lookback=LOOKBACK, **kw)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _warm(engine, n_scans: int = 8):
|
|
|
|
|
"""Feed a short-favorable universe (downtrending) so the selector warms + ranks."""
|
|
|
|
|
assets = ["AAAUSDT", "BBBUSDT", "CCCUSDT"]
|
|
|
|
|
for s in range(n_scans):
|
|
|
|
|
prices = [100.0 * (1 - 0.002 * s - 0.0005 * i) for i in range(len(assets))]
|
|
|
|
|
engine.observe({"assets": assets, "asset_prices": prices}, scan_number=s + 1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_short_signal_produces_shadow_decision():
|
|
|
|
|
e = _engine()
|
|
|
|
|
_warm(e)
|
|
|
|
|
d = e.decide(now_ns=10**12, scan_number=99, capital=69_000.0, vel_div=-0.20, vol_ok=True)
|
|
|
|
|
assert d is None or isinstance(d, ShadowDecision)
|
|
|
|
|
if d is not None:
|
|
|
|
|
assert d.side == "SHORT"
|
|
|
|
|
assert d.target_exposure == pytest.approx(69_000.0 * d.notional_fraction)
|
|
|
|
|
assert d.actuated is True
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_decision_when_vel_div_above_threshold():
|
|
|
|
|
e = _engine()
|
|
|
|
|
_warm(e)
|
|
|
|
|
# vel_div above entry threshold (-0.02) → no short signal
|
|
|
|
|
assert e.decide(now_ns=10**12, scan_number=1, capital=69_000.0, vel_div=0.05) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_decision_when_vol_gate_blocks():
|
|
|
|
|
e = _engine()
|
|
|
|
|
_warm(e)
|
|
|
|
|
assert e.decide(now_ns=10**12, scan_number=1, capital=69_000.0, vel_div=-0.20, vol_ok=False) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_no_decision_before_universe_warm():
|
|
|
|
|
e = _engine()
|
|
|
|
|
e.observe({"assets": ["AAAUSDT"], "asset_prices": [100.0]}, scan_number=1) # 1 bar << lookback
|
|
|
|
|
assert e.decide(now_ns=10**12, scan_number=1, capital=69_000.0, vel_div=-0.20) is None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_cadence_suppresses_repeat_within_quantum():
|
|
|
|
|
cp = CadenceControlPlane() # ENTRY default Q = scan (5s)
|
|
|
|
|
e = _engine(cp=cp)
|
|
|
|
|
_warm(e)
|
|
|
|
|
t0 = 10**12
|
|
|
|
|
d1 = e.decide(now_ns=t0, scan_number=10, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
# second call 1ms later: within the scan quantum → suppressed (no actuation)
|
|
|
|
|
d2 = e.decide(now_ns=t0 + 1_000_000, scan_number=11, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
if d1 is not None:
|
|
|
|
|
assert d2 is None
|
|
|
|
|
assert e.suppressed_by_cadence >= 1
|
|
|
|
|
# after a full scan quantum elapses → actuates again
|
|
|
|
|
d3 = e.decide(now_ns=t0 + SCAN_Q_NS, scan_number=12, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
if d1 is not None:
|
|
|
|
|
assert d3 is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_insta_cadence_actuates_every_call():
|
|
|
|
|
cp = CadenceControlPlane()
|
|
|
|
|
cp.set(Action.ENTRY, q_ns=INSTA_Q_NS)
|
|
|
|
|
e = _engine(cp=cp)
|
|
|
|
|
_warm(e)
|
|
|
|
|
t = 10**12
|
|
|
|
|
d1 = e.decide(now_ns=t, scan_number=1, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
d2 = e.decide(now_ns=t + 1, scan_number=2, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
if d1 is not None:
|
|
|
|
|
assert d2 is not None # insta → no suppression
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_evaluate_always_ge_actuate_invariant():
|
|
|
|
|
e = _engine()
|
|
|
|
|
_warm(e)
|
|
|
|
|
t = 10**12
|
|
|
|
|
for k in range(5):
|
|
|
|
|
e.decide(now_ns=t + k * 1_000_000, scan_number=20 + k, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
assert e.evaluations >= e.actuations
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_engine_holds_no_venue_or_kernel():
|
|
|
|
|
# structural no-execution guarantee: the shadow engine has no venue/kernel/submit.
|
|
|
|
|
e = _engine()
|
|
|
|
|
for attr in ("venue", "kernel", "submit", "submit_intent", "execute"):
|
|
|
|
|
assert not hasattr(e, attr)
|
|
|
|
|
|
|
|
|
|
|
2026-06-14 21:51:09 +02:00
|
|
|
def test_stablecoin_set_matches_blue_exactly():
|
|
|
|
|
# Drift guard: VIOLET's exclusion set MUST equal BLUE's _STABLECOIN_SYMBOLS
|
|
|
|
|
# (nautilus_event_trader.py), parsed from source (no heavy import).
|
|
|
|
|
src = Path("/mnt/dolphinng5_predict/prod/nautilus_event_trader.py").read_text()
|
|
|
|
|
m = re.search(r"_STABLECOIN_SYMBOLS\s*=\s*frozenset\(\{(.*?)\}\)", src, re.DOTALL)
|
|
|
|
|
assert m is not None
|
|
|
|
|
blue = set(re.findall(r"'([A-Z0-9]+)'", m.group(1)))
|
|
|
|
|
assert STABLECOIN_SYMBOLS == blue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_stablecoin_never_selected():
|
|
|
|
|
# Even with a strong short signal, a stablecoin must never be picked (BLUE :3906).
|
|
|
|
|
e = _engine()
|
|
|
|
|
assets = ["USDCUSDT", "BBBUSDT", "CCCUSDT"]
|
|
|
|
|
for s in range(8):
|
|
|
|
|
# USDC trends down hardest (would rank top by IRP if not excluded)
|
|
|
|
|
prices = [100.0 * (1 - 0.01 * s), 100.0 * (1 - 0.001 * s), 100.0 * (1 - 0.0005 * s)]
|
|
|
|
|
e.observe({"assets": assets, "asset_prices": prices}, scan_number=s + 1)
|
|
|
|
|
assert "USDCUSDT" not in e._history # never accumulated
|
|
|
|
|
for k in range(3):
|
|
|
|
|
d = e.decide(now_ns=10**12 + k * SCAN_Q_NS, scan_number=20 + k,
|
|
|
|
|
capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
if d is not None:
|
|
|
|
|
assert d.asset != "USDCUSDT"
|
|
|
|
|
|
|
|
|
|
|
2026-06-13 18:51:27 +02:00
|
|
|
def test_determinism_same_inputs_same_decision():
|
|
|
|
|
e1, e2 = _engine(), _engine()
|
|
|
|
|
_warm(e1); _warm(e2)
|
|
|
|
|
d1 = e1.decide(now_ns=10**12, scan_number=5, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
d2 = e2.decide(now_ns=10**12, scan_number=5, capital=69_000.0, vel_div=-0.20)
|
|
|
|
|
assert (d1 is None) == (d2 is None)
|
|
|
|
|
if d1 is not None:
|
|
|
|
|
assert d1.model_dump() == d2.model_dump()
|
2026-06-15 23:35:00 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# ── 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
|