556 lines
29 KiB
Python
556 lines
29 KiB
Python
|
|
"""Validate integrated dynamic beta ACB v6 + full OB stack.
|
|||
|
|
|
|||
|
|
Full engine stack (all layers active):
|
|||
|
|
1. Signal: vel_div <= -0.02 primary threshold
|
|||
|
|
2. Vol gate: > p60
|
|||
|
|
3. IRP ARS asset selector: min_irp_alignment=0.45
|
|||
|
|
4. OB Sub-1: depth-quality hard-skip (asset picker lift)
|
|||
|
|
5. OB Sub-2: per-asset imbalance (SmartPlacer / MM rates, better fills)
|
|||
|
|
6. OB Sub-3: cross-asset consensus multiplier (overall market health indicator)
|
|||
|
|
7. OB Sub-4: withdrawal cascade beta (4th dimension, ACB integration)
|
|||
|
|
8. ACBv6 dynamic beta: INVERSE_MODE, log-boost, 3-scale meta-boost (`if beta>0:` gate)
|
|||
|
|
9. Cubic-convex dynamic leverage: max 5x, convexity 3.0
|
|||
|
|
10. ExF: 4 NPZ factors (funding/dvol/fng/taker) via ACB._load_external_factors()
|
|||
|
|
11. EsoF: tail clipper, hazard=0.0 (neutral — N=6 tail events, insufficient for non-zero)
|
|||
|
|
12. MC-Forewarner: SVM envelope + XGB watchdog, per-date RED/ORANGE gate
|
|||
|
|
Note: envelope is broad (SVM trained on 1.5-12x range, N=6 tail events).
|
|||
|
|
0 interventions on current 55-day dataset — all configs within envelope.
|
|||
|
|
Wired correctly; will activate when re-trained on multi-year dataset.
|
|||
|
|
10. TP/max-hold exits: 99bps TP, 120-bar max hold (OB-aware accordion)
|
|||
|
|
|
|||
|
|
OB calibration (real-observed 2025-01-15 data via MockOBProvider):
|
|||
|
|
BTC: -0.086 imbalance (sell pressure, confirms SHORT)
|
|||
|
|
ETH: -0.092 imbalance (sell pressure, confirms SHORT)
|
|||
|
|
BNB: +0.05 imbalance (mild buy pressure, mild contradict)
|
|||
|
|
SOL: +0.05 imbalance (mild buy pressure, mild contradict)
|
|||
|
|
|
|||
|
|
Full-stack benchmark (55-day dataset, 2025-12-31 to 2026-02-25, abs_max_leverage=6.0):
|
|||
|
|
ROI ~+44.89%, PF ~1.123, DD ~14.95%, Sharpe ~2.50, Trades ~2128
|
|||
|
|
DD < champion freeze (21.41%). Sharpe improved vs uncapped (1.89→2.50).
|
|||
|
|
|
|||
|
|
Pre-cap benchmark (abs_max_leverage=∞, historical reference only):
|
|||
|
|
ROI ~+104.85%, PF ~1.120, DD ~32.35%, Sharpe ~1.89
|
|||
|
|
|
|||
|
|
ACBv6-only baseline (no OB, no cap):
|
|||
|
|
ROI ~+61.86%, PF ~1.125, DD ~29.57%, Sharpe ~1.81
|
|||
|
|
|
|||
|
|
Leverage cap doctrine: ACB/EsoF/MC steer from 5x toward 6x ceiling; 6x is never breached.
|
|||
|
|
process_day() encapsulates all per-date/per-bar orchestration — test is data-loading only.
|
|||
|
|
|
|||
|
|
Note on benchmark history:
|
|||
|
|
- ACBv6-only baseline 61.86% is data-driven (Feb 5-8, 2026 cluster adds 4 adverse
|
|||
|
|
high-leverage days against SHORT). Code is correct; update after dataset expansion.
|
|||
|
|
- Two real bugs were fixed (see git log):
|
|||
|
|
1. evaluate() TypeError: regime_size_mult kwarg — now accepted as no-op param.
|
|||
|
|
2. ACBv6 leverage clamp was dead code; reverted to uncapped (champion behavior).
|
|||
|
|
"""
|
|||
|
|
import sys, time, math, json, csv
|
|||
|
|
sys.stdout.reconfigure(encoding='utf-8', errors='replace')
|
|||
|
|
from pathlib import Path
|
|||
|
|
from datetime import datetime
|
|||
|
|
from collections import defaultdict
|
|||
|
|
import numpy as np
|
|||
|
|
import pandas as pd
|
|||
|
|
|
|||
|
|
sys.path.insert(0, str(Path(__file__).parent))
|
|||
|
|
|
|||
|
|
print("Compiling numba kernels...")
|
|||
|
|
t0c = 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 (
|
|||
|
|
OBFeatureEngine, compute_imbalance_nb, compute_depth_1pct_nb,
|
|||
|
|
compute_depth_quality_nb, compute_fill_probability_nb, compute_spread_proxy_nb,
|
|||
|
|
compute_depth_asymmetry_nb, compute_imbalance_persistence_nb,
|
|||
|
|
compute_withdrawal_velocity_nb, compute_market_agreement_nb, compute_cascade_signal_nb,
|
|||
|
|
)
|
|||
|
|
from nautilus_dolphin.nautilus.ob_provider import MockOBProvider
|
|||
|
|
|
|||
|
|
_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)
|
|||
|
|
# OB kernel warmup
|
|||
|
|
_b = np.array([100.0, 200.0, 300.0, 400.0, 500.0], dtype=np.float64)
|
|||
|
|
_a = np.array([110.0, 190.0, 310.0, 390.0, 510.0], dtype=np.float64)
|
|||
|
|
compute_imbalance_nb(_b, _a); compute_depth_1pct_nb(_b, _a)
|
|||
|
|
compute_depth_quality_nb(210.0, 200.0); compute_fill_probability_nb(1.0)
|
|||
|
|
compute_spread_proxy_nb(_b, _a); compute_depth_asymmetry_nb(_b, _a)
|
|||
|
|
compute_imbalance_persistence_nb(np.array([0.1, -0.1], dtype=np.float64), 2)
|
|||
|
|
compute_withdrawal_velocity_nb(np.array([100.0, 110.0], dtype=np.float64), 1)
|
|||
|
|
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() - t0c:.1f}s")
|
|||
|
|
|
|||
|
|
# EsoF orchestrator: has set_esoteric_hazard_multiplier + updated set_mc_forewarner_status
|
|||
|
|
from nautilus_dolphin.nautilus.esf_alpha_orchestrator import NDAlphaEngine
|
|||
|
|
from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker
|
|||
|
|
from mc.mc_ml import DolphinForewarner
|
|||
|
|
|
|||
|
|
VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache")
|
|||
|
|
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-Forewarner: champion config dict (frozen, max_leverage overridden per-date inside engine) ---
|
|||
|
|
MC_MODELS_DIR = str(Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\nautilus_dolphin\mc_results\models"))
|
|||
|
|
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,
|
|||
|
|
}
|
|||
|
|
print("\nLoading MC-Forewarner trained models...")
|
|||
|
|
forewarner = DolphinForewarner(models_dir=MC_MODELS_DIR)
|
|||
|
|
print(" MC-Forewarner ready (One-Class SVM envelope + XGBoost champion/catas classifiers)")
|
|||
|
|
|
|||
|
|
parquet_files = sorted(VBT_DIR.glob("*.parquet"))
|
|||
|
|
parquet_files = [p for p in parquet_files if 'catalog' not in str(p)]
|
|||
|
|
|
|||
|
|
# --- Initialize ACB v6 with dynamic beta ---
|
|||
|
|
print("\nInitializing ACB v6...")
|
|||
|
|
acb = AdaptiveCircuitBreaker()
|
|||
|
|
date_strings = [pf.stem for pf in parquet_files]
|
|||
|
|
acb.preload_w750(date_strings)
|
|||
|
|
print(f" w750 threshold (p60): {acb._w750_threshold:.6f}")
|
|||
|
|
print(f" Dates with w750 data: {sum(1 for v in acb._w750_vel_cache.values() if v != 0.0)}/{len(date_strings)}")
|
|||
|
|
|
|||
|
|
# Show per-date beta assignments
|
|||
|
|
print("\n Per-date dynamic beta:")
|
|||
|
|
for ds in date_strings:
|
|||
|
|
info = acb.get_dynamic_boost_for_date(ds)
|
|||
|
|
w = acb._w750_vel_cache.get(ds, 0.0)
|
|||
|
|
print(f" {ds}: w750={w:+.6f} beta={info['beta']:.1f} boost={info['boost']:.2f}x signals={info['signals']:.1f}")
|
|||
|
|
|
|||
|
|
# Pre-load data
|
|||
|
|
all_vols = []
|
|||
|
|
for pf in parquet_files[:2]:
|
|||
|
|
df = pd.read_parquet(pf)
|
|||
|
|
if 'BTCUSDT' not in df.columns: continue
|
|||
|
|
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)
|
|||
|
|
vol_p60 = float(np.percentile(all_vols, 60))
|
|||
|
|
|
|||
|
|
pq_data = {}
|
|||
|
|
all_assets = set()
|
|||
|
|
for pf in parquet_files:
|
|||
|
|
df = pd.read_parquet(pf)
|
|||
|
|
ac = [c for c in df.columns if c not in META_COLS]
|
|||
|
|
all_assets.update(ac)
|
|||
|
|
bp = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None
|
|||
|
|
dv = np.full(len(df), np.nan)
|
|||
|
|
if bp is not None:
|
|||
|
|
for i in range(50, len(bp)):
|
|||
|
|
seg = bp[max(0,i-50):i]
|
|||
|
|
if len(seg)<10: continue
|
|||
|
|
dv[i] = float(np.std(np.diff(seg)/seg[:-1]))
|
|||
|
|
pq_data[pf.stem] = (df, ac, dv)
|
|||
|
|
|
|||
|
|
# --- Build OB engine (4 dimensions, real-calibrated MockOBProvider) ---
|
|||
|
|
# Calibration from real Binance OB data (2025-01-15 observation):
|
|||
|
|
# BTC: -0.086 (sell pressure, confirms SHORT)
|
|||
|
|
# ETH: -0.092 (sell pressure, confirms SHORT)
|
|||
|
|
# BNB: +0.05 (mild buy pressure, mild contradict)
|
|||
|
|
# SOL: +0.05 (mild buy pressure, mild contradict)
|
|||
|
|
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)
|
|||
|
|
|
|||
|
|
# --- Assemble full stack ---
|
|||
|
|
print(f"\n=== Running full stack: ACBv6 + OB 4D + MC-Forewarner + EsoF(neutral) + ExF ===")
|
|||
|
|
t0 = time.time()
|
|||
|
|
engine = NDAlphaEngine(**ENGINE_KWARGS)
|
|||
|
|
engine.set_ob_engine(ob_eng) # OB Sub-1/2/3/4
|
|||
|
|
engine.set_acb(acb) # ACBv6 dynamic beta + 3-scale meta-boost (internal)
|
|||
|
|
engine.set_mc_forewarner(forewarner, MC_BASE_CFG) # per-date envelope gate (internal)
|
|||
|
|
engine.set_esoteric_hazard_multiplier(0.0) # EsoF neutral (hazard=0 → 6x ceiling intact)
|
|||
|
|
engine._bar_log_enabled = True # Enable per-bar state capture for E2E parity tests
|
|||
|
|
# MC-Forewarner historical status: AMBER (catastrophic_prob=3.1% during 55-day period)
|
|||
|
|
# AMBER → bet_sizer.max_leverage stays at 5.0x (not GREEN → no 6x unlock)
|
|||
|
|
# abs_max_leverage=6.0 is the ceiling; ACB intelligence steers 5x→6x within it.
|
|||
|
|
|
|||
|
|
# --- Main loop: one call per day, engine handles everything internally ---
|
|||
|
|
dstats = []
|
|||
|
|
bar_records = []
|
|||
|
|
for pf in parquet_files:
|
|||
|
|
ds = pf.stem
|
|||
|
|
df, acols, dvol = pq_data[ds]
|
|||
|
|
vol_ok = np.where(np.isfinite(dvol), dvol > vol_p60, False)
|
|||
|
|
stats = engine.process_day(ds, df, acols, vol_regime_ok=vol_ok)
|
|||
|
|
dstats.append({**stats, 'cap': engine.capital})
|
|||
|
|
bar_records.extend({'date': ds, **b} for b in engine._bar_log)
|
|||
|
|
|
|||
|
|
elapsed = time.time() - t0
|
|||
|
|
|
|||
|
|
# Results
|
|||
|
|
tr = engine.trade_history
|
|||
|
|
w = [t for t in tr if t.pnl_absolute > 0]; l = [t for t in tr if t.pnl_absolute <= 0]
|
|||
|
|
gw = sum(t.pnl_absolute for t in w) if w else 0
|
|||
|
|
gl = abs(sum(t.pnl_absolute for t in l)) if l else 0
|
|||
|
|
roi = (engine.capital - 25000) / 25000 * 100
|
|||
|
|
pf = gw / gl if gl > 0 else 999
|
|||
|
|
dr = [s['pnl']/25000*100 for s in dstats]
|
|||
|
|
sharpe = np.mean(dr) / np.std(dr) * np.sqrt(365) if np.std(dr) > 0 else 0
|
|||
|
|
peak_cap = 25000.0; max_dd = 0.0
|
|||
|
|
for s in dstats:
|
|||
|
|
peak_cap = max(peak_cap, s['cap'])
|
|||
|
|
dd = (peak_cap - s['cap']) / peak_cap * 100
|
|||
|
|
max_dd = max(max_dd, dd)
|
|||
|
|
|
|||
|
|
mid = len(parquet_files) // 2
|
|||
|
|
h1 = sum(s['pnl'] for s in dstats[:mid])
|
|||
|
|
h2 = sum(s['pnl'] for s in dstats[mid:])
|
|||
|
|
|
|||
|
|
w_count = len(w); l_count = len(l)
|
|||
|
|
wr = w_count / len(tr) * 100 if tr else 0.0
|
|||
|
|
avg_win = float(np.mean([t.pnl_pct for t in w]) * 100) if w else 0.0
|
|||
|
|
avg_loss = float(np.mean([t.pnl_pct for t in l]) * 100) if l else 0.0
|
|||
|
|
|
|||
|
|
# MC-Forewarner intervention summary
|
|||
|
|
red_days = [s for s in dstats if s['mc_status'] == 'RED']
|
|||
|
|
orng_days = [s for s in dstats if s['mc_status'] == 'ORANGE']
|
|||
|
|
print(f"\n MC-Forewarner interventions ({len(red_days)} RED, {len(orng_days)} ORANGE):")
|
|||
|
|
for s in dstats:
|
|||
|
|
if s['mc_status'] != 'OK':
|
|||
|
|
print(f" {s['date']}: {s['mc_status']:6s} boost={s['boost']:.2f}x P&L={s['pnl']:+.0f}")
|
|||
|
|
|
|||
|
|
print(f"\n{'='*65}")
|
|||
|
|
print(f" Full Stack: ACBv6 + OB 4D + MC-Forewarner + EsoF(neutral) + ExF")
|
|||
|
|
print(f"{'='*65}")
|
|||
|
|
print(f" ROI: {roi:+.2f}%")
|
|||
|
|
print(f" PF: {pf:.3f}")
|
|||
|
|
print(f" DD: {max_dd:.2f}%")
|
|||
|
|
print(f" Sharpe: {sharpe:.2f}")
|
|||
|
|
print(f" WR: {wr:.1f}% (W={w_count} L={l_count})")
|
|||
|
|
print(f" AvgWin: {avg_win:+.3f}% AvgLoss: {avg_loss:+.3f}%")
|
|||
|
|
print(f" Trades: {len(tr)}")
|
|||
|
|
print(f" Capital: ${engine.capital:,.2f}")
|
|||
|
|
print(f" H1 P&L: ${h1:+,.2f}")
|
|||
|
|
print(f" H2 P&L: ${h2:+,.2f}")
|
|||
|
|
h2h1 = h2/h1 if h1 != 0 else float('nan')
|
|||
|
|
print(f" H2/H1: {h2h1:.2f}")
|
|||
|
|
print(f" Time: {elapsed:.0f}s")
|
|||
|
|
print(f"{'='*65}")
|
|||
|
|
print(f" ACBv6-only baseline (no OB): ROI=+61.86%, PF=1.125, DD=29.57%")
|
|||
|
|
print(f" Champion git freeze target: ROI=+85.72%, PF=1.178, DD=21.41%")
|
|||
|
|
print(f" Full-stack confirmed: ROI=+104.85%, PF=1.120, DD=32.35%, H2/H1=0.98")
|
|||
|
|
print(f"{'='*65}")
|
|||
|
|
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
# ROBUSTNESS & OVERFIT ANALYSIS
|
|||
|
|
# ═══════════════════════════════════════════════════════════════
|
|||
|
|
# Import scipy here (after all parquet/data loading) to avoid heap fragmentation
|
|||
|
|
from scipy.stats import chi2, skew as sp_skew, kurtosis as sp_kurt
|
|||
|
|
|
|||
|
|
dr_all = np.array([s['pnl'] / 25000.0 * 100 for s in dstats])
|
|||
|
|
n_d = len(dstats)
|
|||
|
|
actual_sh = float(np.mean(dr_all) / np.std(dr_all) * np.sqrt(365)) if np.std(dr_all) > 0 else 0.0
|
|||
|
|
|
|||
|
|
def _sub_metrics(sub, label, cap_start):
|
|||
|
|
"""Print metrics for a dstats sub-list; normalise daily returns by cap_start."""
|
|||
|
|
if not sub:
|
|||
|
|
return {}
|
|||
|
|
pnls_s = [s['pnl'] for s in sub]
|
|||
|
|
caps_s = [s['cap'] for s in sub]
|
|||
|
|
dr_s = [p / cap_start * 100 for p in pnls_s]
|
|||
|
|
roi_s = (caps_s[-1] - cap_start) / cap_start * 100
|
|||
|
|
sh_s = float(np.mean(dr_s) / np.std(dr_s) * np.sqrt(365)) if np.std(dr_s) > 0 else 0.0
|
|||
|
|
pk = cap_start; mdd_s = 0.0
|
|||
|
|
for c in caps_s:
|
|||
|
|
pk = max(pk, c); mdd_s = max(mdd_s, (pk - c) / pk * 100)
|
|||
|
|
n_t = sum(s['trades'] for s in sub)
|
|||
|
|
print(f" {label:<38} ROI={roi_s:+6.1f}% DD={mdd_s:4.1f}% Sh={sh_s:+5.2f} T={n_t}")
|
|||
|
|
return {'roi': roi_s, 'dd': mdd_s, 'sharpe': sh_s, 'trades': n_t}
|
|||
|
|
|
|||
|
|
print(f"\n{'═'*65}")
|
|||
|
|
print(f" ROBUSTNESS & OVERFIT ANALYSIS")
|
|||
|
|
print(f"{'═'*65}")
|
|||
|
|
|
|||
|
|
# ─── A. Sub-period breakdown ───────────────────────────────────
|
|||
|
|
print(f"\n A. SUB-PERIOD (Q1/Q2/Q3/Q4 + H1/H2 + monthly)")
|
|||
|
|
q = n_d // 4
|
|||
|
|
for qi, (qa, qb) in enumerate([(0,q),(q,2*q),(2*q,3*q),(3*q,n_d)]):
|
|||
|
|
sl = dstats[qa:qb]
|
|||
|
|
if not sl: continue
|
|||
|
|
ic_q = dstats[qa-1]['cap'] if qa > 0 else 25000.0
|
|||
|
|
_sub_metrics(sl, f" Q{qi+1} ({sl[0]['date']} – {sl[-1]['date']})", ic_q)
|
|||
|
|
print()
|
|||
|
|
mid_d = n_d // 2
|
|||
|
|
_sub_metrics(dstats[:mid_d], f" H1 ({dstats[0]['date']} – {dstats[mid_d-1]['date']})", 25000.0)
|
|||
|
|
_sub_metrics(dstats[mid_d:], f" H2 ({dstats[mid_d]['date']} – {dstats[-1]['date']})", dstats[mid_d-1]['cap'])
|
|||
|
|
print()
|
|||
|
|
date_to_idx = {s['date']: i for i, s in enumerate(dstats)}
|
|||
|
|
monthly_buckets = defaultdict(list)
|
|||
|
|
for s in dstats: monthly_buckets[s['date'][:7]].append(s)
|
|||
|
|
for mo in sorted(monthly_buckets):
|
|||
|
|
sl_m = monthly_buckets[mo]
|
|||
|
|
fi_m = date_to_idx[sl_m[0]['date']]
|
|||
|
|
ic_m = dstats[fi_m-1]['cap'] if fi_m > 0 else 25000.0
|
|||
|
|
_sub_metrics(sl_m, f" {mo} ({sl_m[0]['date']} – {sl_m[-1]['date']})", ic_m)
|
|||
|
|
|
|||
|
|
# ─── B. Walk-Forward 3-Fold Validation ────────────────────────
|
|||
|
|
print(f"\n B. WALK-FORWARD 3-FOLD (fresh engine per fold, ~{n_d//3} days/fold)")
|
|||
|
|
fold_n = n_d // 3
|
|||
|
|
fold_def = [(0, fold_n, 'Fold-1'), (fold_n, 2*fold_n, 'Fold-2'), (2*fold_n, n_d, 'Fold-3')]
|
|||
|
|
wf_sharpes = []
|
|||
|
|
for fa, fb, fn in fold_def:
|
|||
|
|
fold_files = parquet_files[fa:fb]
|
|||
|
|
ef = NDAlphaEngine(**ENGINE_KWARGS)
|
|||
|
|
ef.set_ob_engine(ob_eng)
|
|||
|
|
ef.set_acb(acb)
|
|||
|
|
ef.set_mc_forewarner(forewarner, MC_BASE_CFG)
|
|||
|
|
ef.set_esoteric_hazard_multiplier(0.0)
|
|||
|
|
fd_stats = []
|
|||
|
|
for fold_pf in fold_files:
|
|||
|
|
df_f, ac_f, dv_f = pq_data[fold_pf.stem]
|
|||
|
|
vok_f = np.where(np.isfinite(dv_f), dv_f > vol_p60, False)
|
|||
|
|
st_f = ef.process_day(fold_pf.stem, df_f, ac_f, vol_regime_ok=vok_f)
|
|||
|
|
fd_stats.append({**st_f, 'cap': ef.capital})
|
|||
|
|
tr_f = ef.trade_history
|
|||
|
|
w_f = [t for t in tr_f if t.pnl_absolute > 0]
|
|||
|
|
l_f = [t for t in tr_f if t.pnl_absolute <= 0]
|
|||
|
|
gw_f = sum(t.pnl_absolute for t in w_f) if w_f else 0.0
|
|||
|
|
gl_f = abs(sum(t.pnl_absolute for t in l_f)) if l_f else 0.0
|
|||
|
|
pff = gw_f / gl_f if gl_f > 0 else 999.0
|
|||
|
|
dr_f = np.array([s['pnl'] / 25000.0 * 100 for s in fd_stats])
|
|||
|
|
sh_f = float(np.mean(dr_f) / np.std(dr_f) * np.sqrt(365)) if np.std(dr_f) > 0 else 0.0
|
|||
|
|
pk_f = 25000.0; mdd_f = 0.0
|
|||
|
|
for s in fd_stats:
|
|||
|
|
pk_f = max(pk_f, s['cap']); mdd_f = max(mdd_f, (pk_f - s['cap']) / pk_f * 100)
|
|||
|
|
wr_f = len(w_f) / len(tr_f) * 100 if tr_f else 0.0
|
|||
|
|
roi_f = (ef.capital - 25000.0) / 25000.0 * 100
|
|||
|
|
d0, d1 = fold_files[0].stem, fold_files[-1].stem
|
|||
|
|
wf_sharpes.append(sh_f)
|
|||
|
|
print(f" {fn} ({d0}–{d1}): ROI={roi_f:+6.1f}% PF={pff:.3f} DD={mdd_f:4.1f}% Sh={sh_f:+.2f} WR={wr_f:.1f}% T={len(tr_f)}")
|
|||
|
|
wf_consistent = all(s > 0 for s in wf_sharpes)
|
|||
|
|
print(f" → All folds Sh>0: {'YES — temporal consistency confirmed' if wf_consistent else 'NO — temporal inconsistency detected'}")
|
|||
|
|
|
|||
|
|
# ─── C. Bootstrap 95% CIs ─────────────────────────────────────
|
|||
|
|
print(f"\n C. BOOTSTRAP 95% CIs (2000 resamples, daily P&L + trade-level PF)")
|
|||
|
|
rng_b = np.random.default_rng(42)
|
|||
|
|
boot_sh = []; boot_pf_b = []
|
|||
|
|
pnls_abs = np.array([t.pnl_absolute for t in tr])
|
|||
|
|
for _ in range(2000):
|
|||
|
|
idx = rng_b.integers(0, n_d, size=n_d)
|
|||
|
|
s_b = dr_all[idx]
|
|||
|
|
boot_sh.append(float(np.mean(s_b) / np.std(s_b) * np.sqrt(365)) if np.std(s_b) > 0 else 0.0)
|
|||
|
|
idx_t = rng_b.integers(0, len(pnls_abs), size=len(pnls_abs))
|
|||
|
|
p_b = pnls_abs[idx_t]
|
|||
|
|
gw_b = p_b[p_b > 0].sum(); gl_b = abs(p_b[p_b <= 0].sum())
|
|||
|
|
boot_pf_b.append(gw_b / gl_b if gl_b > 0 else 999.0)
|
|||
|
|
boot_sh = np.array(boot_sh)
|
|||
|
|
boot_pf_b = np.array(boot_pf_b)
|
|||
|
|
print(f" Sharpe actual={actual_sh:+.3f} 95% CI: [{np.percentile(boot_sh,2.5):+.2f}, {np.percentile(boot_sh,97.5):+.2f}]")
|
|||
|
|
print(f" PF actual={pf:.3f} 95% CI: [{np.percentile(boot_pf_b,2.5):.3f}, {np.percentile(boot_pf_b,97.5):.3f}]")
|
|||
|
|
print(f" Sharpe > 0: {100*np.mean(boot_sh>0):.1f}% of resamples positive")
|
|||
|
|
print(f" PF > 1.0: {100*np.mean(boot_pf_b>1.0):.1f}% of resamples > 1.0")
|
|||
|
|
|
|||
|
|
# ─── D. Permutation Significance Test ─────────────────────────
|
|||
|
|
# H₀: daily returns have zero mean (no edge). Sharpe is order-invariant so we
|
|||
|
|
# permute CENTERED returns — each shuffle has mean≈0, std unchanged → Sharpe≈0.
|
|||
|
|
# p-value = fraction of null Sharpes >= actual. This is the correct one-sided test.
|
|||
|
|
print(f"\n D. PERMUTATION TEST (1000 shuffles, H0: mean=0 via centered returns)")
|
|||
|
|
dr_centered = dr_all - np.mean(dr_all)
|
|||
|
|
def _perm_sharpe(rng, arr):
|
|||
|
|
s = rng.permutation(arr)
|
|||
|
|
sd = np.std(s)
|
|||
|
|
return float(np.mean(s) / sd * np.sqrt(365)) if sd > 0 else 0.0
|
|||
|
|
perm_sh = np.array([_perm_sharpe(rng_b, dr_centered) for _ in range(1000)])
|
|||
|
|
pval = float(np.mean(perm_sh >= actual_sh))
|
|||
|
|
t_stat = actual_sh / np.sqrt(365) * np.sqrt(n_d)
|
|||
|
|
print(f" Actual Sharpe: {actual_sh:.3f} (t-stat ≈ {t_stat:.2f} for N={n_d} days)")
|
|||
|
|
print(f" Null (H0 mean=0): mean={np.mean(perm_sh):.3f} std={np.std(perm_sh):.3f} p95={np.percentile(perm_sh,95):.3f}")
|
|||
|
|
print(f" p-value: {pval:.4f} → {'SIGNIFICANT (p<0.05)' if pval<0.05 else f'p>{0.05:.2f} — N={n_d} days too short for formal significance at 5%'}")
|
|||
|
|
print(f" (Ref: need t≈1.67 for 5% one-tailed; need ~{int((1.67*np.sqrt(365)/actual_sh)**2)+1} days for this Sharpe to reach significance)")
|
|||
|
|
|
|||
|
|
# ─── E. Return Autocorrelation + Ljung-Box ────────────────────
|
|||
|
|
print(f"\n E. RETURN AUTOCORRELATION (daily P&L, lags 1–7)")
|
|||
|
|
dm = dr_all - np.mean(dr_all)
|
|||
|
|
var0 = float(np.sum(dm**2))
|
|||
|
|
acf_vals = [float(np.sum(dm[k:] * dm[:n_d-k]) / var0) if var0 > 0 else 0.0 for k in range(1, 8)]
|
|||
|
|
bart_ci = 1.96 / np.sqrt(n_d)
|
|||
|
|
for k, ac in enumerate(acf_vals, 1):
|
|||
|
|
bar_str = '▐' * max(0, int(abs(ac) * 25))
|
|||
|
|
sig_str = ' ← significant' if abs(ac) > bart_ci else ''
|
|||
|
|
print(f" lag={k} ACF={ac:+.4f} {bar_str}{sig_str}")
|
|||
|
|
lb_q = float(n_d * (n_d + 2) * sum(acf_vals[k-1]**2 / (n_d - k) for k in range(1, 6)))
|
|||
|
|
lb_p = float(1.0 - chi2.cdf(lb_q, df=5))
|
|||
|
|
print(f" Ljung-Box Q(5)={lb_q:.3f} p={lb_p:.4f} → {'serial correlation present' if lb_p<0.05 else 'no significant serial correlation (OK)'}")
|
|||
|
|
print(f" Lag-1 ACF={acf_vals[0]:+.4f} (pre-OB baseline was +0.082; OB target was -0.053)")
|
|||
|
|
|
|||
|
|
# ─── F. Trade Distribution ────────────────────────────────────
|
|||
|
|
print(f"\n F. TRADE DISTRIBUTION STATISTICS")
|
|||
|
|
pnl_pcts = np.array([t.pnl_pct * 100 for t in tr])
|
|||
|
|
levs_arr = np.array([t.leverage for t in tr])
|
|||
|
|
bh_arr = np.array([t.bars_held for t in tr])
|
|||
|
|
exit_ctr = {}
|
|||
|
|
for t in tr: exit_ctr[t.exit_reason] = exit_ctr.get(t.exit_reason, 0) + 1
|
|||
|
|
print(f" P&L: mean={np.mean(pnl_pcts):+.4f}% σ={np.std(pnl_pcts):.4f}% skew={float(sp_skew(pnl_pcts)):.3f} kurt={float(sp_kurt(pnl_pcts)):.3f}")
|
|||
|
|
print(f" Leverage: mean={np.mean(levs_arr):.2f}x max={np.max(levs_arr):.2f}x p90={np.percentile(levs_arr,90):.2f}x p95={np.percentile(levs_arr,95):.2f}x p99={np.percentile(levs_arr,99):.2f}x")
|
|||
|
|
print(f" BarsHeld: mean={np.mean(bh_arr):.1f} median={np.median(bh_arr):.0f} p90={np.percentile(bh_arr,90):.0f} (cap=120)")
|
|||
|
|
print(f" Exits: {dict(sorted(exit_ctr.items()))}")
|
|||
|
|
w_pnls = pnl_pcts[pnl_pcts > 0]; l_pnls = pnl_pcts[pnl_pcts <= 0]
|
|||
|
|
if len(w_pnls) and len(l_pnls):
|
|||
|
|
print(f" Tail: p95-win={np.percentile(w_pnls,95):+.3f}% p05-loss={np.percentile(l_pnls,5):+.3f}% ratio={float(np.percentile(w_pnls,95)/abs(np.percentile(l_pnls,5))):.2f}")
|
|||
|
|
# Win/loss streak lengths
|
|||
|
|
streaks_w = []; streaks_l = []; cur_w = 0; cur_l = 0; prev_is_w = None
|
|||
|
|
for t in tr:
|
|||
|
|
is_w = t.pnl_absolute > 0
|
|||
|
|
if prev_is_w is None:
|
|||
|
|
cur_w = int(is_w); cur_l = int(not is_w)
|
|||
|
|
elif is_w == prev_is_w:
|
|||
|
|
if is_w: cur_w += 1
|
|||
|
|
else: cur_l += 1
|
|||
|
|
else:
|
|||
|
|
if prev_is_w: streaks_w.append(cur_w); cur_w = 1; cur_l = 0
|
|||
|
|
else: streaks_l.append(cur_l); cur_l = 1; cur_w = 0
|
|||
|
|
prev_is_w = is_w
|
|||
|
|
if prev_is_w: streaks_w.append(cur_w)
|
|||
|
|
else: streaks_l.append(cur_l)
|
|||
|
|
max_win_streak = max(streaks_w) if streaks_w else 0
|
|||
|
|
max_loss_streak = max(streaks_l) if streaks_l else 0
|
|||
|
|
print(f" Streaks: max-win={max_win_streak} max-loss={max_loss_streak} avg-win-run={np.mean(streaks_w):.1f} avg-loss-run={np.mean(streaks_l):.1f}")
|
|||
|
|
|
|||
|
|
# ─── G. Save logs ─────────────────────────────────────────────
|
|||
|
|
print(f"\n G. SAVING LOGS")
|
|||
|
|
LOG_DIR = Path(__file__).parent / "run_logs"
|
|||
|
|
LOG_DIR.mkdir(exist_ok=True)
|
|||
|
|
run_ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|||
|
|
|
|||
|
|
trades_path = LOG_DIR / f"trades_{run_ts}.csv"
|
|||
|
|
with open(trades_path, 'w', newline='') as f:
|
|||
|
|
cw = csv.writer(f)
|
|||
|
|
cw.writerow(['trade_id','asset','direction','entry_price','exit_price',
|
|||
|
|
'entry_bar','exit_bar','bars_held','leverage','notional',
|
|||
|
|
'pnl_pct','pnl_absolute','exit_reason','bucket_idx'])
|
|||
|
|
for t in tr:
|
|||
|
|
cw.writerow([t.trade_id, t.asset, t.direction,
|
|||
|
|
f"{t.entry_price:.6f}", f"{t.exit_price:.6f}",
|
|||
|
|
t.entry_bar, t.exit_bar, t.bars_held,
|
|||
|
|
f"{t.leverage:.4f}", f"{t.notional:.4f}",
|
|||
|
|
f"{t.pnl_pct:.8f}", f"{t.pnl_absolute:.4f}",
|
|||
|
|
t.exit_reason, t.bucket_idx])
|
|||
|
|
|
|||
|
|
daily_path = LOG_DIR / f"daily_{run_ts}.csv"
|
|||
|
|
with open(daily_path, 'w', newline='') as f:
|
|||
|
|
cw = csv.writer(f)
|
|||
|
|
cw.writerow(['date','pnl','capital','dd_pct','boost','beta','mc_status','trades'])
|
|||
|
|
pk_log = 25000.0
|
|||
|
|
for s in dstats:
|
|||
|
|
pk_log = max(pk_log, s['cap'])
|
|||
|
|
cw.writerow([s['date'], f"{s['pnl']:.4f}", f"{s['cap']:.4f}",
|
|||
|
|
f"{(pk_log - s['cap'])/pk_log*100:.4f}",
|
|||
|
|
f"{s['boost']:.4f}", f"{s['beta']:.2f}",
|
|||
|
|
s['mc_status'], s['trades']])
|
|||
|
|
|
|||
|
|
summary_d = {
|
|||
|
|
'run_ts': run_ts, 'n_days': n_d, 'n_trades': len(tr),
|
|||
|
|
# ── Performance ──
|
|||
|
|
'roi_pct': round(roi, 4), 'pf': round(pf, 4),
|
|||
|
|
'max_dd_pct': round(max_dd, 4), 'sharpe_annualized': round(actual_sh, 4),
|
|||
|
|
'wr_pct': round(wr, 4), 'capital_final': round(engine.capital, 4),
|
|||
|
|
'avg_win_pct': round(float(np.mean(pnl_pcts[pnl_pcts>0])),4) if any(pnl_pcts>0) else None,
|
|||
|
|
'avg_loss_pct': round(float(np.mean(pnl_pcts[pnl_pcts<=0])),4) if any(pnl_pcts<=0) else None,
|
|||
|
|
'h1_pnl': round(h1, 4), 'h2_pnl': round(h2, 4),
|
|||
|
|
'h2h1_ratio': round(h2h1, 4) if not math.isnan(h2h1) else None,
|
|||
|
|
# ── Robustness ──
|
|||
|
|
'walk_forward_sharpes': [round(s, 4) for s in wf_sharpes],
|
|||
|
|
'walk_forward_consistent': wf_consistent,
|
|||
|
|
'bootstrap_sharpe_ci95': [round(float(np.percentile(boot_sh, 2.5)), 3),
|
|||
|
|
round(float(np.percentile(boot_sh, 97.5)), 3)],
|
|||
|
|
'bootstrap_pf_ci95': [round(float(np.percentile(boot_pf_b, 2.5)), 3),
|
|||
|
|
round(float(np.percentile(boot_pf_b, 97.5)), 3)],
|
|||
|
|
'bootstrap_sharpe_pct_positive': round(float(100 * np.mean(boot_sh > 0)), 1),
|
|||
|
|
'bootstrap_pf_pct_above_1': round(float(100 * np.mean(boot_pf_b > 1.0)), 1),
|
|||
|
|
'permutation_pvalue': round(pval, 4),
|
|||
|
|
't_stat': round(t_stat, 4),
|
|||
|
|
'ljung_box_q5': round(lb_q, 4), 'ljung_box_p5': round(lb_p, 4),
|
|||
|
|
'lag1_acf': round(acf_vals[0], 4), 'lag2_acf': round(acf_vals[1], 4),
|
|||
|
|
# ── Trade distribution ──
|
|||
|
|
'leverage_mean': round(float(np.mean(levs_arr)), 3),
|
|||
|
|
'leverage_max': round(float(np.max(levs_arr)), 3),
|
|||
|
|
'leverage_p90': round(float(np.percentile(levs_arr, 90)), 3),
|
|||
|
|
'leverage_p95': round(float(np.percentile(levs_arr, 95)), 3),
|
|||
|
|
'leverage_p99': round(float(np.percentile(levs_arr, 99)), 3),
|
|||
|
|
'bars_held_mean': round(float(np.mean(bh_arr)), 2),
|
|||
|
|
'bars_held_median': int(np.median(bh_arr)),
|
|||
|
|
'pnl_skew': round(float(sp_skew(pnl_pcts)), 4),
|
|||
|
|
'pnl_kurt': round(float(sp_kurt(pnl_pcts)), 4),
|
|||
|
|
'max_win_streak': max_win_streak, 'max_loss_streak': max_loss_streak,
|
|||
|
|
'exit_reasons': exit_ctr,
|
|||
|
|
# ── Full parameter snapshot (all hyperparams) ──
|
|||
|
|
'engine_kwargs': ENGINE_KWARGS,
|
|||
|
|
'mc_base_cfg': MC_BASE_CFG,
|
|||
|
|
'acb_config': {
|
|||
|
|
'w750_threshold_pct': 60,
|
|||
|
|
'beta_high': acb._beta_high if hasattr(acb, '_beta_high') else 0.8,
|
|||
|
|
'beta_low': acb._beta_low if hasattr(acb, '_beta_low') else 0.2,
|
|||
|
|
'w750_threshold_value': round(float(acb._w750_threshold), 8),
|
|||
|
|
'n_dates': len(date_strings),
|
|||
|
|
},
|
|||
|
|
'ob_config': {
|
|||
|
|
'n_dims': 4,
|
|||
|
|
'provider': 'MockOBProvider',
|
|||
|
|
'static_maker_fill_prob': 0.62,
|
|||
|
|
'calibration': 'real Binance 2025-01-15',
|
|||
|
|
},
|
|||
|
|
'esof_hazard': 0.0,
|
|||
|
|
'abs_max_leverage': ENGINE_KWARGS.get('max_leverage', 5.0), # soft-cap applied inside engine
|
|||
|
|
'abs_max_leverage_ceiling': 6.0,
|
|||
|
|
}
|
|||
|
|
summary_path = LOG_DIR / f"summary_{run_ts}.json"
|
|||
|
|
with open(summary_path, 'w') as f:
|
|||
|
|
json.dump(summary_d, f, indent=2)
|
|||
|
|
|
|||
|
|
bars_path = LOG_DIR / f"bars_{run_ts}.csv"
|
|||
|
|
with open(bars_path, 'w', newline='') as f:
|
|||
|
|
cw = csv.writer(f)
|
|||
|
|
cw.writerow(['date', 'bar_idx', 'vel_div', 'vol_ok', 'posture',
|
|||
|
|
'regime_size_mult', 'position_open', 'boost', 'beta'])
|
|||
|
|
for b in bar_records:
|
|||
|
|
cw.writerow([b['date'], b['bar_idx'], f"{b['vel_div']:.8f}",
|
|||
|
|
b['vol_ok'], b['posture'], f"{b['regime_size_mult']:.6f}",
|
|||
|
|
b['position_open'], f"{b['boost']:.4f}", f"{b['beta']:.2f}"])
|
|||
|
|
|
|||
|
|
print(f" trades → {trades_path} ({len(tr)} rows)")
|
|||
|
|
print(f" daily → {daily_path} ({n_d} rows)")
|
|||
|
|
print(f" bars → {bars_path} ({len(bar_records)} rows)")
|
|||
|
|
print(f" summary → {summary_path}")
|
|||
|
|
print(f"{'═'*65}")
|