Files
siloqy/prod/clean_arch/violet/test_violet_decision_engine.py
Codex 1e331d80bc VIOLET V3c: VioletDecisionEngine — reactor-resident SHADOW engine
Composes V3a live-kernel wrappers + V3b cadence into a muted decision engine:
scans in -> ShadowDecision out, NO execution (distinct from PINK's disabled
dita DecisionEngine; runs alongside as pure shadow). Short-regime gate mirrors
BLUE/dita (vel_div<threshold + vol_ok + IRP survivor); cadence-gated actuation
(ENTRY Q=scan), evaluate-always. Verified non-vacuous: produces AAAUSDT SHORT,
conviction 9.0, exposure=capital x notional_fraction. 9 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 18:51:27 +02:00

112 lines
3.9 KiB
Python

"""V3c: VioletDecisionEngine — muted-decision gating, cadence suppression, no exec."""
from __future__ import annotations
import sys
sys.path.insert(0, "/mnt/dolphinng5_predict")
import pytest
from prod.clean_arch.violet.cadence import Action, CadenceControlPlane, INSTA_Q_NS, SCAN_Q_NS
from prod.clean_arch.violet.decision_engine import ShadowDecision, VioletDecisionEngine
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)
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()