Coverage for src / monte_neo / core / generator.py: 100%
76 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
1"""Indicator generator module.
3Core engine for generating robust trading indicators.
4"""
6from __future__ import annotations
8from collections.abc import Callable
9from typing import TYPE_CHECKING
11import numpy as np
12import pandas as pd
14from monte_neo.core.config import GeneratorConfig, GeneratorResult
15from monte_neo.core.evolution import EvolutionEngine
16from monte_neo.core.generator_search import run_search
17from monte_neo.core.generator_utils import PARAM_SPACES, estimate_time
18from monte_neo.core.gpu_engine import MLXBacktestEngine
19from monte_neo.indicators.base import BaseIndicator
20from monte_neo.indicators.code_gen import CodeGenerator
21from monte_neo.indicators.dynamic import DynamicIndicator
22from monte_neo.indicators.technical import MACDIndicator, RSIIndicator, SMAIndicator
23from monte_neo.metrics.calculator import MetricsCalculator
24from monte_neo.monte_carlo.engine import MCConfig, MonteCarloEngine
25from monte_neo.utils.logger import get_logger
26from monte_neo.utils.parallel import ParallelExecutor
28if TYPE_CHECKING:
29 from monte_neo.monte_carlo.engine import MCResult
31logger = get_logger(__name__)
34class IndicatorGenerator:
35 """Generate robust trading indicators."""
37 def __init__(self, config: GeneratorConfig | None = None) -> None:
38 """Initialize generator.
40 Args:
41 config: Generator configuration.
42 """
43 self.config = config or GeneratorConfig()
44 self.rng = np.random.default_rng()
45 self.metrics_calc = MetricsCalculator(
46 initial_capital=self.config.initial_capital,
47 leverage=self.config.leverage
48 )
49 self.gpu_engine = MLXBacktestEngine(
50 precision=self.config.gpu_precision,
51 metal_driver=self.config.metal_driver,
52 initial_capital=self.config.initial_capital,
53 leverage=self.config.leverage
54 )
55 self.executor: ParallelExecutor | None = None
57 # State
58 self.population: list[BaseIndicator] = []
59 self._progress_callback: Callable[[int, int, str], None] | None = None
60 self._candidates: list[tuple[BaseIndicator, float]] = []
62 def set_progress_callback(
63 self,
64 callback: Callable[[int, int, str], None],
65 ) -> None:
66 """Set progress callback."""
67 self._progress_callback = callback
69 def _run_mc_validation(
70 self,
71 data: pd.DataFrame,
72 indicator: BaseIndicator,
73 scenarios: list[pd.DataFrame] | None = None
74 ) -> MCResult:
75 """Run Monte Carlo validation for a single indicator."""
76 mc_config = MCConfig(
77 iterations=self.config.mc_iterations,
78 use_shuffling=self.config.use_mc_shuffling,
79 use_noise=self.config.use_mc_noise,
80 use_sensitivity=self.config.use_mc_sensitivity,
81 use_walk_forward=self.config.use_mc_walk_forward,
82 use_block_bootstrap=self.config.use_mc_block_bootstrap,
83 use_sequential=self.config.use_sequential_mc,
84 pass_threshold=self.config.mc_pass_threshold,
85 use_sl_tp=self.config.use_sl_tp,
86 sl_pct=self.config.stop_loss_pct,
87 tp_pct=self.config.take_profit_pct,
88 use_gpu=self.config.use_gpu,
89 gpu_precision=self.config.gpu_precision,
90 metal_driver=self.config.metal_driver,
91 initial_capital=self.config.initial_capital,
92 leverage=self.config.leverage,
93 )
94 mc_engine = MonteCarloEngine(mc_config, executor=self.executor)
96 return mc_engine.run(
97 data,
98 indicator,
99 self.metrics_calc,
100 self.config.target_metrics,
101 existing_scenarios=scenarios,
102 interactive=False
103 )
105 def generate(self, data: pd.DataFrame) -> GeneratorResult:
106 """Generate a robust indicator."""
107 return run_search(self, data)
109 def _mutate_indicator(self, indicator: BaseIndicator) -> BaseIndicator:
110 """Mutate an indicator (wrapper for EvolutionEngine)."""
111 evo = EvolutionEngine(self.config)
112 return evo._mutate_indicator(indicator)
114 def _generate_random_indicator(self) -> BaseIndicator:
115 """Generate a random indicator with random parameters."""
116 ind_type = self.rng.choice(self.config.indicator_types)
117 indicator: BaseIndicator
119 if ind_type == "sma":
120 indicator = SMAIndicator()
121 elif ind_type == "rsi":
122 indicator = RSIIndicator()
123 elif ind_type == "macd":
124 indicator = MACDIndicator()
125 elif ind_type == "dynamic":
126 indicator = DynamicIndicator()
127 code_gen = CodeGenerator(self.rng)
128 code = code_gen.generate_code()
129 indicator.set_parameter("source_code", code)
130 return indicator
131 else:
132 indicator = SMAIndicator()
134 # Set random parameters
135 param_space = PARAM_SPACES.get(ind_type, {})
136 for param_name, (min_val, max_val) in param_space.items():
137 value = int(self.rng.integers(min_val, max_val + 1))
138 indicator.set_parameter(param_name, value)
140 return indicator
142 def _meets_basic_targets(self, metrics: dict) -> bool:
143 """Check if metrics meet basic targets."""
144 for name, target in self.config.target_metrics.items():
145 if name not in metrics:
146 continue
147 actual = metrics[name]
148 if name in ["max_drawdown", "consecutive_losses"]:
149 if actual > target:
150 return False
151 else:
152 if actual < target:
153 return False
154 return True
156 def _run_evolution(
157 self,
158 data: pd.DataFrame,
159 initial_population: list[BaseIndicator] | None = None
160 ) -> BaseIndicator | None:
161 """Run evolutionary optimization on candidates."""
162 population = initial_population if initial_population is not None else [c[0] for c in self._candidates]
163 evolution = EvolutionEngine(
164 self.config,
165 metrics_calc=self.metrics_calc,
166 progress_callback=self._progress_callback
167 )
168 return evolution.run(data, population, executor=self.executor)
170 def estimate_time(self, data: pd.DataFrame) -> float:
171 """Estimate generation time in minutes."""
172 return estimate_time(self, data)