Files
DOLPHIN/nautilus_dolphin/mc/mc_executor.py

388 lines
14 KiB
Python
Raw Normal View History

"""
Monte Carlo Trial Executor
==========================
Trial execution harness for running backtests with parameter configurations.
This module interfaces with the Nautilus-Dolphin system to run backtests
with sampled parameter configurations and extract metrics.
Reference: MONTE_CARLO_SYSTEM_ENVELOPE_SPEC.md Section 5
"""
import time
from typing import Dict, List, Optional, Any, Tuple
from pathlib import Path
from datetime import datetime
import numpy as np
from .mc_sampler import MCTrialConfig
from .mc_validator import MCValidator, ValidationResult
from .mc_metrics import MCMetrics, MCTrialResult
class MCExecutor:
"""
Monte Carlo Trial Executor.
Runs backtests for parameter configurations and extracts metrics.
"""
def __init__(
self,
initial_capital: float = 25000.0,
data_period: Tuple[str, str] = ('2025-12-31', '2026-02-18'),
preflight_bars: int = 500,
preflight_min_trades: int = 2,
verbose: bool = False
):
"""
Initialize the executor.
Parameters
----------
initial_capital : float
Starting capital for backtests
data_period : Tuple[str, str]
(start_date, end_date) for backtest
preflight_bars : int
Bars for preflight check (V4)
preflight_min_trades : int
Minimum trades for preflight to pass
verbose : bool
Print detailed execution info
"""
self.initial_capital = initial_capital
self.data_period = data_period
self.preflight_bars = preflight_bars
self.preflight_min_trades = preflight_min_trades
self.verbose = verbose
self.validator = MCValidator(verbose=verbose)
self.metrics = MCMetrics(initial_capital=initial_capital)
# Try to import Nautilus-Dolphin components
self._init_nd_components()
def _init_nd_components(self):
"""Initialize Nautilus-Dolphin components if available."""
self.nd_available = False
try:
# Import key components from Nautilus-Dolphin
from nautilus_dolphin.nautilus.strategy_config import DolphinStrategyConfig
from nautilus_dolphin.nautilus.backtest_runner import run_backtest
self.DolphinStrategyConfig = DolphinStrategyConfig
self.run_nd_backtest = run_backtest
self.nd_available = True
if self.verbose:
print("[OK] Nautilus-Dolphin components loaded")
except ImportError as e:
if self.verbose:
print(f"[WARN] Nautilus-Dolphin not available: {e}")
print("[WARN] Will use simulation mode for testing")
def execute_trial(
self,
config: MCTrialConfig,
skip_validation: bool = False
) -> MCTrialResult:
"""
Execute a single MC trial.
Parameters
----------
config : MCTrialConfig
Trial configuration
skip_validation : bool
Skip validation (if already validated)
Returns
-------
MCTrialResult
Complete trial result with metrics
"""
start_time = time.time()
# Step 1: Validation (V1-V4)
if not skip_validation:
validation = self.validator.validate(config)
if not validation.is_valid():
result = MCTrialResult(
trial_id=config.trial_id,
config=config,
status=validation.status.value,
error_message=validation.reject_reason
)
result.execution_time_sec = time.time() - start_time
return result
# Step 2: Preflight check (V4 lightweight)
preflight_passed, preflight_msg = self._run_preflight(config)
if not preflight_passed:
result = MCTrialResult(
trial_id=config.trial_id,
config=config,
status='PREFLIGHT_FAIL',
error_message=preflight_msg
)
result.execution_time_sec = time.time() - start_time
return result
# Step 3: Full backtest
try:
if self.nd_available:
trades, daily_pnls, date_stats, signal_stats = self._run_nd_backtest(config)
else:
trades, daily_pnls, date_stats, signal_stats = self._run_simulated_backtest(config)
# Step 4: Compute metrics
execution_time = time.time() - start_time
result = self.metrics.compute(
config, trades, daily_pnls, date_stats, signal_stats, execution_time
)
if self.verbose:
print(f" Trial {config.trial_id}: ROI={result.roi_pct:.2f}%, "
f"Trades={result.n_trades}, Sharpe={result.sharpe_ratio:.2f}")
return result
except Exception as e:
if self.verbose:
print(f" Trial {config.trial_id}: ERROR - {e}")
result = MCTrialResult(
trial_id=config.trial_id,
config=config,
status='ERROR',
error_message=str(e)
)
result.execution_time_sec = time.time() - start_time
return result
def _run_preflight(self, config: MCTrialConfig) -> Tuple[bool, str]:
"""
Run lightweight preflight check (V4).
Returns (passed, message).
"""
# Check for extreme values that would cause issues
# Fraction too small
if config.fraction < 0.02:
return False, f"FRACTION_TOO_SMALL: {config.fraction}"
# Leverage range issues
leverage_range = config.max_leverage - config.min_leverage
if leverage_range < 0.5 and config.leverage_convexity > 2.0:
return False, f"NARROW_RANGE_HIGH_CONVEXITY"
# Hold period too short
if config.max_hold_bars < config.vd_trend_lookback + 10:
return False, f"HOLD_TOO_SHORT"
# TP/SL ratio check
tp_sl_ratio = config.fixed_tp_pct / (config.stop_pct / 100)
if tp_sl_ratio > 10:
return False, f"TP_SL_RATIO_EXTREME: {tp_sl_ratio}"
return True, "OK"
def _run_nd_backtest(
self,
config: MCTrialConfig
) -> Tuple[List[Dict], List[float], List[Dict], Dict[str, Any]]:
"""
Run actual Nautilus-Dolphin backtest.
Returns (trades, daily_pnls, date_stats, signal_stats).
"""
# Convert MC config to ND config
nd_config = self._mc_to_nd_config(config)
# Run backtest
backtest_result = self.run_nd_backtest(nd_config)
# Extract results
trades = backtest_result.get('trades', [])
daily_pnls = backtest_result.get('daily_pnls', [])
date_stats = backtest_result.get('date_stats', [])
signal_stats = backtest_result.get('signal_stats', {})
return trades, daily_pnls, date_stats, signal_stats
def _mc_to_nd_config(self, config: MCTrialConfig) -> Dict[str, Any]:
"""Convert MC trial config to Nautilus-Dolphin config."""
return {
'venue': 'BINANCE_FUTURES',
'environment': 'BACKTEST',
'trader_id': f'DOLPHIN-MC-{config.trial_id}',
'strategy': {
'venue': 'BINANCE_FUTURES',
'direction': 'SHORT',
'vel_div_threshold': config.vel_div_threshold,
'vel_div_extreme': config.vel_div_extreme,
'max_leverage': config.max_leverage,
'min_leverage': config.min_leverage,
'leverage_convexity': config.leverage_convexity,
'capital_fraction': config.fraction,
'max_hold_bars': config.max_hold_bars,
'tp_bps': int(config.fixed_tp_pct * 10000),
'fixed_tp_pct': config.fixed_tp_pct,
'stop_pct': config.stop_pct,
'use_trailing': False,
'irp_alignment_min': config.min_irp_alignment,
'lookback': config.lookback,
'excluded_assets': ['TUSDUSDT', 'USDCUSDT'],
'acb_enabled': True,
'max_concurrent_positions': 1,
'daily_loss_limit_pct': 10.0,
'use_sp_fees': config.use_sp_fees,
'use_sp_slippage': config.use_sp_slippage,
'sp_maker_fill_rate': config.sp_maker_entry_rate,
'sp_maker_exit_rate': config.sp_maker_exit_rate,
'use_ob_edge': config.use_ob_edge,
'ob_edge_bps': config.ob_edge_bps,
'ob_confirm_rate': config.ob_confirm_rate,
'ob_imbalance_bias': config.ob_imbalance_bias,
'ob_depth_scale': config.ob_depth_scale,
'use_direction_confirm': config.use_direction_confirm,
'dc_lookback_bars': config.dc_lookback_bars,
'dc_min_magnitude_bps': config.dc_min_magnitude_bps,
'dc_skip_contradicts': config.dc_skip_contradicts,
'dc_leverage_boost': config.dc_leverage_boost,
'dc_leverage_reduce': config.dc_leverage_reduce,
'use_alpha_layers': config.use_alpha_layers,
'use_dynamic_leverage': config.use_dynamic_leverage,
'acb_beta_high': config.acb_beta_high,
'acb_beta_low': config.acb_beta_low,
'acb_w750_threshold_pct': config.acb_w750_threshold_pct,
},
'data_catalog': {
'eigenvalues_dir': '../eigenvalues',
'catalog_path': 'nautilus_dolphin/catalog',
'start_date': self.data_period[0],
'end_date': self.data_period[1],
'assets': [
'BTCUSDT', 'ETHUSDT', 'ADAUSDT', 'SOLUSDT', 'DOTUSDT',
'AVAXUSDT', 'MATICUSDT', 'LINKUSDT', 'UNIUSDT', 'ATOMUSDT'
],
},
}
def _run_simulated_backtest(
self,
config: MCTrialConfig
) -> Tuple[List[Dict], List[float], List[Dict], Dict[str, Any]]:
"""
Run simulated backtest for testing without Nautilus.
This produces realistic-looking results based on parameter configuration
without actually running a full backtest.
"""
# Number of trades based on vel_div_threshold (lower = more trades)
base_trades = 500
threshold_factor = abs(-0.02 / config.vel_div_threshold)
n_trades = int(base_trades * threshold_factor * np.random.uniform(0.8, 1.2))
n_trades = max(20, min(2000, n_trades))
# Win rate based on parameters
base_wr = 0.48
if config.use_direction_confirm:
base_wr += 0.05
if config.use_ob_edge:
base_wr += 0.02
win_rate = np.clip(base_wr + np.random.normal(0, 0.05), 0.3, 0.7)
# Generate trades
trades = []
n_wins = int(n_trades * win_rate)
n_losses = n_trades - n_wins
for i in range(n_trades):
is_win = i < n_wins
if is_win:
pnl_pct = np.random.exponential(0.008) + 0.002
pnl = pnl_pct * self.initial_capital * config.fraction * config.max_leverage
exit_type = 'tp' if np.random.random() < 0.7 else 'hold'
else:
pnl_pct = -np.random.exponential(0.006) - 0.001
pnl = pnl_pct * self.initial_capital * config.fraction * config.max_leverage
exit_type = np.random.choice(['stop', 'hold'], p=[0.3, 0.7])
trades.append({
'pnl': pnl,
'pnl_pct': pnl_pct,
'exit_type': exit_type,
'bars_held': np.random.randint(10, config.max_hold_bars),
'asset': np.random.choice(['BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'ADAUSDT']),
})
# Shuffle trades
np.random.shuffle(trades)
# Generate daily P&Ls (48 days)
daily_pnls = []
date_stats = []
trades_per_day = len(trades) // 48
for day in range(48):
day_trades = trades[day * trades_per_day:(day + 1) * trades_per_day]
day_pnl = sum(t['pnl'] for t in day_trades)
daily_pnls.append(day_pnl)
date_str = f'2026-01-{day % 31 + 1:02d}' if day < 31 else f'2026-02-{day - 30:02d}'
date_stats.append({
'date': date_str,
'pnl': day_pnl,
})
# Signal stats
signal_stats = {
'dc_skip_rate': 0.1 if config.use_direction_confirm else 0.0,
'ob_skip_rate': 0.05 if config.use_ob_edge else 0.0,
'dc_confirm_rate': 0.7 if config.use_direction_confirm else 0.0,
'irp_match_rate': 0.6 if config.use_asset_selection else 0.0,
'entry_attempt_rate': 0.3,
'signal_to_trade_rate': len(trades) / (48 * 1000), # Approximate
}
return trades, daily_pnls, date_stats, signal_stats
def execute_batch(
self,
configs: List[MCTrialConfig],
progress_interval: int = 10
) -> List[MCTrialResult]:
"""
Execute a batch of trials.
Parameters
----------
configs : List[MCTrialConfig]
Trial configurations
progress_interval : int
Print progress every N trials
Returns
-------
List[MCTrialResult]
Results for all trials
"""
results = []
total = len(configs)
for i, config in enumerate(configs):
result = self.execute_trial(config)
results.append(result)
if (i + 1) % progress_interval == 0 or i == total - 1:
print(f" Progress: {i+1}/{total} ({(i+1)/total*100:.1f}%)")
return results