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.
296 lines
12 KiB
Python
Executable File
296 lines
12 KiB
Python
Executable File
"""
|
|
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()
|