292 lines
12 KiB
Python
292 lines
12 KiB
Python
|
|
"""
|
|||
|
|
56-Day VBT-Vector Nautilus Backtest
|
|||
|
|
=====================================
|
|||
|
|
Linux-native port of replicate_181_gold.py
|
|||
|
|
Uses local vbt_cache_klines/, D_LIQ engine, static vol_p60 (gold path).
|
|||
|
|
|
|||
|
|
Gold reference (D_LIQ_GOLD, Windows/full stack):
|
|||
|
|
ROI=+181.81%, Trades=2155, DD=17.65%
|
|||
|
|
|
|||
|
|
Current champion (SYSTEM_BIBLE, ACBv6 refactor, this parquet state):
|
|||
|
|
ROI=+54.67%, Trades=2145, DD=15.80%
|
|||
|
|
(gold regression ~111% expected from ACB/orchestrator refactor — not agent-caused)
|
|||
|
|
|
|||
|
|
What this tests:
|
|||
|
|
- NDAlphaEngine + D_LIQ_GOLD config (8x soft / 9x hard)
|
|||
|
|
- MockOBProvider OB approximation (asset-specific biases)
|
|||
|
|
- ACBv6 with NG6 eigenvalues (if mounted)
|
|||
|
|
- MC forewarner gate (if models present)
|
|||
|
|
- Stochastic fill sim: sp_maker_entry_rate=0.62, sp_maker_exit_rate=0.50
|
|||
|
|
- GOLD vol_p60: static, calibrated from first 2 parquets (correct gold path)
|
|||
|
|
- NO set_esoteric_hazard_multiplier call (gold path invariant)
|
|||
|
|
- Lazy loading per day (RAM-safe)
|
|||
|
|
"""
|
|||
|
|
import sys
|
|||
|
|
import time
|
|||
|
|
import gc
|
|||
|
|
import json
|
|||
|
|
from pathlib import Path
|
|||
|
|
from datetime import datetime, timezone
|
|||
|
|
|
|||
|
|
import numpy as np
|
|||
|
|
import pandas as pd
|
|||
|
|
|
|||
|
|
# ── Path setup ────────────────────────────────────────────────────────────────
|
|||
|
|
_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_klines'
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
# ── Gold 56-day window ────────────────────────────────────────────────────────
|
|||
|
|
WINDOW_START = '2025-12-31'
|
|||
|
|
WINDOW_END = '2026-02-25' # inclusive; gold spec: 56-day Dec31–Feb25
|
|||
|
|
|
|||
|
|
# ── Champion ENGINE_KWARGS (frozen — mirrors blue.yml + gold spec) ────────────
|
|||
|
|
ENGINE_KWARGS = dict(
|
|||
|
|
initial_capital=25000.0,
|
|||
|
|
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.45,
|
|||
|
|
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.45, 'lookback': 100,
|
|||
|
|
'acb_beta_high': 0.80, 'acb_beta_low': 0.20, 'acb_w750_threshold_pct': 60,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
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',
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_parquet_files():
|
|||
|
|
"""Return sorted parquet files within the gold 56-day window."""
|
|||
|
|
all_pq = sorted(VBT_KLINES_DIR.glob('*.parquet'))
|
|||
|
|
filtered = [p for p in all_pq
|
|||
|
|
if 'catalog' not in str(p)
|
|||
|
|
and WINDOW_START <= p.stem <= WINDOW_END]
|
|||
|
|
return filtered
|
|||
|
|
|
|||
|
|
|
|||
|
|
def calibrate_vol_p60(parquet_files):
|
|||
|
|
"""Static gold-path vol_p60: calibrate from first 2 files only."""
|
|||
|
|
all_vols = []
|
|||
|
|
for pf in parquet_files[:2]:
|
|||
|
|
df = pd.read_parquet(pf)
|
|||
|
|
if 'BTCUSDT' in df.columns:
|
|||
|
|
pr = df['BTCUSDT'].values
|
|||
|
|
for i in range(60, len(pr)):
|
|||
|
|
seg = pr[max(0, i - 50):i]
|
|||
|
|
if len(seg) < 10:
|
|||
|
|
continue
|
|||
|
|
v = float(np.std(np.diff(seg) / seg[:-1]))
|
|||
|
|
if v > 0:
|
|||
|
|
all_vols.append(v)
|
|||
|
|
del df
|
|||
|
|
vp60 = float(np.percentile(all_vols, 60)) if all_vols else 0.0002
|
|||
|
|
print(f" Static vol_p60 (gold method, 2-file calibration): {vp60:.8f}")
|
|||
|
|
return vp60
|
|||
|
|
|
|||
|
|
|
|||
|
|
def build_ob_engine(parquet_files):
|
|||
|
|
"""Build MockOBProvider with gold-spec asset biases."""
|
|||
|
|
from nautilus_dolphin.nautilus.ob_features import OBFeatureEngine
|
|||
|
|
from nautilus_dolphin.nautilus.ob_provider import MockOBProvider
|
|||
|
|
|
|||
|
|
# Scan all files to collect asset universe
|
|||
|
|
all_assets = set()
|
|||
|
|
for pf in parquet_files:
|
|||
|
|
df_cols = pd.read_parquet(pf, columns=None).columns
|
|||
|
|
all_assets.update(c for c in df_cols if c not in META_COLS)
|
|||
|
|
OB_ASSETS = sorted(list(all_assets))
|
|||
|
|
|
|||
|
|
mock_ob = MockOBProvider(
|
|||
|
|
imbalance_bias=-0.09, depth_scale=1.0, assets=OB_ASSETS,
|
|||
|
|
imbalance_biases={
|
|||
|
|
'BTCUSDT': -0.086, 'ETHUSDT': -0.092,
|
|||
|
|
'BNBUSDT': +0.05, 'SOLUSDT': +0.05,
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
ob_eng = OBFeatureEngine(mock_ob)
|
|||
|
|
ob_eng.preload_date('mock', OB_ASSETS)
|
|||
|
|
print(f" OB_ASSETS={len(OB_ASSETS)}")
|
|||
|
|
return ob_eng, OB_ASSETS
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_backtest():
|
|||
|
|
print('=' * 70)
|
|||
|
|
print('56-DAY VBT-VECTOR NAUTILUS BACKTEST — D_LIQ_GOLD CONFIG')
|
|||
|
|
print(f'Window: {WINDOW_START} → {WINDOW_END}')
|
|||
|
|
print('=' * 70)
|
|||
|
|
|
|||
|
|
# ── Imports ────────────────────────────────────────────────────────────
|
|||
|
|
from nautilus_dolphin.nautilus.proxy_boost_engine import create_d_liq_engine
|
|||
|
|
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
|
|||
|
|
|
|||
|
|
t_start = time.time()
|
|||
|
|
|
|||
|
|
# ── Data setup ─────────────────────────────────────────────────────────
|
|||
|
|
parquet_files = get_parquet_files()
|
|||
|
|
if not parquet_files:
|
|||
|
|
print(f'ERROR: No parquet files found in {VBT_KLINES_DIR} for window {WINDOW_START}–{WINDOW_END}')
|
|||
|
|
return None
|
|||
|
|
print(f' Parquet files: {len(parquet_files)} ({parquet_files[0].stem} → {parquet_files[-1].stem})')
|
|||
|
|
|
|||
|
|
vol_p60 = calibrate_vol_p60(parquet_files)
|
|||
|
|
ob_eng, OB_ASSETS = build_ob_engine(parquet_files)
|
|||
|
|
|
|||
|
|
# ── Engine creation ─────────────────────────────────────────────────────
|
|||
|
|
kw = ENGINE_KWARGS.copy()
|
|||
|
|
eng = create_d_liq_engine(**kw)
|
|||
|
|
eng.set_ob_engine(ob_eng)
|
|||
|
|
print(f' Engine: {type(eng).__name__} | leverage: soft={eng.base_max_leverage}x abs={eng.abs_max_leverage}x')
|
|||
|
|
|
|||
|
|
# ── ACBv6 setup ─────────────────────────────────────────────────────────
|
|||
|
|
date_strings = [p.stem for p in parquet_files]
|
|||
|
|
acb = AdaptiveCircuitBreaker()
|
|||
|
|
try:
|
|||
|
|
acb.preload_w750(date_strings)
|
|||
|
|
eng.set_acb(acb)
|
|||
|
|
print(' ACBv6: loaded with NG6 eigenvalues')
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f' ACBv6: preload failed ({e}) — running without external factors')
|
|||
|
|
|
|||
|
|
# ── MC Forewarner ───────────────────────────────────────────────────────
|
|||
|
|
if Path(MC_MODELS_DIR).exists():
|
|||
|
|
try:
|
|||
|
|
from mc.mc_ml import DolphinForewarner
|
|||
|
|
forewarner = DolphinForewarner(models_dir=MC_MODELS_DIR)
|
|||
|
|
eng.set_mc_forewarner(forewarner, MC_BASE_CFG)
|
|||
|
|
print(f' MC Forewarner: wired ({MC_MODELS_DIR})')
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f' MC Forewarner: init failed ({e}) — disabled')
|
|||
|
|
else:
|
|||
|
|
print(f' MC Forewarner: models dir not found — disabled')
|
|||
|
|
|
|||
|
|
# ── NOTE: NO set_esoteric_hazard_multiplier call (gold path invariant) ─
|
|||
|
|
print(f' Hazard call: NOT called (gold path — base_max_leverage stays at {eng.base_max_leverage}x)')
|
|||
|
|
|
|||
|
|
print(f'\n Starting 56-day loop...\n {"Day":<6} {"Date":<12} {"Capital":>12} {"Trades":>7} {"DayPnL":>10}')
|
|||
|
|
print(f' {"-"*6} {"-"*12} {"-"*12} {"-"*7} {"-"*10}')
|
|||
|
|
|
|||
|
|
daily_caps = []
|
|||
|
|
daily_pnls = []
|
|||
|
|
total_days = len(parquet_files)
|
|||
|
|
|
|||
|
|
for i, pf in enumerate(parquet_files):
|
|||
|
|
ds = pf.stem
|
|||
|
|
|
|||
|
|
# Lazy load + float64 (gold path: no float32 cast to preserve precision)
|
|||
|
|
df = pd.read_parquet(pf)
|
|||
|
|
acols = [c for c in df.columns if c not in META_COLS]
|
|||
|
|
|
|||
|
|
# dvol approximation (identical to replicate_181_gold.py)
|
|||
|
|
bp = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None
|
|||
|
|
dvol = np.full(len(df), np.nan)
|
|||
|
|
if bp is not None:
|
|||
|
|
diffs = np.zeros(len(bp), dtype=np.float64)
|
|||
|
|
diffs[1:] = np.diff(bp) / bp[:-1]
|
|||
|
|
for j in range(50, len(bp)):
|
|||
|
|
dvol[j] = np.std(diffs[j - 50:j])
|
|||
|
|
|
|||
|
|
# Static vol_p60 (gold path — NOT rolling)
|
|||
|
|
vol_ok = np.where(np.isfinite(dvol), dvol > vol_p60, False)
|
|||
|
|
|
|||
|
|
cap_before = eng.capital
|
|||
|
|
eng.process_day(ds, df, acols, vol_regime_ok=vol_ok)
|
|||
|
|
day_pnl = eng.capital - cap_before
|
|||
|
|
daily_caps.append(eng.capital)
|
|||
|
|
daily_pnls.append(day_pnl)
|
|||
|
|
|
|||
|
|
if i == 0 or i == total_days - 1 or (i + 1) % 10 == 0 or abs(day_pnl) > 500:
|
|||
|
|
elapsed = time.time() - t_start
|
|||
|
|
print(f' {i+1:<6} {ds:<12} ${eng.capital:>11,.2f} {len(eng.trade_history):>7} {day_pnl:>+10.2f} [{elapsed:.0f}s]')
|
|||
|
|
|
|||
|
|
del df
|
|||
|
|
gc.collect()
|
|||
|
|
|
|||
|
|
# ── Metrics ────────────────────────────────────────────────────────────
|
|||
|
|
elapsed_total = time.time() - t_start
|
|||
|
|
tr = eng.trade_history
|
|||
|
|
n = len(tr)
|
|||
|
|
roi = (eng.capital - 25000.0) / 25000.0 * 100.0
|
|||
|
|
|
|||
|
|
print(f'\n {"="*60}')
|
|||
|
|
print(f' RESULT: ROI={roi:+.2f}% | Trades={n} | Capital=${eng.capital:,.2f}')
|
|||
|
|
|
|||
|
|
if n == 0:
|
|||
|
|
print(' No trades — check signal thresholds or data')
|
|||
|
|
return dict(roi=roi, trades=0, pf=0, dd=0, wr=0, sharpe=0)
|
|||
|
|
|
|||
|
|
def _abs(t):
|
|||
|
|
return t.pnl_absolute if hasattr(t, 'pnl_absolute') else t.pnl_pct * 250.0
|
|||
|
|
|
|||
|
|
wins = [t for t in tr if _abs(t) > 0]
|
|||
|
|
losses = [t for t in tr if _abs(t) <= 0]
|
|||
|
|
wr = len(wins) / n * 100.0
|
|||
|
|
pf = sum(_abs(t) for t in wins) / max(abs(sum(_abs(t) for t in losses)), 1e-9)
|
|||
|
|
|
|||
|
|
# Drawdown from daily equity curve
|
|||
|
|
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))
|
|||
|
|
|
|||
|
|
# Sharpe (daily returns)
|
|||
|
|
daily_rets = np.array(daily_pnls) / 25000.0
|
|||
|
|
sharpe = float(np.mean(daily_rets) / (np.std(daily_rets) + 1e-12) * np.sqrt(252))
|
|||
|
|
|
|||
|
|
print(f' PF={pf:.3f} | WR={wr:.1f}% | MaxDD={max_dd:.2f}% | Sharpe={sharpe:.2f}')
|
|||
|
|
print(f' Elapsed: {elapsed_total:.1f}s')
|
|||
|
|
print(f'\n GOLD REFERENCE (D_LIQ_GOLD, Windows): ROI=+181.81%, T=2155, DD=17.65%')
|
|||
|
|
print(f' EXPECTED (post-ACB-refactor regression): ROI~+111%, T~1959')
|
|||
|
|
print(f' {"="*60}')
|
|||
|
|
|
|||
|
|
result = dict(
|
|||
|
|
roi=round(roi, 2), trades=n, pf=round(pf, 3),
|
|||
|
|
dd=round(max_dd, 2), wr=round(wr, 1), sharpe=round(sharpe, 2),
|
|||
|
|
capital=round(eng.capital, 2),
|
|||
|
|
window=f'{WINDOW_START}:{WINDOW_END}',
|
|||
|
|
days=total_days,
|
|||
|
|
elapsed_s=round(elapsed_total, 1),
|
|||
|
|
engine='D_LIQ_GOLD (8x/9x)',
|
|||
|
|
run_ts=datetime.now(timezone.utc).isoformat(),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Save result
|
|||
|
|
out_path = RUN_LOGS_DIR / f'vbt_56day_nautilus_{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()
|