chore: safety snapshot 2026-03-05 — HCM infrastructure before 2y klines experiment
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>
This commit is contained in:
734
dolphin_paper_trade_adaptive_cb_v2.py
Normal file
734
dolphin_paper_trade_adaptive_cb_v2.py
Normal file
@@ -0,0 +1,734 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user