VIOLET V3a: V-TYPES alpha-kernel wrappers (selector/sizer/exit-v7) + parity/drift guards
L1 pure-alpha layer: wrap BLUE's live AlphaAssetSelector/AlphaBetSizer/ AlphaExitEngineV7 behind V-TYPES boundaries (wrap, not reimplement). max_leverage is a required explicit param (live default 5.0 / blue_parity 8.0 / recorded 9.0); smoke + tests confirm max_leverage=9.0 reproduces recorded sizing exactly (notional_fraction 0.20x9=1.8 = recorded our_leverage max). 7 tests pass. Exchange-agnostic: conviction leverage sizes quantity; exchange-lev map is L3. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
87
prod/clean_arch/violet/test_violet_alpha_wrappers.py
Normal file
87
prod/clean_arch/violet/test_violet_alpha_wrappers.py
Normal file
@@ -0,0 +1,87 @@
|
||||
"""V3a: V-TYPES wrappers over BLUE's live alpha kernels — parity + drift guards."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
||||
|
||||
import pytest
|
||||
from hypothesis import given, settings, strategies as st
|
||||
from pydantic import ValidationError
|
||||
|
||||
from prod.clean_arch.violet.alpha_wrappers import (
|
||||
AssetPick, SizeDecision, VioletAssetSelector, VioletBetSizer,
|
||||
)
|
||||
|
||||
|
||||
def _sizer(max_leverage: float = 9.0) -> VioletBetSizer:
|
||||
return VioletBetSizer(base_fraction=0.20, min_leverage=0.5, max_leverage=max_leverage)
|
||||
|
||||
|
||||
def _universe(n: int = 4, bars: int = 60) -> dict:
|
||||
return {
|
||||
f"A{i}USDT": [100.0 * (1 + 0.001 * math.sin(0.1 * t) + 0.0005 * i * t) for t in range(bars)]
|
||||
for i in range(n)
|
||||
}
|
||||
|
||||
|
||||
# ── sizing reproduces the recorded model ──────────────────────────────────────
|
||||
|
||||
def test_notional_fraction_is_fraction_times_conviction():
|
||||
d = _sizer().calculate(capital=69_000.0, vel_div=-0.05, trade_direction=-1)
|
||||
assert d.notional_fraction == pytest.approx(d.fraction * d.conviction_leverage)
|
||||
|
||||
|
||||
def test_extreme_short_signal_saturates_at_max_leverage_9():
|
||||
# vel_div at/under extreme drives conviction to the cap; with max_leverage=9
|
||||
# and base_fraction 0.20, notional_fraction == 1.8 — the recorded our_leverage max.
|
||||
d = _sizer(9.0).calculate(capital=69_000.0, vel_div=-0.20, trade_direction=-1)
|
||||
assert d.conviction_leverage == pytest.approx(9.0)
|
||||
assert d.notional_fraction == pytest.approx(1.8)
|
||||
|
||||
|
||||
def test_max_leverage_parameterization_is_live_not_a_default():
|
||||
# The drift guard: the kernel default is 5.0, blue_parity passes 8.0, live is
|
||||
# 9.0. Passing different caps MUST change the saturated conviction — proving
|
||||
# VIOLET never inherits a hardcoded/drifted value.
|
||||
cap5 = _sizer(5.0).calculate(capital=69_000.0, vel_div=-0.20, trade_direction=-1)
|
||||
cap9 = _sizer(9.0).calculate(capital=69_000.0, vel_div=-0.20, trade_direction=-1)
|
||||
assert cap5.conviction_leverage == pytest.approx(5.0)
|
||||
assert cap9.conviction_leverage == pytest.approx(9.0)
|
||||
assert cap5.conviction_leverage != cap9.conviction_leverage
|
||||
|
||||
|
||||
def test_size_decision_is_typed_and_frozen():
|
||||
d = _sizer().calculate(capital=50_000.0, vel_div=-0.04, trade_direction=-1)
|
||||
assert isinstance(d, SizeDecision)
|
||||
with pytest.raises(ValidationError):
|
||||
d.fraction = 0.5 # frozen
|
||||
|
||||
|
||||
# ── selection returns a typed pick or None ────────────────────────────────────
|
||||
|
||||
def test_selector_returns_typed_short_pick():
|
||||
pick = VioletAssetSelector(lookback_horizon=50).pick(_universe(), regime_direction=-1)
|
||||
if pick is not None:
|
||||
assert isinstance(pick, AssetPick)
|
||||
assert pick.side == "SHORT"
|
||||
|
||||
|
||||
def test_selector_empty_universe_is_none():
|
||||
assert VioletAssetSelector(lookback_horizon=50).pick({}, regime_direction=-1) is None
|
||||
|
||||
|
||||
# ── property: conviction always within the kernel envelope; output finite ─────
|
||||
|
||||
@given(
|
||||
vel_div=st.floats(min_value=-0.5, max_value=0.5, allow_nan=False, allow_infinity=False),
|
||||
cap=st.floats(min_value=1.0, max_value=1e7, allow_nan=False, allow_infinity=False),
|
||||
)
|
||||
@settings(max_examples=60, deadline=None)
|
||||
def test_conviction_within_envelope_and_finite(vel_div, cap):
|
||||
d = _sizer(9.0).calculate(capital=cap, vel_div=vel_div, trade_direction=-1)
|
||||
assert 0.0 <= d.conviction_leverage <= 9.0 + 1e-9
|
||||
assert math.isfinite(d.notional_fraction) and d.notional_fraction >= 0.0
|
||||
assert 0 <= d.bucket_idx <= 3
|
||||
Reference in New Issue
Block a user