""" nautilus_native_backtest.py -- Nautilus as the authoritative backtesting ledger ================================================================================== Architecture (PROPER Nautilus certification): - All VBT parquet asset columns registered as Nautilus instruments - Each 5-second parquet row converted to Nautilus Bar objects (all assets) - DolphinActor receives on_bar() for EVERY real 5s bar (not one synthetic) - Orders filled by Nautilus at real parquet prices - Nautilus account balance = authoritative capital ledger - engine.capital = shadow book, reconciled vs Nautilus at end of each day Day loop (per-day engine to limit RAM): Per day: 1. Load parquet (~8000-17000 rows x N assets) 2. Convert rows -> Nautilus Bar objects for every asset 3. BacktestEngine.add_data(bars) 4. BacktestEngine.run() -> DolphinActor.on_bar() fires ~8000-17000 times 5. Orders fill at real bar-close prices (Nautilus settles) 6. End of day: Nautilus account balance carried to next day Gold reference: ROI=+181.81%, Trades=2155, DD=17.65% """ import sys import time import json from datetime import datetime, timezone, timedelta from decimal import Decimal from pathlib import Path import math import numpy as np import pandas as pd _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)) # --------------------------------------------------------------------------- # Config # --------------------------------------------------------------------------- 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-26' # 56-day window (gold: T changed 2143→2155 when Feb26 added) INITIAL_CAPITAL = 25000.0 BAR_INTERVAL_S = 5 # 5-second bars RECON_TOLERANCE = 0.05 # 5% divergence threshold -> WARN RECON_CRITICAL = 0.20 # 20% divergence -> HALT # Gold-calibrated vol threshold — computed from full 5-year BTC history. # Used for LIVE TRADING ONLY (filters dead markets). VOL_P60_THRESHOLD = 0.00026414 # Gold static vol_p60 — exact value from exp_shared.load_data() (gold certification method): # for pf in parquet_files[:2]: pr=BTCUSDT; for i in range(60,len): seg=pr[i-50:i]; # v=std(np.diff(seg)/seg[:-1]); if v>0: collect → percentile(60) # Source: exp_shared.py:load_data(); verified 0.00009868 on this server. VOL_P60_INWINDOW = 0.00009868 TRADER_ID = 'DOLPHIN-NATIVE-BT-001' CHAMPION_ENGINE_CFG = dict( boost_mode='d_liq', vel_div_threshold=-0.02, vel_div_extreme=-0.05, min_leverage=0.5, max_leverage=8.0, # gold spec: 8x soft cap leverage_convexity=3.0, fraction=0.20, fixed_tp_pct=0.0095, stop_pct=1.0, max_hold_bars=120, # gold spec: 120 bars (exp_shared.ENGINE_KWARGS) 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.45, # gold spec: 0.45 IRP filter (exp_shared.ENGINE_KWARGS) 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, } # --------------------------------------------------------------------------- # Feature store (module-level singleton) - populated per day before engine.run() # DolphinActor reads from this in native_mode # --------------------------------------------------------------------------- _FEATURE_STORE = {} # ts_event_ns -> {vel_div, v50, v750, inst50, vol_ok} 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] # --------------------------------------------------------------------------- # Instrument factory # --------------------------------------------------------------------------- def _make_instrument(symbol: str, venue, price_precision: int = 5): """Create a Nautilus CurrencyPair instrument for a crypto USDT pair.""" from nautilus_trader.model.instruments import CurrencyPair from nautilus_trader.model.identifiers import InstrumentId, Symbol from nautilus_trader.model.objects import Price, Quantity, Currency from decimal import Decimal if symbol.endswith("USDT"): base = symbol[:-4] quote = "USDT" else: base = symbol[:3] quote = symbol[3:] instr_id = InstrumentId(Symbol(symbol), venue) return CurrencyPair( instrument_id=instr_id, raw_symbol=Symbol(symbol), base_currency=Currency.from_str(base), quote_currency=Currency.from_str(quote), price_precision=price_precision, size_precision=5, price_increment=Price(10**(-price_precision), price_precision), size_increment=Quantity(10**(-5), 5), lot_size=None, max_quantity=None, min_quantity=None, max_notional=None, min_notional=None, max_price=None, min_price=None, margin_init=Decimal("0.02"), margin_maint=Decimal("0.01"), maker_fee=Decimal("0.0002"), taker_fee=Decimal("0.0002"), ts_event=0, ts_init=0, ) # --------------------------------------------------------------------------- # Bar factory # --------------------------------------------------------------------------- def _make_bar(bar_type, price_float: float, ts_event_ns: int, price_precision: int = 5): """Create a Nautilus Bar with OHLC=close (flat bar from close-only data).""" from nautilus_trader.model.data import Bar from nautilus_trader.model.objects import Price, Quantity if price_float <= 0 or not np.isfinite(price_float): return None px = Price(price_float, precision=price_precision) qty = Quantity(1.0, precision=5) return Bar( bar_type=bar_type, open=px, high=px, low=px, close=px, volume=qty, ts_event=ts_event_ns, ts_init=ts_event_ns, ) # --------------------------------------------------------------------------- # Per-day runner # --------------------------------------------------------------------------- def _safe_f(val, default=0.0) -> float: """NaN-safe float conversion — used in hot data loading path.""" if val is None: return default try: f = float(val) return f if math.isfinite(f) else default except (TypeError, ValueError): return default def _compute_dvol_arb512(btc_prices, n_rows: int, threshold: float): """Rolling 50-bar dvol using flint.arb at 512-bit precision. Input data is float64 (parquet), but all arithmetic — differences, mean, variance, sqrt — is performed at 512-bit to prevent rounding accumulation across the rolling window. Benchmark: ~295ms/day vs ~94ms/day numpy (3.1x); zero precision divergence vs float64 for this signal (MATCH=True, 0 diffs). Returns: bool numpy array (vol_ok_mask), or None if flint unavailable. """ try: from flint import arb, ctx # python-flint v0.8.0 ctx.prec = 512 except ImportError: return None # Caller falls back to numpy float64 vol_ok_mask = np.zeros(n_rows, dtype=bool) threshold_arb = arb(str(threshold)) # Convert prices to arb once — O(n), only valid (positive, finite) entries prices_arb = [None] * n_rows for i in range(n_rows): p = float(btc_prices[i]) if p > 0.0 and math.isfinite(p): prices_arb[i] = arb(p) # arb(float) preserves float64 input precision # Compute returns (p[i] - p[i-1]) / p[i-1] at 512-bit returns_arb = [None] * n_rows for i in range(1, n_rows): if prices_arb[i] is not None and prices_arb[i - 1] is not None: returns_arb[i] = (prices_arb[i] - prices_arb[i - 1]) / prices_arb[i - 1] # Rolling population std over 50-bar window (population std matches gold VBT np.std) for i in range(50, n_rows): window = [returns_arb[j] for j in range(i - 50, i) if returns_arb[j] is not None] if len(window) < 10: continue n_w = arb(len(window)) mean = sum(window) / n_w variance = sum((x - mean) ** 2 for x in window) / n_w std = variance.sqrt() vol_ok_mask[i] = bool(std > threshold_arb) return vol_ok_mask def _precompute_continuous_vol_ok(parquet_files: list, threshold: float) -> dict: """Pre-compute vol_ok across all parquets as ONE continuous rolling window. Gold VBT methodology: data is one contiguous time-series, not per-day resets. Rolling 50-bar dvol window carries over day boundaries. Per-day dvol resets at midnight (2750 warmup bars lost across 55 days) and also loses context from prior-day volatility spikes — producing ~2.7% pass rate on this window vs 27.1% with continuous. Uses arb512 primary (flint); numpy float64 fallback. Returns dict: {ts_ns (int) -> vol_ok (bool)} """ # First pass: collect all BTC prices and timestamps in order all_ts_lists = [] all_btc_lists = [] for pf in parquet_files: df = pd.read_parquet(pf) if 'timestamp' in df.columns: ts = df['timestamp'].values.astype('int64') else: day_dt = datetime.strptime(pf.stem, '%Y-%m-%d').replace(tzinfo=timezone.utc) day_start_ns = int(day_dt.timestamp() * 1e9) ts = np.array([day_start_ns + i * BAR_INTERVAL_S * 1_000_000_000 for i in range(len(df))], dtype='int64') btc_col = df['BTCUSDT'].values.astype(float) if 'BTCUSDT' in df.columns else np.zeros(len(df)) all_ts_lists.append(ts) all_btc_lists.append(btc_col) all_ts_arr = np.concatenate(all_ts_lists).astype('int64') all_btc_arr = np.concatenate(all_btc_lists) n_total = len(all_ts_arr) print(f" [VOL] Computing continuous cross-day dvol ({n_total:,} rows, arb512)...") t0 = time.time() vol_ok_arr = _compute_dvol_arb512(all_btc_arr, n_total, threshold) if vol_ok_arr is None: # float64 fallback dvol = np.full(n_total, np.nan) diffs = np.zeros(n_total) valid = (all_btc_arr > 0) & np.isfinite(all_btc_arr) diffs[1:] = np.where(valid[1:] & valid[:-1], np.diff(all_btc_arr) / all_btc_arr[:-1], np.nan) for i in range(50, n_total): w = diffs[i - 50:i] fw = w[np.isfinite(w)] if len(fw) >= 10: dvol[i] = float(np.std(fw)) vol_ok_arr = np.isfinite(dvol) & (dvol > threshold) elapsed = time.time() - t0 n_ok = int(vol_ok_arr.sum()) print(f" [VOL] Done in {elapsed:.1f}s vol_ok=True: {n_ok:,}/{n_total:,} ({n_ok/n_total*100:.1f}%)") return {int(all_ts_arr[i]): bool(vol_ok_arr[i]) for i in range(n_total)} def _precompute_vol_ok_gold(parquet_files: list) -> dict: """Pre-compute vol_ok matching exp_shared.run_backtest() exactly. Methodology: - Per-day rets reset: rets = np.diff(bp) / (bp[:-1] + 1e-9) - 50-return window: v = np.std(rets[j-50:j]) - 1-bar shift: dvol[j+1] = v (same as run_backtest) - Dynamic threshold: vp60 accumulated across all processed days, recomputed after each day's vols are added (len > 1000 gate) Returns dict: {ts_ns (int) -> vol_ok (bool)} """ all_vols = [] result = {} for pf in parquet_files: df = pd.read_parquet(pf) run_date = pf.stem if 'timestamp' in df.columns: ts_ns_arr = df['timestamp'].values.astype('int64') else: day_dt = datetime.strptime(run_date, '%Y-%m-%d').replace(tzinfo=timezone.utc) day_start_ns = int(day_dt.timestamp() * 1e9) n = len(df) ts_ns_arr = np.array([day_start_ns + i * BAR_INTERVAL_S * 1_000_000_000 for i in range(n)], dtype='int64') bp = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None n = len(df) dvol = np.zeros(n, dtype=np.float64) if bp is not None and len(bp) > 1: rets = np.diff(bp.astype('float64')) / (bp[:-1].astype('float64') + 1e-9) for j in range(50, len(rets)): v = float(np.std(rets[j - 50:j])) dvol[j + 1] = v if v > 0: all_vols.append(v) vp60 = float(np.percentile(all_vols, 60)) if len(all_vols) > 1000 else VOL_P60_INWINDOW for i in range(n): result[int(ts_ns_arr[i])] = bool(dvol[i] > 0 and dvol[i] > vp60) return result def _write_run_log(actor, result: dict, run_id: str) -> Path: """Write full trade history + run state to RUN_LOGS_DIR/.json. Works in both backtest and paper-trade modes (no HZ required). Returns the path written. """ eng = getattr(actor, 'engine', None) # Trade history — NDAlphaEngine stores list of trade dicts trade_history = [] if eng is not None: raw_trades = getattr(eng, 'trade_history', []) for t in raw_trades: try: trade_history.append({k: (float(v) if hasattr(v, '__float__') else v) for k, v in (t.items() if hasattr(t, 'items') else {})}) except Exception: trade_history.append(str(t)) # Performance summary (if engine supports it) perf = {} if eng is not None and hasattr(eng, 'get_performance_summary'): try: perf = eng.get_performance_summary() except Exception: pass payload = { 'run_id': run_id, 'run_ts': datetime.now(timezone.utc).isoformat(), 'window': f'{WINDOW_START}:{WINDOW_END}', 'engine_cfg': {k: v for k, v in CHAMPION_ENGINE_CFG.items()}, 'vol_threshold': VOL_P60_THRESHOLD, 'initial_capital': INITIAL_CAPITAL, 'result': result, 'total_scans': getattr(eng, 'total_scans', None), # diagnostic: total step_bar calls 'bar_count': getattr(eng, '_bar_count', None), 'perf_summary': perf, 'trades': trade_history, 'trade_count': len(trade_history), 'open_positions': [str(p) for p in getattr(eng, 'open_positions', [])], 'stale_state_events': getattr(actor, '_stale_state_events', 0), } out_path = RUN_LOGS_DIR / f'{run_id}.json' with open(out_path, 'w') as fh: json.dump(payload, fh, indent=2, default=str) return out_path def run_continuous_native( initial_capital: float, preload_dates: list, assets: list, all_instruments: dict, # symbol -> Nautilus instrument parquet_files: list, # All parquets to process continuously ) -> dict: global _FEATURE_STORE from nautilus_trader.backtest.engine import BacktestEngine, BacktestEngineConfig from nautilus_trader.model.identifiers import Venue from nautilus_trader.model.data import BarType from nautilus_trader.model.objects import Money, Currency from nautilus_trader.model.enums import OmsType, AccountType from nautilus_dolphin.nautilus.dolphin_actor import DolphinActor import gc print(" [RAM] Building continuous state (20M+ bars). This may take up to 2 minutes...") _FEATURE_STORE.clear() all_bars = [] bar_type_map = {} for sym in assets: bt_str = f"{sym}.BINANCE-5-SECOND-LAST-EXTERNAL" bar_type_map[sym] = BarType.from_str(bt_str) # Load all days into RAM for idx, pf in enumerate(parquet_files): run_date = pf.stem print(f" - Loading {run_date} ({idx+1}/{len(parquet_files)})...", end='\r', flush=True) df = pd.read_parquet(pf) n_rows = len(df) # ── Timestamp alignment: use parquet's real timestamps (datetime64[ns]) ── has_ts_col = 'timestamp' in df.columns if has_ts_col: ts_ns_arr = df['timestamp'].values.astype('int64') else: day_dt = datetime.strptime(run_date, '%Y-%m-%d').replace(tzinfo=timezone.utc) day_start_ns = int(day_dt.timestamp() * 1e9) ts_ns_arr = np.array([day_start_ns + i * BAR_INTERVAL_S * 1_000_000_000 for i in range(n_rows)], dtype='int64') if idx == 0: print(f"\n [WARN] No 'timestamp' col in {pf.name} — using synthetic ts") btc = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None # ── Per-day vol_ok: rolling 50-bar dvol vs static in-window threshold ── # Gold cert methodology: VOL_P60_INWINDOW = d['vol_p60'] from exp_shared.load_data() vol_ok_mask = np.zeros(n_rows, dtype=bool) if btc is not None and len(btc) >= 51: btc_f = btc.astype(np.float64) for j in range(50, n_rows): seg = btc_f[max(0, j - 50):j] if len(seg) < 10: continue diffs_seg = np.diff(seg) denom = seg[:-1] if np.any(denom == 0): continue v = float(np.std(diffs_seg / denom)) if math.isfinite(v) and v > 0: vol_ok_mask[j] = v > VOL_P60_INWINDOW # ── Vectorised column index lookup for speed ── cols = df.columns.tolist() vd_idx = cols.index('vel_div') if 'vel_div' in cols else -1 v50_idx = cols.index('v50_lambda_max_velocity') if 'v50_lambda_max_velocity' in cols else -1 v750_idx = cols.index('v750_lambda_max_velocity') if 'v750_lambda_max_velocity' in cols else -1 i50_idx = cols.index('instability_50') if 'instability_50' in cols else -1 asset_col_indices = {sym: cols.index(sym) for sym in assets if sym in cols} data_arr = df.values # numpy array for O(1) row/col access for row_i in range(n_rows): ts_ns = int(ts_ns_arr[row_i]) row_vals = data_arr[row_i] vd_raw = _safe_f(row_vals[vd_idx]) if vd_idx != -1 else 0.0 _FEATURE_STORE[ts_ns] = { 'vel_div': vd_raw if math.isfinite(vd_raw) else None, 'v50': _safe_f(row_vals[v50_idx]) if v50_idx != -1 else 0.0, 'v750': _safe_f(row_vals[v750_idx]) if v750_idx != -1 else 0.0, 'inst50': _safe_f(row_vals[i50_idx]) if i50_idx != -1 else 0.0, 'vol_ok': bool(vol_ok_mask[row_i]), # per-day dvol vs in-window static threshold 'row_i': row_i, } # Deterministic ordering: non-BTC first, BTCUSDT last (sync gate trigger) for sym in assets: if sym == 'BTCUSDT' or sym not in asset_col_indices: continue p = row_vals[asset_col_indices[sym]] if p is not None and math.isfinite(float(p)) and float(p) > 0: bar = _make_bar(bar_type_map[sym], float(p), ts_ns) if bar is not None: all_bars.append(bar) # BTC last — triggers on_bar() sync gate if 'BTCUSDT' in asset_col_indices: p = row_vals[asset_col_indices['BTCUSDT']] if p is not None and math.isfinite(float(p)) and float(p) > 0: bar = _make_bar(bar_type_map['BTCUSDT'], float(p), ts_ns) if bar is not None: all_bars.append(bar) # Free memory incrementally if btc is not None: del btc del df, data_arr gc.collect() print(f" [RAM] Memory load complete. {len(all_bars):,} bars synthesized.") # Build Actor actor_cfg = { 'engine': dict(CHAMPION_ENGINE_CFG), 'paper_trade': {'initial_capital': initial_capital}, 'posture_override': 'APEX', 'live_mode': False, 'native_mode': True, 'run_date': WINDOW_START, 'bar_type': 'BTCUSDT.BINANCE-5-SECOND-LAST-EXTERNAL', 'mc_models_dir': MC_MODELS_DIR, 'mc_base_cfg': MC_BASE_CFG, 'venue': 'BINANCE', 'vol_p60': VOL_P60_THRESHOLD, # unified gold constant 'acb_preload_dates': preload_dates, 'assets': assets, 'parquet_dir': 'vbt_cache', 'registered_assets': assets, } actor_cfg['engine']['initial_capital'] = initial_capital be_cfg = BacktestEngineConfig(trader_id=TRADER_ID) bt_engine = BacktestEngine(config=be_cfg) venue = Venue('BINANCE') usdt = Currency.from_str('USDT') bt_engine.add_venue( venue=venue, oms_type=OmsType.HEDGING, account_type=AccountType.MARGIN, base_currency=usdt, starting_balances=[Money(str(round(initial_capital, 8)), usdt)], ) for sym, instr in all_instruments.items(): bt_engine.add_instrument(instr) actor = DolphinActor(config=actor_cfg) bt_engine.add_strategy(actor) bt_engine.add_data(all_bars) print(" [NATIVE] Starting Continuous Execution Engine...") t0 = time.time() try: bt_engine.run() except Exception as e: print(f" [WARN] bt_engine.run() error bounds: {e}") elapsed = round(time.time() - t0, 1) try: accounts = list(bt_engine.trader.portfolio.accounts()) nautilus_account = accounts[0] if accounts else None if nautilus_account: print(f" [CAPITAL] Nautilus Account Final: {nautilus_account.balances()}") naut_bal_obj = nautilus_account.balance_total(usdt) naut_capital = float(naut_bal_obj.as_double()) if naut_bal_obj else initial_capital else: naut_capital = initial_capital except Exception: naut_capital = initial_capital shadow_capital = float(getattr(actor.engine, 'capital', initial_capital)) trades = len(getattr(actor.engine, 'trade_history', [])) if abs(naut_capital - initial_capital) < 1e-4 and trades > 0: recon_status = 'SHADOW_USED' final_capital = shadow_capital else: recon_status = 'NAUTILUS' final_capital = naut_capital divergence = abs(naut_capital - shadow_capital) / max(shadow_capital, 1.0) if divergence > RECON_CRITICAL and trades > 0 and abs(naut_capital - initial_capital) > 1.0: recon_status = f'DIVERGE_CRITICAL_{divergence:.1%}' elif divergence > RECON_TOLERANCE and trades > 0 and abs(naut_capital - initial_capital) > 1.0: recon_status = f'DIVERGE_WARN_{divergence:.1%}' pnl = final_capital - initial_capital result = { 'date': f'{WINDOW_START}_{WINDOW_END}', 'capital': final_capital, 'shadow_capital': shadow_capital, 'naut_capital': naut_capital, 'pnl': pnl, 'trades': trades, 'recon': recon_status, 'elapsed_s': elapsed, 'stale_state_events': getattr(actor, '_stale_state_events', 0), } # Write full trade + state log before disposing engine run_id = f"nautilus_native_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}" log_path = _write_run_log(actor, result, run_id) print(f" [LOG] Run log: {log_path.name} ({result['trades']} trades)") bt_engine.dispose() return result # --------------------------------------------------------------------------- # Main backtest loop # --------------------------------------------------------------------------- def run_native_backtest(): print('=' * 72) print('NAUTILUS CONTINUOUS NATIVE BACKTEST (32GB+ RAM) - SINGLE STATE') print(f'Window: {WINDOW_START} -> {WINDOW_END}') print(f'Mode: VBT parquet -> Nautilus Bar stream -> DolphinActor.on_bar()') print('=' * 72) from nautilus_trader.model.identifiers import Venue as NVenue NV = NVenue('BINANCE') # Get parquet files parquet_files = get_parquet_files() if not parquet_files: print(f'ERROR: No parquets in {VBT_KLINES_DIR}') return None print(f' Parquet files: {len(parquet_files)} ({parquet_files[0].stem} -> {parquet_files[-1].stem})') # Get asset list df0 = pd.read_parquet(parquet_files[0]) asset_cols = [c for c in df0.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' Assets: {len(asset_cols)} | {asset_cols[:5]}...') # Build instruments (once, reused each day) print(f' Building {len(asset_cols)} Nautilus instruments...') all_instruments = {} for sym in asset_cols: try: all_instruments[sym] = _make_instrument(sym, NV) except Exception as e: print(f' [WARN] Could not create instrument for {sym}: {e}') print(f' Registered: {len(all_instruments)} instruments') print(f' vol gate : per-day dvol > {VOL_P60_INWINDOW:.8f} (gold cert: static from exp_shared.load_data())') print(f' vol threshold : {VOL_P60_THRESHOLD:.8f} (live trading only)') print(f' min_irp_align : {CHAMPION_ENGINE_CFG["min_irp_alignment"]} (gold spec)') all_window_dates = [pf.stem for pf in parquet_files] # Run continuous execution print('\n Starting continuous backtest engine instantiation...') t_total = time.time() result = run_continuous_native( float(INITIAL_CAPITAL), all_window_dates, asset_cols, all_instruments, parquet_files ) final_capital = result['capital'] pnl_f = result['pnl'] trades_d = result['trades'] recon = result.get('recon', '?') elapsed = result.get('elapsed_s', 0) naut_str = f"${result.get('naut_capital', 0):,.2f}" shad_str = f"${result.get('shadow_capital', 0):,.2f}" print() print(f' {"Date":<25} {"Nautilus$":>11} {"Shadow$":>11} ' f'{"Trades":>6} {"PnL":>9} {"Recon":<25} {"Sec":>5}') print(f' {"-"*25} {"-"*11} {"-"*11} {"-"*6} {"-"*9} {"-"*25} {"-"*5}') print(f' {result["date"]:<25} {naut_str:>11} {shad_str:>11} ' f'{trades_d:>6} {pnl_f:>+9.2f} {recon:<25} {elapsed:>5.0f}s') # Final metrics roi = (final_capital - INITIAL_CAPITAL) / INITIAL_CAPITAL * 100.0 # In continuous mode, daily dd array isn't extracted here but we can default max_dd = 0.0 total_elapsed = time.time() - t_total print() print('=' * 72) print(f' Final Capital : ${final_capital:,.2f}') print(f' ROI : {roi:+.2f}% (gold ref: +181.81%)') print(f' Total Trades : {trades_d} (gold ref: 2155)') print(f' Total Elapsed : {total_elapsed/60:.1f} min') print('=' * 72) result_payload = { 'roi': round(roi, 4), 'trades': trades_d, 'dd': round(max_dd, 4), 'capital': round(final_capital, 4), 'window': f'{WINDOW_START}:{WINDOW_END}', 'days': len(parquet_files), 'elapsed_s': round(total_elapsed, 1), 'engine': 'Nautilus Native (real bars + Nautilus ledger)', 'run_ts': datetime.now(timezone.utc).isoformat(), } out_path = RUN_LOGS_DIR / f"nautilus_native_{datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')}.json" with open(out_path, 'w') as f: json.dump(result_payload, f, indent=2) print(f' Saved: {out_path.name}') return result_payload if __name__ == '__main__': import sys sys.modules['prod.nautilus_native_backtest'] = sys.modules['__main__'] run_native_backtest()