Captures critical infrastructure surrounding the nautilus_dolphin core package: - dolphin_vbt_real.py: VBT vectorized backtest engine (6008 lines) - dolphin_paper_trade_adaptive_cb_v2.py: champion runner (champion_5x_f20) - _update_vbt_cache.py / update_VBT_parquet_cache.bat: cache builder - external_factors/: ExF system (all 85 indicator fetchers + NPZ cache) - mc_forewarning_qlabs_fork/: QLabs-enhanced MC-Forewarner research fork - DATA_LOCATIONS.md: source-of-truth path registry - .gitignore: excludes vbt_cache*, backfilled_data, .venv, models, etc. Note: nautilus_dolphin/ has own git repo (inner) — safety snapshot committed there separately. Champion state: WR=49.3%, ROI=+44.89%, PF=1.123, DD=14.95%, Sharpe=2.50 (55d, full-stack, abs_max_lev=6.0). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
735 lines
30 KiB
Python
735 lines
30 KiB
Python
"""
|
|
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()
|