260 lines
11 KiB
Python
260 lines
11 KiB
Python
|
|
"""
|
|||
|
|
Shared infrastructure for proxy-B experiments (exp1–exp3, fast sweep).
|
|||
|
|
Provides: data loading, run_backtest() with gold-matching metrics, log_results().
|
|||
|
|
|
|||
|
|
Gold baseline (2026-03-14 confirmed):
|
|||
|
|
ROI=+88.55%, PF=1.215, DD=15.05%, Sharpe=4.38, WR=50.5%, Trades=2155
|
|||
|
|
"""
|
|||
|
|
import sys, time, math, json
|
|||
|
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|||
|
|
from pathlib import Path
|
|||
|
|
import numpy as np
|
|||
|
|
import pandas as pd
|
|||
|
|
|
|||
|
|
_HERE = Path(__file__).resolve().parent
|
|||
|
|
_ND_ROOT = _HERE.parent
|
|||
|
|
sys.path.insert(0, str(_ND_ROOT))
|
|||
|
|
|
|||
|
|
# ── Lazy JIT warmup (done once per process) ──────────────────────────────────
|
|||
|
|
_jit_done = False
|
|||
|
|
def ensure_jit():
|
|||
|
|
global _jit_done
|
|||
|
|
if _jit_done: return
|
|||
|
|
print("JIT warmup...")
|
|||
|
|
t0 = time.time()
|
|||
|
|
from nautilus_dolphin.nautilus.alpha_asset_selector import compute_irp_nb, compute_ars_nb, rank_assets_irp_nb
|
|||
|
|
from nautilus_dolphin.nautilus.alpha_bet_sizer import compute_sizing_nb
|
|||
|
|
from nautilus_dolphin.nautilus.alpha_signal_generator import check_dc_nb
|
|||
|
|
from nautilus_dolphin.nautilus.ob_features import (
|
|||
|
|
compute_imbalance_nb, compute_depth_1pct_nb, compute_market_agreement_nb,
|
|||
|
|
compute_cascade_signal_nb,
|
|||
|
|
)
|
|||
|
|
_p = np.array([1.0, 2.0, 3.0], dtype=np.float64)
|
|||
|
|
compute_irp_nb(_p, -1); compute_ars_nb(1.0, 0.5, 0.01)
|
|||
|
|
rank_assets_irp_nb(np.ones((10, 2), dtype=np.float64), 8, -1, 5, 500.0, 20, 0.20)
|
|||
|
|
compute_sizing_nb(-0.03,-0.02,-0.05,3.0,0.5,5.0,0.20,True,True,0.0,
|
|||
|
|
np.zeros(4,dtype=np.int64),np.zeros(4,dtype=np.int64),
|
|||
|
|
np.zeros(5,dtype=np.float64),0,-1,0.01,0.04)
|
|||
|
|
check_dc_nb(_p, 3, 1, 0.75)
|
|||
|
|
_b = np.array([100.,200.,300.,400.,500.], dtype=np.float64)
|
|||
|
|
_a = np.array([110.,190.,310.,390.,510.], dtype=np.float64)
|
|||
|
|
compute_imbalance_nb(_b,_a); compute_depth_1pct_nb(_b,_a)
|
|||
|
|
compute_market_agreement_nb(np.array([0.1,-0.05],dtype=np.float64),2)
|
|||
|
|
compute_cascade_signal_nb(np.array([-0.05,-0.15],dtype=np.float64),2,-0.10)
|
|||
|
|
print(f" JIT: {time.time()-t0:.1f}s")
|
|||
|
|
_jit_done = True
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── Paths ─────────────────────────────────────────────────────────────────────
|
|||
|
|
VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache")
|
|||
|
|
MC_MODELS_DIR= str(Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\nautilus_dolphin\mc_results\models"))
|
|||
|
|
|
|||
|
|
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'}
|
|||
|
|
|
|||
|
|
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,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
GOLD = dict(roi=88.55, pf=1.215, dd=15.05, sharpe=4.38, wr=50.5, trades=2155)
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ── Data loading (cached per process) ────────────────────────────────────────
|
|||
|
|
_data_cache = {}
|
|||
|
|
|
|||
|
|
def load_data():
|
|||
|
|
"""Returns metadata only; actual data loaded lazily in run_backtest."""
|
|||
|
|
from nautilus_dolphin.nautilus.ob_features import OBFeatureEngine
|
|||
|
|
from nautilus_dolphin.nautilus.ob_provider import MockOBProvider
|
|||
|
|
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
|
|||
|
|
|
|||
|
|
parquet_files = sorted(VBT_DIR.glob("*.parquet"))
|
|||
|
|
parquet_files = [p for p in parquet_files if 'catalog' not in str(p)]
|
|||
|
|
date_strings = [p.stem for p in parquet_files]
|
|||
|
|
|
|||
|
|
# Sample a few files to get vol_p60
|
|||
|
|
all_vols = []
|
|||
|
|
for pf in parquet_files[:3]:
|
|||
|
|
tmp = pd.read_parquet(pf)
|
|||
|
|
if 'BTCUSDT' in tmp.columns:
|
|||
|
|
bp = tmp['BTCUSDT'].values
|
|||
|
|
diffs = np.diff(bp) / bp[:-1]
|
|||
|
|
for i in range(50, len(diffs)):
|
|||
|
|
all_vols.append(np.std(diffs[i-50:i]))
|
|||
|
|
del tmp
|
|||
|
|
|
|||
|
|
vol_p60 = float(np.percentile(all_vols, 60)) if all_vols else 0.0002
|
|||
|
|
print(f" Calibrated vol_p60: {vol_p60:.8f}")
|
|||
|
|
|
|||
|
|
OB_ASSETS = ["BTCUSDT", "ETHUSDT"]
|
|||
|
|
_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)
|
|||
|
|
# Preload only the core assets for 56 days (memory-safe now)
|
|||
|
|
ob_eng.preload_date("mock", OB_ASSETS)
|
|||
|
|
|
|||
|
|
return dict(
|
|||
|
|
parquet_files=parquet_files, date_strings=date_strings,
|
|||
|
|
vol_p60=vol_p60, ob_eng=ob_eng, OB_ASSETS=OB_ASSETS,
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def load_forewarner():
|
|||
|
|
try:
|
|||
|
|
from mc.mc_ml import DolphinForewarner
|
|||
|
|
fw = DolphinForewarner(models_dir=MC_MODELS_DIR)
|
|||
|
|
print(" MC-Forewarner loaded (5 models)")
|
|||
|
|
return fw
|
|||
|
|
except Exception as e:
|
|||
|
|
print(f" MC-Forewarner unavailable: {e}")
|
|||
|
|
return None
|
|||
|
|
|
|||
|
|
|
|||
|
|
def run_backtest(engine_factory, name, forewarner=None, extra_kwargs=None):
|
|||
|
|
"""
|
|||
|
|
Run full 55-day backtest with gold-matching metrics (Lazy loading).
|
|||
|
|
"""
|
|||
|
|
import gc
|
|||
|
|
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
|
|||
|
|
d = load_data()
|
|||
|
|
kw = ENGINE_KWARGS.copy()
|
|||
|
|
if extra_kwargs: kw.update(extra_kwargs)
|
|||
|
|
|
|||
|
|
acb = AdaptiveCircuitBreaker()
|
|||
|
|
acb.preload_w750(d['date_strings'])
|
|||
|
|
|
|||
|
|
eng = engine_factory(kw)
|
|||
|
|
eng.set_ob_engine(d['ob_eng'])
|
|||
|
|
eng.set_acb(acb)
|
|||
|
|
if forewarner is not None:
|
|||
|
|
eng.set_mc_forewarner(forewarner, MC_BASE_CFG)
|
|||
|
|
eng.set_esoteric_hazard_multiplier(0.0)
|
|||
|
|
|
|||
|
|
daily_caps, daily_pnls = [], []
|
|||
|
|
all_vols = []
|
|||
|
|
|
|||
|
|
for i, pf in enumerate(d['parquet_files']):
|
|||
|
|
ds = pf.stem
|
|||
|
|
# Lazy Load and cast to float32 to save RAM
|
|||
|
|
df = pd.read_parquet(pf)
|
|||
|
|
for c in df.columns:
|
|||
|
|
if df[c].dtype == 'float64':
|
|||
|
|
df[c] = df[c].astype('float32')
|
|||
|
|
acols = [c for c in df.columns if c not in META_COLS]
|
|||
|
|
|
|||
|
|
# Per-day OB Preloading (Crucial for 230MB RAM)
|
|||
|
|
if eng.ob_engine is not None:
|
|||
|
|
eng.ob_engine.preload_date(ds, d['OB_ASSETS'])
|
|||
|
|
|
|||
|
|
# Optimized 5s dvol approximation
|
|||
|
|
bp = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None
|
|||
|
|
dvol = np.zeros(len(df), dtype=np.float32)
|
|||
|
|
if bp is not None:
|
|||
|
|
rets = np.diff(bp.astype('float64')) / (bp[:-1].astype('float64') + 1e-9)
|
|||
|
|
for j in range(50, len(rets)):
|
|||
|
|
v = np.std(rets[j-50:j])
|
|||
|
|
dvol[j+1] = v
|
|||
|
|
if v > 0: all_vols.append(v)
|
|||
|
|
|
|||
|
|
cap_before = eng.capital
|
|||
|
|
vp60 = np.percentile(all_vols, 60) if len(all_vols) > 1000 else d['vol_p60']
|
|||
|
|
|
|||
|
|
vol_ok = np.where(dvol > 0, dvol > vp60, False)
|
|||
|
|
eng.process_day(ds, df, acols, vol_regime_ok=vol_ok)
|
|||
|
|
daily_caps.append(eng.capital)
|
|||
|
|
daily_pnls.append(eng.capital - cap_before)
|
|||
|
|
|
|||
|
|
# CLEAR OB CACHE FOR DAY
|
|||
|
|
if eng.ob_engine is not None:
|
|||
|
|
eng.ob_engine._preloaded_placement.clear()
|
|||
|
|
eng.ob_engine._preloaded_signal.clear()
|
|||
|
|
eng.ob_engine._preloaded_market.clear()
|
|||
|
|
eng.ob_engine._ts_to_idx.clear()
|
|||
|
|
|
|||
|
|
del df
|
|||
|
|
gc.collect()
|
|||
|
|
|
|||
|
|
tr = eng.trade_history
|
|||
|
|
n = len(tr)
|
|||
|
|
roi = (eng.capital - 25000.0) / 25000.0 * 100.0
|
|||
|
|
|
|||
|
|
if n == 0:
|
|||
|
|
return dict(name=name, roi=roi, pf=0, dd=0, wr=0, sharpe=0, trades=0)
|
|||
|
|
|
|||
|
|
def _abs(t): return t.pnl_absolute if hasattr(t,'pnl_absolute') else t.pnl_pct*250.
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
peak_cap, max_dd = 25000.0, 0.0
|
|||
|
|
for cap in daily_caps:
|
|||
|
|
peak_cap = max(peak_cap, cap)
|
|||
|
|
max_dd = max(max_dd, (peak_cap - cap) / peak_cap * 100.0)
|
|||
|
|
|
|||
|
|
dr = np.array([p/25000.*100. for p in daily_pnls])
|
|||
|
|
sharpe = float(dr.mean()/(dr.std()+1e-9)*math.sqrt(365)) if len(dr)>1 else 0.
|
|||
|
|
|
|||
|
|
# Gather any engine-specific extra stats
|
|||
|
|
extra = {}
|
|||
|
|
for attr in ('gate_suppressed','gate_allowed','early_exits','sizing_scale_mean'):
|
|||
|
|
v = getattr(eng, attr, None)
|
|||
|
|
if v is not None: extra[attr] = v
|
|||
|
|
|
|||
|
|
return dict(name=name, roi=roi, pf=pf, dd=max_dd, wr=wr, sharpe=sharpe,
|
|||
|
|
trades=n, **extra)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def print_table(results, gold=None):
|
|||
|
|
hdr = f"{'Config':<42} {'ROI%':>7} {'PF':>6} {'DD%':>6} {'WR%':>6} {'Sharpe':>7} {'Trades':>7}"
|
|||
|
|
print(hdr); print('-'*83)
|
|||
|
|
if gold:
|
|||
|
|
g = gold
|
|||
|
|
print(f"{'*** GOLD ***':<42} {g['roi']:>7.2f} {g['pf']:>6.4f} {g['dd']:>6.2f} "
|
|||
|
|
f"{g['wr']:>6.2f} {g['sharpe']:>7.3f} {g['trades']:>7d}")
|
|||
|
|
print('-'*83)
|
|||
|
|
for r in results:
|
|||
|
|
extra = ''
|
|||
|
|
if 'suppression_rate' in r: extra += f" gate_supp={r['suppression_rate']:.1f}%"
|
|||
|
|
if 'early_exits' in r: extra += f" early_exits={r['early_exits']}"
|
|||
|
|
if 'sizing_scale_mean' in r: extra += f" scale_mean={r['sizing_scale_mean']:.3f}"
|
|||
|
|
print(f"{r['name']:<42} {r['roi']:>7.2f} {r['pf']:>6.4f} {r['dd']:>6.2f} "
|
|||
|
|
f"{r['wr']:>6.2f} {r['sharpe']:>7.3f} {r['trades']:>7d}{extra}")
|
|||
|
|
|
|||
|
|
|
|||
|
|
def log_results(results, outfile, gold=None, meta=None):
|
|||
|
|
payload = {'gold': gold or GOLD, 'results': results}
|
|||
|
|
if meta: payload['meta'] = meta
|
|||
|
|
outfile = Path(outfile)
|
|||
|
|
outfile.parent.mkdir(parents=True, exist_ok=True)
|
|||
|
|
with open(outfile, 'w', encoding='utf-8') as f:
|
|||
|
|
json.dump(payload, f, indent=2)
|
|||
|
|
print(f"\n Logged → {outfile}")
|