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

@@ -40,6 +40,14 @@ class DecisionRow(StrictModel):
ars_score: float = Field(allow_inf_nan=False)
bucket_idx: int = Field(ge=0, le=255) # UInt8
actuated: int = Field(ge=0, le=1)
# V3.4 full-sizing breakdown (Nullable(Float64) in DDL): NULL on the base-only
# path, populated when live SizingFactors drive the decision. ge=0.0 — leverage
# and all four multipliers are non-negative by BLUE's construction.
base_leverage: Optional[float] = Field(default=None, ge=0.0, allow_inf_nan=False)
dc_lev_mult: Optional[float] = Field(default=None, ge=0.0, allow_inf_nan=False)
regime_size_mult: Optional[float] = Field(default=None, ge=0.0, allow_inf_nan=False)
market_ob_mult: Optional[float] = Field(default=None, ge=0.0, allow_inf_nan=False)
esof_size_mult: Optional[float] = Field(default=None, ge=0.0, allow_inf_nan=False)
class VioletDecisionJournal:
@@ -70,6 +78,13 @@ class VioletDecisionJournal:
ars_score=float(decision.ars_score),
bucket_idx=int(decision.bucket_idx),
actuated=1 if decision.actuated else 0,
# getattr-with-None: duck-typed decisions (and the base-only path)
# may omit the breakdown; it stays NULL rather than raising here.
base_leverage=getattr(decision, "base_leverage", None),
dc_lev_mult=getattr(decision, "dc_lev_mult", None),
regime_size_mult=getattr(decision, "regime_size_mult", None),
market_ob_mult=getattr(decision, "market_ob_mult", None),
esof_size_mult=getattr(decision, "esof_size_mult", None),
)
except ValidationError as exc:
self.rows_rejected += 1