Reverted a redundant widening of the main MC gate (typical ranges) after confirming
test_gate_mc_extreme_multipliers already bit-identity-tests boost in [1,5], beta in
{0,0.2,0.8,1}, mc in {0,0.5,1}, and the OB agreement boundary (N=200k, exact !=).
Added a cross-reference note. All 6 gates green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1813 lines
73 KiB
Python
1813 lines
73 KiB
Python
"""V3.3: full sizing-parity — VioletSizer composes BLUE's 5-multiplier conviction.
|
|
|
|
Test layers:
|
|
- unit: each factor producer matches BLUE's constants / formula; compose
|
|
applies the caps (soft/abs/STALKER) + floor in the right order.
|
|
- hypothesis: envelope invariants over the joint input space.
|
|
- @gate: (1) MC bit-identity, N>=1e6, vs BLUE's real kernels + the
|
|
orchestrator's own composition transcribed verbatim; (2) the same
|
|
chain driven through the REAL orchestrator ``_try_entry`` on a
|
|
subset (proves the transcription == BLUE's inline code); (3) a DC
|
|
CONFIRM end-to-end case; (4) upstream replay vs recorded
|
|
``dolphin.trade_events``. Gate report -> prod/VIOLET_dev/reports/.
|
|
|
|
Bit-identity is float-for-float (``==``): a statistical match hides composition
|
|
bugs (op-order / rounding / cap); exact equality does not.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import math
|
|
import sys
|
|
import threading
|
|
import time
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Optional, Tuple
|
|
|
|
import numpy as np
|
|
import pytest
|
|
from hypothesis import given, settings, strategies as st
|
|
|
|
sys.path.insert(0, "/mnt/dolphinng5_predict")
|
|
sys.path.insert(0, "/mnt/dolphinng5_predict/nautilus_dolphin")
|
|
|
|
from prod.clean_arch.violet.alpha_wrappers import SizeDecision
|
|
from prod.clean_arch.violet.sizing import (
|
|
FullSizeDecision, SizingBreakdown, VioletSizer,
|
|
)
|
|
|
|
REPORTS_DIR = Path("/mnt/dolphinng5_predict/prod/VIOLET_dev/reports")
|
|
PROJECT_ROOT = Path("/mnt/dolphinng5_predict")
|
|
|
|
|
|
def _sizer(**kw: Any) -> VioletSizer:
|
|
return VioletSizer(**kw)
|
|
|
|
|
|
def _orch():
|
|
"""The real BLUE orchestrator, gold-spec caps, sizing-only config."""
|
|
from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine
|
|
return NDAlphaEngine(
|
|
initial_capital=69000.0, max_leverage=8.0, abs_max_leverage=9.0,
|
|
min_leverage=0.5, fraction=0.20, use_asset_selection=False,
|
|
use_direction_confirm=False,
|
|
)
|
|
|
|
|
|
class _MockOB:
|
|
"""Minimal OBFeatureEngine stand-in returning controlled market consensus."""
|
|
|
|
def __init__(self) -> None:
|
|
from nautilus_dolphin.nautilus.ob_features import (
|
|
OBMarketFeatures, OBPlacementFeatures,
|
|
)
|
|
self._M = OBMarketFeatures
|
|
self._P = OBPlacementFeatures
|
|
self._m = OBMarketFeatures(0.0, 0.5, 0.0)
|
|
|
|
def set(self, imbalance: float, agreement: float) -> None:
|
|
self._m = self._M(imbalance, agreement, 0.0)
|
|
|
|
def get_market(self, ts: float, assets: Any = None) -> Any:
|
|
return self._m
|
|
|
|
def get_placement(self, asset: str, bar_idx: int) -> Any:
|
|
return self._P(1e6, 1.0, 1.0, 1.0)
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# UNIT — factor producers match BLUE's constants / formula
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def test_gold_spec_caps_are_default():
|
|
sz = _sizer()
|
|
assert sz.base_max_leverage == 8.0 # soft cap (FROZEN_ALGO_SPEC §4)
|
|
assert sz.abs_max_leverage == 9.0 # hard cap
|
|
assert sz.min_leverage == 0.5
|
|
|
|
|
|
def test_base_sizer_max_leverage_is_base_soft_cap():
|
|
# The orchestrator builds bet_sizer with max_leverage == base_max_leverage;
|
|
# the sizer's own clamp must match (the boost lifts TOWARD abs, not past soft).
|
|
sz = _sizer()
|
|
assert sz._bet_sizer.max_leverage == 8.0
|
|
|
|
|
|
def test_rejects_base_above_abs():
|
|
with pytest.raises(ValueError):
|
|
_sizer(base_max_leverage=10.0, abs_max_leverage=9.0)
|
|
|
|
|
|
# ── strength_cubic: verbatim orchestrator _strength_cubic ─────────────────────
|
|
|
|
def test_strength_short_boundaries():
|
|
sz = _sizer()
|
|
assert sz.strength_cubic(-0.02) == 0.0 # at threshold -> 0
|
|
assert sz.strength_cubic(-0.05) == 1.0 # at extreme -> 1
|
|
assert sz.strength_cubic(-0.019) == 0.0 # above threshold -> 0
|
|
assert sz.strength_cubic(-1.0) == 1.0 # beyond extreme -> 1
|
|
|
|
|
|
def test_strength_long_boundaries():
|
|
sz = _sizer()
|
|
assert sz.strength_cubic(0.01, trade_direction=1) == 0.0 # threshold
|
|
assert sz.strength_cubic(0.04, trade_direction=1) == 1.0 # extreme
|
|
assert sz.strength_cubic(0.005, trade_direction=1) == 0.0 # below threshold
|
|
|
|
|
|
def test_strength_cubic_matches_orchestrator():
|
|
eng = _orch()
|
|
sz = _sizer()
|
|
for vd in np.linspace(-0.5, -0.021, 50):
|
|
assert sz.strength_cubic(float(vd)) == eng._strength_cubic(float(vd))
|
|
|
|
|
|
# ── regime_size_mult: 3-scale ACB formula ─────────────────────────────────────
|
|
|
|
def test_regime_beta_zero_is_boost_times_mc():
|
|
sz = _sizer()
|
|
assert sz.regime_size_mult(-0.10, boost=1.3, beta=0.0, mc_scale=1.0) == 1.3
|
|
assert sz.regime_size_mult(-0.10, boost=1.5, beta=0.0, mc_scale=0.5) == 0.75
|
|
|
|
|
|
def test_regime_beta_positive_uses_strength_cubed():
|
|
sz = _sizer()
|
|
# strength(-0.10) = 1.0 -> regime = 1.0*(1+0.8*1.0)*1.0 = 1.8
|
|
assert sz.regime_size_mult(-0.10, boost=1.0, beta=0.8, mc_scale=1.0) == 1.8
|
|
# strength(-0.035): raw=(-0.02+0.035)/0.03=0.5 -> 0.5^3=0.125
|
|
s = sz.strength_cubic(-0.035)
|
|
assert sz.regime_size_mult(-0.035, boost=1.0, beta=0.8, mc_scale=1.0) == 1.0 * (1.0 + 0.8 * s) * 1.0
|
|
|
|
|
|
def test_regime_matches_orchestrator_update():
|
|
eng = _orch()
|
|
sz = _sizer()
|
|
for vd in np.linspace(-0.5, -0.021, 40):
|
|
for beta in (0.0, 0.2, 0.8):
|
|
eng._day_base_boost = 1.3
|
|
eng._day_beta = beta
|
|
eng._day_mc_scale = 0.5
|
|
eng._update_regime_size_mult(float(vd))
|
|
assert sz.regime_size_mult(
|
|
float(vd), boost=1.3, beta=beta, mc_scale=0.5
|
|
) == eng.regime_size_mult
|
|
|
|
|
|
# ── esof_size_mult: wraps esof_size_mult_from_score (RAW, no clamp) ────────────
|
|
|
|
def test_esof_band_values():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import (
|
|
ESOF_NEUTRAL_CORE_MULT, ESOF_STALE_FALLBACK_MULT, ESOF_UNFAVORABLE_CORE_MULT,
|
|
)
|
|
sz = _sizer()
|
|
assert sz.esof_size_mult(0.0) == ESOF_NEUTRAL_CORE_MULT # 0.80
|
|
assert sz.esof_size_mult(-0.5) == ESOF_UNFAVORABLE_CORE_MULT # 0.30
|
|
assert sz.esof_size_mult(None) == ESOF_STALE_FALLBACK_MULT # 0.40
|
|
assert sz.esof_size_mult(0.5) == 1.0 # full size
|
|
|
|
|
|
def test_esof_equals_blue_fn_raw():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import esof_size_mult_from_score
|
|
sz = _sizer()
|
|
for sc in [0.3, 0.07, 0.05, 0.03, 0.0, -0.03, -0.07, -0.2, -0.25, -0.3, -1.0, None]:
|
|
assert sz.esof_size_mult(sc) == float(esof_size_mult_from_score(sc))
|
|
|
|
|
|
# ── market_ob_mult: verbatim orchestrator :587-595 ─────────────────────────────
|
|
|
|
def test_ob_no_consensus_is_one():
|
|
sz = _sizer()
|
|
# agreement below 0.70 -> no modulation
|
|
assert sz.market_ob_mult(-0.5, 0.50, trade_direction=-1) == 1.0
|
|
assert sz.market_ob_mult(0.5, 0.69, trade_direction=-1) == 1.0
|
|
# imbalance below 0.08 threshold
|
|
assert sz.market_ob_mult(-0.05, 0.90, trade_direction=-1) == 1.0
|
|
|
|
|
|
def test_ob_short_confirmed_boosts():
|
|
sz = _sizer()
|
|
# SHORT, negative imbalance (sell pressure confirms): eff_imb = +0.5
|
|
m = sz.market_ob_mult(-0.5, 0.90, trade_direction=-1)
|
|
assert m == 1.0 + min(0.20, 0.5 * 0.9 * 0.5)
|
|
assert m == pytest.approx(1.20) # capped at +20%
|
|
|
|
|
|
def test_ob_short_contradicted_haircuts():
|
|
sz = _sizer()
|
|
# SHORT, positive imbalance (contradicts): eff_imb = -0.5
|
|
m = sz.market_ob_mult(0.5, 0.90, trade_direction=-1)
|
|
assert m == max(0.85, 1.0 - 0.5 * 0.9 * 0.3)
|
|
assert m == pytest.approx(0.865)
|
|
|
|
|
|
def test_ob_boost_capped_at_20pct():
|
|
sz = _sizer()
|
|
# extreme imbalance would exceed 20% but is capped
|
|
m = sz.market_ob_mult(-1.0, 1.0, trade_direction=-1)
|
|
assert m == 1.20
|
|
|
|
|
|
def test_ob_haircut_floored_at_85pct():
|
|
sz = _sizer()
|
|
m = sz.market_ob_mult(1.0, 1.0, trade_direction=-1)
|
|
assert m == 0.85
|
|
|
|
|
|
def test_ob_long_flips_sign():
|
|
sz = _sizer()
|
|
# LONG, positive imbalance confirms: eff_imb = +0.5
|
|
assert sz.market_ob_mult(0.5, 0.90, trade_direction=1) == pytest.approx(1.20)
|
|
# LONG, negative imbalance contradicts
|
|
assert sz.market_ob_mult(-0.5, 0.90, trade_direction=1) == pytest.approx(0.865)
|
|
|
|
|
|
# ── dc_lev_mult ────────────────────────────────────────────────────────────────
|
|
|
|
def test_dc_lev_mult_confirm_vs_else():
|
|
sz_default = _sizer()
|
|
assert sz_default.dc_lev_mult("CONFIRM") == 1.0 # default boost
|
|
assert sz_default.dc_lev_mult("NONE") == 1.0
|
|
assert sz_default.dc_lev_mult("NEUTRAL") == 1.0
|
|
|
|
sz_boost = _sizer(dc_leverage_boost=1.25)
|
|
assert sz_boost.dc_lev_mult("CONFIRM") == 1.25
|
|
assert sz_boost.dc_lev_mult("NONE") == 1.0
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# UNIT — compose: caps / floor / STALKER / op-order / notional
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def _base(vel_div: float = -0.20) -> SizeDecision:
|
|
return _sizer().base_size(capital=69000.0, vel_div=vel_div, trade_direction=-1)
|
|
|
|
|
|
def test_compose_identity_multipliers_returns_base_clamped():
|
|
sz = _sizer()
|
|
base = _base(-0.035) # partial strength
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="APEX")
|
|
assert d.conviction_leverage == base.conviction_leverage
|
|
assert d.fraction == base.fraction
|
|
|
|
|
|
def test_compose_abs_cap_9_enforced():
|
|
sz = _sizer()
|
|
base = _base(-0.20) # saturated at 8.0 (base soft cap)
|
|
# huge multipliers would push raw past 9 but abs cap holds
|
|
d = sz.compose(base, dc_lev_mult=1.5, regime_size_mult=1.5,
|
|
market_ob_mult=1.2, esof_size_mult=1.0, posture="APEX")
|
|
assert d.conviction_leverage == 9.0
|
|
|
|
|
|
def test_compose_soft_cap_path():
|
|
sz = _sizer()
|
|
base = _base(-0.035)
|
|
# clamped_max = min(8.0 * 1.0 * 1.0 * 0.8, 9.0) = 6.4
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=0.8, posture="APEX")
|
|
# raw = base * 0.8, clamped_max = 6.4; raw < clamped -> raw wins
|
|
assert d.conviction_leverage == pytest.approx(base.conviction_leverage * 0.8)
|
|
|
|
|
|
def test_compose_stalker_caps_at_2():
|
|
sz = _sizer()
|
|
base = _base(-0.20)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="STALKER")
|
|
assert d.conviction_leverage == 2.0
|
|
|
|
|
|
def test_compose_stalker_floor_wins_when_base_tiny():
|
|
sz = _sizer(min_leverage=0.5)
|
|
base = _base(-0.021) # near-threshold -> base ~ min_leverage
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=0.5,
|
|
market_ob_mult=0.85, esof_size_mult=0.3, posture="STALKER")
|
|
# STALKER cap 2.0, but min_leverage floor 0.5 is higher than the haircut result
|
|
assert d.conviction_leverage == max(0.5, min(
|
|
base.conviction_leverage * 1.0 * 0.5 * 0.85 * 0.3, 2.0))
|
|
|
|
|
|
def test_compose_min_leverage_floor():
|
|
sz = _sizer(min_leverage=0.5)
|
|
base = _base(-0.021)
|
|
# aggressive haircut drives raw below floor
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=0.85, esof_size_mult=0.3, posture="APEX")
|
|
assert d.conviction_leverage == 0.5
|
|
|
|
|
|
def test_compose_preserves_fraction_notional_is_fraction_times_leverage():
|
|
sz = _sizer()
|
|
base = _base(-0.06)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.3,
|
|
market_ob_mult=1.1, esof_size_mult=0.8, posture="APEX")
|
|
assert d.fraction == base.fraction # multipliers scale leverage only
|
|
assert d.notional_fraction == pytest.approx(d.fraction * d.conviction_leverage)
|
|
|
|
|
|
def test_compose_op_order_matches_spec():
|
|
# The spec's exact op order: clamped = min(soft*regime*ob*esof, abs);
|
|
# raw = base*dc*regime*ob*esof; STALKER; leverage=min(raw,clamped); max(min,lev).
|
|
sz = _sizer()
|
|
base = _base(-0.10)
|
|
dc, rsm, obm, esm = 1.0, 1.8, 1.15, 0.8
|
|
clamped = min(8.0 * rsm * obm * esm, 9.0)
|
|
raw = base.conviction_leverage * dc * rsm * obm * esm
|
|
expect = max(0.5, min(raw, clamped))
|
|
d = sz.compose(base, dc_lev_mult=dc, regime_size_mult=rsm,
|
|
market_ob_mult=obm, esof_size_mult=esm, posture="APEX")
|
|
assert d.conviction_leverage == expect
|
|
|
|
|
|
def test_full_size_decision_returns_breakdown():
|
|
r = _sizer().size(capital=69000.0, vel_div=-0.10, boost=1.3, beta=0.8,
|
|
mc_scale=1.0, esof_score=0.0, posture="APEX")
|
|
assert isinstance(r, FullSizeDecision)
|
|
assert isinstance(r.breakdown, SizingBreakdown)
|
|
assert r.breakdown.dc_lev_mult == 1.0
|
|
assert r.breakdown.regime_size_mult > 1.0
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# V-TYPES / drift guards
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def test_size_decision_frozen():
|
|
from pydantic import ValidationError
|
|
d = _base()
|
|
with pytest.raises(ValidationError):
|
|
d.conviction_leverage = 99.0
|
|
|
|
|
|
def test_sizing_breakdown_frozen():
|
|
r = _sizer().size(capital=1000.0, vel_div=-0.05)
|
|
with pytest.raises(Exception):
|
|
r.breakdown.raw_leverage = -1.0
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# HYPOTHESIS — envelope invariants
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
_short_vd = st.floats(min_value=-0.5, max_value=-0.021, allow_nan=False, allow_infinity=False)
|
|
_boost = st.floats(min_value=1.0, max_value=2.5, allow_nan=False, allow_infinity=False)
|
|
_beta = st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
|
|
_mc = st.sampled_from([0.5, 1.0])
|
|
_esof = st.floats(min_value=-1.0, max_value=1.0, allow_nan=False, allow_infinity=False)
|
|
_imb = st.floats(min_value=-1.0, max_value=1.0, allow_nan=False, allow_infinity=False)
|
|
_agree = st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
|
|
_cap = st.floats(min_value=1e4, max_value=1e6, allow_nan=False, allow_infinity=False)
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc,
|
|
esof=_esof, imb=_imb, agree=_agree, cap=_cap)
|
|
@settings(max_examples=200, deadline=None)
|
|
def test_leverage_within_envelope(vel_div, boost, beta, mc_scale, esof, imb, agree, cap):
|
|
sz = _sizer()
|
|
r = sz.size(capital=cap, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree, posture="APEX")
|
|
lev = r.decision.conviction_leverage
|
|
assert math.isfinite(lev)
|
|
assert sz.min_leverage - 1e-9 <= lev <= sz.abs_max_leverage + 1e-9
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc, esof=_esof,
|
|
imb=_imb, agree=_agree, cap=_cap)
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_stalker_caps_at_2(vel_div, boost, beta, mc_scale, esof, imb, agree, cap):
|
|
sz = _sizer()
|
|
r = sz.size(capital=cap, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree, posture="STALKER")
|
|
lev = r.decision.conviction_leverage
|
|
assert math.isfinite(lev)
|
|
assert sz.min_leverage - 1e-9 <= lev <= 2.0 + 1e-9
|
|
|
|
|
|
@given(vel_div=_short_vd, cap=_cap)
|
|
@settings(max_examples=60, deadline=None)
|
|
def test_notional_fraction_identity(vel_div, cap):
|
|
sz = _sizer()
|
|
r = sz.size(capital=cap, vel_div=vel_div)
|
|
d = r.decision
|
|
assert d.notional_fraction == pytest.approx(d.fraction * d.conviction_leverage)
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# @gate — MC BIT-IDENTITY (N >= 1e6) vs BLUE's real kernels + verbatim composition
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def _blue_ref_leverage(eng, mob, sz, vd, boost, beta, mc, esof, imb, agree, posture, cap):
|
|
"""BLUE's actual-code sizing: the orchestrator's REAL bet_sizer + set_esof +
|
|
_update_regime_size_mult + OB formula + the verbatim composition (:600-619).
|
|
|
|
Uses the orchestrator's own kernel objects — only the ~8-line arithmetic is
|
|
transcribed (trivial deterministic float math). Validated == _try_entry below.
|
|
"""
|
|
eng._day_base_boost = boost
|
|
eng._day_beta = beta
|
|
eng._day_mc_scale = mc
|
|
eng._day_posture = posture
|
|
eng.set_esof_advisory_score(esof)
|
|
eng._update_regime_size_mult(vd)
|
|
sr = eng.bet_sizer.calculate_size(
|
|
capital=cap, vel_div=vd, vel_div_trend=0.0, trade_direction=-1)
|
|
obm = 1.0
|
|
if imb is not None:
|
|
mob.set(imb, agree)
|
|
om = mob.get_market(1.0, ["BTCUSDT"])
|
|
ei = -om.median_imbalance
|
|
if ei > 0.08 and om.agreement_pct > 0.70:
|
|
obm = 1.0 + min(0.20, ei * om.agreement_pct * 0.5)
|
|
elif ei < -0.08 and om.agreement_pct > 0.70:
|
|
obm = max(0.85, 1.0 - abs(ei) * om.agreement_pct * 0.3)
|
|
clamped = min(
|
|
eng.base_max_leverage * eng.regime_size_mult * obm * eng._esof_size_mult,
|
|
eng.abs_max_leverage)
|
|
raw = sr["leverage"] * 1.0 * eng.regime_size_mult * obm * eng._esof_size_mult
|
|
if posture == "STALKER":
|
|
clamped = min(clamped, 2.0)
|
|
return max(eng.bet_sizer.min_leverage, min(raw, clamped))
|
|
|
|
|
|
@pytest.mark.gate
|
|
def test_gate_mc_bit_identity():
|
|
"""N>=1e6: VIOLET == BLUE float-for-float across the ENTIRE joint input space."""
|
|
N = 1_000_000
|
|
rng = np.random.default_rng(20260615)
|
|
vel_div = rng.uniform(-0.50, -0.021, N)
|
|
boost = rng.uniform(1.0, 2.5, N)
|
|
beta = rng.choice([0.2, 0.8], N)
|
|
mc_scale = rng.choice([0.5, 1.0], N)
|
|
esof = rng.uniform(-1.0, 1.0, N)
|
|
imb = rng.uniform(-1.0, 1.0, N)
|
|
agree = rng.uniform(0.0, 1.0, N)
|
|
posture = rng.choice(["APEX", "STALKER", "RESTORED"], N)
|
|
capital = rng.uniform(1e4, 1e6, N)
|
|
# NOTE: boost>2.5, β∈{0,1}, mc=0, and the OB agreement boundary are covered by
|
|
# test_gate_mc_extreme_multipliers (typical-vs-extreme split — verified 2026-06-15).
|
|
|
|
sz = _sizer()
|
|
eng = _orch()
|
|
mob = _MockOB()
|
|
# JIT warmup (amortize numba compilation outside the timed comparison)
|
|
for _ in range(8):
|
|
eng.bet_sizer.calculate_size(capital=1.0, vel_div=-0.1)
|
|
sz._bet_sizer.calculate(capital=1.0, vel_div=-0.1)
|
|
|
|
blue = np.empty(N)
|
|
violet = np.empty(N)
|
|
t0 = time.time()
|
|
for i in range(N):
|
|
vd = float(vel_div[i]); bo = float(boost[i]); be = float(beta[i])
|
|
ms = float(mc_scale[i]); es = float(esof[i]); im = float(imb[i])
|
|
ag = float(agree[i]); po = str(posture[i]); cp = float(capital[i])
|
|
blue[i] = _blue_ref_leverage(eng, mob, sz, vd, bo, be, ms, es, im, ag, po, cp)
|
|
v = sz.size(capital=cp, vel_div=vd, boost=bo, beta=be, mc_scale=ms,
|
|
esof_score=es, ob_median_imbalance=im, ob_agreement_pct=ag,
|
|
posture=po)
|
|
violet[i] = v.decision.conviction_leverage
|
|
elapsed = time.time() - t0
|
|
|
|
mismatches = int(np.count_nonzero(blue != violet))
|
|
if mismatches:
|
|
idx = np.nonzero(blue != violet)[0][:10]
|
|
sample = [{
|
|
"i": int(k), "vd": float(vel_div[k]), "boost": float(boost[k]),
|
|
"beta": float(beta[k]), "mc": float(mc_scale[k]),
|
|
"esof": float(esof[k]), "imb": float(imb[k]), "agree": float(agree[k]),
|
|
"posture": str(posture[k]), "blue": float(blue[k]), "violet": float(violet[k]),
|
|
} for k in idx]
|
|
_write_gate_report("sizing", N=N, elapsed_s=elapsed, mismatches=mismatches,
|
|
sample_mismatches=sample, passed=False)
|
|
assert mismatches == 0, (
|
|
f"BIT-IDENTITY BROKEN: {mismatches}/{N} mismatches (first: "
|
|
f"vd={float(vel_div[idx[0]])} blue={float(blue[idx[0]])!r} "
|
|
f"violet={float(violet[idx[0]])!r})")
|
|
|
|
_write_gate_report("sizing", N=N, elapsed_s=elapsed, mismatches=0,
|
|
passed=True, note="float-for-float == vs BLUE kernels")
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# @gate — end-to-end _try_entry parity (transcription == BLUE's inline code)
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
@pytest.mark.gate
|
|
def test_gate_try_entry_end_to_end():
|
|
"""Drive the REAL orchestrator _try_entry and confirm VIOLET matches its
|
|
INLINE composition (proves the MC reference transcription == BLUE's code)."""
|
|
N = 30_000
|
|
rng = np.random.default_rng(7)
|
|
vel_div = rng.uniform(-0.50, -0.021, N)
|
|
boost = rng.uniform(1.0, 2.5, N)
|
|
beta = rng.choice([0.2, 0.8], N)
|
|
mc_scale = rng.choice([0.5, 1.0], N)
|
|
esof = rng.uniform(-1.0, 1.0, N)
|
|
imb = rng.uniform(-1.0, 1.0, N)
|
|
agree = rng.uniform(0.0, 1.0, N)
|
|
posture = rng.choice(["APEX", "STALKER"], N)
|
|
capital = rng.uniform(1e4, 1e6, N)
|
|
|
|
sz = _sizer()
|
|
eng = _orch()
|
|
mob = _MockOB()
|
|
mismatches = 0
|
|
for i in range(N):
|
|
vd = float(vel_div[i]); bo = float(boost[i]); be = float(beta[i])
|
|
ms = float(mc_scale[i]); es = float(esof[i]); im = float(imb[i])
|
|
ag = float(agree[i]); po = str(posture[i]); cp = float(capital[i])
|
|
# --- BLUE: real _try_entry ---
|
|
eng.regime_direction = -1
|
|
eng.capital = cp
|
|
eng.position = None
|
|
eng._day_base_boost = bo
|
|
eng._day_beta = be
|
|
eng._day_mc_scale = ms
|
|
eng._day_posture = po
|
|
eng.set_esof_advisory_score(es)
|
|
eng._update_regime_size_mult(vd)
|
|
mob.set(im, ag)
|
|
eng.ob_engine = mob
|
|
res = eng._try_entry(bar_idx=1, vel_div=vd, prices={"BTCUSDT": 100.0},
|
|
price_histories=None)
|
|
blue_lev = res["leverage"] if res else None
|
|
# --- VIOLET ---
|
|
v = sz.size(capital=cp, vel_div=vd, boost=bo, beta=be, mc_scale=ms,
|
|
esof_score=es, ob_median_imbalance=im, ob_agreement_pct=ag,
|
|
posture=po)
|
|
violet_lev = v.decision.conviction_leverage
|
|
if blue_lev != violet_lev:
|
|
mismatches += 1
|
|
if mismatches <= 3:
|
|
print(f" MISMATCH i={i} vd={vd} blue={blue_lev!r} violet={violet_lev!r}")
|
|
assert mismatches == 0, f"{mismatches}/{N} _try_entry mismatches"
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# @gate — DC CONFIRM end-to-end (dc_lev_mult != 1.0 through real signal_gen)
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
@pytest.mark.gate
|
|
def test_gate_dc_confirm_end_to_end():
|
|
"""Craft a CONFIRM price history; verify the dc boost flows through both the
|
|
real orchestrator and VIOLET bit-identically (dc_lev_mult != 1.0)."""
|
|
from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine
|
|
# check_dc_nb compares prices[-1] vs prices[-lookback-1]; need >= 0.75 bps fall.
|
|
ph = [100.0] * 10 + [99.97] * 3 # ~3 bps fall in the last 7 bars
|
|
for boost in (1.25, 1.5):
|
|
eng = NDAlphaEngine(
|
|
initial_capital=69000.0, max_leverage=8.0, abs_max_leverage=9.0,
|
|
min_leverage=0.5, fraction=0.20, use_asset_selection=False,
|
|
use_direction_confirm=True, dc_leverage_boost=boost)
|
|
sz = _sizer(dc_leverage_boost=boost)
|
|
eng.regime_direction = -1
|
|
eng.set_esof_advisory_score(0.3) # ~full mult
|
|
eng._day_base_boost = 1.0
|
|
eng._day_beta = 0.0
|
|
eng._day_mc_scale = 1.0
|
|
eng._day_posture = "APEX"
|
|
eng._update_regime_size_mult(-0.035) # partial strength (not saturated)
|
|
sig = eng.signal_gen.generate(
|
|
vel_div=-0.035, vel_div_history=None, asset_price_history=ph,
|
|
trade_direction=-1, asset=None)
|
|
assert sig.dc_status == "CONFIRM", f"expected CONFIRM, got {sig.dc_status}"
|
|
res = eng._try_entry(bar_idx=13, vel_div=-0.035, prices={"BTCUSDT": 99.97},
|
|
price_histories={"BTCUSDT": ph})
|
|
blue_lev = res["leverage"]
|
|
v = sz.size(capital=69000.0, vel_div=-0.035, boost=1.0, beta=0.0,
|
|
mc_scale=1.0, esof_score=0.3, dc_status="CONFIRM", posture="APEX")
|
|
assert v.decision.conviction_leverage == blue_lev, (
|
|
f"dc boost={boost}: blue={blue_lev!r} violet={v.decision.conviction_leverage!r}")
|
|
assert v.breakdown.dc_lev_mult == boost
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# @gate — upstream replay vs recorded dolphin.trade_events
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def _load_recorded_trades(limit: int = 2000):
|
|
"""Pull (vel_div_entry, posture, capital_before, leverage, date) from CH."""
|
|
import urllib.request
|
|
sql = (
|
|
"WITH d AS (SELECT trade_id, any(vel_div_entry) vd, any(posture) po, "
|
|
"any(capital_before) cap, any(leverage) lev, any(date) dt "
|
|
"FROM dolphin.trade_events WHERE leverage>0 AND bars_held>0 "
|
|
"GROUP BY trade_id) "
|
|
f"SELECT vd, po, cap, lev, dt FROM d WHERE vd < -0.02 "
|
|
f"LIMIT {int(limit)} FORMAT TSV"
|
|
).encode()
|
|
req = urllib.request.Request(
|
|
"http://localhost:8123/", data=sql,
|
|
headers={"X-ClickHouse-User": "dolphin", "X-ClickHouse-Key": "dolphin_ch_2026"})
|
|
rows = []
|
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
|
for line in resp.read().decode().splitlines():
|
|
a = line.split("\t")
|
|
rows.append((float(a[0]), a[1], float(a[2]), float(a[3]), a[4]))
|
|
return rows
|
|
|
|
|
|
@pytest.mark.gate
|
|
def test_gate_upstream_replay():
|
|
"""Replay recorded trade_events through the wrapped chain; compare to recorded
|
|
leverage. Tolerance accounts for live-ACB-vs-recorded + missing esof/OB at
|
|
entry (spec §5.3). The gate is: VIOLET tracks recorded leverage (positive
|
|
correlation, median within the modulation envelope), not bit-identity."""
|
|
try:
|
|
trades = _load_recorded_trades(limit=2000)
|
|
except Exception as exc:
|
|
pytest.skip(f"ClickHouse unreachable: {exc}")
|
|
if len(trades) < 50:
|
|
pytest.skip("insufficient recorded trades")
|
|
|
|
# Live ACB for boost/beta (spec: "use the live ACB to produce boosts").
|
|
acb = None
|
|
try:
|
|
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import (
|
|
AdaptiveCircuitBreaker)
|
|
acb = AdaptiveCircuitBreaker()
|
|
dates = sorted({t[4] for t in trades})
|
|
acb.preload_w750(dates)
|
|
except Exception:
|
|
acb = None # eigenvalues data may not cover these dates -> defaults
|
|
|
|
sz = _sizer()
|
|
recorded, violet = [], []
|
|
for vd, posture, cap, lev, date in trades:
|
|
boost, beta = 1.0, 0.0
|
|
if acb is not None:
|
|
try:
|
|
info = acb.get_dynamic_boost_for_date(date)
|
|
boost = float(info["boost"])
|
|
beta = float(info["beta"])
|
|
except Exception:
|
|
pass
|
|
r = sz.size(capital=cap, vel_div=vd, boost=boost, beta=beta,
|
|
esof_score=None, posture=posture)
|
|
recorded.append(lev)
|
|
violet.append(r.decision.conviction_leverage)
|
|
recorded = np.array(recorded)
|
|
violet = np.array(violet)
|
|
|
|
abs_err = np.abs(violet - recorded)
|
|
med_err = float(np.median(abs_err))
|
|
# Pearson: VIOLET must track the recorded conviction direction.
|
|
mx, my = recorded.mean(), violet.mean()
|
|
sxy = np.sum((recorded - mx) * (violet - my))
|
|
sxx = np.sum((recorded - mx) ** 2)
|
|
syy = np.sum((violet - my) ** 2)
|
|
r_pearson = float(sxy / math.sqrt(sxx * syy)) if sxx > 0 and syy > 0 else 0.0
|
|
within_2 = float(np.mean(abs_err <= 2.0))
|
|
|
|
_write_gate_report(
|
|
"upstream_replay", n_trades=len(trades), median_abs_err=med_err,
|
|
pearson_r=r_pearson, pct_within_2x=within_2, acb_available=acb is not None,
|
|
passed=(r_pearson >= 0.80 and med_err <= 3.0),
|
|
note="approximate: recorded boost/beta are placeholder 1.0; esof/OB not "
|
|
"recorded at entry; gap attributable to live-ACB-vs-recorded (spec §5.3)")
|
|
# The gate: the wrapped chain TRACKS recorded leverage strongly. Observed r≈0.94,
|
|
# median_err≈1.44 (review 2026-06-15); thresholds set well inside that with margin
|
|
# for sample variation — meaningful, not the prior near-vacuous r>0.
|
|
assert r_pearson >= 0.80, (
|
|
f"upstream replay correlation too weak: r={r_pearson:.3f} "
|
|
f"(expected ≥0.80; median_err={med_err:.3f})")
|
|
assert med_err <= 3.0, (
|
|
f"upstream replay median abs error too large: {med_err:.3f} (expected ≤3.0)")
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# gate report helper
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
def _write_gate_report(name: str, **fields: Any) -> None:
|
|
REPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
|
ts = datetime.now(timezone.utc).strftime("%Y%m%d_%H%M%S")
|
|
import socket
|
|
payload = {
|
|
"generated_utc": datetime.now(timezone.utc).isoformat(),
|
|
"host": socket.gethostname(),
|
|
"layer": f"violet_v3_{name}",
|
|
**fields,
|
|
}
|
|
path = REPORTS_DIR / f"violet_v3_{name}_{ts}.json"
|
|
path.write_text(json.dumps(payload, indent=2, default=str))
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
# EXPANDED SUITE — error handling, off-by-ones, typing, fuzz/chaos, concurrency
|
|
# ══════════════════════════════════════════════════════════════════════════════
|
|
|
|
# ── A. Construction & initialization validation ────────────────────────────────
|
|
|
|
def test_construction_base_equals_abs_allowed():
|
|
sz = _sizer(base_max_leverage=8.0, abs_max_leverage=8.0)
|
|
assert sz.base_max_leverage == sz.abs_max_leverage == 8.0
|
|
|
|
|
|
def test_construction_preserves_vel_div_thresholds():
|
|
sz = _sizer(vel_div_threshold=-0.03, vel_div_extreme=-0.06)
|
|
assert sz.vel_div_threshold == -0.03
|
|
assert sz.vel_div_extreme == -0.06
|
|
|
|
|
|
def test_construction_long_thresholds_propagated():
|
|
sz = _sizer(long_vel_div_threshold=0.02, long_vel_div_extreme=0.05)
|
|
assert sz.long_vel_div_threshold == 0.02
|
|
assert sz.long_vel_div_extreme == 0.05
|
|
|
|
|
|
def test_construction_custom_dc_boost():
|
|
sz = _sizer(dc_leverage_boost=1.337)
|
|
assert sz.dc_leverage_boost == 1.337
|
|
|
|
|
|
def test_construction_leverage_convexity_propagated():
|
|
sz = _sizer(leverage_convexity=2.0)
|
|
assert sz.leverage_convexity == 2.0
|
|
|
|
|
|
def test_construction_min_leverage_propagated():
|
|
sz = _sizer(min_leverage=1.0)
|
|
assert sz.min_leverage == 1.0
|
|
assert sz._bet_sizer._sizer.min_leverage == 1.0
|
|
|
|
|
|
def test_rejects_base_just_above_abs():
|
|
with pytest.raises(ValueError):
|
|
_sizer(base_max_leverage=9.001, abs_max_leverage=9.0)
|
|
|
|
|
|
def test_construction_fraction_propagated():
|
|
sz = _sizer(base_fraction=0.15)
|
|
d = sz.base_size(capital=1000.0, vel_div=-0.10)
|
|
assert d.fraction <= 0.15
|
|
|
|
|
|
# ── B. strength_cubic: exhaustive boundary matrix ──────────────────────────────
|
|
|
|
def test_strength_short_just_above_threshold():
|
|
assert _sizer().strength_cubic(-0.019) == 0.0
|
|
|
|
|
|
def test_strength_short_just_below_threshold():
|
|
assert _sizer().strength_cubic(-0.021) > 0.0
|
|
|
|
|
|
def test_strength_short_at_extreme_returns_one():
|
|
assert _sizer().strength_cubic(-0.05) == 1.0
|
|
|
|
|
|
def test_strength_short_beyond_extreme():
|
|
assert _sizer().strength_cubic(-0.0500001) == 1.0
|
|
assert _sizer().strength_cubic(-1.0) == 1.0
|
|
|
|
|
|
def test_strength_short_midpoint_exact():
|
|
assert _sizer().strength_cubic(-0.035) == pytest.approx(0.125)
|
|
|
|
|
|
def test_strength_long_just_below_threshold():
|
|
assert _sizer().strength_cubic(0.009, trade_direction=1) == 0.0
|
|
|
|
|
|
def test_strength_long_at_extreme_returns_one():
|
|
assert _sizer().strength_cubic(0.04, trade_direction=1) == 1.0
|
|
|
|
|
|
def test_strength_long_midpoint():
|
|
assert _sizer().strength_cubic(0.025, trade_direction=1) == pytest.approx(0.125)
|
|
|
|
|
|
def test_strength_convexity_cubed_not_squared():
|
|
sz = _sizer(leverage_convexity=3.0)
|
|
assert sz.strength_cubic(-0.035) == pytest.approx(0.125)
|
|
assert sz.strength_cubic(-0.035) != pytest.approx(0.25)
|
|
|
|
|
|
def test_strength_nan_returns_zero():
|
|
assert _sizer().strength_cubic(float("nan")) == 0.0
|
|
|
|
|
|
def test_strength_inf_short_returns_zero():
|
|
assert _sizer().strength_cubic(float("inf")) == 0.0
|
|
|
|
|
|
def test_strength_neg_inf_short_returns_one():
|
|
assert _sizer().strength_cubic(float("-inf")) == 1.0
|
|
|
|
|
|
def test_strength_custom_convexity_changes_curve():
|
|
sz2 = _sizer(leverage_convexity=2.0)
|
|
sz3 = _sizer(leverage_convexity=3.0)
|
|
vd = -0.035
|
|
assert sz2.strength_cubic(vd) == pytest.approx(0.25)
|
|
assert sz3.strength_cubic(vd) == pytest.approx(0.125)
|
|
|
|
|
|
def test_strength_monotonic_short():
|
|
# Going from weak (-0.021) to strong (-0.05): strength increases monotonically.
|
|
sz = _sizer()
|
|
vals = [sz.strength_cubic(vd) for vd in np.linspace(-0.021, -0.05, 30)]
|
|
assert all(vals[i] <= vals[i + 1] + 1e-12 for i in range(len(vals) - 1))
|
|
|
|
|
|
def test_strength_monotonic_increasing_long():
|
|
sz = _sizer()
|
|
vals = [sz.strength_cubic(vd, trade_direction=1) for vd in np.linspace(0.011, 0.04, 30)]
|
|
assert all(vals[i] <= vals[i + 1] + 1e-12 for i in range(len(vals) - 1))
|
|
|
|
|
|
def test_strength_quarter_and_three_quarters():
|
|
sz = _sizer()
|
|
assert sz.strength_cubic(-0.0275) == pytest.approx(0.25 ** 3)
|
|
assert sz.strength_cubic(-0.0425) == pytest.approx(0.75 ** 3)
|
|
|
|
|
|
# ── C. regime_size_mult: formula edge cases ────────────────────────────────────
|
|
|
|
def test_regime_boost_zero_beta_zero():
|
|
assert _sizer().regime_size_mult(-0.10, boost=0.0, beta=0.0, mc_scale=1.0) == 0.0
|
|
|
|
|
|
def test_regime_mc_scale_zero():
|
|
assert _sizer().regime_size_mult(-0.10, boost=1.5, beta=0.8, mc_scale=0.0) == 0.0
|
|
|
|
|
|
def test_regime_beta_only_active_when_positive():
|
|
sz = _sizer()
|
|
r0 = sz.regime_size_mult(-0.035, boost=1.0, beta=0.0, mc_scale=1.0)
|
|
assert r0 == 1.0
|
|
r1 = sz.regime_size_mult(-0.035, boost=1.0, beta=0.8, mc_scale=1.0)
|
|
assert r1 == pytest.approx(1.0 * (1.0 + 0.8 * 0.125))
|
|
|
|
|
|
def test_regime_saturated_strength():
|
|
sz = _sizer()
|
|
assert sz.regime_size_mult(-0.10, boost=1.3, beta=0.8, mc_scale=0.5) == pytest.approx(1.3 * 1.8 * 0.5)
|
|
|
|
|
|
def test_regime_near_threshold_low_strength():
|
|
sz = _sizer()
|
|
r = sz.regime_size_mult(-0.021, boost=1.5, beta=0.8, mc_scale=1.0)
|
|
s = sz.strength_cubic(-0.021)
|
|
assert r == pytest.approx(1.5 * (1.0 + 0.8 * s) * 1.0)
|
|
|
|
|
|
def test_regime_matches_orchestrator_long_direction():
|
|
eng = _orch()
|
|
sz = _sizer()
|
|
eng.regime_direction = 1
|
|
for vd in np.linspace(0.011, 0.04, 20):
|
|
for beta in (0.0, 0.2, 0.8):
|
|
eng._day_base_boost = 1.2
|
|
eng._day_beta = beta
|
|
eng._day_mc_scale = 0.8
|
|
eng._update_regime_size_mult(float(vd))
|
|
assert sz.regime_size_mult(
|
|
float(vd), boost=1.2, beta=beta, mc_scale=0.8, trade_direction=1
|
|
) == eng.regime_size_mult
|
|
|
|
|
|
# ── D. esof_size_mult: band transitions & exotic inputs ────────────────────────
|
|
|
|
def test_esof_full_positive_above_edge():
|
|
assert _sizer().esof_size_mult(0.07) == 1.0
|
|
|
|
|
|
def test_esof_positive_shoulder_transition():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_NEUTRAL_CORE_MULT
|
|
val = _sizer().esof_size_mult(0.05)
|
|
assert ESOF_NEUTRAL_CORE_MULT < val < 1.0
|
|
|
|
|
|
def test_esof_neutral_negative_shoulder():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_NEUTRAL_CORE_MULT
|
|
val = _sizer().esof_size_mult(-0.05)
|
|
assert ESOF_NEUTRAL_CORE_MULT < val < 1.0
|
|
|
|
|
|
def test_esof_unfavorable_shoulder():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_UNFAVORABLE_CORE_MULT
|
|
val = _sizer().esof_size_mult(-0.25)
|
|
assert ESOF_UNFAVORABLE_CORE_MULT < val < 1.0
|
|
|
|
|
|
def test_esof_nan_returns_fallback():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_STALE_FALLBACK_MULT
|
|
assert _sizer().esof_size_mult(float("nan")) == ESOF_STALE_FALLBACK_MULT
|
|
|
|
|
|
def test_esof_inf_returns_fallback():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_STALE_FALLBACK_MULT
|
|
assert _sizer().esof_size_mult(float("inf")) == ESOF_STALE_FALLBACK_MULT
|
|
assert _sizer().esof_size_mult(float("-inf")) == ESOF_STALE_FALLBACK_MULT
|
|
|
|
|
|
def test_esof_string_coercible():
|
|
assert _sizer().esof_size_mult("0.5") == 1.0
|
|
|
|
|
|
def test_esof_string_non_coercible_fallback():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_STALE_FALLBACK_MULT
|
|
assert _sizer().esof_size_mult("not_a_number") == ESOF_STALE_FALLBACK_MULT
|
|
|
|
|
|
def test_esof_bool_true_is_full():
|
|
assert _sizer().esof_size_mult(True) == 1.0
|
|
|
|
|
|
def test_esof_bool_false_is_neutral():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_NEUTRAL_CORE_MULT
|
|
assert _sizer().esof_size_mult(False) == ESOF_NEUTRAL_CORE_MULT
|
|
|
|
|
|
def test_esof_object_fallback():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_STALE_FALLBACK_MULT
|
|
assert _sizer().esof_size_mult(object()) == ESOF_STALE_FALLBACK_MULT
|
|
|
|
|
|
def test_esof_list_fallback():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_STALE_FALLBACK_MULT
|
|
assert _sizer().esof_size_mult([0.5]) == ESOF_STALE_FALLBACK_MULT
|
|
|
|
|
|
def test_esof_range_never_below_unfavorable():
|
|
sz = _sizer()
|
|
for sc in np.linspace(-1.0, 1.0, 500):
|
|
assert sz.esof_size_mult(float(sc)) >= 0.30 - 1e-9
|
|
|
|
|
|
def test_esof_range_never_above_one_plus_epsilon():
|
|
sz = _sizer()
|
|
mx = max(sz.esof_size_mult(float(sc)) for sc in np.linspace(-1.0, 1.0, 1000))
|
|
assert mx <= 1.0 + 1e-9
|
|
|
|
|
|
def test_esof_raw_vs_modulation_clamped():
|
|
from prod.clean_arch.violet.modulation import VioletSizeModulation
|
|
mod = VioletSizeModulation()
|
|
sz = _sizer()
|
|
for sc in np.linspace(-1.0, 1.0, 300):
|
|
raw = sz.esof_size_mult(float(sc))
|
|
clamped = mod.mult_for(float(sc))
|
|
assert clamped == max(0.0, min(1.0, raw))
|
|
|
|
|
|
# ── E. market_ob_mult: threshold off-by-ones ───────────────────────────────────
|
|
|
|
def test_ob_at_exactly_008_positive_short():
|
|
assert _sizer().market_ob_mult(0.08, 0.80, trade_direction=-1) == 1.0
|
|
|
|
|
|
def test_ob_at_exactly_neg008_short():
|
|
assert _sizer().market_ob_mult(-0.08, 0.80, trade_direction=-1) == 1.0
|
|
|
|
|
|
def test_ob_at_exactly_070_agreement():
|
|
assert _sizer().market_ob_mult(-0.50, 0.70, trade_direction=-1) == 1.0
|
|
|
|
|
|
def test_ob_069_agreement_no_effect():
|
|
assert _sizer().market_ob_mult(-0.50, 0.69, trade_direction=-1) == 1.0
|
|
|
|
|
|
def test_ob_071_agreement_modulates():
|
|
assert _sizer().market_ob_mult(-0.50, 0.71, trade_direction=-1) > 1.0
|
|
|
|
|
|
def test_ob_just_above_008_boosts():
|
|
assert _sizer().market_ob_mult(-0.081, 0.80, trade_direction=-1) > 1.0
|
|
|
|
|
|
def test_ob_just_below_neg008_haircuts():
|
|
assert _sizer().market_ob_mult(0.081, 0.80, trade_direction=-1) < 1.0
|
|
|
|
|
|
def test_ob_boost_exactly_at_cap():
|
|
assert _sizer().market_ob_mult(-0.50, 0.80, trade_direction=-1) == 1.20
|
|
|
|
|
|
def test_ob_haircut_exactly_at_floor():
|
|
assert _sizer().market_ob_mult(0.50, 1.0, trade_direction=-1) == 0.85
|
|
|
|
|
|
def test_ob_neutral_zone_between_thresholds():
|
|
sz = _sizer()
|
|
for imb in np.linspace(-0.079, 0.079, 20):
|
|
assert sz.market_ob_mult(float(imb), 0.95, trade_direction=-1) == 1.0
|
|
|
|
|
|
def test_ob_short_zero_imbalance():
|
|
assert _sizer().market_ob_mult(0.0, 0.90, trade_direction=-1) == 1.0
|
|
|
|
|
|
def test_ob_long_zero_imbalance():
|
|
assert _sizer().market_ob_mult(0.0, 0.90, trade_direction=1) == 1.0
|
|
|
|
|
|
def test_ob_long_confirmed_boosts():
|
|
assert _sizer().market_ob_mult(0.50, 0.90, trade_direction=1) == pytest.approx(1.20)
|
|
|
|
|
|
def test_ob_long_contradicted_haircuts():
|
|
assert _sizer().market_ob_mult(-0.50, 0.90, trade_direction=1) == pytest.approx(0.865)
|
|
|
|
|
|
def test_ob_extreme_capped_and_floored():
|
|
sz = _sizer()
|
|
assert sz.market_ob_mult(-1.0, 1.0, trade_direction=-1) == 1.20
|
|
assert sz.market_ob_mult(1.0, 1.0, trade_direction=-1) == 0.85
|
|
|
|
|
|
def test_ob_long_mirrors_short_exactly():
|
|
sz = _sizer()
|
|
for imb in np.linspace(-1.0, 1.0, 50):
|
|
for agree in (0.5, 0.8, 1.0):
|
|
short_v = sz.market_ob_mult(float(imb), agree, trade_direction=-1)
|
|
long_v = sz.market_ob_mult(float(-imb), agree, trade_direction=1)
|
|
assert short_v == pytest.approx(long_v)
|
|
|
|
|
|
# ── F. dc_lev_mult: status matrix ──────────────────────────────────────────────
|
|
|
|
def test_dc_all_non_confirm_statuses():
|
|
sz = _sizer(dc_leverage_boost=1.5)
|
|
for status in ("NONE", "NEUTRAL", "CONTRADICT", "SKIP_CONTRADICT", "OB_SKIP", ""):
|
|
assert sz.dc_lev_mult(status) == 1.0
|
|
|
|
|
|
def test_dc_boost_zero():
|
|
assert _sizer(dc_leverage_boost=0.0).dc_lev_mult("CONFIRM") == 0.0
|
|
|
|
|
|
def test_dc_boost_large():
|
|
assert _sizer(dc_leverage_boost=3.0).dc_lev_mult("CONFIRM") == 3.0
|
|
|
|
|
|
def test_dc_lowercase_confirm_not_matched():
|
|
assert _sizer(dc_leverage_boost=1.5).dc_lev_mult("confirm") == 1.0
|
|
|
|
|
|
# ── G. compose: cap/floor/order edge cases ─────────────────────────────────────
|
|
|
|
def _base_dec(vel_div: float = -0.05) -> SizeDecision:
|
|
return _sizer().base_size(capital=69000.0, vel_div=vel_div, trade_direction=-1)
|
|
|
|
|
|
def test_compose_abs_cap_exact_boundary():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.20)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.125,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="APEX")
|
|
assert d.conviction_leverage == 9.0
|
|
|
|
|
|
def test_compose_raw_equals_clamped_boundary():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.035)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="APEX")
|
|
assert d.conviction_leverage == min(base.conviction_leverage, 8.0)
|
|
|
|
|
|
def test_compose_zero_regime_floors_to_min():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.05)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=0.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="APEX")
|
|
assert d.conviction_leverage == 0.5
|
|
|
|
|
|
def test_compose_zero_all_mults_floors_to_min():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.05)
|
|
d = sz.compose(base, dc_lev_mult=0.0, regime_size_mult=0.0,
|
|
market_ob_mult=0.0, esof_size_mult=0.0, posture="APEX")
|
|
assert d.conviction_leverage == 0.5
|
|
|
|
|
|
def test_compose_nan_dc_absorbed_by_min_max():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.05)
|
|
d = sz.compose(base, dc_lev_mult=float("nan"), regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="APEX")
|
|
assert math.isfinite(d.conviction_leverage)
|
|
assert d.conviction_leverage >= sz.min_leverage
|
|
|
|
|
|
def test_compose_stalker_caps_below_soft():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.20)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="STALKER")
|
|
assert d.conviction_leverage == 2.0
|
|
|
|
|
|
def test_compose_stalker_when_raw_below_2():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.025)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0, posture="STALKER")
|
|
assert d.conviction_leverage < 2.0 + 1e-9
|
|
|
|
|
|
def test_compose_bucket_idx_preserved():
|
|
base = _base_dec(-0.10)
|
|
d = _sizer().compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0)
|
|
assert d.bucket_idx == base.bucket_idx
|
|
|
|
|
|
def test_compose_signal_bucket_preserved():
|
|
base = _base_dec(-0.10)
|
|
d = _sizer().compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0)
|
|
assert d.signal_bucket == base.signal_bucket
|
|
|
|
|
|
def test_compose_strength_score_preserved():
|
|
base = _base_dec(-0.10)
|
|
d = _sizer().compose(base, dc_lev_mult=1.0, regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0)
|
|
assert d.strength_score == base.strength_score
|
|
|
|
|
|
def test_compose_notional_fraction_exact_identity():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.07)
|
|
d = sz.compose(base, dc_lev_mult=1.0, regime_size_mult=1.3,
|
|
market_ob_mult=1.1, esof_size_mult=0.9, posture="APEX")
|
|
assert d.notional_fraction == d.fraction * d.conviction_leverage
|
|
|
|
|
|
def test_compose_op_order_raw_first_then_clamp():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.10)
|
|
dc, rsm, obm, esm = 1.0, 1.5, 1.0, 0.8
|
|
raw = base.conviction_leverage * dc * rsm * obm * esm
|
|
clamped = min(8.0 * rsm * obm * esm, 9.0)
|
|
expect = max(0.5, min(raw, clamped))
|
|
d = sz.compose(base, dc_lev_mult=dc, regime_size_mult=rsm,
|
|
market_ob_mult=obm, esof_size_mult=esm, posture="APEX")
|
|
assert d.conviction_leverage == expect
|
|
|
|
|
|
def test_compose_extreme_multipliers_abs_holds():
|
|
sz = _sizer()
|
|
base = _base_dec(-0.05)
|
|
d = sz.compose(base, dc_lev_mult=100.0, regime_size_mult=100.0,
|
|
market_ob_mult=1.20, esof_size_mult=1.0, posture="APEX")
|
|
assert d.conviction_leverage == 9.0
|
|
|
|
|
|
# ── H. size(): end-to-end coverage ─────────────────────────────────────────────
|
|
|
|
def test_size_all_defaults():
|
|
r = _sizer().size(capital=69000.0, vel_div=-0.05)
|
|
assert r.decision.conviction_leverage >= 0.5
|
|
assert r.breakdown.regime_size_mult == 1.0
|
|
assert r.breakdown.market_ob_mult == 1.0
|
|
assert r.breakdown.dc_lev_mult == 1.0
|
|
|
|
|
|
def test_size_without_ob_is_ob_one():
|
|
r = _sizer().size(capital=1000.0, vel_div=-0.05, esof_score=0.0)
|
|
assert r.breakdown.market_ob_mult == 1.0
|
|
|
|
|
|
def test_size_without_esof_is_stale_fallback():
|
|
from nautilus_dolphin.nautilus.esof_size_gate import ESOF_STALE_FALLBACK_MULT
|
|
r = _sizer().size(capital=1000.0, vel_div=-0.05, esof_score=None)
|
|
assert r.breakdown.esof_size_mult == ESOF_STALE_FALLBACK_MULT
|
|
|
|
|
|
def test_size_long_direction():
|
|
r = _sizer().size(capital=69000.0, vel_div=0.03, trade_direction=1)
|
|
assert r.decision.conviction_leverage >= 0.5
|
|
|
|
|
|
def test_size_all_postures_envelope():
|
|
sz = _sizer()
|
|
for posture in ("APEX", "STALKER", "RESTORED", "TURTLE", "HIBERNATE"):
|
|
r = sz.size(capital=69000.0, vel_div=-0.05, posture=posture)
|
|
assert sz.min_leverage - 1e-9 <= r.decision.conviction_leverage <= sz.abs_max_leverage + 1e-9
|
|
|
|
|
|
def test_size_breakdown_contains_all_factors():
|
|
r = _sizer().size(capital=69000.0, vel_div=-0.10, boost=1.3, beta=0.8,
|
|
mc_scale=0.5, esof_score=0.0, dc_status="CONFIRM",
|
|
posture="STALKER")
|
|
bd = r.breakdown
|
|
assert bd.dc_lev_mult == 1.0
|
|
assert bd.regime_size_mult > 0
|
|
assert bd.esof_size_mult == 0.8
|
|
assert bd.posture == "STALKER"
|
|
assert bd.base_max_leverage == 8.0
|
|
assert bd.abs_max_leverage == 9.0
|
|
assert bd.min_leverage == 0.5
|
|
assert math.isfinite(bd.raw_leverage)
|
|
assert math.isfinite(bd.clamped_max_leverage)
|
|
|
|
|
|
def test_size_capital_does_not_affect_leverage():
|
|
sz = _sizer()
|
|
r1 = sz.size(capital=1000.0, vel_div=-0.05)
|
|
r2 = sz.size(capital=100000.0, vel_div=-0.05)
|
|
assert r1.decision.conviction_leverage == r2.decision.conviction_leverage
|
|
|
|
|
|
def test_size_dc_confirm_flows_through():
|
|
sz = _sizer(dc_leverage_boost=1.5)
|
|
r = sz.size(capital=69000.0, vel_div=-0.035, dc_status="CONFIRM")
|
|
assert r.breakdown.dc_lev_mult == 1.5
|
|
|
|
|
|
# ── I. V-TYPES rejection: boundary poison rejection ────────────────────────────
|
|
|
|
def test_vtypes_size_decision_rejects_nan_leverage():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=float("nan"),
|
|
notional_fraction=0.2, bucket_idx=0, strength_score=0.5,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_inf_notional():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=1.0,
|
|
notional_fraction=float("inf"), bucket_idx=0,
|
|
strength_score=0.5, signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_neg_fraction():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=-0.1, conviction_leverage=1.0,
|
|
notional_fraction=0.2, bucket_idx=0, strength_score=0.5,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_bad_bucket_high():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=1.0,
|
|
notional_fraction=0.2, bucket_idx=5, strength_score=0.5,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_bad_bucket_neg():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=1.0,
|
|
notional_fraction=0.2, bucket_idx=-1, strength_score=0.5,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_neg_strength():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=1.0,
|
|
notional_fraction=0.2, bucket_idx=0, strength_score=-1.0,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_extra_field():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=1.0,
|
|
notional_fraction=0.2, bucket_idx=0, strength_score=0.5,
|
|
signal_bucket="x", evil="payload")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_leverage_over_64():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=100.0,
|
|
notional_fraction=0.2, bucket_idx=0, strength_score=0.5,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_leverage_neg():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=0.2, conviction_leverage=-1.0,
|
|
notional_fraction=0.2, bucket_idx=0, strength_score=0.5,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_size_decision_rejects_fraction_over_one():
|
|
with pytest.raises(Exception):
|
|
SizeDecision(fraction=1.5, conviction_leverage=1.0,
|
|
notional_fraction=0.2, bucket_idx=0, strength_score=0.5,
|
|
signal_bucket="x")
|
|
|
|
|
|
def test_vtypes_breakdown_rejects_nan_raw():
|
|
from prod.clean_arch.violet.sizing import SizingBreakdown
|
|
with pytest.raises(Exception):
|
|
SizingBreakdown(
|
|
base_leverage=1.0, base_fraction=0.2, dc_lev_mult=1.0,
|
|
regime_size_mult=1.0, market_ob_mult=1.0, esof_size_mult=1.0,
|
|
strength_cubic=0.5, raw_leverage=float("nan"),
|
|
clamped_max_leverage=8.0, posture="APEX", min_leverage=0.5,
|
|
base_max_leverage=8.0, abs_max_leverage=9.0)
|
|
|
|
|
|
def test_vtypes_breakdown_rejects_neg_base_leverage():
|
|
from prod.clean_arch.violet.sizing import SizingBreakdown
|
|
with pytest.raises(Exception):
|
|
SizingBreakdown(
|
|
base_leverage=-1.0, base_fraction=0.2, dc_lev_mult=1.0,
|
|
regime_size_mult=1.0, market_ob_mult=1.0, esof_size_mult=1.0,
|
|
strength_cubic=0.5, raw_leverage=1.0,
|
|
clamped_max_leverage=8.0, posture="APEX", min_leverage=0.5,
|
|
base_max_leverage=8.0, abs_max_leverage=9.0)
|
|
|
|
|
|
def test_vtypes_breakdown_rejects_extra_field():
|
|
from prod.clean_arch.violet.sizing import SizingBreakdown
|
|
with pytest.raises(Exception):
|
|
SizingBreakdown(
|
|
base_leverage=1.0, base_fraction=0.2, dc_lev_mult=1.0,
|
|
regime_size_mult=1.0, market_ob_mult=1.0, esof_size_mult=1.0,
|
|
strength_cubic=0.5, raw_leverage=1.0,
|
|
clamped_max_leverage=8.0, posture="APEX", min_leverage=0.5,
|
|
base_max_leverage=8.0, abs_max_leverage=9.0, extra="bad")
|
|
|
|
|
|
def test_vtypes_breakdown_rejects_inf_dc_mult():
|
|
from prod.clean_arch.violet.sizing import SizingBreakdown
|
|
with pytest.raises(Exception):
|
|
SizingBreakdown(
|
|
base_leverage=1.0, base_fraction=0.2, dc_lev_mult=float("inf"),
|
|
regime_size_mult=1.0, market_ob_mult=1.0, esof_size_mult=1.0,
|
|
strength_cubic=0.5, raw_leverage=1.0,
|
|
clamped_max_leverage=8.0, posture="APEX", min_leverage=0.5,
|
|
base_max_leverage=8.0, abs_max_leverage=9.0)
|
|
|
|
|
|
def test_vtypes_full_decision_rejects_bad_nested():
|
|
bad_decision = {
|
|
"fraction": 0.2, "conviction_leverage": float("nan"),
|
|
"notional_fraction": 0.2, "bucket_idx": 0,
|
|
"strength_score": 0.5, "signal_bucket": "x",
|
|
}
|
|
bad_breakdown = {
|
|
"base_leverage": 1.0, "base_fraction": 0.2, "dc_lev_mult": 1.0,
|
|
"regime_size_mult": 1.0, "market_ob_mult": 1.0, "esof_size_mult": 1.0,
|
|
"strength_cubic": 0.5, "raw_leverage": 1.0,
|
|
"clamped_max_leverage": 8.0, "posture": "APEX", "min_leverage": 0.5,
|
|
"base_max_leverage": 8.0, "abs_max_leverage": 9.0,
|
|
}
|
|
with pytest.raises(Exception):
|
|
FullSizeDecision(decision=bad_decision, breakdown=bad_breakdown)
|
|
|
|
|
|
# ── J. beartype / @typed enforcement ───────────────────────────────────────────
|
|
|
|
def test_typed_strength_rejects_str():
|
|
with pytest.raises(Exception):
|
|
_sizer().strength_cubic("hello")
|
|
|
|
|
|
def test_typed_strength_rejects_none():
|
|
with pytest.raises(Exception):
|
|
_sizer().strength_cubic(None)
|
|
|
|
|
|
def test_typed_strength_rejects_list():
|
|
with pytest.raises(Exception):
|
|
_sizer().strength_cubic([1, 2])
|
|
|
|
|
|
def test_typed_base_size_rejects_str_capital():
|
|
with pytest.raises(Exception):
|
|
_sizer().base_size(capital="abc", vel_div=-0.1)
|
|
|
|
|
|
def test_typed_base_size_rejects_none_vel_div():
|
|
with pytest.raises(Exception):
|
|
_sizer().base_size(capital=1000.0, vel_div=None)
|
|
|
|
|
|
def test_typed_regime_rejects_str_boost():
|
|
with pytest.raises(Exception):
|
|
_sizer().regime_size_mult(-0.1, boost="high", beta=0.8, mc_scale=1.0)
|
|
|
|
|
|
def test_typed_compose_rejects_str_mult():
|
|
base = _base_dec(-0.05)
|
|
with pytest.raises(Exception):
|
|
_sizer().compose(base, dc_lev_mult="x", regime_size_mult=1.0,
|
|
market_ob_mult=1.0, esof_size_mult=1.0)
|
|
|
|
|
|
def test_typed_market_ob_rejects_str_imbalance():
|
|
with pytest.raises(Exception):
|
|
_sizer().market_ob_mult("big", 0.8)
|
|
|
|
|
|
def test_typed_strength_accepts_int_as_float():
|
|
assert _sizer().strength_cubic(-0.05) == 1.0
|
|
|
|
|
|
def test_typed_esof_accepts_any_type():
|
|
assert _sizer().esof_size_mult(0.0) == 0.8
|
|
assert _sizer().esof_size_mult(None) == 0.4
|
|
|
|
|
|
# ── K. Fuzz / chaos / property-based ───────────────────────────────────────────
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc,
|
|
esof=_esof, imb=_imb, agree=_agree, cap=_cap)
|
|
@settings(max_examples=150, deadline=None)
|
|
def test_fuzz_leverage_never_negative(vel_div, boost, beta, mc_scale, esof, imb, agree, cap):
|
|
r = _sizer().size(capital=cap, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree)
|
|
assert r.decision.conviction_leverage >= 0.0
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc,
|
|
esof=_esof, imb=_imb, agree=_agree, cap=_cap)
|
|
@settings(max_examples=150, deadline=None)
|
|
def test_fuzz_notional_fraction_exact_identity(vel_div, boost, beta, mc_scale, esof, imb, agree, cap):
|
|
r = _sizer().size(capital=cap, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree)
|
|
d = r.decision
|
|
assert d.notional_fraction == pytest.approx(d.fraction * d.conviction_leverage, rel=1e-12, abs=1e-15)
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc, esof=_esof,
|
|
imb=_imb, agree=_agree)
|
|
@settings(max_examples=120, deadline=None)
|
|
def test_fuzz_final_leverage_leq_raw(vel_div, boost, beta, mc_scale, esof, imb, agree):
|
|
r = _sizer().size(capital=1000.0, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree)
|
|
assert r.decision.conviction_leverage <= max(r.breakdown.raw_leverage, 0.5) + 1e-9
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc, esof=_esof,
|
|
imb=_imb, agree=_agree)
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_fuzz_fraction_unchanged_by_compose(vel_div, boost, beta, mc_scale, esof, imb, agree):
|
|
sz = _sizer()
|
|
base = sz.base_size(capital=1000.0, vel_div=vel_div)
|
|
r = sz.size(capital=1000.0, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree)
|
|
assert r.decision.fraction == base.fraction
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc)
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_fuzz_regime_geq_boost_times_mc(vel_div, boost, beta, mc_scale):
|
|
sz = _sizer()
|
|
rsm = sz.regime_size_mult(vel_div, boost=boost, beta=beta, mc_scale=mc_scale)
|
|
assert rsm >= boost * mc_scale - 1e-9
|
|
|
|
|
|
@given(score=st.floats(min_value=-1.0, max_value=1.0, allow_nan=False, allow_infinity=False))
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_fuzz_esof_range_valid_scores(score):
|
|
v = _sizer().esof_size_mult(score)
|
|
assert 0.30 - 1e-9 <= v <= 1.0 + 1e-9
|
|
|
|
|
|
@given(imb=_imb, agree=_agree)
|
|
@settings(max_examples=100, deadline=None)
|
|
def test_fuzz_ob_range(imb, agree):
|
|
v = _sizer().market_ob_mult(imb, agree, trade_direction=-1)
|
|
assert 0.85 - 1e-9 <= v <= 1.20 + 1e-9
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc,
|
|
esof=_esof, imb=_imb, agree=_agree)
|
|
@settings(max_examples=50, deadline=None)
|
|
def test_fuzz_deterministic_same_inputs(vel_div, boost, beta, mc_scale, esof, imb, agree):
|
|
sz = _sizer()
|
|
kwargs = dict(capital=1000.0, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree)
|
|
r1 = sz.size(**kwargs)
|
|
r2 = sz.size(**kwargs)
|
|
assert r1.decision.conviction_leverage == r2.decision.conviction_leverage
|
|
|
|
|
|
@given(imb=_imb, agree=st.floats(min_value=0.71, max_value=1.0, allow_nan=False))
|
|
@settings(max_examples=80, deadline=None)
|
|
def test_fuzz_long_ob_mirrors_short(imb, agree):
|
|
sz = _sizer()
|
|
short_val = sz.market_ob_mult(imb, agree, trade_direction=-1)
|
|
long_val = sz.market_ob_mult(-imb, agree, trade_direction=1)
|
|
assert short_val == pytest.approx(long_val)
|
|
|
|
|
|
@given(vd=st.floats(min_value=-0.50, max_value=-0.021, allow_nan=False))
|
|
@settings(max_examples=50, deadline=None)
|
|
def test_fuzz_strength_monotonic_short(vd):
|
|
sz = _sizer()
|
|
assert sz.strength_cubic(vd - 0.001) >= sz.strength_cubic(vd) - 1e-12
|
|
|
|
|
|
@given(vd=st.floats(min_value=0.011, max_value=0.04, allow_nan=False))
|
|
@settings(max_examples=50, deadline=None)
|
|
def test_fuzz_strength_monotonic_long(vd):
|
|
sz = _sizer()
|
|
assert sz.strength_cubic(vd + 0.001, trade_direction=1) >= sz.strength_cubic(vd, trade_direction=1) - 1e-12
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc,
|
|
esof=_esof, imb=_imb, agree=_agree)
|
|
@settings(max_examples=80, deadline=None)
|
|
def test_fuzz_stalker_never_exceeds_2(vel_div, boost, beta, mc_scale, esof, imb, agree):
|
|
r = _sizer().size(capital=1000.0, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree, posture="STALKER")
|
|
assert r.decision.conviction_leverage <= 2.0 + 1e-9
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc,
|
|
esof=_esof, imb=_imb, agree=_agree)
|
|
@settings(max_examples=80, deadline=None)
|
|
def test_fuzz_abs_cap_never_exceeded(vel_div, boost, beta, mc_scale, esof, imb, agree):
|
|
r = _sizer().size(capital=1000.0, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree, posture="APEX")
|
|
assert r.decision.conviction_leverage <= 9.0 + 1e-9
|
|
|
|
|
|
@given(vel_div=_short_vd, boost=_boost, beta=_beta, mc_scale=_mc,
|
|
esof=_esof, imb=_imb, agree=_agree)
|
|
@settings(max_examples=80, deadline=None)
|
|
def test_fuzz_min_floor_never_breached(vel_div, boost, beta, mc_scale, esof, imb, agree):
|
|
r = _sizer().size(capital=1000.0, vel_div=vel_div, boost=boost, beta=beta,
|
|
mc_scale=mc_scale, esof_score=esof,
|
|
ob_median_imbalance=imb, ob_agreement_pct=agree)
|
|
assert r.decision.conviction_leverage >= 0.5 - 1e-9
|
|
|
|
|
|
def test_chaos_extreme_multipliers_no_crash():
|
|
sz = _sizer()
|
|
base = sz.base_size(capital=1000.0, vel_div=-0.05)
|
|
d = sz.compose(base, dc_lev_mult=100.0, regime_size_mult=100.0,
|
|
market_ob_mult=1.20, esof_size_mult=1.0, posture="APEX")
|
|
assert d.conviction_leverage == 9.0
|
|
|
|
|
|
def test_chaos_all_esof_zones():
|
|
sz = _sizer()
|
|
for sc in [0.5, 0.06, 0.04, 0.0, -0.04, -0.06, -0.20, -0.24, -0.26, -0.50]:
|
|
v = sz.esof_size_mult(sc)
|
|
assert math.isfinite(v)
|
|
assert 0.3 - 1e-9 <= v <= 1.0 + 1e-9
|
|
|
|
|
|
def test_chaos_alternating_postures():
|
|
sz = _sizer()
|
|
for _ in range(100):
|
|
for posture in ("APEX", "STALKER", "RESTORED"):
|
|
r = sz.size(capital=69000.0, vel_div=-0.05, posture=posture)
|
|
assert math.isfinite(r.decision.conviction_leverage)
|
|
|
|
|
|
def test_chaos_tiny_capital():
|
|
r = _sizer().size(capital=0.01, vel_div=-0.10)
|
|
assert math.isfinite(r.decision.conviction_leverage)
|
|
|
|
|
|
def test_chaos_huge_capital():
|
|
r = _sizer().size(capital=1e12, vel_div=-0.10)
|
|
assert math.isfinite(r.decision.conviction_leverage)
|
|
|
|
|
|
def test_chaos_all_dc_statuses():
|
|
sz = _sizer(dc_leverage_boost=1.5)
|
|
for status in ("CONFIRM", "NONE", "NEUTRAL", "CONTRADICT", "SKIP_CONTRADICT",
|
|
"OB_SKIP", "", "UNKNOWN"):
|
|
r = sz.size(capital=69000.0, vel_div=-0.05, dc_status=status)
|
|
assert math.isfinite(r.decision.conviction_leverage)
|
|
|
|
|
|
def test_chaos_rapid_alternating_size_calls():
|
|
sz = _sizer()
|
|
results = []
|
|
for i in range(200):
|
|
vd = -0.02 - 0.001 * (i % 100)
|
|
r = sz.size(capital=1000.0, vel_div=vd, posture="APEX" if i % 2 else "STALKER")
|
|
results.append(r.decision.conviction_leverage)
|
|
assert all(math.isfinite(x) for x in results)
|
|
assert len(set(results)) > 1
|
|
|
|
|
|
# ── L. State isolation / determinism / concurrency ─────────────────────────────
|
|
|
|
def test_determinism_1000_repeated_identical():
|
|
sz = _sizer()
|
|
vals = set()
|
|
for _ in range(1000):
|
|
r = sz.size(capital=69000.0, vel_div=-0.07, boost=1.3, beta=0.8,
|
|
mc_scale=1.0, esof_score=0.0, posture="APEX")
|
|
vals.add(r.decision.conviction_leverage)
|
|
assert len(vals) == 1
|
|
|
|
|
|
def test_two_sizers_independent():
|
|
sa = _sizer(dc_leverage_boost=1.5)
|
|
sb = _sizer(dc_leverage_boost=2.0)
|
|
assert sa.dc_lev_mult("CONFIRM") == 1.5
|
|
assert sb.dc_lev_mult("CONFIRM") == 2.0
|
|
assert sa.dc_lev_mult("CONFIRM") == 1.5
|
|
|
|
|
|
def test_factor_producers_are_pure():
|
|
sz = _sizer()
|
|
assert sz.strength_cubic(-0.035) == sz.strength_cubic(-0.035)
|
|
assert sz.regime_size_mult(-0.035, boost=1.3, beta=0.8, mc_scale=1.0) == \
|
|
sz.regime_size_mult(-0.035, boost=1.3, beta=0.8, mc_scale=1.0)
|
|
|
|
|
|
def test_thread_safe_concurrent_identical():
|
|
sz = _sizer()
|
|
results = []
|
|
errors = []
|
|
barrier = threading.Barrier(8)
|
|
|
|
def worker():
|
|
barrier.wait()
|
|
try:
|
|
for _ in range(200):
|
|
r = sz.size(capital=69000.0, vel_div=-0.05, boost=1.3,
|
|
beta=0.8, mc_scale=1.0, esof_score=0.0, posture="APEX")
|
|
results.append(r.decision.conviction_leverage)
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
threads = [threading.Thread(target=worker) for _ in range(8)]
|
|
[t.start() for t in threads]
|
|
[t.join() for t in threads]
|
|
assert len(errors) == 0
|
|
assert len(results) == 1600
|
|
assert len(set(results)) == 1
|
|
|
|
|
|
def test_thread_safe_concurrent_different_inputs():
|
|
sz = _sizer()
|
|
errors = []
|
|
all_results = []
|
|
|
|
def worker(seed):
|
|
rng = np.random.default_rng(seed)
|
|
try:
|
|
for _ in range(100):
|
|
vd = float(rng.uniform(-0.5, -0.021))
|
|
r = sz.size(capital=1000.0, vel_div=vd, posture="APEX")
|
|
all_results.append(r.decision.conviction_leverage)
|
|
except Exception as e:
|
|
errors.append(e)
|
|
|
|
threads = [threading.Thread(target=worker, args=(i,)) for i in range(8)]
|
|
[t.start() for t in threads]
|
|
[t.join() for t in threads]
|
|
assert len(errors) == 0
|
|
assert len(all_results) == 800
|
|
assert all(math.isfinite(x) for x in all_results)
|
|
|
|
|
|
def test_compose_no_side_effects_on_base():
|
|
sz = _sizer()
|
|
base = sz.base_size(capital=1000.0, vel_div=-0.10)
|
|
original_lev = base.conviction_leverage
|
|
original_frac = base.fraction
|
|
for _ in range(100):
|
|
sz.compose(base, dc_lev_mult=1.5, regime_size_mult=1.3,
|
|
market_ob_mult=1.1, esof_size_mult=0.8, posture="APEX")
|
|
assert base.conviction_leverage == original_lev
|
|
assert base.fraction == original_frac
|
|
|
|
|
|
def test_base_size_caches_nothing_between_calls():
|
|
sz = _sizer()
|
|
d1 = sz.base_size(capital=1000.0, vel_div=-0.03)
|
|
d2 = sz.base_size(capital=1000.0, vel_div=-0.10)
|
|
assert d1.conviction_leverage != d2.conviction_leverage
|
|
|
|
|
|
def test_size_call_does_not_mutate_sizer_state():
|
|
sz = _sizer()
|
|
orig_boost = sz.dc_leverage_boost
|
|
orig_base_max = sz.base_max_leverage
|
|
sz.size(capital=69000.0, vel_div=-0.10, boost=1.5, beta=0.8,
|
|
esof_score=0.0, posture="STALKER")
|
|
assert sz.dc_leverage_boost == orig_boost
|
|
assert sz.base_max_leverage == orig_base_max
|
|
|
|
|
|
def test_orchestrator_position_isolation():
|
|
"""The orchestrator accumulates position/trade state; verify VIOLET does not."""
|
|
eng = _orch()
|
|
sz = _sizer()
|
|
eng.regime_direction = -1
|
|
eng._day_base_boost = 1.0; eng._day_beta = 0.0
|
|
eng._day_mc_scale = 1.0; eng._day_posture = "APEX"
|
|
eng.set_esof_advisory_score(0.0)
|
|
eng._update_regime_size_mult(-0.05)
|
|
eng.ob_engine = None
|
|
for _ in range(5):
|
|
eng.position = None
|
|
eng._try_entry(bar_idx=1, vel_div=-0.05, prices={"BTCUSDT": 100.0}, price_histories=None)
|
|
for _ in range(5):
|
|
r = sz.size(capital=69000.0, vel_div=-0.05)
|
|
assert r.decision.conviction_leverage == sz.size(
|
|
capital=69000.0, vel_div=-0.05).decision.conviction_leverage
|
|
|
|
|
|
# ── M. Additional @gate stress tests ───────────────────────────────────────────
|
|
|
|
@pytest.mark.gate
|
|
def test_gate_mc_long_direction_bit_identity():
|
|
"""MC bit-identity for LONG direction (strength_cubic long + OB sign flip)."""
|
|
N = 200_000
|
|
rng = np.random.default_rng(20260616)
|
|
vel_div = rng.uniform(0.011, 0.50, N)
|
|
boost = rng.uniform(1.0, 2.5, N)
|
|
beta = rng.choice([0.2, 0.8], N)
|
|
mc_scale = rng.choice([0.5, 1.0], N)
|
|
esof = rng.uniform(-1.0, 1.0, N)
|
|
imb = rng.uniform(-1.0, 1.0, N)
|
|
agree = rng.uniform(0.0, 1.0, N)
|
|
posture = rng.choice(["APEX", "STALKER"], N)
|
|
capital = rng.uniform(1e4, 1e6, N)
|
|
|
|
sz = _sizer()
|
|
eng = _orch()
|
|
mob = _MockOB()
|
|
mismatches = 0
|
|
for i in range(N):
|
|
vd = float(vel_div[i]); bo = float(boost[i]); be = float(beta[i])
|
|
ms = float(mc_scale[i]); es = float(esof[i]); im = float(imb[i])
|
|
ag = float(agree[i]); po = str(posture[i]); cp = float(capital[i])
|
|
eng._day_base_boost = bo; eng._day_beta = be; eng._day_mc_scale = ms
|
|
eng._day_posture = po
|
|
eng.regime_direction = 1 # LONG: orchestrator _strength_cubic uses this
|
|
eng.set_esof_advisory_score(es)
|
|
eng._update_regime_size_mult(vd)
|
|
sr = eng.bet_sizer.calculate_size(
|
|
capital=cp, vel_div=vd, vel_div_trend=0.0, trade_direction=1)
|
|
obm = 1.0
|
|
mob.set(im, ag)
|
|
om = mob.get_market(1.0, ["x"])
|
|
ei = om.median_imbalance
|
|
if ei > 0.08 and om.agreement_pct > 0.70:
|
|
obm = 1.0 + min(0.20, ei * om.agreement_pct * 0.5)
|
|
elif ei < -0.08 and om.agreement_pct > 0.70:
|
|
obm = max(0.85, 1.0 - abs(ei) * om.agreement_pct * 0.3)
|
|
clamped = min(
|
|
eng.base_max_leverage * eng.regime_size_mult * obm * eng._esof_size_mult,
|
|
eng.abs_max_leverage)
|
|
raw = sr["leverage"] * 1.0 * eng.regime_size_mult * obm * eng._esof_size_mult
|
|
if po == "STALKER":
|
|
clamped = min(clamped, 2.0)
|
|
blue = max(eng.bet_sizer.min_leverage, min(raw, clamped))
|
|
v = sz.size(capital=cp, vel_div=vd, boost=bo, beta=be, mc_scale=ms,
|
|
esof_score=es, ob_median_imbalance=im, ob_agreement_pct=ag,
|
|
posture=po, trade_direction=1)
|
|
if blue != v.decision.conviction_leverage:
|
|
mismatches += 1
|
|
if mismatches <= 3:
|
|
print(f" MISMATCH i={i} vd={vd} blue={blue!r} violet={v.decision.conviction_leverage!r}")
|
|
assert mismatches == 0, f"{mismatches}/{N} LONG-direction mismatches"
|
|
|
|
|
|
@pytest.mark.gate
|
|
def test_gate_mc_extreme_multipliers():
|
|
"""MC bit-identity with extreme multiplier combos (cap@9, floor@min, STALKER@2)."""
|
|
N = 200_000
|
|
rng = np.random.default_rng(999)
|
|
vel_div = rng.choice([-0.50, -0.30, -0.05, -0.021], N)
|
|
boost = rng.uniform(1.0, 5.0, N)
|
|
beta = rng.choice([0.0, 0.2, 0.8, 1.0], N)
|
|
mc_scale = rng.choice([0.0, 0.5, 1.0], N)
|
|
esof = rng.choice([-1.0, -0.5, -0.25, 0.0, 0.5, 1.0], N)
|
|
imb = rng.choice([-1.0, -0.5, 0.0, 0.5, 1.0], N)
|
|
agree = rng.choice([0.0, 0.5, 0.69, 0.71, 1.0], N)
|
|
posture = rng.choice(["APEX", "STALKER", "RESTORED"], N)
|
|
|
|
sz = _sizer()
|
|
eng = _orch()
|
|
mob = _MockOB()
|
|
mismatches = 0
|
|
for i in range(N):
|
|
vd = float(vel_div[i]); bo = float(boost[i]); be = float(beta[i])
|
|
ms = float(mc_scale[i]); es = float(esof[i]); im = float(imb[i])
|
|
ag = float(agree[i]); po = str(posture[i])
|
|
blue = _blue_ref_leverage(eng, mob, sz, vd, bo, be, ms, es, im, ag, po, 1000.0)
|
|
v = sz.size(capital=1000.0, vel_div=vd, boost=bo, beta=be, mc_scale=ms,
|
|
esof_score=es, ob_median_imbalance=im, ob_agreement_pct=ag, posture=po)
|
|
if blue != v.decision.conviction_leverage:
|
|
mismatches += 1
|
|
if mismatches <= 3:
|
|
print(f" MISMATCH i={i} vd={vd} boost={bo} beta={be} mc={ms} "
|
|
f"esof={es} imb={im} agree={ag} post={po}")
|
|
print(f" blue={blue!r} violet={v.decision.conviction_leverage!r}")
|
|
assert mismatches == 0, f"{mismatches}/{N} extreme-multiplier mismatches"
|