Files
DOLPHIN/nautilus_dolphin/test_pf_dynamic_beta_validate.py
hjnormey 01c19662cb initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
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.
2026-04-21 16:58:38 +02:00

556 lines
29 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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 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}")