Files
DOLPHIN/nautilus_dolphin/test_pf_dynamic_beta_validate.py

556 lines
29 KiB
Python
Raw Normal View History

"""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.892.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 17)")
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}")