"""Dynamic beta: find external factors that predict optimal beta per date. Phase 1: Load ALL external factors (85 API + 45 scan_global) per date Phase 2: Run meta-boost at fixed beta=0.5, record per-date P&L Phase 3: Rank-correlate each factor with daily P&L -> candidate beta governors Phase 4: Test dynamic beta governed by top factors vs fixed beta Phase 5: Half-split overfitting validation """ import sys, time, math from pathlib import Path import numpy as np import pandas as pd from scipy import stats as sp_stats 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 _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) print(f" JIT: {time.time() - t0c:.1f}s") from nautilus_dolphin.nautilus.alpha_orchestrator import NDAlphaEngine from nautilus_dolphin.nautilus.adaptive_circuit_breaker import AdaptiveCircuitBreaker VBT_DIR = Path(r"C:\Users\Lenovo\Documents\- DOLPHIN NG HD HCM TSF Predict\vbt_cache") EIGEN_DIR = Path(r"C:\Users\Lenovo\Documents\- Dolphin NG HD (NG3)\correlation_arb512\eigenvalues") 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.0099, 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, ) VD_THRESH = -0.02; VD_EXTREME = -0.05; CONVEXITY = 3.0 # ─── PHASE 1: Load ALL external factors per date ─── print("\n=== PHASE 1: Loading external factors ===") acb = AdaptiveCircuitBreaker() parquet_files = sorted(VBT_DIR.glob("*.parquet")) acb_signals = {pf.stem: acb.get_cut_for_date(pf.stem)['signals'] for pf in parquet_files} # Load full factor matrix from NPZ files date_factors = {} # date -> {factor_name: value} api_name_set = set() global_name_set = set() for pf in parquet_files: ds = pf.stem date_path = EIGEN_DIR / ds if not date_path.exists(): continue files = sorted(date_path.glob('scan_*__Indicators.npz'))[:10] if not files: continue # Collect from multiple scans, take median api_vals = {} # name -> [values] glob_vals = {} # name -> [values] for f in files: try: data = np.load(f, allow_pickle=True) # API indicators if 'api_names' in data and 'api_indicators' in data: names = list(data['api_names']) vals = data['api_indicators'] succ = data['api_success'] if 'api_success' in data else np.ones(len(names), dtype=bool) for i, nm in enumerate(names): if i < len(vals) and (i >= len(succ) or succ[i]) and np.isfinite(vals[i]): api_vals.setdefault(nm, []).append(float(vals[i])) api_name_set.add(nm) # Scan global metrics if 'scan_global_names' in data and 'scan_global' in data: gnames = list(data['scan_global_names']) gvals = data['scan_global'] for i, nm in enumerate(gnames): if i < len(gvals) and np.isfinite(gvals[i]): glob_vals.setdefault(nm, []).append(float(gvals[i])) global_name_set.add(nm) except Exception: continue factors = {} for nm, vs in api_vals.items(): factors[f"api_{nm}"] = float(np.median(vs)) for nm, vs in glob_vals.items(): factors[f"glob_{nm}"] = float(np.median(vs)) factors['acb_signals'] = acb_signals[ds] date_factors[ds] = factors print(f" Loaded factors for {len(date_factors)}/{len(parquet_files)} dates") print(f" API indicators: {len(api_name_set)}, Global metrics: {len(global_name_set)}") # ─── PHASE 2: Run fixed beta=0.5, record per-date P&L ─── print("\n=== PHASE 2: Running meta-boost beta=0.5 baseline ===") # Pre-load parquet 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 = {} for pf in parquet_files: df = pd.read_parquet(pf) ac = [c for c in df.columns if c not in META_COLS] 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) def log05(s): return 1.0 + 0.5 * math.log1p(s) if s >= 1.0 else 1.0 def strength_cubic(vel_div): if vel_div >= VD_THRESH: return 0.0 raw = (VD_THRESH - vel_div) / (VD_THRESH - VD_EXTREME) return min(1.0, max(0.0, raw)) ** CONVEXITY def run_with_beta_fn(beta_fn): """Run engine where beta_fn(date_str, factors) -> beta for that date.""" engine = NDAlphaEngine(**ENGINE_KWARGS) bar_idx = 0; ph = {}; dstats = [] for pf in parquet_files: ds = pf.stem; cs = engine.capital signals = acb_signals[ds] base_boost = log05(signals) engine.regime_direction = -1 engine.regime_dd_halt = False # Get beta for this date factors = date_factors.get(ds, {}) beta = beta_fn(ds, factors) df, acols, dvol = pq_data[ds] bid = 0 for ri in range(len(df)): row = df.iloc[ri]; vd = row.get("vel_div") if vd is None or not np.isfinite(vd): bar_idx+=1; bid+=1; continue prices = {} for ac in acols: p = row[ac] if p and p > 0 and np.isfinite(p): prices[ac] = float(p) if ac not in ph: ph[ac] = [] ph[ac].append(float(p)) if not prices: bar_idx+=1; bid+=1; continue vrok = False if bid < 100 else (np.isfinite(dvol[ri]) and dvol[ri] > vol_p60) if beta > 0 and base_boost > 1.0: ss = strength_cubic(float(vd)) engine.regime_size_mult = base_boost * (1.0 + beta * ss) else: engine.regime_size_mult = base_boost engine.process_bar(bar_idx=bar_idx, vel_div=float(vd), prices=prices, vol_regime_ok=vrok, price_histories=ph) bar_idx+=1; bid+=1 dstats.append({'date': ds, 'pnl': engine.capital - cs, 'cap': engine.capital, 'beta': beta}) 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 dr = [s['pnl']/25000*100 for s in dstats] 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) return { 'roi': (engine.capital - 25000) / 25000 * 100, 'pf': gw / gl if gl > 0 else 999, 'dd': max_dd, 'sharpe': np.mean(dr) / np.std(dr) * np.sqrt(365) if np.std(dr) > 0 else 0, 'trades': len(tr), 'cap': engine.capital, }, dstats t0 = time.time() r_fixed, ds_fixed = run_with_beta_fn(lambda ds, f: 0.5) print(f" Fixed beta=0.5: ROI={r_fixed['roi']:+.2f}%, PF={r_fixed['pf']:.3f}, " f"DD={r_fixed['dd']:.2f}%, Sharpe={r_fixed['sharpe']:.2f} [{time.time()-t0:.0f}s]") # ─── PHASE 3: Rank-correlate factors with per-date P&L ─── print(f"\n=== PHASE 3: Factor -> P&L correlation scan ===") # Build aligned arrays: factor[date] vs pnl[date] pnl_by_date = {s['date']: s['pnl'] for s in ds_fixed} dates_with_factors = [ds for ds in pnl_by_date if ds in date_factors] pnl_arr = np.array([pnl_by_date[ds] for ds in dates_with_factors]) # Already-used factors (exclude from "new" candidates) USED_FACTORS = {'api_funding_btc', 'api_dvol_btc', 'api_fng', 'api_taker'} # Collect all factor names all_factor_names = set() for f in date_factors.values(): all_factor_names.update(f.keys()) all_factor_names -= {'acb_signals'} # meta, not a raw factor correlations = [] for fname in sorted(all_factor_names): vals = [] valid = True for ds in dates_with_factors: v = date_factors[ds].get(fname, np.nan) if not np.isfinite(v): valid = False; break vals.append(v) if not valid or len(set(vals)) < 3: continue vals = np.array(vals) # Spearman rank correlation (robust to non-linearity) rho, pval = sp_stats.spearmanr(vals, pnl_arr) if np.isfinite(rho): correlations.append({ 'factor': fname, 'rho': rho, 'pval': pval, 'abs_rho': abs(rho), 'is_new': fname not in USED_FACTORS, }) correlations.sort(key=lambda x: -x['abs_rho']) print(f"\n{'FACTOR':<35} {'RHO':>7} {'P-VAL':>8} {'NEW?':>5}") print(f"{'-'*60}") for c in correlations[:30]: marker = " ***" if c['is_new'] and c['abs_rho'] > 0.25 else " *" if c['is_new'] else "" print(f" {c['factor']:<33} {c['rho']:>+7.3f} {c['pval']:>8.4f} {'YES' if c['is_new'] else 'no':>5}{marker}") # Filter: new factors with |rho| > 0.2 and p < 0.15 candidates = [c for c in correlations if c['is_new'] and c['abs_rho'] > 0.20 and c['pval'] < 0.15] print(f"\n Candidate beta governors (new, |rho|>0.20, p<0.15): {len(candidates)}") for c in candidates[:10]: print(f" {c['factor']:<33} rho={c['rho']:+.3f} p={c['pval']:.4f}") # ─── PHASE 4: Test dynamic beta strategies ─── print(f"\n=== PHASE 4: Dynamic beta tests ===") # Strategy: scale beta by factor percentile # High-rho factor -> when factor is elevated, beta should be higher (or lower, depending on sign) def make_percentile_beta_fn(factor_name, rho_sign, base_beta=0.5, min_beta=0.1, max_beta=1.0): """Scale beta based on percentile of factor across dates.""" # Collect factor values across all dates fvals = [] for ds in dates_with_factors: v = date_factors[ds].get(factor_name, np.nan) if np.isfinite(v): fvals.append(v) if not fvals: return lambda ds, f: base_beta p25, p50, p75 = np.percentile(fvals, [25, 50, 75]) def beta_fn(ds, factors): v = factors.get(factor_name, np.nan) if not np.isfinite(v): return base_beta # Normalize to [0, 1] via percentile rank rank = sp_stats.percentileofscore(fvals, v) / 100.0 if rho_sign > 0: # Higher factor -> higher beta beta = min_beta + (max_beta - min_beta) * rank else: # Higher factor -> lower beta beta = min_beta + (max_beta - min_beta) * (1.0 - rank) return beta return beta_fn # Also test: composite of top-N factors def make_composite_beta_fn(factor_list, base_beta=0.5, min_beta=0.1, max_beta=1.0): """Average percentile rank of multiple factors -> beta.""" # Pre-compute percentile distributions factor_dists = {} for fname, rho_sign in factor_list: fvals = [date_factors[ds].get(fname, np.nan) for ds in dates_with_factors] fvals = [v for v in fvals if np.isfinite(v)] if fvals: factor_dists[fname] = (fvals, rho_sign) def beta_fn(ds, factors): ranks = [] for fname, (fvals, rho_sign) in factor_dists.items(): v = factors.get(fname, np.nan) if np.isfinite(v): r = sp_stats.percentileofscore(fvals, v) / 100.0 ranks.append(r if rho_sign > 0 else 1.0 - r) if not ranks: return base_beta avg_rank = np.mean(ranks) return min_beta + (max_beta - min_beta) * avg_rank return beta_fn # Also test: VIX-like regime switch (simple threshold) def make_threshold_beta_fn(factor_name, rho_sign, threshold_pct=60, beta_low=0.3, beta_high=0.7): """Binary: factor above/below threshold -> two beta levels.""" fvals = [date_factors[ds].get(factor_name, np.nan) for ds in dates_with_factors] fvals = [v for v in fvals if np.isfinite(v)] thresh = np.percentile(fvals, threshold_pct) if fvals else 0 def beta_fn(ds, factors): v = factors.get(factor_name, np.nan) if not np.isfinite(v): return (beta_low + beta_high) / 2 if rho_sign > 0: return beta_high if v >= thresh else beta_low else: return beta_low if v >= thresh else beta_high return beta_fn print(f"\n{'STRATEGY':<40} {'ROI%':>7} {'PF':>6} {'DD%':>6} {'SHARPE':>7} {'TRADES':>7}") print(f"{'='*75}") # Fixed baselines for fb in [0.0, 0.3, 0.5, 0.7]: label = f"fixed_beta={fb}" t1 = time.time() r, ds = run_with_beta_fn(lambda ds, f, b=fb: b) print(f" {label:<38} {r['roi']:>+7.2f} {r['pf']:>6.3f} {r['dd']:>6.2f} {r['sharpe']:>7.2f} {r['trades']:>7} [{time.time()-t1:.0f}s]") # Test top candidate factors tested_strategies = {} for c in candidates[:5]: fname = c['factor'] rho_sign = 1 if c['rho'] > 0 else -1 short_name = fname.replace('api_', '').replace('glob_', 'g:') # Percentile scaling label = f"pctl_{short_name}" t1 = time.time() beta_fn = make_percentile_beta_fn(fname, rho_sign, min_beta=0.1, max_beta=0.9) r, ds = run_with_beta_fn(beta_fn) tested_strategies[label] = (r, ds) print(f" {label:<38} {r['roi']:>+7.2f} {r['pf']:>6.3f} {r['dd']:>6.2f} {r['sharpe']:>7.2f} {r['trades']:>7} [{time.time()-t1:.0f}s]") # Threshold switch label = f"thresh_{short_name}" t1 = time.time() beta_fn = make_threshold_beta_fn(fname, rho_sign, beta_low=0.2, beta_high=0.8) r, ds = run_with_beta_fn(beta_fn) tested_strategies[label] = (r, ds) print(f" {label:<38} {r['roi']:>+7.2f} {r['pf']:>6.3f} {r['dd']:>6.2f} {r['sharpe']:>7.2f} {r['trades']:>7} [{time.time()-t1:.0f}s]") # Composite: top 3 new factors if len(candidates) >= 3: top3 = [(c['factor'], 1 if c['rho'] > 0 else -1) for c in candidates[:3]] label = "composite_top3" t1 = time.time() beta_fn = make_composite_beta_fn(top3, min_beta=0.1, max_beta=0.9) r, ds = run_with_beta_fn(beta_fn) tested_strategies[label] = (r, ds) print(f" {label:<38} {r['roi']:>+7.2f} {r['pf']:>6.3f} {r['dd']:>6.2f} {r['sharpe']:>7.2f} {r['trades']:>7} [{time.time()-t1:.0f}s]") # ─── PHASE 5: Overfitting check ─── print(f"\n=== PHASE 5: Overfitting validation (H1 vs H2) ===") mid = len(parquet_files) // 2 print(f" H1: {parquet_files[0].stem} to {parquet_files[mid-1].stem}") print(f" H2: {parquet_files[mid].stem} to {parquet_files[-1].stem}") # Fixed beta baselines for fb in [0.0, 0.3, 0.5]: label = f"fixed_beta={fb}" _, ds = run_with_beta_fn(lambda ds, f, b=fb: b) h1 = sum(s['pnl'] for s in ds[:mid]) h2 = sum(s['pnl'] for s in ds[mid:]) ratio = h2/h1 if h1 != 0 else 0 print(f" {label:<38} H1=${h1:>+9.2f} H2=${h2:>+9.2f} H2/H1={ratio:.2f}") # Dynamic strategies for label, (r, ds) in tested_strategies.items(): h1 = sum(s['pnl'] for s in ds[:mid]) h2 = sum(s['pnl'] for s in ds[mid:]) ratio = h2/h1 if h1 != 0 else 0 stable = "YES" if 0.3 < ratio < 3.0 else "OVERFIT" print(f" {label:<38} H1=${h1:>+9.2f} H2=${h2:>+9.2f} H2/H1={ratio:.2f} {stable}") # Per-date beta values for best dynamic strategy best_dyn = max(tested_strategies.items(), key=lambda x: x[1][0]['roi']) print(f"\n--- BEST DYNAMIC: {best_dyn[0]} ---") print(f" ROI={best_dyn[1][0]['roi']:+.2f}%, PF={best_dyn[1][0]['pf']:.3f}, " f"Sharpe={best_dyn[1][0]['sharpe']:.2f}, DD={best_dyn[1][0]['dd']:.2f}%") print(f" Per-date beta values:") for s in best_dyn[1][1]: marker = " $$$" if s['pnl'] > 100 else " ---" if s['pnl'] < -100 else "" print(f" {s['date']} beta={s['beta']:.2f} pnl=${s['pnl']:>+8.2f}{marker}") print(f"\nTotal time: {time.time()-t0:.0f}s")