VIOLET V3.4b/V3e: journal the full-sizing breakdown (DDL + row guard)

ShadowDecision already carries the V3.4 5-factor breakdown (base_leverage,
dc_lev_mult, regime_size_mult, market_ob_mult, esof_size_mult), but the journal
row and CH DDL dropped it — so a DARK soak would record decisions WITHOUT the
factor decomposition that V3.4 exists to expose. Thread it through:

- 22_violet_decisions.sql: 5 additive Nullable(Float64) breakdown columns. NULL
  on the legacy base-only path; populated once the launcher feeds live factor
  planes. Note added: on a pre-existing table use ALTER ... ADD COLUMN instead of
  the CREATE IF NOT EXISTS (no live table yet — VIOLET is DARK, never soaked).
- shadow_journal.py: DecisionRow gains the 5 Optional[float] fields (ge=0.0,
  finite-guarded); journal() populates them via getattr(..., None) so the
  base-only path and duck-typed reject tests stay NULL/rejected rather than
  raising on attribute access.
- test_violet_shadow_journal.py: breakdown round-trips on the full path; NULL on
  base-only; a negative multiplier is rejected at the row guard. The existing
  DecisionRow-fields == DDL-columns parity test still holds with the new columns.

violet-only; no shared-file edits; no soak. 29 violet tests green
(7 journal + 22 engine/DDL-apply).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-16 13:04:30 +02:00
parent 6d08e97e28
commit 722fd9f054
3 changed files with 69 additions and 1 deletions

View File

@@ -71,3 +71,47 @@ def test_negative_exposure_rejected():
)
assert j.journal(bad, mono_ns=1) is False
assert j.rows_rejected == 1
_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