"""Inverse ACB: BOOST shorts on stress days, normal on calm days. Instead of ACB cutting size (hurts SHORT system), we BOOST on stress days where shorts historically profit, and stay normal elsewhere. Regime mapping: 0 signals → SHORT normal (size_mult=1.0) 1 signal → SHORT normal (size_mult=1.0) — don't cut winning days! 2 signals → SHORT boost (size_mult=1.3) 3+ signals → SHORT boost (size_mult=1.5) Plus: Intraday DD guard at 3% to protect against whipsaw (Feb 6 style). """ import sys, time from pathlib import Path from collections import Counter import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).parent)) print("Compiling numba kernels...") t_jit = 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 compile: {time.time() - t_jit:.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") 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, ) acb = AdaptiveCircuitBreaker() parquet_files = sorted(VBT_DIR.glob("*.parquet")) acb_cuts = {pf.stem: acb.get_cut_for_date(pf.stem) for pf in parquet_files} def get_inverse_regime(cut_info): """Inverse ACB: boost on stress, normal elsewhere.""" s = cut_info['signals'] if s >= 3: return 1.5 # Crash: big boost (shorts print) elif s >= 2: return 1.3 # High stress: boost else: return 1.0 # Normal: no change print("\n=== Inverse ACB Regime ===") for pf in parquet_files: d = pf.stem ci = acb_cuts[d] mult = get_inverse_regime(ci) if mult != 1.0: print(f" {d}: x{mult:.1f} (signals={ci['signals']:.1f})") boost_days = sum(1 for pf in parquet_files if get_inverse_regime(acb_cuts[pf.stem]) > 1.0) print(f"Boost days: {boost_days}/{len(parquet_files)}") # Vol percentiles all_vols = [] for pf in parquet_files[:2]: df = pd.read_parquet(pf) if 'BTCUSDT' not in df.columns: continue prices = df['BTCUSDT'].values for i in range(60, len(prices)): seg = prices[max(0, i-50):i] if len(seg) < 10: continue rets = np.diff(seg) / seg[:-1] v = float(np.std(rets)) if v > 0: all_vols.append(v) vol_p60 = float(np.percentile(all_vols, 60)) def run_backtest(mode="baseline"): engine = NDAlphaEngine(**ENGINE_KWARGS) bar_idx = 0 price_histories = {} date_stats = [] peak_capital = engine.capital max_dd = 0.0 for pf in parquet_files: date_str = pf.stem cap_start = engine.capital trades_start = len(engine.trade_history) # Always SHORT — just adjust size multiplier engine.regime_direction = -1 engine.regime_dd_halt = False if mode == "inverse": engine.regime_size_mult = get_inverse_regime(acb_cuts[date_str]) else: engine.regime_size_mult = 1.0 day_peak = cap_start dd_threshold = 0.03 # 3% intraday DD guard df = pd.read_parquet(pf) asset_cols = [c for c in df.columns if c not in META_COLS] btc_prices = df['BTCUSDT'].values if 'BTCUSDT' in df.columns else None date_vol = np.full(len(df), np.nan) if btc_prices is not None: for i in range(50, len(btc_prices)): seg = btc_prices[max(0, i-50):i] if len(seg) < 10: continue rets = np.diff(seg) / seg[:-1] date_vol[i] = float(np.std(rets)) bars_in_date = 0 for row_i in range(len(df)): row = df.iloc[row_i] vel_div = row.get("vel_div") if vel_div is None or not np.isfinite(vel_div): bar_idx += 1; bars_in_date += 1; continue prices = {} for ac in asset_cols: p = row[ac] if p and p > 0 and np.isfinite(p): prices[ac] = float(p) if ac not in price_histories: price_histories[ac] = [] price_histories[ac].append(float(p)) if not prices: bar_idx += 1; bars_in_date += 1; continue if bars_in_date < 100: vol_regime_ok = False else: v = date_vol[row_i] vol_regime_ok = (np.isfinite(v) and v > vol_p60) engine.process_bar( bar_idx=bar_idx, vel_div=float(vel_div), prices=prices, vol_regime_ok=vol_regime_ok, price_histories=price_histories, ) # Intraday DD guard (only on boost days) if mode == "inverse" and engine.regime_size_mult > 1.0: day_peak = max(day_peak, engine.capital) if day_peak > 0: intraday_dd = (day_peak - engine.capital) / day_peak if intraday_dd > dd_threshold: engine.regime_dd_halt = True bar_idx += 1; bars_in_date += 1 cap_end = engine.capital date_pnl = cap_end - cap_start peak_capital = max(peak_capital, cap_end) dd = (peak_capital - cap_end) / peak_capital * 100 if peak_capital > 0 else 0 max_dd = max(max_dd, dd) date_stats.append({ 'date': date_str, 'pnl': date_pnl, 'roi_pct': date_pnl / cap_start * 100 if cap_start > 0 else 0, 'capital': cap_end, 'dd_pct': dd, 'size_mult': engine.regime_size_mult if mode == "inverse" else 1.0, 'trades': len(engine.trade_history) - trades_start, }) return engine, date_stats, max_dd, peak_capital print("\n=== Running BASELINE ===") t0 = time.time() eng_base, stats_base, dd_base, peak_base = run_backtest("baseline") print(f" Done: {time.time()-t0:.0f}s") print("\n=== Running INVERSE ACB ===") t1 = time.time() eng_inv, stats_inv, dd_inv, peak_inv = run_backtest("inverse") print(f" Done: {time.time()-t1:.0f}s") # Per-date comparison (only show changed days) print(f"\n{'='*95}") print(f"{'DATE':<12} {'MULT':>5} {'BASE PnL':>10} {'INV PnL':>10} {'DELTA':>10} {'BASE CAP':>10} {'INV CAP':>10} {'B DD%':>7} {'I DD%':>7}") print(f"{'='*95}") for sb, si in zip(stats_base, stats_inv): marker = "" if si['pnl'] > sb['pnl'] + 50: marker = " ++" elif si['pnl'] < sb['pnl'] - 50: marker = " --" if si['size_mult'] != 1.0 or marker: print(f"{sb['date']:<12} {si['size_mult']:>5.2f} {sb['pnl']:>+10.2f} {si['pnl']:>+10.2f} " f"{si['pnl']-sb['pnl']:>+10.2f} {sb['capital']:>10.2f} {si['capital']:>10.2f} " f"{sb['dd_pct']:>6.2f}% {si['dd_pct']:>6.2f}%{marker}") # Summary def summarize(label, engine, max_dd, peak, stats): trades = engine.trade_history if not trades: print(f"\n{label}: 0 trades"); return wins = [t for t in trades if t.pnl_absolute > 0] losses = [t for t in trades if t.pnl_absolute <= 0] gross_win = sum(t.pnl_absolute for t in wins) if wins else 0 gross_loss = abs(sum(t.pnl_absolute for t in losses)) if losses else 0 pf_val = gross_win / gross_loss if gross_loss > 0 else float("inf") daily_rets = [s['roi_pct'] for s in stats] sharpe = np.mean(daily_rets) / np.std(daily_rets) * np.sqrt(365) if np.std(daily_rets) > 0 else 0 print(f"\n{'='*50}") print(f" {label}") print(f"{'='*50}") print(f"Trades: {len(trades)}, WR: {len(wins)/len(trades)*100:.1f}%") print(f"PF: {pf_val:.3f}") print(f"ROI: {(engine.capital - 25000) / 25000 * 100:+.2f}%") print(f"Final capital: ${engine.capital:.2f}") print(f"Peak: ${peak:.2f}, MAX DRAWDOWN: {max_dd:.2f}%") print(f"Sharpe (ann.): {sharpe:.2f}") print(f"Fees: {engine.total_fees:.2f}") summarize("BASELINE (SHORT-only)", eng_base, dd_base, peak_base, stats_base) summarize("INVERSE ACB (boost on stress)", eng_inv, dd_inv, peak_inv, stats_inv) base_roi = (eng_base.capital - 25000) / 25000 * 100 inv_roi = (eng_inv.capital - 25000) / 25000 * 100 print(f"\n{'='*50}") print(f" DELTA") print(f"{'='*50}") print(f"ROI: {base_roi:+.2f}% -> {inv_roi:+.2f}% ({inv_roi-base_roi:+.2f}%)") print(f"Max DD: {dd_base:.2f}% -> {dd_inv:.2f}% ({dd_inv-dd_base:+.2f}%)") print(f"Total time: {time.time() - t0:.0f}s")