""" DOLPHIN Paper Trading Simulation — ADAPTIVE CIRCUIT BREAKER v2 =============================================================== Multi-signal confirmation approach to reduce false positives. FIXES from v1: - FNG alone no longer triggers large cuts - Requires 2+ confirming signals for meaningful cuts - Lower base cut (30% vs 45%) - Severity-weighted scoring KEY INSIGHT from research: - Cohen's d analysis shows taker ratio (d=3.57) is strongest predictor - FNG alone has low predictive power (conflicts with funding/DVOL) - Multi-signal confirmation required for high-confidence cuts Strategies tested: 1. Champion (5x cvx3 f20) — highest PF 2. Growth (25x cvx3 f10) — best PF/ROI balance 3. Aggressive (25x cvx3 f20) — max ROI 4. Conservative (5x cvx3 f10) — min risk Run: python dolphin_paper_trade_adaptive_cb_v2.py [--no-cb] [--compare] Output: vbt_results/dolphin_paper_trade_acbv2_*.json vbt_results/dolphin_paper_trade_acbv2_*.csv """ import sys import json import time import csv import argparse from pathlib import Path from datetime import datetime from dataclasses import replace, asdict from collections import defaultdict import numpy as np import pandas as pd sys.path.insert(0, str(Path(__file__).parent)) sys.path.insert(0, str(Path(__file__).parent / 'external_factors')) from dolphin_vbt_real import ( load_all_data, run_full_backtest, Strategy, CACHE_DIR, RESULTS_DIR, ) from realtime_exf_service import calculate_adaptive_cut_v4, load_external_factors_lagged from nautilus_dolphin.mc.mc_ml import DolphinForewarner from nautilus_dolphin.mc.mc_sampler import MCTrialConfig import logging logging.getLogger("xgboost").setLevel(logging.ERROR) # ══════════════════════════════════════════════════════════════════════ # CONFIGURATION # ══════════════════════════════════════════════════════════════════════ EIGENVALUES_BASE_PATH = Path(r'C:/Users/Lenovo/Documents/- Dolphin NG HD (NG3)/correlation_arb512/eigenvalues') # Adaptive CB v2 Configuration ACBV2_CONFIG = { 'enabled': True, 'base_cut': 0.0, # 0% base cut - CB only activates on stress signals 'max_cut': 0.80, # 80% max position cut # Multi-signal thresholds 'thresholds': { 'funding_btc_very_bearish': -0.0001, 'funding_btc_bearish': 0.0, 'dvol_extreme': 80, 'dvol_elevated': 55, 'fng_extreme_fear': 25, 'fng_fear': 40, 'taker_selling': 0.8, 'taker_mild_selling': 0.9, } } # ══════════════════════════════════════════════════════════════════════ # STRATEGY DEFINITIONS # ══════════════════════════════════════════════════════════════════════ BASE_PARAMS = dict( vel_div_threshold=-0.02, direction='SHORT', leverage=2.5, stop_pct=1.0, max_hold=120, use_trailing=False, vol_filter='high', use_asset_selection=True, min_irp_alignment=0.45, use_sp_fees=True, use_sp_slippage=True, use_ob_edge=True, ob_edge_bps=5.0, dynamic_leverage=True, min_leverage=0.5, use_alpha_layers=True, use_fixed_tp=True, fixed_tp_pct=0.0099, use_direction_confirm=True, dc_skip_contradicts=True, dc_leverage_boost=1.0, dc_leverage_reduce=0.5, dc_lookback_bars=7, dc_min_magnitude_bps=0.75, ) STRATEGIES = { 'champion_5x_f20': Strategy( name='champion_5x_f20', max_leverage=5.0, fraction=0.20, leverage_convexity=3.0, **BASE_PARAMS, ), 'growth_25x_f10': Strategy( name='growth_25x_f10', max_leverage=25.0, fraction=0.10, leverage_convexity=3.0, **BASE_PARAMS, ), 'aggressive_25x_f20': Strategy( name='aggressive_25x_f20', max_leverage=25.0, fraction=0.20, leverage_convexity=3.0, **BASE_PARAMS, ), 'conservative_5x_f10': Strategy( name='conservative_5x_f10', max_leverage=5.0, fraction=0.10, leverage_convexity=3.0, **BASE_PARAMS, ), } INIT_CAPITAL = 10_000.0 # ══════════════════════════════════════════════════════════════════════ # ADAPTIVE CIRCUIT BREAKER v2 - MULTI-SIGNAL CONFIRMATION # ══════════════════════════════════════════════════════════════════════ def load_external_factors_fast(date_str: str, max_scans: int = 1000) -> dict: """Load daily-aggregated external factors from indicator files.""" date_path = EIGENVALUES_BASE_PATH / date_str if not date_path.exists(): return {} files = list(date_path.glob('scan_*__Indicators.npz'))[:max_scans] if not files: return {} indicators = defaultdict(list) for f in files: try: data = np.load(f, allow_pickle=True) if 'api_success_rate' in data and data['api_success_rate'][0] < 0.3: continue api_names = data.get('api_names', data.get('api_indicator_names', [])) api_values = data.get('api_indicators', data.get('external', [])) api_success = data.get('api_success', data.get('external_success', [])) for name, value, success in zip(api_names, api_values, api_success): if success and not np.isnan(value): indicators[name].append(float(value)) except Exception: continue result = {} for name, values in indicators.items(): if values: result[name] = np.mean(values) result[f'{name}_std'] = np.std(values) result[f'{name}_count'] = len(values) return result def calculate_adaptive_cut_v2(ext_factors: dict, config: dict = None) -> tuple: """ Calculate adaptive position cut using multi-signal confirmation. v2 Changes: - FNG alone does NOT trigger large cuts - Requires 2+ confirming signals for meaningful cuts - Lower base cut (30% vs 45%) - Severity-weighted scoring Returns: Tuple of (cut_percentage, signal_count, severity, details_dict) """ config = config or ACBV2_CONFIG if not ext_factors or not config.get('enabled', True): return config.get('base_cut', 0.30), 0, 0, {'status': 'disabled'} signals = 0 severity = 0 details = {} # Signal 1: Funding (bearish confirmation) funding_btc = ext_factors.get('funding_btc', 0) if funding_btc < config['thresholds']['funding_btc_very_bearish']: signals += 1 severity += 2 details['funding'] = f'{funding_btc:.6f} (very bearish, +1 signal, +2 severity)' elif funding_btc < config['thresholds']['funding_btc_bearish']: signals += 1 severity += 1 details['funding'] = f'{funding_btc:.6f} (bearish, +1 signal, +1 severity)' else: details['funding'] = f'{funding_btc:.6f} (neutral/bullish)' # Signal 2: DVOL (volatility confirmation) dvol_btc = ext_factors.get('dvol_btc', 50) if dvol_btc > config['thresholds']['dvol_extreme']: signals += 1 severity += 2 details['dvol'] = f'{dvol_btc:.1f} (extreme, +1 signal, +2 severity)' elif dvol_btc > config['thresholds']['dvol_elevated']: signals += 1 severity += 1 details['dvol'] = f'{dvol_btc:.1f} (elevated, +1 signal, +1 severity)' else: details['dvol'] = f'{dvol_btc:.1f} (normal)' # Signal 3: Fear & Greed (ONLY counts if funding is negative OR DVOL elevated) # Rationale: FNG alone has low predictive power per Cohen's d analysis fng = ext_factors.get('fng', 50) funding_bearish = funding_btc < 0 dvol_elevated = dvol_btc > 55 if fng < config['thresholds']['fng_extreme_fear'] and (funding_bearish or dvol_elevated): signals += 1 severity += 1 details['fng'] = f'{fng:.1f} (extreme fear, confirmed, +1 signal, +1 severity)' elif fng < config['thresholds']['fng_fear'] and (funding_bearish or dvol_elevated): signals += 0.5 severity += 0.5 details['fng'] = f'{fng:.1f} (fear, confirmed, +0.5 signal, +0.5 severity)' elif fng < config['thresholds']['fng_extreme_fear']: details['fng'] = f'{fng:.1f} (extreme fear, NOT confirmed by funding/DVOL)' elif fng < config['thresholds']['fng_fear']: details['fng'] = f'{fng:.1f} (fear, NOT confirmed by funding/DVOL)' else: details['fng'] = f'{fng:.1f} (neutral/greed)' # Signal 4: Taker ratio (strongest predictor - Cohen's d = 3.57) # This signal always counts (strongest discriminator) taker = ext_factors.get('taker', 1.0) if taker < config['thresholds']['taker_selling']: signals += 1 severity += 2 details['taker'] = f'{taker:.3f} (heavy selling, +1 signal, +2 severity)' elif taker < config['thresholds']['taker_mild_selling']: signals += 0.5 severity += 1 details['taker'] = f'{taker:.3f} (mild selling, +0.5 signal, +1 severity)' else: details['taker'] = f'{taker:.3f} (neutral/buying)' # Calculate cut based on signal count and severity # NORMAL DAYS (0 signals): 0% cut (full position size) if signals >= 3 and severity >= 5: cut = 0.75 # Extreme stress (3+ signals, high severity) elif signals >= 3: cut = 0.65 # High stress (3+ signals, moderate severity) elif signals >= 2 and severity >= 3: cut = 0.55 # Moderate-high stress (2+ signals, high severity) elif signals >= 2: cut = 0.45 # Moderate stress (2+ signals) elif signals >= 1: cut = 0.30 # Mild stress (1 signal) else: cut = 0.0 # Normal (0 signals) = NO CUT details['signals'] = signals details['severity'] = severity details['base_cut'] = config['base_cut'] return cut, signals, severity, details def apply_circuit_breaker(strategy: Strategy, cut_pct: float) -> Strategy: """Apply position size reduction to strategy.""" new_fraction = strategy.fraction * (1 - cut_pct) return replace(strategy, fraction=new_fraction) # ══════════════════════════════════════════════════════════════════════ # PAPER TRADING ENGINE # ══════════════════════════════════════════════════════════════════════ def run_paper_portfolio(df, strategies, init_capital=INIT_CAPITAL, use_acb=True, acb_config=None, verbose=True, use_mc_forewarn=False, forewarner=None): """Run paper trading with optional Adaptive CB v4 and MC Forewarning.""" acb_config = acb_config or ACBV2_CONFIG df = df.copy() if 'date_str' not in df.columns: df['date_str'] = df['timestamp'].dt.date.astype(str) dates = sorted(df['date_str'].unique()) if verbose: mode = "ADAPTIVE CB v4 (META-ADAPTIVE LAGS)" if use_acb else "CB DISABLED (baseline)" if use_mc_forewarn: mode += " + MC FOREWARNING" print(f" Paper trading {len(dates)} days, {len(strategies)} strategies") print(f" Mode: {mode}") print(f" Initial capital: ${init_capital:,.2f}") print() all_daily_vals = {} if use_acb: print(" Prefetching all external factors for latency-aware v4 lag reduction...") for ds in dates: all_daily_vals[ds] = load_external_factors_fast(ds) portfolio = {} for sname in strategies: portfolio[sname] = { 'capital': init_capital, 'total_trades': 0, 'total_wins': 0, 'total_fees': 0.0, 'total_slippage': 0.0, 'peak_capital': init_capital, 'max_drawdown_pct': 0.0, 'daily_log': [], 'winning_days': 0, 'losing_days': 0, 'flat_days': 0, } acb_log = [] for day_idx, date_str in enumerate(dates): df_day = df[df['date_str'] == date_str].copy() n_rows = len(df_day) ext_factors = {} adaptive_cut = 0.0 signal_count = 0 severity = 0 acb_details = {} if use_acb and n_rows >= 200: ext_factors = load_external_factors_lagged(date_str, all_daily_vals, dates) if ext_factors: adaptive_cut, signal_count, severity, acb_details = calculate_adaptive_cut_v4(ext_factors, acb_config) acb_log.append({ 'date': date_str, 'cut_pct': adaptive_cut, 'signals': signal_count, 'severity': severity, 'funding_btc': ext_factors.get('funding_btc', np.nan), 'dvol_btc': ext_factors.get('dvol_btc', np.nan), 'fng': ext_factors.get('fng', np.nan), 'taker': ext_factors.get('taker', np.nan), 'details': acb_details, }) if n_rows < 200: for sname in strategies: p = portfolio[sname] p['daily_log'].append({ 'day': day_idx + 1, 'date': date_str, 'rows': n_rows, 'skipped': True, 'reason': 'sparse_data', 'capital_start': p['capital'], 'capital_end': p['capital'], 'day_pnl': 0.0, 'day_roi_pct': 0.0, 'trades': 0, 'wins': 0, 'win_rate': 0.0, 'pf': 0.0, 'day_fees': 0.0, 'day_slippage': 0.0, 'tp_exits': 0, 'hold_exits': 0, 'adaptive_cut': 0.0, 'mc_red_alert': False, 'mc_orange_alert': False, 'cumulative_roi_pct': (p['capital'] - init_capital) / init_capital * 100, 'drawdown_pct': 0.0, }) p['flat_days'] += 1 continue for sname, strategy in strategies.items(): p = portfolio[sname] cap_start = p['capital'] if use_acb and adaptive_cut > 0: adjusted_strategy = apply_circuit_breaker(strategy, adaptive_cut) else: adjusted_strategy = strategy mc_red_alert = False mc_orange_alert = False if use_mc_forewarn and forewarner is not None: cfg_dict = { 'trial_id': 0, 'vel_div_threshold': adjusted_strategy.vel_div_threshold, 'vel_div_extreme': -0.050, 'use_direction_confirm': adjusted_strategy.use_direction_confirm, 'dc_lookback_bars': adjusted_strategy.dc_lookback_bars, 'dc_min_magnitude_bps': adjusted_strategy.dc_min_magnitude_bps, 'dc_skip_contradicts': adjusted_strategy.dc_skip_contradicts, 'dc_leverage_boost': adjusted_strategy.dc_leverage_boost, 'dc_leverage_reduce': adjusted_strategy.dc_leverage_reduce, 'vd_trend_lookback': 10, 'min_leverage': adjusted_strategy.min_leverage, 'max_leverage': adjusted_strategy.max_leverage, 'leverage_convexity': adjusted_strategy.leverage_convexity, 'fraction': adjusted_strategy.fraction, 'use_alpha_layers': adjusted_strategy.use_alpha_layers, 'use_dynamic_leverage': adjusted_strategy.dynamic_leverage, 'fixed_tp_pct': adjusted_strategy.fixed_tp_pct if adjusted_strategy.use_fixed_tp else 0.0099, 'stop_pct': adjusted_strategy.stop_pct, 'max_hold_bars': adjusted_strategy.max_hold, 'use_sp_fees': adjusted_strategy.use_sp_fees, 'use_sp_slippage': adjusted_strategy.use_sp_slippage, 'sp_maker_entry_rate': 0.62, 'sp_maker_exit_rate': 0.50, 'use_ob_edge': adjusted_strategy.use_ob_edge, 'ob_edge_bps': adjusted_strategy.ob_edge_bps, 'ob_confirm_rate': 0.40, 'ob_imbalance_bias': -0.09, 'ob_depth_scale': 1.00, 'use_asset_selection': adjusted_strategy.use_asset_selection, 'min_irp_alignment': adjusted_strategy.min_irp_alignment, 'lookback': 100, 'acb_beta_high': 0.80, 'acb_beta_low': 0.20, 'acb_w750_threshold_pct': 60, } report = forewarner.assess_config_dict(cfg_dict) if report.catastrophic_probability > 0.25 or report.envelope_score < -1.0: mc_red_alert = True elif report.envelope_score < 0 or report.catastrophic_probability > 0.10: mc_orange_alert = True adjusted_strategy = replace(adjusted_strategy, fraction=adjusted_strategy.fraction * 0.5) if mc_red_alert: result = { 'capital': cap_start, 'trades': 0, 'wins': 0, 'win_rate': 0.0, 'profit_factor': 0.0, 'total_fees': 0.0, 'total_slippage_cost': 0.0, 'tp_exits': 0, 'hold_exits': 0 } else: result = run_full_backtest( df_day, adjusted_strategy, init_cash=cap_start, seed=42, verbose=False, ) cap_end = result['capital'] day_pnl = cap_end - cap_start day_roi = day_pnl / cap_start * 100 if cap_start > 0 else 0 trades = result['trades'] wins = result['wins'] wr = result['win_rate'] pf = result['profit_factor'] fees = result['total_fees'] slippage = result['total_slippage_cost'] tp_exits = result.get('tp_exits', 0) hold_exits = result.get('hold_exits', 0) p['capital'] = cap_end p['total_trades'] += trades p['total_wins'] += wins p['total_fees'] += fees p['total_slippage'] += slippage if cap_end > p['peak_capital']: p['peak_capital'] = cap_end drawdown = (p['peak_capital'] - cap_end) / p['peak_capital'] * 100 if drawdown > p['max_drawdown_pct']: p['max_drawdown_pct'] = drawdown if day_pnl > 0.01: p['winning_days'] += 1 elif day_pnl < -0.01: p['losing_days'] += 1 else: p['flat_days'] += 1 cumulative_roi = (cap_end - init_capital) / init_capital * 100 p['daily_log'].append({ 'day': day_idx + 1, 'date': date_str, 'rows': n_rows, 'skipped': False, 'capital_start': round(cap_start, 2), 'capital_end': round(cap_end, 2), 'day_pnl': round(day_pnl, 2), 'day_roi_pct': round(day_roi, 4), 'trades': trades, 'wins': wins, 'win_rate': round(wr, 2), 'pf': round(pf, 4), 'day_fees': round(fees, 2), 'day_slippage': round(slippage, 2), 'tp_exits': tp_exits, 'hold_exits': hold_exits, 'adaptive_cut': round(adaptive_cut, 2), 'acb_signals': signal_count, 'acb_severity': severity, 'mc_red_alert': mc_red_alert, 'mc_orange_alert': mc_orange_alert, 'cumulative_roi_pct': round(cumulative_roi, 4), 'drawdown_pct': round(drawdown, 4), 'peak_capital': round(p['peak_capital'], 2), }) if verbose and ((day_idx + 1) % 10 == 0 or day_idx == len(dates) - 1): caps = {sn: f"${portfolio[sn]['capital']:,.0f}" for sn in strategies} cut_info = f" [ACBv2:{adaptive_cut:.0%}|S:{signal_count}]" if use_acb and adaptive_cut > 0 else "" print(f" Day {day_idx+1}/{len(dates)} ({date_str}){cut_info}: {caps}") return portfolio, dates, acb_log def generate_summary(portfolio, strategies, dates, init_capital, acb_log=None): """Generate per-strategy summary stats.""" summaries = {} for sname in strategies: p = portfolio[sname] total_roi = (p['capital'] - init_capital) / init_capital * 100 active_days = p['winning_days'] + p['losing_days'] win_day_pct = p['winning_days'] / max(active_days, 1) * 100 avg_daily_roi = total_roi / max(len(dates), 1) total_wr = p['total_wins'] / max(p['total_trades'], 1) * 100 daily_rets = [d['day_roi_pct'] for d in p['daily_log'] if not d.get('skipped')] if len(daily_rets) > 1: sharpe = np.mean(daily_rets) / max(np.std(daily_rets, ddof=1), 1e-8) sharpe_annual = sharpe * np.sqrt(365) else: sharpe_annual = 0.0 streak_w = 0 streak_l = 0 max_streak_w = 0 max_streak_l = 0 for d in p['daily_log']: if d.get('skipped'): continue if d['day_pnl'] > 0.01: streak_w += 1 streak_l = 0 elif d['day_pnl'] < -0.01: streak_l += 1 streak_w = 0 else: streak_w = 0 streak_l = 0 max_streak_w = max(max_streak_w, streak_w) max_streak_l = max(max_streak_l, streak_l) active_logs = [d for d in p['daily_log'] if not d.get('skipped')] best_day = max(active_logs, key=lambda d: d['day_pnl']) if active_logs else {} worst_day = min(active_logs, key=lambda d: d['day_pnl']) if active_logs else {} acb_cuts = [d.get('adaptive_cut', 0) for d in p['daily_log'] if not d.get('skipped')] avg_acb_cut = np.mean(acb_cuts) if acb_cuts else 0.0 max_acb_cut = max(acb_cuts) if acb_cuts else 0.0 summaries[sname] = { 'strategy_params': { 'max_leverage': strategies[sname].max_leverage, 'fraction': strategies[sname].fraction, 'convexity': strategies[sname].leverage_convexity, }, 'performance': { 'init_capital': init_capital, 'final_capital': round(p['capital'], 2), 'total_roi_pct': round(total_roi, 4), 'total_pnl': round(p['capital'] - init_capital, 2), 'total_trades': p['total_trades'], 'total_wins': p['total_wins'], 'total_win_rate': round(total_wr, 2), }, 'risk': { 'max_drawdown_pct': round(p['max_drawdown_pct'], 4), 'peak_capital': round(p['peak_capital'], 2), 'sharpe_annual': round(sharpe_annual, 4), 'winning_days': p['winning_days'], 'losing_days': p['losing_days'], 'win_day_pct': round(win_day_pct, 2), }, 'best_day': { 'date': best_day.get('date', ''), 'pnl': best_day.get('day_pnl', 0), }, 'worst_day': { 'date': worst_day.get('date', ''), 'pnl': worst_day.get('day_pnl', 0), }, 'acb_stats': { 'avg_cut_pct': round(avg_acb_cut * 100, 2), 'max_cut_pct': round(max_acb_cut * 100, 2), }, } return summaries def main(): parser = argparse.ArgumentParser(description='DOLPHIN Paper Trading with Adaptive CB v2') parser.add_argument('--no-cb', action='store_true', help='Run WITHOUT circuit breaker') parser.add_argument('--mc-forewarn', action='store_true', help='Enable MC Forewarning ML System') parser.add_argument('--compare', action='store_true', help='Run both and compare') args = parser.parse_args() print("=" * 80) print("DOLPHIN PAPER TRADING — ADAPTIVE CIRCUIT BREAKER v4 & MC-FOREWARNER") print("Multi-signal confirmation approach & ML Geometry Check") print("=" * 80) print("\nLoading data...") df = load_all_data() print(f"Loaded: {len(df):,} rows") if args.compare: print("\n" + "=" * 80) print("RUNNING BASELINE (NO CB)") print("=" * 80) portfolio_base, dates, _ = run_paper_portfolio( df, STRATEGIES, INIT_CAPITAL, use_acb=False, use_mc_forewarn=False, verbose=True ) summaries_base = generate_summary(portfolio_base, STRATEGIES, dates, INIT_CAPITAL) print("\n" + "=" * 80) print("RUNNING ADAPTIVE CB v4 (Meta-Adaptive Lags)") print("=" * 80) portfolio_acb, dates, acb_log = run_paper_portfolio( df, STRATEGIES, INIT_CAPITAL, use_acb=True, use_mc_forewarn=False, verbose=True ) summaries_acb = generate_summary(portfolio_acb, STRATEGIES, dates, INIT_CAPITAL, acb_log) if args.mc_forewarn: print("\n" + "=" * 80) print("RUNNING ADAPTIVE CB v4 + MC FOREWARNER") print("=" * 80) forewarner = DolphinForewarner(models_dir=str(Path(__file__).parent / "nautilus_dolphin" / "mc_results" / "models")) portfolio_mc, dates_mc, acb_log_mc = run_paper_portfolio( df, STRATEGIES, INIT_CAPITAL, use_acb=True, use_mc_forewarn=True, forewarner=forewarner, verbose=True ) summaries_mc = generate_summary(portfolio_mc, STRATEGIES, dates_mc, INIT_CAPITAL, acb_log_mc) # Comparison print("\n" + "=" * 80) print("COMPARISON: Baseline vs Adaptive CB v4" + (" vs MC" if args.mc_forewarn else "")) print("=" * 80) if args.mc_forewarn: print(f"{'Strategy':<25} {'No CB':<12} {'ACB v4':<12} {'MC-Forewarn':<12}") else: print(f"{'Strategy':<25} {'No CB':<12} {'ACB v4':<12} {'Delta':<12} {'ACB Cut':<10}") print("-" * 80) for sname in STRATEGIES.keys(): base_roi = summaries_base[sname]['performance']['total_roi_pct'] acb_roi = summaries_acb[sname]['performance']['total_roi_pct'] if args.mc_forewarn: mc_roi = summaries_mc[sname]['performance']['total_roi_pct'] print(f"{sname:<25} {base_roi:>+10.2f}% {acb_roi:>+10.2f}% {mc_roi:>+10.2f}%") else: acb_cut = summaries_acb[sname]['acb_stats']['avg_cut_pct'] print(f"{sname:<25} {base_roi:>+10.2f}% {acb_roi:>+10.2f}% {acb_roi-base_roi:>+10.2f}% {acb_cut:>8.1f}%") print("\n--- ACB v2 DECISIONS (last 10) ---") for log in acb_log[-10:]: print(f" {log['date']}: {log['cut_pct']:.0%} cut ({log['signals']:.1f} signals, severity={log['severity']})") else: use_acb = not args.no_cb use_mc = args.mc_forewarn mode_str = "ADAPTIVE CB v4 + MC FOREWARN" if use_mc else ("ADAPTIVE CB v4" if use_acb else "NO CB (baseline)") print(f"\nRunning: {mode_str}") forewarner = DolphinForewarner(models_dir=str(Path(__file__).parent / "nautilus_dolphin" / "mc_results" / "models")) if use_mc else None t0 = time.time() portfolio, dates, acb_log = run_paper_portfolio( df, STRATEGIES, INIT_CAPITAL, use_acb=use_acb, use_mc_forewarn=use_mc, forewarner=forewarner, verbose=True ) elapsed = time.time() - t0 summaries = generate_summary(portfolio, STRATEGIES, dates, INIT_CAPITAL, acb_log) print(f"\n{'='*80}") print(f"RESULTS — {mode_str}") print(f"{'='*80}") print(f"Period: {dates[0]} to {dates[-1]} ({len(dates)} days)") print(f"Time: {elapsed:.0f}s") print(f"\n{'Strategy':<25} {'Final $':>10} {'ROI':>8} {'Trades':>7} {'WR%':>6} {'MaxDD':>7} {'Sharpe':>7}") print("-" * 90) for sname, s in summaries.items(): perf = s['performance'] risk = s['risk'] print(f"{sname:<25} ${perf['final_capital']:>9,.0f} " f"{perf['total_roi_pct']:>+7.1f}% " f"{perf['total_trades']:>6} " f"{perf['total_win_rate']:>5.1f} " f"{risk['max_drawdown_pct']:>6.1f}% " f"{risk['sharpe_annual']:>6.2f}") if use_acb and acb_log: print("\n--- ACB v2 DECISIONS ---") for log in acb_log[-10:]: print(f" {log['date']}: {log['cut_pct']:.0%} cut ({log['signals']:.1f} signals, sev={log['severity']})") print(f"\n{'='*80}") print("DONE") print(f"{'='*80}") if __name__ == '__main__': main()