Coverage for src / monte_neo / core / generator_search.py: 84%
130 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-28 16:27 +0200
1from __future__ import annotations
3import time
4from typing import TYPE_CHECKING
6from monte_neo.core.config import GeneratorResult
7from monte_neo.indicators.dynamic import DynamicIndicator
8from monte_neo.monte_carlo.engine import MCConfig, MonteCarloEngine
9from monte_neo.monte_carlo.workers import init_worker_data
10from monte_neo.utils.logger import get_logger
11from monte_neo.utils.parallel import ParallelExecutor
13if TYPE_CHECKING:
14 import pandas as pd
16 from monte_neo.core.generator import IndicatorGenerator
18logger = get_logger(__name__)
21def run_search(generator: IndicatorGenerator, data: pd.DataFrame) -> GeneratorResult:
22 """Run the search process."""
23 start_time = time.time()
24 best_indicator = None
25 best_mc_rate = 0.0
26 best_mc_details = {}
27 iterations_tried = 0
28 final_metrics = {}
30 mc_cache: dict[str, float] = {}
31 fallback_indicator = None
32 fallback_metrics = {}
33 best_performance_score = -float('inf')
35 # Initialize persistent executor
36 if generator.config.use_sequential_mc:
37 generator.executor = ParallelExecutor(
38 n_workers=1,
39 use_processes=False,
40 initializer=init_worker_data,
41 initargs=(data,)
42 )
43 else:
44 generator.executor = ParallelExecutor(
45 initializer=init_worker_data,
46 initargs=(data,)
47 )
48 generator.executor.__enter__()
50 logger.info(f"Starting indicator generation (max {generator.config.max_iterations} iterations)")
52 # Optimization: Use larger batch size to reduce IPC and parallel overhead
53 batch_size = max(500, generator.config.population_size * 5)
54 total_iterations = generator.config.max_iterations
56 # Pre-generate MC scenarios
57 mc_scenarios = _pre_generate_scenarios(generator, data, total_iterations)
59 try:
60 if generator._progress_callback:
61 generator._progress_callback(0, total_iterations, "Starting search...")
63 for batch_start in range(0, total_iterations, batch_size):
64 actual_batch_size = min(batch_size, total_iterations - batch_start)
65 batch_indicators = [generator._generate_random_indicator() for _ in range(actual_batch_size)]
67 # GPU Backtest
68 try:
69 gpu_results = generator.gpu_engine.backtest_batch(
70 data,
71 batch_indicators,
72 executor=generator.executor,
73 use_shared_data=True,
74 use_sl_tp=generator.config.use_sl_tp,
75 sl_pct=generator.config.stop_loss_pct,
76 tp_pct=generator.config.take_profit_pct,
77 force_parallel=True # Force parallel for large batches
78 )
79 except Exception as e:
80 logger.warning(f"Backtest failed: {e}. Skipping batch.")
81 gpu_results = []
83 # Process results
84 for i, result in enumerate(gpu_results):
85 iterations_tried += 1
86 indicator = batch_indicators[i]
87 metrics = result["metrics"]
89 # Fallback logic
90 perf_score = (metrics.get("total_return", 0) * metrics.get("profit_factor", 1)) / (metrics.get("max_drawdown", 0) + 0.01)
91 if perf_score > best_performance_score:
92 best_performance_score = perf_score
93 fallback_indicator = indicator
94 fallback_metrics = metrics
96 if not generator._meets_basic_targets(metrics):
97 continue
99 if metrics.get("trade_count", 0) < generator.config.min_trades:
100 continue
102 # MC Validation
103 try:
104 ind_id = indicator.get_id()
105 if ind_id in mc_cache:
106 mc_pass_rate = mc_cache[ind_id]
107 else:
108 mc_result = generator._run_mc_validation(data, indicator, scenarios=mc_scenarios)
109 mc_pass_rate = mc_result.pass_rate
110 mc_cache[ind_id] = mc_pass_rate
112 if mc_pass_rate > best_mc_rate:
113 best_mc_rate = mc_pass_rate
114 best_indicator = indicator
115 final_metrics = metrics
116 best_mc_details = {
117 "step_results": [
118 {"method": r.method_name, "passed": r.passed, "rate": r.pass_rate, "advice": r.advice}
119 for r in mc_result.step_results
120 ],
121 "timing_stats": getattr(mc_result, "timing_stats", {})
122 }
123 logger.info(f"New best: {indicator.name} MC rate={mc_pass_rate:.2%}")
125 if mc_pass_rate > 0.0:
126 generator._candidates.append((indicator, mc_pass_rate))
128 except Exception as e:
129 logger.warning(f"Error validating indicator: {e}")
130 continue
132 # Progress callback
133 _update_progress(generator, start_time, batch_start, actual_batch_size, total_iterations, best_mc_rate)
135 # Early stopping
136 if generator.config.early_stopping and best_mc_rate >= 0.95:
137 logger.info(f"Early stopping: found solution at iteration {batch_start + actual_batch_size}")
138 break
140 # Check for shutdown requested
141 if generator.executor and getattr(generator.executor, "_shutdown_requested", False):
142 logger.info("Shutdown requested. Stopping search.")
143 break
145 except KeyboardInterrupt:
146 logger.info("KeyboardInterrupt caught in generator. Cleaning up...")
147 if generator._progress_callback:
148 generator._progress_callback(iterations_tried, total_iterations, "Interrupted by user")
149 if generator.executor:
150 generator.executor.__exit__(None, None, None)
151 generator.executor = None
152 # Return what we found so far instead of crashing
153 return _create_result(generator, best_indicator, fallback_indicator, best_mc_rate, best_mc_details, final_metrics, fallback_metrics, iterations_tried, start_time, data)
155 # Evolution Phase
156 best_indicator, best_mc_rate, best_mc_details = _run_evolution_phase(
157 generator, data, best_indicator, best_mc_rate, best_mc_details
158 )
160 elapsed = time.time() - start_time
161 if generator._progress_callback:
162 generator._progress_callback(total_iterations, total_iterations, f"Best MC rate: {best_mc_rate:.1%} [Done]")
164 if generator.executor:
165 generator.executor.__exit__(None, None, None)
166 generator.executor = None
168 return _create_result(
169 generator, best_indicator, fallback_indicator, best_mc_rate,
170 best_mc_details, final_metrics, fallback_metrics, iterations_tried, start_time, data
171 )
174def _create_result(
175 generator: IndicatorGenerator,
176 best_indicator,
177 fallback_indicator,
178 best_mc_rate: float,
179 best_mc_details: dict,
180 final_metrics: dict,
181 fallback_metrics: dict,
182 iterations_tried: int,
183 start_time: float,
184 data: pd.DataFrame
185) -> GeneratorResult:
186 """Helper to create GeneratorResult."""
187 if not best_indicator and fallback_indicator:
188 best_indicator = fallback_indicator
189 final_metrics = fallback_metrics
191 if best_indicator and not final_metrics:
192 signals = best_indicator.generate_signals(data)
193 final_metrics = generator.metrics_calc.calculate_all(
194 data, signals, use_sl_tp=generator.config.use_sl_tp,
195 sl_pct=generator.config.stop_loss_pct, tp_pct=generator.config.take_profit_pct
196 )
198 elapsed = time.time() - start_time
200 return GeneratorResult(
201 success=best_mc_rate >= generator.config.mc_pass_threshold,
202 indicator=best_indicator,
203 parameters=best_indicator.get_parameters() if best_indicator else {},
204 final_metrics=final_metrics,
205 mc_pass_rate=best_mc_rate,
206 mc_details=best_mc_details,
207 iterations_tried=iterations_tried,
208 elapsed_time=elapsed,
209 candidates_found=len(generator._candidates),
210 )
213def _pre_generate_scenarios(generator: IndicatorGenerator, data: pd.DataFrame, total_iterations: int) -> list[pd.DataFrame] | None:
214 # If GPU is enabled and we are using shuffling, skip pre-generation to use end-to-end GPU path
215 if generator.config.use_gpu and generator.config.use_mc_shuffling and not any([
216 generator.config.use_mc_noise,
217 generator.config.use_mc_sensitivity,
218 generator.config.use_mc_walk_forward,
219 generator.config.use_mc_block_bootstrap
220 ]):
221 logger.info("Skipping pre-generation to use end-to-end GPU path")
222 return None
224 if not generator.config.use_mc_block_bootstrap:
225 try:
226 temp_mc_config = MCConfig(
227 iterations=generator.config.mc_iterations,
228 use_shuffling=generator.config.use_mc_shuffling,
229 use_noise=generator.config.use_mc_noise,
230 use_sensitivity=generator.config.use_mc_sensitivity,
231 use_walk_forward=generator.config.use_mc_walk_forward,
232 use_block_bootstrap=False,
233 )
234 if generator._progress_callback:
235 generator._progress_callback(0, total_iterations, "Generating shared MC scenarios...")
237 temp_engine = MonteCarloEngine(temp_mc_config)
238 return temp_engine.scenario_builder.generate(data)
239 except Exception as e:
240 logger.warning(f"Failed to pre-generate scenarios: {e}. Will generate per candidate.")
241 return None
244def _update_progress(generator: IndicatorGenerator, start_time: float, batch_start: int, batch_size: int, total: int, best_rate: float):
245 if generator._progress_callback:
246 current_iter = min(batch_start + batch_size, total)
247 elapsed = time.time() - start_time
248 ops_sec = current_iter / elapsed if elapsed > 0 else 0.0
249 status = f"Best MC rate: {best_rate:.1%} | Speed: {ops_sec:.1f} op/s"
250 generator._progress_callback(current_iter, total, status)
253def _run_evolution_phase(generator: IndicatorGenerator, data: pd.DataFrame, best_ind, best_rate, best_details):
254 if "dynamic" in generator.config.indicator_types and len(generator._candidates) >= 2:
255 logger.info(f"Starting evolutionary optimization on {len(generator._candidates)} candidates...")
256 try:
257 dynamic_candidates = [c[0] for c in generator._candidates if isinstance(c[0], DynamicIndicator)]
258 if len(dynamic_candidates) >= 2:
259 evolved_best = generator._run_evolution(data, initial_population=dynamic_candidates)
260 if evolved_best:
261 mc_result = generator._run_mc_validation(data, evolved_best)
262 mc_rate = mc_result.pass_rate
264 if mc_rate > 0:
265 generator._candidates.append((evolved_best, mc_rate))
267 if mc_rate > best_rate:
268 best_rate = mc_rate
269 best_ind = evolved_best
270 best_details = {
271 "step_results": [
272 {"method": r.method_name, "passed": r.passed, "rate": r.pass_rate, "advice": r.advice}
273 for r in mc_result.step_results
274 ],
275 "timing_stats": getattr(mc_result, "timing_stats", {})
276 }
277 logger.info(f"Evolution found better indicator: {evolved_best.name} MC rate={mc_rate:.2%}")
278 except Exception as e:
279 logger.error(f"Evolutionary optimization failed: {e}")
280 return best_ind, best_rate, best_details