Files
siloqy/dolphin_paper_trade_adaptive_cb_v2.py

735 lines
30 KiB
Python
Raw Normal View History

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