"""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