VIOLET V3.4: integrate full 5-factor VioletSizer into VioletDecisionEngine

Additive (non-breaking): decide(factors=None) keeps the V3a base-only path (existing
11 tests unchanged); decide(factors=SizingFactors(...)) produces BLUE-complete
conviction via VioletSizer (base_max=8 + dc/regime(ACB)/ob/esof, capped@9) with the
full factor breakdown on ShadowDecision (base_leverage/dc_lev_mult/regime_size_mult/
market_ob_mult/esof_size_mult, None on the base path). SizingFactors value object =
the live-plane inputs the launcher will source (V3.4b). 6 new tests incl. consistency
vs VioletSizer, STALKER cap, EsoF-stale haircut. 17 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-15 23:35:00 +02:00
parent f1ee1368d2
commit a97bb90bf6
2 changed files with 137 additions and 4 deletions

View File

@@ -29,6 +29,7 @@ from pydantic import Field
from .alpha_wrappers import AssetPick, SizeDecision, VioletAssetSelector, VioletBetSizer
from .cadence import Action, CadenceControlPlane
from .domain import StrictModel, Symbol, typed
from .sizing import VioletSizer
# Stablecoins / pegged assets that must NEVER be selected as a trade asset.
@@ -42,6 +43,27 @@ STABLECOIN_SYMBOLS = frozenset({
})
class SizingFactors(StrictModel):
"""Live factor inputs for BLUE's full 5-multiplier sizing (V3.4).
Supplied by the caller (launcher) from the live planes: ACB day-state
(``boost``/``beta`` via AdaptiveCircuitBreaker), MC-Forewarner (``mc_scale``),
EsoF advisory (``esof_score``), OB consensus (``ob_*`` via OBFeatureEngine), the
DC signal (``dc_status``), and the day ``posture``. When ``decide()`` is given
these, it produces BLUE-complete conviction via ``VioletSizer``; when omitted,
``decide()`` uses the V3a base-only sizer (legacy/no-factor path). Defaults are
BLUE's own neutral sentinels (NOT ours) — only finite/non-negative poison guards."""
boost: float = Field(default=1.0, ge=0.0, allow_inf_nan=False)
beta: float = Field(default=0.0, ge=0.0, allow_inf_nan=False)
mc_scale: float = Field(default=1.0, ge=0.0, allow_inf_nan=False)
esof_score: Optional[float] = Field(default=None, allow_inf_nan=False)
ob_median_imbalance: Optional[float] = Field(default=None, allow_inf_nan=False)
ob_agreement_pct: Optional[float] = Field(default=None, allow_inf_nan=False)
dc_status: str = "NONE"
posture: str = "APEX"
class ShadowDecision(StrictModel):
"""One muted decision — what BLUE *would* do this scan. Never executed."""
@@ -57,6 +79,14 @@ class ShadowDecision(StrictModel):
ars_score: float = Field(allow_inf_nan=False)
bucket_idx: int = Field(ge=0, le=3)
actuated: bool
# full-sizing breakdown (V3.4) — populated only when SizingFactors are supplied;
# None for the legacy base-only path. conviction_leverage above is then the FULL
# BLUE conviction (base × dc × regime × ob × esof, capped); these expose the factors.
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 VioletDecisionEngine:
@@ -84,6 +114,14 @@ class VioletDecisionEngine:
base_fraction=base_fraction, min_leverage=min_leverage,
max_leverage=max_leverage, vel_div_threshold=entry_vel_div_threshold,
)
# V3.4 full 5-factor sizer: base_max=8 (soft) + dc/regime(ACB)/ob/esof mults
# lifting toward abs_max. Used when decide() is given live SizingFactors;
# bit-identical to BLUE's esf_alpha_orchestrator composition (see sizing.py).
self.full_sizer = VioletSizer(
base_fraction=base_fraction, min_leverage=min_leverage,
base_max_leverage=8.0, abs_max_leverage=max_leverage,
vel_div_threshold=entry_vel_div_threshold,
)
self.entry_threshold = float(entry_vel_div_threshold)
self.regime_direction = int(regime_direction)
self.lookback = int(lookback) if lookback > 0 else self.selector.lookback
@@ -138,6 +176,7 @@ class VioletDecisionEngine:
def decide(
self, *, now_ns: int, scan_number: int, capital: float,
vel_div: float, vol_ok: bool = True,
factors: Optional[SizingFactors] = None,
) -> Optional[ShadowDecision]:
"""Evaluate the would-be decision (always); actuate only when ENTRY cadence
is due. Returns the ShadowDecision when a short signal fires, else None.
@@ -162,9 +201,30 @@ class VioletDecisionEngine:
self._last_entry_actuation_ns = int(now_ns)
self.actuations += 1
size: SizeDecision = self.sizer.calculate(
capital=capital, vel_div=vel_div, trade_direction=self.regime_direction,
)
if factors is None:
# Legacy base-only path (V3a sizer) — unchanged behavior.
size: SizeDecision = self.sizer.calculate(
capital=capital, vel_div=vel_div, trade_direction=self.regime_direction,
)
extra: Dict[str, float] = {}
else:
# V3.4 full BLUE sizing: base × dc × regime(ACB) × ob × esof, capped @9.
full = self.full_sizer.size(
capital=capital, vel_div=vel_div,
boost=factors.boost, beta=factors.beta, mc_scale=factors.mc_scale,
esof_score=factors.esof_score,
ob_median_imbalance=factors.ob_median_imbalance,
ob_agreement_pct=factors.ob_agreement_pct,
dc_status=factors.dc_status, posture=factors.posture,
trade_direction=self.regime_direction,
)
size = full.decision
b = full.breakdown
extra = dict(
base_leverage=b.base_leverage, dc_lev_mult=b.dc_lev_mult,
regime_size_mult=b.regime_size_mult, market_ob_mult=b.market_ob_mult,
esof_size_mult=b.esof_size_mult,
)
return ShadowDecision(
ts_ns=int(now_ns), scan_number=int(scan_number),
asset=pick.asset, side=pick.side, vel_div=float(vel_div),
@@ -172,4 +232,5 @@ class VioletDecisionEngine:
notional_fraction=size.notional_fraction,
target_exposure=float(capital) * size.notional_fraction,
ars_score=pick.ars_score, bucket_idx=size.bucket_idx, actuated=True,
**extra,
)