Coverage for src / monte_neo / monte_carlo / sequential.py: 97%

87 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-28 16:27 +0200

1"""Sequential Monte Carlo execution module.""" 

2 

3from __future__ import annotations 

4 

5import time 

6from typing import TYPE_CHECKING 

7 

8import pandas as pd 

9import questionary 

10from rich.console import Console 

11from rich.panel import Panel 

12from rich.table import Table 

13 

14from monte_neo.monte_carlo.types import MCResult, MCStepResult 

15 

16if TYPE_CHECKING: 

17 from monte_neo.indicators.base import BaseIndicator 

18 from monte_neo.metrics.calculator import MetricsCalculator 

19 from monte_neo.monte_carlo.engine import MonteCarloEngine 

20 

21 

22from monte_neo.cli.styles import CUSTOM_STYLE 

23 

24console = Console() 

25 

26 

27class SequentialMCRunner: 

28 """Runner for sequential Monte Carlo methods.""" 

29 

30 def __init__(self, engine: MonteCarloEngine): 

31 """Initialize runner. 

32 

33 Args: 

34 engine: Base MonteCarloEngine. 

35 """ 

36 self.engine = engine 

37 

38 def run( 

39 self, 

40 data: pd.DataFrame, 

41 indicator: BaseIndicator, 

42 metrics_calc: MetricsCalculator, 

43 target_metrics: dict[str, float], 

44 interactive: bool = True, 

45 ) -> MCResult: 

46 """Run MC methods sequentially. 

47 

48 Args: 

49 data: OHLCV data. 

50 indicator: Indicator to test. 

51 metrics_calc: Metrics calculator. 

52 target_metrics: Target metrics. 

53 interactive: Whether to ask for confirmation before each step. 

54 

55 Returns: 

56 MCResult with sequential results. 

57 """ 

58 start_time = time.time() 

59 step_results = [] 

60 all_passed = True 

61 

62 # Define methods in requested order 

63 methods = [ 

64 ("Walk-Forward Analysis", "walk_forward"), 

65 ("Block Bootstrap", "block_bootstrap"), 

66 ("Return Shuffling", "shuffling"), 

67 ("Noise Injection", "noise"), 

68 ("Sensitivity Analysis", "sensitivity"), 

69 ] 

70 

71 enabled_methods = [ 

72 (display, key) for (display, key) in methods 

73 if getattr(self.engine.config, f"use_{key}") 

74 ] 

75 

76 # Educational descriptions for each method 

77 descriptions = { 

78 "walk_forward": "Validates strategy performance on 'future' data not used during training. Helps detect overfitting.", 

79 "block_bootstrap": "Creates new market scenarios by shuffling historical data blocks. Tests strategy resilience to market regime changes.", 

80 "shuffling": "Shuffles the sequence of returns, destroying temporal structure. If a strategy relies on real patterns, performance should degrade on shuffled data.", 

81 "noise": "Adds random noise to OHLC prices. Tests strategy sensitivity to minor price changes and volatility.", 

82 "sensitivity": "Varies indicator parameters within a small range (e.g., ±10%). A robust strategy should not break with small setting changes." 

83 } 

84 

85 # Use rich table for sequential output if it's the main display 

86 console.print(f"\n[bold yellow]🔍 Sequential MC Validation for: {indicator.name}[/]") 

87 

88 if not interactive: 

89 console.print(f"[dim]Running in automated mode. All {len(enabled_methods)} steps will be executed.[/]") 

90 

91 table = Table(title="Monte Carlo Steps", show_header=True, header_style="bold magenta") 

92 table.add_column("Step", justify="right") 

93 table.add_column("Method", style="cyan") 

94 table.add_column("Pass Rate", justify="right") 

95 table.add_column("Status", justify="center") 

96 

97 total_steps = len(enabled_methods) 

98 for idx, (display_name, method_key) in enumerate(enabled_methods, start=1): 

99 console.print(f"\n[bold magenta]👉 Stage {idx}/{total_steps}: {display_name}[/]") 

100 console.print(Panel(descriptions.get(method_key, ""), title="Educational Info", border_style="blue")) 

101 

102 # Run method 

103 step_result = self._run_step( 

104 display_name, method_key, data, indicator, metrics_calc, target_metrics 

105 ) 

106 step_results.append(step_result) 

107 

108 # Update output 

109 status = "[green]PASSED[/]" if step_result.passed else "[red]FAILED[/]" 

110 table.add_row(f"{idx}/{total_steps}", display_name, f"{step_result.pass_rate:.1%}", status) 

111 

112 # Print current state 

113 if interactive: 

114 console.clear() 

115 console.print(f"\n[bold yellow]🔍 Sequential MC Validation for: {indicator.name}[/]") 

116 console.print(table) 

117 

118 # Print Detailed Advice for the current step 

119 console.print(f"\n[bold cyan]💡 Analysis & Advice for {display_name}:[/]") 

120 console.print(f"[italic]{step_result.advice}[/]") 

121 

122 # Show key metrics for this step 

123 if step_result.metrics_summary: 

124 pf = step_result.metrics_summary.get("profit_factor", {}).get("mean", 0.0) 

125 sr = step_result.metrics_summary.get("sharpe_ratio", {}).get("mean", 0.0) 

126 dd = step_result.metrics_summary.get("max_drawdown", {}).get("mean", 0.0) 

127 console.print(f"[dim]Stats: PF={pf:.2f}, Sharpe={sr:.2f}, DD={dd:.1%}[/]") 

128 console.print( 

129 f"[dim]Pass Rate: {step_result.pass_rate:.1%} | " 

130 f"Threshold: {self.engine.config.pass_threshold:.1%} | " 

131 f"Iterations: {step_result.iterations}[/]" 

132 ) 

133 

134 if not step_result.passed: 

135 all_passed = False 

136 console.print(f"\n[bold red]❌ FAILED: {display_name} did not meet robustness criteria.[/]") 

137 console.print("[red]To reach Production-Ready status, the indicator must pass all stages.[/]") 

138 console.print("[red]Review the advice above and adjust your strategy parameters or logic.[/]") 

139 

140 if interactive: 

141 if not questionary.confirm("Continue to next stage anyway (not recommended)?", default=False, style=CUSTOM_STYLE).ask(): 

142 break 

143 # В неинтерактивном режиме продолжаем выполнение всех шагов для полного анализа 

144 else: 

145 console.print(f"\n[bold green]✅ STAGE PASSED: {display_name}[/]") 

146 if interactive and idx < total_steps: 

147 questionary.press_any_key_to_continue("Press any key to proceed to next stage...", style=CUSTOM_STYLE).ask() 

148 

149 elapsed = time.time() - start_time 

150 total_enabled = max(1, len(enabled_methods)) 

151 total_executed = len(step_results) 

152 pass_rate = (len([r for r in step_results if r.passed]) / total_executed) if total_executed else 0.0 

153 total_iterations = sum(r.iterations for r in step_results) 

154 

155 # Populate high-level metrics summary from all steps 

156 metrics_summary = {} 

157 if step_results: 

158 # For simplicity, use metrics from the last step or combine them 

159 # Here we'll just take the last step's summary as the overall summary 

160 metrics_summary = step_results[-1].metrics_summary 

161 

162 return MCResult( 

163 passed=all_passed and total_executed > 0, 

164 pass_rate=pass_rate, 

165 iterations_run=total_iterations, 

166 elapsed_time=elapsed, 

167 metrics_summary=metrics_summary, 

168 step_results=step_results, 

169 ) 

170 

171 def _run_step( 

172 self, 

173 name: str, 

174 key: str, 

175 data: pd.DataFrame, 

176 indicator: BaseIndicator, 

177 metrics_calc: MetricsCalculator, 

178 target_metrics: dict[str, float], 

179 ) -> MCStepResult: 

180 """Run a single MC step.""" 

181 scenarios = [] 

182 iterations = self.engine.config.iterations 

183 

184 if key == "walk_forward": 

185 scenarios = self.engine.scenario_builder.generate_walk_forward(data) 

186 elif key == "block_bootstrap": 

187 scenarios = self.engine.scenario_builder.generate_block_bootstrap(data, iterations) 

188 elif key == "shuffling": 

189 scenarios = self.engine.scenario_builder.generate_shuffling(data, iterations) 

190 elif key == "noise": 

191 scenarios = self.engine.scenario_builder.generate_noise(data, iterations) 

192 elif key == "sensitivity": 

193 # Sensitivity is special as it varies parameters, not data 

194 return self._run_sensitivity_step(indicator, data, metrics_calc, target_metrics) 

195 

196 # Run backtests for scenarios 

197 results = self.engine.gpu_engine.backtest_scenarios( 

198 indicator, 

199 scenarios, 

200 executor=self.engine.executor, 

201 use_sl_tp=self.engine.config.use_sl_tp, 

202 sl_pct=self.engine.config.sl_pct, 

203 tp_pct=self.engine.config.tp_pct, 

204 ) 

205 

206 passed_count = 0 

207 for r in results: 

208 metrics = r.get("metrics", {}) 

209 passed = True 

210 for metric_name, target_value in target_metrics.items(): 

211 if metric_name not in metrics: 

212 continue 

213 actual = metrics[metric_name] 

214 if metric_name in ["max_drawdown", "consecutive_losses"]: 

215 if actual > target_value: 

216 passed = False 

217 break 

218 else: 

219 if actual < target_value: 

220 passed = False 

221 break 

222 if passed: 

223 passed_count += 1 

224 pass_rate = passed_count / len(results) if results else 0.0 

225 passed = pass_rate >= self.engine.config.pass_threshold 

226 

227 from monte_neo.monte_carlo.utils import summarize_metrics 

228 summary = summarize_metrics(results) 

229 advice = self._generate_advice(name, pass_rate, summary) 

230 

231 return MCStepResult( 

232 method_name=name, 

233 passed=passed, 

234 pass_rate=pass_rate, 

235 metrics_summary=summary, 

236 advice=advice, 

237 iterations=len(results), 

238 ) 

239 

240 def _run_sensitivity_step( 

241 self, 

242 indicator: BaseIndicator, 

243 data: pd.DataFrame, 

244 metrics_calc: MetricsCalculator, 

245 target_metrics: dict[str, float], 

246 ) -> MCStepResult: 

247 """Run sensitivity analysis step.""" 

248 from monte_neo.monte_carlo.sensitivity import SensitivityAnalyzer 

249 analyzer = SensitivityAnalyzer(variation_range=self.engine.config.sensitivity_range) 

250 results = analyzer.analyze_all_parameters(indicator, data, metrics_calc) 

251 report = analyzer.get_stability_report(results) 

252 

253 pass_rate = report["average_stability"] 

254 passed = report["overall_stable"] 

255 

256 summary = { 

257 "stability_score": {"mean": pass_rate}, 

258 "stable_params": {"mean": report["stable_parameters"]}, 

259 "total_params": {"mean": report["total_parameters"]}, 

260 } 

261 

262 advice = self._generate_advice("Sensitivity Analysis", pass_rate, summary) 

263 

264 return MCStepResult( 

265 method_name="Sensitivity Analysis", 

266 passed=passed, 

267 pass_rate=pass_rate, 

268 metrics_summary=summary, 

269 advice=advice, 

270 iterations=len(results), 

271 ) 

272 

273 def _generate_advice(self, method_name: str, pass_rate: float, summary: dict) -> str: 

274 """Generate advice based on results.""" 

275 name = method_name.lower() 

276 if pass_rate >= 0.95: 

277 if "walk" in name: 

278 return "Excellent stability over time. The strategy adapts well to different market regimes." 

279 if "block" in name or "bootstrap" in name: 

280 return "High statistical significance. The edge is likely not due to random price sequences." 

281 if "shuffling" in name: 

282 return "The strategy captures real market structure, not just random price distributions (Passed)." 

283 if "noise" in name: 

284 return "Robust against price execution noise and minor volatility spikes." 

285 if "sensitivity" in name: 

286 return "Parameters are well-tuned and stable. Not over-optimized for specific values." 

287 return "Strategy passed this stage with high confidence." 

288 

289 if pass_rate >= 0.80: 

290 return f"Strategy is mostly stable but shows some weakness in {method_name}. Consider slight adjustments." 

291 

292 if "walk" in name: 

293 return "Strategy fails to maintain performance across different time periods. Risk of over-fitting to specific dates." 

294 if "block" in name or "bootstrap" in name: 

295 return "Low statistical significance. The strategy might be capturing noise or specific patterns that don't repeat." 

296 if "shuffling" in name: 

297 return "Performance is similar to random entry (Failed). The 'edge' might be an illusion of price distribution." 

298 if "noise" in name: 

299 return "Strategy is very sensitive to price noise. Might fail in real-market execution with slippage." 

300 if "sensitivity" in name: 

301 return "High sensitivity to parameter changes. Likely over-optimized (curve-fitted)." 

302 

303 return f"Strategy failed to meet robustness criteria in {method_name} (pass rate: {pass_rate:.1%})."