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