""" 56-Day Nautilus DolphinActor Backtest ======================================== Exercises the FULL Nautilus trading core: - Nautilus BacktestEngine kernel (not just NDAlphaEngine direct) - DolphinActor.on_bar() → begin_day() → step_bar() cycle - Nautilus order submission (Strategy A Direct path) - ACBv6 Hz listener wired inside actor - MC Forewarner gate via DolphinForewarner - Capital continuity tracked in memory across 56 days This is the harness the user asked for: VBT vectors → Nautilus trading core. Architecture: Per-day loop: BacktestEngine.run() with one synthetic midnight bar → DolphinActor.on_bar() fires once → actor loads vbt_cache_klines/{date}.parquet (replay mode) → iterates 1439 rows via NDAlphaEngine.step_bar() → submits Nautilus orders per signal (gracefully no-ops for unregistered instruments) → NDAlphaEngine.capital is authoritative for P&L Capital carries forward: next day engine starts with prior day's capital. Gold reference: ROI=+181.81%, Trades=2155, DD=17.65% (Windows, full stack) Expected (post-regression): ROI~+54-111%, Trades~1959-2145 """ import sys import time import json from pathlib import Path from datetime import datetime, timezone _PROD_DIR = Path(__file__).resolve().parent _HCM_DIR = _PROD_DIR.parent _ND_DIR = _HCM_DIR / 'nautilus_dolphin' sys.path.insert(0, str(_HCM_DIR)) sys.path.insert(0, str(_ND_DIR)) VBT_KLINES_DIR = _HCM_DIR / 'vbt_cache' MC_MODELS_DIR = str(_ND_DIR / 'mc_results' / 'models') RUN_LOGS_DIR = _ND_DIR / 'run_logs' RUN_LOGS_DIR.mkdir(parents=True, exist_ok=True) WINDOW_START = '2025-12-31' WINDOW_END = '2026-02-25' INITIAL_CAPITAL = 25000.0 # Gold-calibrated vol threshold — must be identical across all codepaths. VOL_P60_THRESHOLD = 0.00026414 def _safe_capital(val): if val is None: return 25000.0 try: import numpy as np f = float(val) if not np.isfinite(f): return 25000.0 return f except: return 25000.0 # Champion engine config — nested under 'engine' key as DolphinActor expects CHAMPION_ENGINE_CFG = dict( boost_mode='d_liq', vel_div_threshold=-0.02, vel_div_extreme=-0.05, min_leverage=0.5, max_leverage=5.0, leverage_convexity=3.0, fraction=0.20, fixed_tp_pct=0.0095, stop_pct=1.0, max_hold_bars=120, 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, # gold spec: no IRP filter 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, ) MC_BASE_CFG = { 'trial_id': 0, 'vel_div_threshold': -0.020, 'vel_div_extreme': -0.050, '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, 'vd_trend_lookback': 10, 'min_leverage': 0.50, 'max_leverage': 5.00, 'leverage_convexity': 3.00, 'fraction': 0.20, 'use_alpha_layers': True, 'use_dynamic_leverage': True, 'fixed_tp_pct': 0.0095, 'stop_pct': 1.00, 'max_hold_bars': 120, '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, 'ob_imbalance_bias': -0.09, 'ob_depth_scale': 1.00, 'use_asset_selection': True, 'min_irp_alignment': 0.0, 'lookback': 100, # gold spec 'acb_beta_high': 0.80, 'acb_beta_low': 0.20, 'acb_w750_threshold_pct': 60, } def get_parquet_files(): all_pq = sorted(VBT_KLINES_DIR.glob('*.parquet')) return [p for p in all_pq if 'catalog' not in str(p) and WINDOW_START <= p.stem <= WINDOW_END] def run_one_day_nautilus(run_date: str, initial_capital: float, vol_p60: float, preload_dates: list, assets: list) -> dict: """Run one day through Nautilus BacktestEngine + DolphinActor.""" from nautilus_trader.backtest.engine import BacktestEngine, BacktestEngineConfig from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.data import Bar, BarType from nautilus_trader.model.objects import Price, Quantity, Money, Currency from nautilus_trader.model.enums import OmsType, AccountType from nautilus_trader.core.datetime import dt_to_unix_nanos from nautilus_trader.test_kit.providers import TestInstrumentProvider from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor BAR_TYPE_STR = 'BTCUSDT.BINANCE-5-SECOND-LAST-EXTERNAL' actor_cfg = { 'engine': dict(CHAMPION_ENGINE_CFG), 'paper_trade': {'initial_capital': initial_capital}, 'posture_override': 'APEX', 'live_mode': False, # replay mode: actor loads parquet internally 'run_date': run_date, # tells _run_replay_day() which day to process 'bar_type': BAR_TYPE_STR, 'mc_models_dir': MC_MODELS_DIR, 'mc_base_cfg': MC_BASE_CFG, 'venue': 'BINANCE', 'vol_p60': vol_p60, 'acb_preload_dates': preload_dates, 'assets': assets, 'parquet_dir': 'vbt_cache', } actor_cfg['engine']['initial_capital'] = initial_capital be_cfg = BacktestEngineConfig(trader_id='DOLPHIN-NAUTILUS-BT-001') engine = BacktestEngine(config=be_cfg) actor = DolphinActor(config=actor_cfg) engine.add_strategy(actor) venue = Venue('BINANCE') usdt = Currency.from_str('USDT') engine.add_venue( venue=venue, oms_type=OmsType.HEDGING, account_type=AccountType.MARGIN, base_currency=usdt, starting_balances=[Money(str(_safe_capital(initial_capital)), usdt)], ) # Register BTCUSDT instrument (matching parquet asset naming exactly) instrument = TestInstrumentProvider.default_fx_ccy('BTCUSDT', venue) engine.add_instrument(instrument) # One synthetic midnight bar — triggers DolphinActor.on_bar() once bar_type = BarType.from_str(BAR_TYPE_STR) dt_event = datetime.strptime(run_date, '%Y-%m-%d').replace( hour=0, minute=0, second=5, tzinfo=timezone.utc ) bars = [Bar( bar_type=bar_type, open=Price.from_str('10000.00000'), high=Price.from_str('10000.00000'), low=Price.from_str('10000.00000'), close=Price.from_str('10000.00000'), volume=Quantity.from_str('1'), ts_event=dt_to_unix_nanos(dt_event), ts_init=dt_to_unix_nanos(dt_event), )] engine.add_data(bars) t0 = time.time() try: engine.run() except Exception as e: print(f' [WARN] Engine.run() raised: {e}') result = { 'date': run_date, 'capital': initial_capital, 'pnl': 0.0, 'trades': 0, 'elapsed_s': round(time.time() - t0, 3), 'stale_state_events': actor._stale_state_events, } if actor.engine is not None: result['capital'] = _safe_capital(getattr(actor.engine, 'capital', initial_capital)) result['pnl'] = result['capital'] - float(initial_capital) result['trades'] = len(getattr(actor.engine, 'trade_history', [])) # Dispose to free resources engine.dispose() return result def run_backtest(): print('=' * 70) print('56-DAY NAUTILUS DOLPHIN ACTOR BACKTEST - FULL TRADING CORE') print(f'Window: {WINDOW_START} -> {WINDOW_END}') print('=' * 70) parquet_files = get_parquet_files() if not parquet_files: print(f'ERROR: No parquets in {VBT_KLINES_DIR} for window') return None print(f' Parquet files: {len(parquet_files)} ({parquet_files[0].stem} -> {parquet_files[-1].stem})') print(f' DolphinActor: replay mode, loads parquets from {VBT_KLINES_DIR}') print(f' Engine: D_LIQ_GOLD (8x soft / 9x hard)') print(f' MC Forewarner: {MC_MODELS_DIR}') print() # 1. Vol threshold — gold standard constant (5y BTC calibration, matches live trader) import pandas as pd import numpy as np vol_p60 = VOL_P60_THRESHOLD print(f"[*] Vol threshold: {vol_p60:.8f} (gold standard — consistent with live trader)") # 2. Extract Asset List from first parquet first_df = pd.read_parquet(parquet_files[0]) all_assets = [c for c in first_df.columns if c not in { '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' }] print(f"[*] Trading Asset List initialized ({len(all_assets)} assets)") t_total = time.time() capital = INITIAL_CAPITAL daily_caps = [] daily_pnls = [] total_trades_all = 0 all_window_dates = [pf.stem for pf in parquet_files] print(f' {"Day":<6} {"Date":<12} {"Capital":>12} {"Trades":>7} {"DayPnL":>10} {"Stale":>6}') print(f' {"-"*6} {"-"*12} {"-"*12} {"-"*7} {"-"*10} {"-"*6}') from decimal import Decimal capital_dec = Decimal(str(INITIAL_CAPITAL)) for i, pf in enumerate(parquet_files): ds = pf.stem result = run_one_day_nautilus(ds, float(capital_dec), vol_p60, all_window_dates, all_assets) current_cap_f = result['capital'] day_pnl_f = result['pnl'] day_trades = result['trades'] capital_dec += Decimal(str(round(day_pnl_f, 8))) total_trades_all += day_trades daily_caps.append(float(capital_dec)) daily_pnls.append(day_pnl_f) if i == 0 or i == len(parquet_files) - 1 or (i + 1) % 10 == 0 or abs(day_pnl_f) > 500: elapsed = time.time() - t_total print(f' {i+1:<6} {ds:<12} ${float(capital_dec):>11,.2f} {day_trades:>7} ' f'{day_pnl_f:>+10.2f} {result["stale_state_events"]:>6} [{elapsed:.0f}s]') # == Metrics ============================================================ import numpy as np elapsed_total = time.time() - t_total final_capital = float(capital_dec) # Decimal accumulator — correct final value roi = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100.0 caps_arr = np.array(daily_caps) roll_max = np.maximum.accumulate(caps_arr) dd_arr = (roll_max - caps_arr) / roll_max * 100.0 max_dd = float(np.max(dd_arr)) if len(dd_arr) > 0 else 0.0 daily_rets = np.array(daily_pnls) / INITIAL_CAPITAL sharpe = float(np.mean(daily_rets) / (np.std(daily_rets) + 1e-12) * np.sqrt(252)) print(f'\n {"="*60}') print(f' RESULT (Nautilus DolphinActor path):') print(f' ROI={roi:+.2f}% | Trades={total_trades_all} | Capital=${final_capital:,.2f}') print(f' MaxDD={max_dd:.2f}% | Sharpe={sharpe:.2f}') print(f' Elapsed: {elapsed_total:.1f}s') print(f'\n GOLD REF (D_LIQ_GOLD): ROI=+181.81%, T=2155, DD=17.65%') print(f' REALISTIC (w/ slippage): ROI~+111%, T~1959') print(f' {"="*60}') result = dict( roi=round(roi, 2), trades=total_trades_all, dd=round(max_dd, 2), sharpe=round(sharpe, 2), capital=round(final_capital, 2), window=f'{WINDOW_START}:{WINDOW_END}', days=len(parquet_files), elapsed_s=round(elapsed_total, 1), engine='Nautilus DolphinActor + D_LIQ_GOLD (8x/9x)', run_ts=datetime.now(timezone.utc).isoformat(), ) out_path = RUN_LOGS_DIR / f'nautilus_actor_56day_{datetime.now().strftime("%Y%m%d_%H%M%S")}.json' out_path.write_text(json.dumps(result, indent=2)) print(f' Saved: {out_path}') return result if __name__ == '__main__': run_backtest()