541 lines
23 KiB
Python
541 lines
23 KiB
Python
|
|
"""
|
||
|
|
GREEN-BLUE Algorithmic Parity Tests
|
||
|
|
|
||
|
|
Verifies that DolphinActor (GREEN) has full algorithmic parity with
|
||
|
|
the BLUE production system (nautilus_event_trader.py).
|
||
|
|
|
||
|
|
Covers:
|
||
|
|
1. MC_BASE_CFG parameter parity
|
||
|
|
2. ALGO_VERSION constant match
|
||
|
|
3. vel_div formula (v50 - v750)
|
||
|
|
4. vol_ok computation (rolling BTC dvol gate)
|
||
|
|
5. _BUCKET_SL_PCT parity
|
||
|
|
6. Engine kwargs gold-spec values
|
||
|
|
7. Hibernate protection logic
|
||
|
|
8. NG7 normalization parity
|
||
|
|
"""
|
||
|
|
import math
|
||
|
|
import numpy as np
|
||
|
|
import pytest
|
||
|
|
from pathlib import Path
|
||
|
|
from collections import deque
|
||
|
|
|
||
|
|
# ── Import GREEN (DolphinActor) ──────────────────────────────────────────────
|
||
|
|
import sys
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||
|
|
sys.path.insert(0, str(Path(__file__).parent.parent / 'nautilus_dolphin'))
|
||
|
|
|
||
|
|
from nautilus_dolphin.nautilus.dolphin_actor import (
|
||
|
|
DolphinActor,
|
||
|
|
_MC_BASE_CFG,
|
||
|
|
ALGO_VERSION,
|
||
|
|
BTC_VOL_WINDOW,
|
||
|
|
VOL_P60_THRESHOLD,
|
||
|
|
_BUCKET_SL_PCT,
|
||
|
|
_GateSnap,
|
||
|
|
)
|
||
|
|
|
||
|
|
# ── Import BLUE constants for comparison ─────────────────────────────────────
|
||
|
|
_BLUE_ROOT = Path('/mnt/dolphinng5_predict/prod')
|
||
|
|
sys.path.insert(0, str(_BLUE_ROOT))
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 1. MC_BASE_CFG PARITY
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestMCBaseCfgParity:
|
||
|
|
"""GREEN's _MC_BASE_CFG must match BLUE's gold-spec MC config exactly."""
|
||
|
|
|
||
|
|
# BLUE gold values (from nautilus_event_trader.py MC_BASE_CFG)
|
||
|
|
BLUE_GOLD = {
|
||
|
|
'max_leverage': 8.00,
|
||
|
|
'max_hold_bars': 250,
|
||
|
|
'min_irp_alignment': 0.0,
|
||
|
|
'vel_div_threshold': -0.020,
|
||
|
|
'vel_div_extreme': -0.050,
|
||
|
|
'min_leverage': 0.50,
|
||
|
|
'leverage_convexity': 3.00,
|
||
|
|
'fraction': 0.20,
|
||
|
|
'fixed_tp_pct': 0.0095,
|
||
|
|
'stop_pct': 1.00,
|
||
|
|
'use_sp_fees': True,
|
||
|
|
'use_sp_slippage': True,
|
||
|
|
'sp_maker_entry_rate': 0.62,
|
||
|
|
'sp_maker_exit_rate': 0.50,
|
||
|
|
'use_ob_edge': True,
|
||
|
|
'ob_edge_bps': 5.00,
|
||
|
|
'ob_confirm_rate': 0.40,
|
||
|
|
'lookback': 100,
|
||
|
|
'use_direction_confirm': True,
|
||
|
|
'dc_lookback_bars': 7,
|
||
|
|
'dc_min_magnitude_bps': 0.75,
|
||
|
|
'dc_skip_contradicts': True,
|
||
|
|
'dc_leverage_boost': 1.00,
|
||
|
|
'dc_leverage_reduce': 0.50,
|
||
|
|
'use_asset_selection': True,
|
||
|
|
'use_alpha_layers': True,
|
||
|
|
'use_dynamic_leverage': True,
|
||
|
|
'acb_beta_high': 0.80,
|
||
|
|
'acb_beta_low': 0.20,
|
||
|
|
'acb_w750_threshold_pct': 60,
|
||
|
|
}
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("key,expected", list(BLUE_GOLD.items()))
|
||
|
|
def test_mc_cfg_key_matches_blue(self, key, expected):
|
||
|
|
assert key in _MC_BASE_CFG, f"Key '{key}' missing from _MC_BASE_CFG"
|
||
|
|
assert _MC_BASE_CFG[key] == expected, (
|
||
|
|
f"MC_BASE_CFG['{key}']: GREEN={_MC_BASE_CFG[key]} != BLUE={expected}"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_max_leverage_is_8x(self):
|
||
|
|
assert _MC_BASE_CFG['max_leverage'] == 8.0
|
||
|
|
|
||
|
|
def test_max_hold_bars_is_250(self):
|
||
|
|
assert _MC_BASE_CFG['max_hold_bars'] == 250
|
||
|
|
|
||
|
|
def test_min_irp_alignment_is_zero(self):
|
||
|
|
assert _MC_BASE_CFG['min_irp_alignment'] == 0.0
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 2. ALGO_VERSION PARITY
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestAlgoVersion:
|
||
|
|
def test_algo_version_is_v2_gold_fix(self):
|
||
|
|
assert ALGO_VERSION == "v2_gold_fix_v50-v750"
|
||
|
|
|
||
|
|
def test_algo_version_is_string(self):
|
||
|
|
assert isinstance(ALGO_VERSION, str)
|
||
|
|
|
||
|
|
def test_algo_version_not_v1_shakedown(self):
|
||
|
|
assert "v1" not in ALGO_VERSION
|
||
|
|
assert "v150" not in ALGO_VERSION
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 3. VEL_DIV FORMULA PARITY
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestVelDivFormula:
|
||
|
|
"""vel_div must always be v50 - v750, never v50 - v150."""
|
||
|
|
|
||
|
|
def test_v50_minus_v750_basic(self):
|
||
|
|
v50, v750 = -0.03, -0.01
|
||
|
|
assert (v50 - v750) == pytest.approx(-0.02)
|
||
|
|
|
||
|
|
def test_v50_minus_v750_signal_trigger(self):
|
||
|
|
vel_div = -0.035 - 0.005 # v50=-0.035, v750=0.005
|
||
|
|
assert vel_div < -0.02 # should trigger entry
|
||
|
|
|
||
|
|
def test_not_v50_minus_v150(self):
|
||
|
|
"""CRITICAL: v50-v150 was the v1 shakedown bug."""
|
||
|
|
v50, v150, v750 = -0.03, -0.02, -0.01
|
||
|
|
correct = v50 - v750 # -0.02
|
||
|
|
buggy = v50 - v150 # -0.01
|
||
|
|
assert correct != buggy, "v50-v750 must differ from v50-v150"
|
||
|
|
|
||
|
|
def test_ng7_normalize_uses_v750(self):
|
||
|
|
"""Verify _normalize_ng7_scan computes v50-v750."""
|
||
|
|
scan = {
|
||
|
|
'version': 'NG7',
|
||
|
|
'result': {
|
||
|
|
'multi_window_results': {
|
||
|
|
'50': {'tracking_data': {'lambda_max_velocity': -0.04}},
|
||
|
|
'150': {'tracking_data': {'lambda_max_velocity': -0.02}},
|
||
|
|
'750': {'tracking_data': {'lambda_max_velocity': -0.01}},
|
||
|
|
},
|
||
|
|
'pricing_data': {'current_prices': {'BTCUSDT': 70000.0}},
|
||
|
|
'regime_prediction': {'instability_score': 0.5},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
result = DolphinActor._normalize_ng7_scan(scan)
|
||
|
|
expected = -0.04 - (-0.01) # v50 - v750 = -0.03
|
||
|
|
assert abs(result['vel_div'] - expected) < 1e-10, (
|
||
|
|
f"vel_div={result['vel_div']} but expected v50-v750={expected}"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_ng7_normalize_not_v150(self):
|
||
|
|
scan = {
|
||
|
|
'version': 'NG7',
|
||
|
|
'result': {
|
||
|
|
'multi_window_results': {
|
||
|
|
'50': {'tracking_data': {'lambda_max_velocity': -0.04}},
|
||
|
|
'150': {'tracking_data': {'lambda_max_velocity': -0.02}},
|
||
|
|
'750': {'tracking_data': {'lambda_max_velocity': -0.01}},
|
||
|
|
},
|
||
|
|
'pricing_data': {'current_prices': {'BTCUSDT': 70000.0}},
|
||
|
|
'regime_prediction': {'instability_score': 0.5},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
result = DolphinActor._normalize_ng7_scan(scan)
|
||
|
|
buggy = -0.04 - (-0.02) # v50 - v150 = -0.02
|
||
|
|
assert result['vel_div'] != buggy, "vel_div must NOT be v50-v150"
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 4. VOL_OK COMPUTATION PARITY
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestVolOkParity:
|
||
|
|
"""GREEN must use the same BTC rolling dvol gate as BLUE."""
|
||
|
|
|
||
|
|
def _make_actor(self):
|
||
|
|
config = {'strategy_name': 'test', 'engine': {}}
|
||
|
|
return DolphinActor(config)
|
||
|
|
|
||
|
|
def test_vol_p60_threshold_matches_blue(self):
|
||
|
|
assert VOL_P60_THRESHOLD == 0.00009868
|
||
|
|
|
||
|
|
def test_btc_vol_window_matches_blue(self):
|
||
|
|
assert BTC_VOL_WINDOW == 50
|
||
|
|
|
||
|
|
def test_vol_ok_returns_true_when_insufficient_data(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
scan = {'assets': ['BTCUSDT'], 'asset_prices': [70000.0]}
|
||
|
|
assert actor._compute_vol_ok(scan) is True
|
||
|
|
|
||
|
|
def test_vol_ok_returns_true_when_no_btc(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
scan = {'assets': ['ETHUSDT'], 'asset_prices': [3500.0]}
|
||
|
|
assert actor._compute_vol_ok(scan) is True
|
||
|
|
|
||
|
|
def test_vol_ok_returns_true_high_vol(self):
|
||
|
|
"""High volatility regime should pass vol_ok."""
|
||
|
|
actor = self._make_actor()
|
||
|
|
# Simulate volatile BTC prices
|
||
|
|
base = 70000.0
|
||
|
|
for i in range(52):
|
||
|
|
noise = (i % 3 - 1) * 500.0 # large swings
|
||
|
|
actor.btc_prices.append(base + noise)
|
||
|
|
scan = {'assets': ['BTCUSDT'], 'asset_prices': [70000.0 + 500.0]}
|
||
|
|
assert actor._compute_vol_ok(scan) is True
|
||
|
|
|
||
|
|
def test_vol_ok_returns_false_low_vol(self):
|
||
|
|
"""Very low volatility should fail vol_ok."""
|
||
|
|
actor = self._make_actor()
|
||
|
|
base = 70000.0
|
||
|
|
for i in range(52):
|
||
|
|
actor.btc_prices.append(base + i * 0.001) # near-zero variance
|
||
|
|
scan = {'assets': ['BTCUSDT'], 'asset_prices': [70000.052]}
|
||
|
|
assert actor._compute_vol_ok(scan) is False
|
||
|
|
|
||
|
|
def test_vol_ok_empty_scan(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
assert actor._compute_vol_ok({}) is True
|
||
|
|
|
||
|
|
def test_vol_ok_matches_blue_formula(self):
|
||
|
|
"""Verify the dvol computation formula matches BLUE exactly."""
|
||
|
|
actor = self._make_actor()
|
||
|
|
# Use volatile prices so dvol > threshold
|
||
|
|
prices = [70000.0 + ((-1)**i) * (i * 50.0) for i in range(52)]
|
||
|
|
for p in prices:
|
||
|
|
actor.btc_prices.append(p)
|
||
|
|
scan = {'assets': ['BTCUSDT'], 'asset_prices': [70000.0]}
|
||
|
|
|
||
|
|
# Manually compute like BLUE
|
||
|
|
arr = np.array(list(actor.btc_prices))
|
||
|
|
expected_dvol = float(np.std(np.diff(arr) / arr[:-1]))
|
||
|
|
result = actor._compute_vol_ok(scan)
|
||
|
|
assert result == (expected_dvol > VOL_P60_THRESHOLD), (
|
||
|
|
f"dvol={expected_dvol:.8f} threshold={VOL_P60_THRESHOLD} result={result}"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 5. BUCKET SL PCT PARITY
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestBucketSlPctParity:
|
||
|
|
"""GREEN must have the same per-bucket SL percentages as BLUE."""
|
||
|
|
|
||
|
|
BLUE_BUCKET_SL = {
|
||
|
|
0: 0.015, 1: 0.012, 2: 0.015, 3: 0.025,
|
||
|
|
4: 0.008, 5: 0.018, 6: 0.030, 'default': 0.015,
|
||
|
|
}
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("bucket_id,expected", list(BLUE_BUCKET_SL.items()))
|
||
|
|
def test_bucket_sl_matches_blue(self, bucket_id, expected):
|
||
|
|
assert bucket_id in _BUCKET_SL_PCT, f"Bucket {bucket_id} missing"
|
||
|
|
assert _BUCKET_SL_PCT[bucket_id] == expected
|
||
|
|
|
||
|
|
def test_all_7_buckets_present(self):
|
||
|
|
for i in range(7):
|
||
|
|
assert i in _BUCKET_SL_PCT, f"Bucket {i} missing"
|
||
|
|
|
||
|
|
def test_default_present(self):
|
||
|
|
assert 'default' in _BUCKET_SL_PCT
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 6. ENGINE KWARGS GOLD-SPEC PARITY (via green.yml)
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestGreenYmlParity:
|
||
|
|
"""Verify green.yml engine config matches BLUE's ENGINE_KWARGS."""
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def green_config(self):
|
||
|
|
import yaml
|
||
|
|
cfg_path = Path('/mnt/dolphinng5_predict/prod/configs/green.yml')
|
||
|
|
if not cfg_path.exists():
|
||
|
|
pytest.skip("green.yml not found")
|
||
|
|
with open(cfg_path) as f:
|
||
|
|
return yaml.safe_load(f)
|
||
|
|
|
||
|
|
BLUE_GOLD_ENGINE = {
|
||
|
|
'vel_div_threshold': -0.02,
|
||
|
|
'vel_div_extreme': -0.05,
|
||
|
|
'min_leverage': 0.5,
|
||
|
|
'max_leverage': 8.0,
|
||
|
|
'leverage_convexity': 3.0,
|
||
|
|
'fraction': 0.20,
|
||
|
|
'fixed_tp_pct': 0.0095,
|
||
|
|
'stop_pct': 1.0,
|
||
|
|
'max_hold_bars': 250,
|
||
|
|
'use_direction_confirm': True,
|
||
|
|
'dc_lookback_bars': 7,
|
||
|
|
'dc_min_magnitude_bps': 0.75,
|
||
|
|
'dc_skip_contradicts': True,
|
||
|
|
'dc_leverage_boost': 1.0,
|
||
|
|
'dc_leverage_reduce': 0.5,
|
||
|
|
'use_asset_selection': True,
|
||
|
|
'min_irp_alignment': 0.0,
|
||
|
|
'use_sp_fees': True,
|
||
|
|
'use_sp_slippage': True,
|
||
|
|
'sp_maker_entry_rate': 0.62,
|
||
|
|
'sp_maker_exit_rate': 0.50,
|
||
|
|
'use_ob_edge': True,
|
||
|
|
'ob_edge_bps': 5.0,
|
||
|
|
'ob_confirm_rate': 0.40,
|
||
|
|
'lookback': 100,
|
||
|
|
'use_alpha_layers': True,
|
||
|
|
'use_dynamic_leverage': True,
|
||
|
|
'seed': 42,
|
||
|
|
}
|
||
|
|
|
||
|
|
@pytest.mark.parametrize("key,expected", list(BLUE_GOLD_ENGINE.items()))
|
||
|
|
def test_engine_param_matches_blue(self, green_config, key, expected):
|
||
|
|
eng = green_config.get('engine', {})
|
||
|
|
assert eng.get(key) == expected, (
|
||
|
|
f"green.yml engine['{key}']: got={eng.get(key)} expected={expected}"
|
||
|
|
)
|
||
|
|
|
||
|
|
def test_direction_is_short_only(self, green_config):
|
||
|
|
assert green_config.get('direction') == 'short_only'
|
||
|
|
|
||
|
|
def test_strategy_name_is_green(self, green_config):
|
||
|
|
assert green_config.get('strategy_name') == 'green'
|
||
|
|
|
||
|
|
def test_hz_state_map_is_green(self, green_config):
|
||
|
|
hz = green_config.get('hazelcast', {})
|
||
|
|
assert 'GREEN' in hz.get('state_map', ''), "State map must be GREEN-specific"
|
||
|
|
|
||
|
|
def test_hz_pnl_map_is_green(self, green_config):
|
||
|
|
hz = green_config.get('hazelcast', {})
|
||
|
|
assert 'GREEN' in hz.get('imap_pnl', ''), "PNL map must be GREEN-specific"
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 7. HIBERNATE PROTECTION PARITY
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestHibernateProtectionParity:
|
||
|
|
"""Verify GREEN has hibernate protection matching BLUE's behavior."""
|
||
|
|
|
||
|
|
def _make_actor(self):
|
||
|
|
config = {'strategy_name': 'test', 'engine': {}}
|
||
|
|
return DolphinActor(config)
|
||
|
|
|
||
|
|
def test_actor_has_hibernate_protect_method(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
assert hasattr(actor, '_hibernate_protect_position')
|
||
|
|
|
||
|
|
def test_actor_has_hibernate_protect_active_field(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
assert hasattr(actor, '_hibernate_protect_active')
|
||
|
|
assert actor._hibernate_protect_active is None
|
||
|
|
|
||
|
|
def test_actor_has_bucket_assignments_field(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
assert hasattr(actor, '_bucket_assignments')
|
||
|
|
assert isinstance(actor._bucket_assignments, dict)
|
||
|
|
|
||
|
|
def test_actor_has_compute_vol_ok(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
assert hasattr(actor, '_compute_vol_ok')
|
||
|
|
assert callable(actor._compute_vol_ok)
|
||
|
|
|
||
|
|
def test_actor_has_btc_prices_deque(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
assert hasattr(actor, 'btc_prices')
|
||
|
|
assert isinstance(actor.btc_prices, deque)
|
||
|
|
|
||
|
|
def test_hibernate_noop_when_no_position(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
actor.engine = None
|
||
|
|
# Should not raise
|
||
|
|
actor._hibernate_protect_position()
|
||
|
|
|
||
|
|
def test_hibernate_label_map(self):
|
||
|
|
"""Verify the hibernate exit reason re-labeling matches BLUE."""
|
||
|
|
_map = {
|
||
|
|
'FIXED_TP': 'HIBERNATE_TP',
|
||
|
|
'STOP_LOSS': 'HIBERNATE_SL',
|
||
|
|
'MAX_HOLD': 'HIBERNATE_MAXHOLD',
|
||
|
|
}
|
||
|
|
assert _map == {
|
||
|
|
'FIXED_TP': 'HIBERNATE_TP',
|
||
|
|
'STOP_LOSS': 'HIBERNATE_SL',
|
||
|
|
'MAX_HOLD': 'HIBERNATE_MAXHOLD',
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 8. E2E PARITY: REPLAY DAY MATCHES BLUE
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestE2EReplayParity:
|
||
|
|
"""Run a known-good day through both engines and compare capital trajectories."""
|
||
|
|
|
||
|
|
KNOWN_GOOD_DATE = '2026-02-25'
|
||
|
|
|
||
|
|
@pytest.fixture
|
||
|
|
def parquet_path(self):
|
||
|
|
p = Path(f'/mnt/dolphinng5_predict/vbt_cache_klines/{self.KNOWN_GOOD_DATE}.parquet')
|
||
|
|
if not p.exists():
|
||
|
|
pytest.skip(f"Parquet for {self.KNOWN_GOOD_DATE} not found")
|
||
|
|
return p
|
||
|
|
|
||
|
|
def test_replay_day_produces_finite_capital(self, parquet_path):
|
||
|
|
"""Run a full day replay and verify capital is finite and positive."""
|
||
|
|
import pandas as pd
|
||
|
|
from nautilus_dolphin.nautilus.proxy_boost_engine import create_d_liq_engine
|
||
|
|
|
||
|
|
df = pd.read_parquet(parquet_path)
|
||
|
|
meta_cols = {
|
||
|
|
'timestamp', 'scan_number', 'v50_lambda_max_velocity',
|
||
|
|
'v150_lambda_max_velocity', 'v300_lambda_max_velocity',
|
||
|
|
'v750_lambda_max_velocity', 'vel_div', 'instability_50', 'instability_150',
|
||
|
|
}
|
||
|
|
asset_columns = [c for c in df.columns if c not in meta_cols]
|
||
|
|
|
||
|
|
engine = create_d_liq_engine(
|
||
|
|
initial_capital=25000.0,
|
||
|
|
vel_div_threshold=-0.02, vel_div_extreme=-0.05,
|
||
|
|
min_leverage=0.5, max_leverage=8.0,
|
||
|
|
leverage_convexity=3.0, fraction=0.20,
|
||
|
|
fixed_tp_pct=0.0095, stop_pct=1.0, max_hold_bars=250,
|
||
|
|
use_direction_confirm=True, dc_lookback_bars=7,
|
||
|
|
dc_min_magnitude_bps=0.75, dc_skip_contradicts=True,
|
||
|
|
dc_leverage_boost=1.0, dc_leverage_reduce=0.5,
|
||
|
|
use_asset_selection=True, min_irp_alignment=0.0,
|
||
|
|
use_sp_fees=True, use_sp_slippage=True,
|
||
|
|
sp_maker_entry_rate=0.62, sp_maker_exit_rate=0.50,
|
||
|
|
use_ob_edge=True, ob_edge_bps=5.0, ob_confirm_rate=0.40,
|
||
|
|
lookback=100, use_alpha_layers=True,
|
||
|
|
use_dynamic_leverage=True, seed=42,
|
||
|
|
)
|
||
|
|
|
||
|
|
# Vol ok mask
|
||
|
|
vol_ok_mask = np.zeros(len(df), dtype=bool)
|
||
|
|
bp = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None
|
||
|
|
if bp is not None:
|
||
|
|
dv = np.full(len(bp), np.nan)
|
||
|
|
for j in range(50, len(bp)):
|
||
|
|
seg = bp[max(0, j - 50):j]
|
||
|
|
with np.errstate(invalid='ignore', divide='ignore'):
|
||
|
|
rets = np.diff(seg) / seg[:-1]
|
||
|
|
fin = rets[np.isfinite(rets)]
|
||
|
|
if len(fin) >= 5:
|
||
|
|
dv[j] = float(np.std(fin))
|
||
|
|
vol_ok_mask = np.where(np.isfinite(dv), dv > VOL_P60_THRESHOLD, False)
|
||
|
|
|
||
|
|
engine.begin_day(self.KNOWN_GOOD_DATE, posture='APEX', direction=-1)
|
||
|
|
engine.process_day(
|
||
|
|
self.KNOWN_GOOD_DATE, df, asset_columns,
|
||
|
|
vol_regime_ok=vol_ok_mask, direction=-1, posture='APEX',
|
||
|
|
)
|
||
|
|
|
||
|
|
capital = engine.capital
|
||
|
|
assert math.isfinite(capital), f"Capital not finite: {capital}"
|
||
|
|
assert capital > 0, f"Capital not positive: {capital}"
|
||
|
|
|
||
|
|
def test_replay_produces_trades(self, parquet_path):
|
||
|
|
"""Verify the engine actually trades on the known-good date."""
|
||
|
|
import pandas as pd
|
||
|
|
from nautilus_dolphin.nautilus.proxy_boost_engine import create_d_liq_engine
|
||
|
|
|
||
|
|
df = pd.read_parquet(parquet_path)
|
||
|
|
meta_cols = {
|
||
|
|
'timestamp', 'scan_number', 'v50_lambda_max_velocity',
|
||
|
|
'v150_lambda_max_velocity', 'v300_lambda_max_velocity',
|
||
|
|
'v750_lambda_max_velocity', 'vel_div', 'instability_50', 'instability_150',
|
||
|
|
}
|
||
|
|
asset_columns = [c for c in df.columns if c not in meta_cols]
|
||
|
|
|
||
|
|
engine = create_d_liq_engine(
|
||
|
|
initial_capital=25000.0,
|
||
|
|
vel_div_threshold=-0.02, vel_div_extreme=-0.05,
|
||
|
|
min_leverage=0.5, max_leverage=8.0,
|
||
|
|
leverage_convexity=3.0, fraction=0.20,
|
||
|
|
fixed_tp_pct=0.0095, stop_pct=1.0, max_hold_bars=250,
|
||
|
|
use_direction_confirm=True, dc_lookback_bars=7,
|
||
|
|
dc_min_magnitude_bps=0.75, dc_skip_contradicts=True,
|
||
|
|
dc_leverage_boost=1.0, dc_leverage_reduce=0.5,
|
||
|
|
use_asset_selection=True, min_irp_alignment=0.0,
|
||
|
|
use_sp_fees=True, use_sp_slippage=True,
|
||
|
|
sp_maker_entry_rate=0.62, sp_maker_exit_rate=0.50,
|
||
|
|
use_ob_edge=True, ob_edge_bps=5.0, ob_confirm_rate=0.40,
|
||
|
|
lookback=100, use_alpha_layers=True,
|
||
|
|
use_dynamic_leverage=True, seed=42,
|
||
|
|
)
|
||
|
|
|
||
|
|
vol_ok_mask = np.ones(len(df), dtype=bool)
|
||
|
|
|
||
|
|
result = engine.process_day(
|
||
|
|
self.KNOWN_GOOD_DATE, df, asset_columns,
|
||
|
|
vol_regime_ok=vol_ok_mask, direction=-1, posture='APEX',
|
||
|
|
)
|
||
|
|
|
||
|
|
trades = result.get('trades', 0)
|
||
|
|
assert trades > 0, f"Expected trades on {self.KNOWN_GOOD_DATE}, got {trades}"
|
||
|
|
|
||
|
|
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
# 9. CH OUTPUT SEPARATION
|
||
|
|
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
|
|
||
|
|
class TestOutputSeparation:
|
||
|
|
"""GREEN must write to GREEN-specific CH/HZ channels, never BLUE."""
|
||
|
|
|
||
|
|
def _make_actor(self):
|
||
|
|
config = {
|
||
|
|
'strategy_name': 'green',
|
||
|
|
'engine': {},
|
||
|
|
'hazelcast': {
|
||
|
|
'imap_pnl': 'DOLPHIN_PNL_GREEN',
|
||
|
|
'state_map': 'DOLPHIN_STATE_GREEN',
|
||
|
|
},
|
||
|
|
}
|
||
|
|
return DolphinActor(config)
|
||
|
|
|
||
|
|
def test_strategy_name_is_green(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
assert actor._strategy_name == 'green'
|
||
|
|
|
||
|
|
def test_default_strategy_name_is_green(self):
|
||
|
|
actor = DolphinActor({})
|
||
|
|
assert actor._strategy_name == 'green'
|
||
|
|
|
||
|
|
def test_hz_pnl_map_from_config(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
imap_name = actor.dolphin_config.get('hazelcast', {}).get('imap_pnl', '')
|
||
|
|
assert 'GREEN' in imap_name
|
||
|
|
|
||
|
|
def test_hz_state_map_from_config(self):
|
||
|
|
actor = self._make_actor()
|
||
|
|
state_map = actor.dolphin_config.get('hazelcast', {}).get('state_map', '')
|
||
|
|
assert 'GREEN' in state_map
|