initial: import DOLPHIN baseline 2026-04-21 from dolphinng5_predict working tree
Includes core prod + GREEN/BLUE subsystems: - prod/ (BLUE harness, configs, scripts, docs) - nautilus_dolphin/ (GREEN Nautilus-native impl + dvae/ preserved) - adaptive_exit/ (AEM engine + models/bucket_assignments.pkl) - Observability/ (EsoF advisor, TUI, dashboards) - external_factors/ (EsoF producer) - mc_forewarning_qlabs_fork/ (MC regime/envelope) Excludes runtime caches, logs, backups, and reproducible artifacts per .gitignore.
This commit is contained in:
387
nautilus_dolphin/mc/mc_executor.py
Executable file
387
nautilus_dolphin/mc/mc_executor.py
Executable file
@@ -0,0 +1,387 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user