2026-06-13 20:29:53 +02:00
|
|
|
"""V3e: shadow-decision journal — DDL-shape parity, sink emission, reject-at-source."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
import sys
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
from types import SimpleNamespace
|
|
|
|
|
|
|
|
|
|
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
|
|
|
|
|
|
|
|
|
from prod.clean_arch.violet.decision_engine import ShadowDecision
|
|
|
|
|
from prod.clean_arch.violet.shadow_journal import DecisionRow, TABLE, VioletDecisionJournal
|
|
|
|
|
|
|
|
|
|
DDL = Path("/mnt/dolphinng5_predict/prod/clickhouse/violet/22_violet_decisions.sql")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _ddl_columns() -> set:
|
|
|
|
|
body = DDL.read_text()
|
|
|
|
|
inner = body[body.index("("): body.rindex(")")]
|
|
|
|
|
return set(re.findall(r"`(\w+)`", inner))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _decision(**over):
|
|
|
|
|
base = dict(
|
|
|
|
|
ts_ns=10**12, scan_number=7, asset="BTCUSDT", side="SHORT", vel_div=-0.2,
|
|
|
|
|
fraction=0.2, conviction_leverage=9.0, notional_fraction=1.8,
|
|
|
|
|
target_exposure=124200.0, ars_score=1.48, bucket_idx=1, actuated=True,
|
|
|
|
|
)
|
|
|
|
|
base.update(over)
|
|
|
|
|
return ShadowDecision(**base)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_decisionrow_fields_equal_ddl_columns():
|
|
|
|
|
assert set(DecisionRow.model_fields.keys()) == _ddl_columns()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_journal_emits_validated_row_to_sink():
|
|
|
|
|
captured = []
|
|
|
|
|
j = VioletDecisionJournal(sink=lambda t, r: captured.append((t, r)), session_id="sess1")
|
|
|
|
|
ok = j.journal(_decision(), mono_ns=999)
|
|
|
|
|
assert ok and j.rows_emitted == 1
|
|
|
|
|
table, row = captured[0]
|
|
|
|
|
assert table == TABLE
|
|
|
|
|
assert set(row.keys()) == _ddl_columns()
|
|
|
|
|
assert row["asset"] == "BTCUSDT" and row["actuated"] == 1
|
|
|
|
|
assert row["session_id"] == "sess1" and row["mono_ns"] == 999
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_reject_at_source_on_malformed_decision():
|
|
|
|
|
captured = []
|
|
|
|
|
j = VioletDecisionJournal(sink=lambda t, r: captured.append(r), session_id="s")
|
|
|
|
|
# duck-typed bad decision: non-finite vel_div must be rejected before the sink.
|
|
|
|
|
bad = SimpleNamespace(
|
|
|
|
|
scan_number=1, asset="BTCUSDT", side="SHORT", vel_div=float("nan"),
|
|
|
|
|
fraction=0.2, conviction_leverage=9.0, notional_fraction=1.8,
|
|
|
|
|
target_exposure=1.0, ars_score=1.0, bucket_idx=1, actuated=True,
|
|
|
|
|
)
|
|
|
|
|
ok = j.journal(bad, mono_ns=1)
|
|
|
|
|
assert ok is False
|
|
|
|
|
assert j.rows_rejected == 1 and j.rows_emitted == 0
|
|
|
|
|
assert captured == [] # nothing reached the spool
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_negative_exposure_rejected():
|
|
|
|
|
j = VioletDecisionJournal(sink=lambda t, r: None, session_id="s")
|
|
|
|
|
bad = SimpleNamespace(
|
|
|
|
|
scan_number=1, asset="BTCUSDT", side="SHORT", vel_div=-0.2,
|
|
|
|
|
fraction=0.2, conviction_leverage=9.0, notional_fraction=1.8,
|
|
|
|
|
target_exposure=-5.0, ars_score=1.0, bucket_idx=1, actuated=True,
|
|
|
|
|
)
|
|
|
|
|
assert j.journal(bad, mono_ns=1) is False
|
|
|
|
|
assert j.rows_rejected == 1
|
2026-06-16 13:04:30 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
_BREAKDOWN_COLS = (
|
|
|
|
|
"base_leverage", "dc_lev_mult", "regime_size_mult",
|
|
|
|
|
"market_ob_mult", "esof_size_mult",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_full_factor_breakdown_round_trips_into_row():
|
|
|
|
|
captured = []
|
|
|
|
|
j = VioletDecisionJournal(sink=lambda t, r: captured.append(r), session_id="s")
|
|
|
|
|
dec = _decision(
|
|
|
|
|
base_leverage=4.0, dc_lev_mult=1.5, regime_size_mult=1.2,
|
|
|
|
|
market_ob_mult=1.4, esof_size_mult=0.95,
|
|
|
|
|
)
|
|
|
|
|
assert j.journal(dec, mono_ns=1) and j.rows_emitted == 1
|
|
|
|
|
row = captured[0]
|
|
|
|
|
assert set(row.keys()) == _ddl_columns() # parity holds with new columns
|
|
|
|
|
assert row["base_leverage"] == 4.0 and row["dc_lev_mult"] == 1.5
|
|
|
|
|
assert row["regime_size_mult"] == 1.2 and row["market_ob_mult"] == 1.4
|
|
|
|
|
assert row["esof_size_mult"] == 0.95
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_base_only_path_leaves_breakdown_null():
|
|
|
|
|
captured = []
|
|
|
|
|
j = VioletDecisionJournal(sink=lambda t, r: captured.append(r), session_id="s")
|
|
|
|
|
assert j.journal(_decision(), mono_ns=1) # no breakdown supplied
|
|
|
|
|
row = captured[0]
|
|
|
|
|
assert all(row[c] is None for c in _BREAKDOWN_COLS)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_negative_breakdown_multiplier_rejected_at_source():
|
|
|
|
|
j = VioletDecisionJournal(sink=lambda t, r: None, session_id="s")
|
|
|
|
|
# ShadowDecision itself guards ge=0.0, so build a duck-typed decision that
|
|
|
|
|
# smuggles a negative multiplier past the engine to prove the row guard catches it.
|
|
|
|
|
bad = SimpleNamespace(
|
|
|
|
|
scan_number=1, asset="BTCUSDT", side="SHORT", vel_div=-0.2,
|
|
|
|
|
fraction=0.2, conviction_leverage=9.0, notional_fraction=1.8,
|
|
|
|
|
target_exposure=1.0, ars_score=1.0, bucket_idx=1, actuated=True,
|
|
|
|
|
base_leverage=4.0, dc_lev_mult=-0.5, regime_size_mult=1.0,
|
|
|
|
|
market_ob_mult=1.0, esof_size_mult=1.0,
|
|
|
|
|
)
|
|
|
|
|
assert j.journal(bad, mono_ns=1) is False
|
|
|
|
|
assert j.rows_rejected == 1
|