388 lines
14 KiB
Python
388 lines
14 KiB
Python
|
|
"""
|
||
|
|
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
|