initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems: - prod/ (BLUE harness, configs, scripts, docs) - nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved) - adaptive_exit/ (AEM engine + models/bucket_assignments.pkl) - Observability/ (EsoF advisor, TUI, dashboards) - external_factors/ (EsoF producer) - mc_forewarning_qlabs_fork/ (MC regime/envelope) Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
540
nautilus_dolphin/tests/test_green_blue_parity.py
Executable file
540
nautilus_dolphin/tests/test_green_blue_parity.py
Executable file
@@ -0,0 +1,540 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user