119 lines
5.2 KiB
Python
119 lines
5.2 KiB
Python
|
|
"""Tests for DolphinExecutionStrategy."""
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from collections import deque
|
||
|
|
from unittest.mock import Mock
|
||
|
|
from nautilus_dolphin.nautilus.strategy import DolphinExecutionStrategyForTesting as DolphinExecutionStrategy
|
||
|
|
|
||
|
|
|
||
|
|
def _make_strategy_vol_ready(strategy, high=True):
|
||
|
|
"""Set vol regime state. high=True → regime is high (vol above p60)."""
|
||
|
|
strategy._vol_regime_ready = True
|
||
|
|
strategy._vol_p60 = 0.0001
|
||
|
|
strategy._current_vol = 0.01 if high else 0.000001
|
||
|
|
strategy._bar_count_in_day = 200 # past warmup
|
||
|
|
|
||
|
|
|
||
|
|
class TestDolphinExecutionStrategy:
|
||
|
|
|
||
|
|
def test_should_trade_vol_regime_filter(self):
|
||
|
|
"""Layer 2: vol regime not ready → reject."""
|
||
|
|
strategy = DolphinExecutionStrategy({})
|
||
|
|
# Default state: _vol_regime_ready=False → vol_regime_not_high
|
||
|
|
signal = {'vel_div': -0.03, 'asset': 'BTCUSDT', 'irp_alignment': 0.50}
|
||
|
|
assert strategy._should_trade(signal) == "vol_regime_not_high"
|
||
|
|
|
||
|
|
def test_should_trade_irp_filter(self):
|
||
|
|
"""Layer 3: IRP alignment filter."""
|
||
|
|
strategy = DolphinExecutionStrategy({})
|
||
|
|
_make_strategy_vol_ready(strategy)
|
||
|
|
|
||
|
|
signal_low_irp = {'vel_div': -0.03, 'asset': 'BTCUSDT', 'irp_alignment': 0.30}
|
||
|
|
assert strategy._should_trade(signal_low_irp) == "irp_too_low"
|
||
|
|
|
||
|
|
signal_good = {'vel_div': -0.03, 'asset': 'BTCUSDT', 'irp_alignment': 0.50}
|
||
|
|
assert strategy._should_trade(signal_good) == ""
|
||
|
|
|
||
|
|
def test_should_trade_direction_contradicted(self):
|
||
|
|
"""Layer 6: rising BTC price contradicts SHORT signal."""
|
||
|
|
strategy = DolphinExecutionStrategy({})
|
||
|
|
_make_strategy_vol_ready(strategy)
|
||
|
|
# BTC rose: p0=10000 (lookback_bars=7+1 ago) → p_now=10100 → +100bps → contradict
|
||
|
|
prices = [10000.0] * 5 + [10100.0] * 5
|
||
|
|
strategy._btc_dc_prices = deque(prices, maxlen=strategy.dc_lookback_bars + 2)
|
||
|
|
signal = {'vel_div': -0.03, 'asset': 'BTCUSDT', 'irp_alignment': 0.50}
|
||
|
|
assert strategy._should_trade(signal) == "direction_contradicted"
|
||
|
|
|
||
|
|
def test_should_trade_excluded_assets(self):
|
||
|
|
"""Asset exclusion filter."""
|
||
|
|
strategy = DolphinExecutionStrategy({})
|
||
|
|
_make_strategy_vol_ready(strategy)
|
||
|
|
signal = {'vel_div': -0.03, 'asset': 'TUSDUSDT', 'irp_alignment': 0.50}
|
||
|
|
assert strategy._should_trade(signal) == "asset_excluded"
|
||
|
|
|
||
|
|
def test_should_trade_position_limits(self):
|
||
|
|
"""Max concurrent positions gate."""
|
||
|
|
config = {'max_concurrent_positions': 2}
|
||
|
|
strategy = DolphinExecutionStrategy(config)
|
||
|
|
_make_strategy_vol_ready(strategy)
|
||
|
|
strategy.active_positions = {'BTCUSDT': {}, 'ETHUSDT': {}}
|
||
|
|
signal = {'vel_div': -0.03, 'asset': 'SOLUSDT', 'irp_alignment': 0.50}
|
||
|
|
assert strategy._should_trade(signal) == "max_positions_reached"
|
||
|
|
|
||
|
|
def test_should_trade_existing_position(self):
|
||
|
|
"""Duplicate position guard."""
|
||
|
|
strategy = DolphinExecutionStrategy({})
|
||
|
|
_make_strategy_vol_ready(strategy)
|
||
|
|
strategy.active_positions = {'BTCUSDT': {}}
|
||
|
|
signal = {'vel_div': -0.03, 'asset': 'BTCUSDT', 'irp_alignment': 0.50}
|
||
|
|
assert strategy._should_trade(signal) == "position_already_exists"
|
||
|
|
|
||
|
|
def test_calculate_leverage_base(self):
|
||
|
|
"""Layer 4: cubic-convex dynamic leverage from vel_div."""
|
||
|
|
config = {'min_leverage': 0.5, 'max_leverage': 5.0, 'leverage_convexity': 3.0}
|
||
|
|
strategy = DolphinExecutionStrategy(config)
|
||
|
|
|
||
|
|
# Near threshold (low strength): vel_div=-0.021 → strength≈0.03
|
||
|
|
lev_low = strategy.calculate_leverage({'vel_div': -0.021})
|
||
|
|
assert 0.5 <= lev_low < 1.0
|
||
|
|
|
||
|
|
# Near extreme (high strength): vel_div=-0.048 → strength≈0.93
|
||
|
|
lev_high = strategy.calculate_leverage({'vel_div': -0.048})
|
||
|
|
assert lev_high > 3.0
|
||
|
|
|
||
|
|
def test_calculate_leverage_with_multipliers(self):
|
||
|
|
"""Layer 5: alpha layer multipliers applied to base leverage."""
|
||
|
|
config = {'min_leverage': 0.5, 'max_leverage': 5.0, 'leverage_convexity': 3.0}
|
||
|
|
strategy = DolphinExecutionStrategy(config)
|
||
|
|
|
||
|
|
# vel_div = -0.035 → strength = 0.5 → scaled = 0.125 → base_lev = 1.0625
|
||
|
|
signal = {
|
||
|
|
'vel_div': -0.035,
|
||
|
|
'bucket_boost': 1.2,
|
||
|
|
'streak_mult': 1.1,
|
||
|
|
'trend_mult': 1.05,
|
||
|
|
}
|
||
|
|
lev = strategy.calculate_leverage(signal)
|
||
|
|
base_lev = 0.5 + (0.5 ** 3.0) * (5.0 - 0.5) # 1.0625
|
||
|
|
expected = base_lev * 1.2 * 1.1 * 1.05
|
||
|
|
assert abs(lev - expected) < 0.01
|
||
|
|
|
||
|
|
def test_calculate_position_size(self):
|
||
|
|
"""Position size: balance * fraction * leverage."""
|
||
|
|
strategy = DolphinExecutionStrategy({'capital_fraction': 0.20})
|
||
|
|
notional = strategy.calculate_position_size({'vel_div': -0.035}, 10000.0)
|
||
|
|
assert notional > 0
|
||
|
|
assert notional <= 10000.0 * 0.5 # sanity cap
|
||
|
|
|
||
|
|
def test_position_size_sanity_cap(self):
|
||
|
|
"""50% of balance hard cap even with high multipliers."""
|
||
|
|
strategy = DolphinExecutionStrategy({'capital_fraction': 0.50, 'max_leverage': 10.0})
|
||
|
|
signal = {
|
||
|
|
'vel_div': -0.08,
|
||
|
|
'bucket_boost': 2.0,
|
||
|
|
'streak_mult': 2.0,
|
||
|
|
'trend_mult': 2.0,
|
||
|
|
}
|
||
|
|
notional = strategy.calculate_position_size(signal, 10000.0)
|
||
|
|
assert notional <= 10000.0 * 0.5
|