diff --git a/prod/clean_arch/violet/decision_engine.py b/prod/clean_arch/violet/decision_engine.py index ef524f3..441a3a9 100644 --- a/prod/clean_arch/violet/decision_engine.py +++ b/prod/clean_arch/violet/decision_engine.py @@ -31,6 +31,17 @@ from .cadence import Action, CadenceControlPlane from .domain import StrictModel, Symbol, typed +# Stablecoins / pegged assets that must NEVER be selected as a trade asset. +# MUST equal nautilus_event_trader.py:_STABLECOIN_SYMBOLS (BLUE removes these from +# prices_dict before selection, :3906) — asserted by test_violet_decision_engine. +# Following BLUE in all regards: picking stays muted-IRP as BLUE, this is BLUE's +# separate exclusion gate around it. +STABLECOIN_SYMBOLS = frozenset({ + "USDCUSDT", "BUSDUSDT", "FDUSDUSDT", "USDTUSDT", "TUSDUSDT", + "DAIUSDT", "FRAXUSDT", "USDDUSDT", "USTCUSDT", "EURUSDT", +}) + + class ShadowDecision(StrictModel): """One muted decision — what BLUE *would* do this scan. Never executed.""" @@ -107,6 +118,8 @@ class VioletDecisionEngine: if px <= 0: continue sym = str(asset).upper() + if sym in STABLECOIN_SYMBOLS: # BLUE :3906 — never a trade asset + continue hist = self._history.get(sym) if hist is None: hist = deque(maxlen=maxlen) diff --git a/prod/clean_arch/violet/test_violet_decision_engine.py b/prod/clean_arch/violet/test_violet_decision_engine.py index 9c3ea94..5bd0240 100644 --- a/prod/clean_arch/violet/test_violet_decision_engine.py +++ b/prod/clean_arch/violet/test_violet_decision_engine.py @@ -8,8 +8,13 @@ sys.path.insert(0, "/mnt/dolphinng5_predict") import pytest +import re +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 ShadowDecision, VioletDecisionEngine +from prod.clean_arch.violet.decision_engine import ( + STABLECOIN_SYMBOLS, ShadowDecision, VioletDecisionEngine, +) LOOKBACK = 5 @@ -101,6 +106,32 @@ def test_engine_holds_no_venue_or_kernel(): assert not hasattr(e, attr) +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" + + def test_determinism_same_inputs_same_decision(): e1, e2 = _engine(), _engine() _warm(e1); _warm(e2)