Coverage for src / monte_neo / monte_carlo / workers.py: 100%
97 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"""Monte Carlo workers.
3Independent worker functions for parallel execution.
4"""
6from __future__ import annotations
8from typing import TYPE_CHECKING, Any
10import numpy as np
11import pandas as pd
13if TYPE_CHECKING:
14 from monte_neo.indicators.base import BaseIndicator
15 from monte_neo.metrics.calculator import MetricsCalculator
17# Global variable for shared memory in workers
18SHARED_SCENARIOS: list[pd.DataFrame] | None = None
19SHARED_DATA: pd.DataFrame | None = None
22def _generate_signals_wrapper(args):
23 """Helper for parallel signal generation."""
24 indicator, df = args
25 # Use shared data if available and df is None
26 if df is None:
27 if SHARED_DATA is None:
28 # Fallback or error? For safety, return zeros or raise
29 # But in parallel execution exceptions might be swallowed or messy.
30 # We assume SHARED_DATA is set if df is None.
31 return np.zeros(0, dtype=np.float32)
32 df = SHARED_DATA
34 sigs = indicator.generate_signals_fast(df)
35 # Return numpy array to reduce IPC
36 return np.asarray(sigs, dtype=np.float32)
39def run_indicator_batch(args: tuple[list[BaseIndicator], pd.DataFrame | None]) -> list[np.ndarray]:
40 """Run a batch of indicators on shared data."""
41 indicators, df = args
42 if df is None:
43 if SHARED_DATA is None:
44 return []
45 df = SHARED_DATA
47 results = []
48 for ind in indicators:
49 try:
50 sigs = ind.generate_signals_fast(df)
51 results.append(np.asarray(sigs, dtype=np.float32))
52 except Exception:
53 # Return zeros on failure to keep alignment
54 results.append(np.zeros(len(df), dtype=np.float32))
55 return results
58def _generate_lazy_scenario_wrapper(args):
59 """Helper for lazy scenario generation."""
60 return run_block_bootstrap_scenario(*args)
63def init_worker(scenarios: list[pd.DataFrame]) -> None:
64 """Initialize worker with shared scenarios."""
65 global SHARED_SCENARIOS
66 SHARED_SCENARIOS = scenarios
69def init_worker_data(data: pd.DataFrame) -> None:
70 """Initialize worker with shared data."""
71 global SHARED_DATA
72 SHARED_DATA = data
75def run_scenario_batch(
76 scenarios: list[pd.DataFrame] | None,
77 indicator: BaseIndicator,
78 metrics_calc: MetricsCalculator,
79 target_metrics: dict[str, float],
80 indices: list[int] | None = None,
81) -> list[tuple[bool, dict[str, float]]]:
82 """Run a batch of scenarios in a single worker task.
84 This reduces IPC overhead and allows reusing compiled indicator code.
85 Can use explicitly passed scenarios OR shared scenarios via indices.
86 """
87 results = []
88 required_metrics = list(target_metrics.keys())
90 # Determine data source
91 batch_data: list[pd.DataFrame]
92 if scenarios is not None:
93 batch_data = scenarios
94 elif SHARED_SCENARIOS is not None and indices is not None:
95 batch_data = [SHARED_SCENARIOS[i] for i in indices]
96 else:
97 # Fallback or error
98 return []
100 # Compile once per batch if needed
101 if hasattr(indicator, "_compile_if_needed"):
102 try:
103 indicator._compile_if_needed()
104 except Exception:
105 pass
107 for data in batch_data:
108 try:
109 signals = indicator.generate_signals_fast(data)
110 metrics = metrics_calc.calculate_all(data, signals, required_metrics=required_metrics)
112 # Inline check
113 passed = True
114 for metric_name, target_value in target_metrics.items():
115 if metric_name not in metrics:
116 continue
117 actual = metrics[metric_name]
118 if metric_name in ["max_drawdown", "consecutive_losses"]:
119 if actual > target_value:
120 passed = False
121 break
122 else:
123 if actual < target_value:
124 passed = False
125 break
126 results.append((passed, metrics))
127 except Exception:
128 results.append((False, {}))
130 return results
133def run_single_scenario(
134 scenario_data: pd.DataFrame,
135 indicator: BaseIndicator,
136 metrics_calc: MetricsCalculator,
137 target_metrics: dict[str, float],
138) -> tuple[bool, dict[str, float]]:
139 """Helper for parallel execution (legacy/single mode)."""
140 signals = indicator.generate_signals_fast(scenario_data)
141 metrics = metrics_calc.calculate_all(
142 scenario_data, signals, required_metrics=list(target_metrics.keys())
143 )
145 # Inline check_targets to avoid dependency on self
146 passed = True
147 for metric_name, target_value in target_metrics.items():
148 if metric_name not in metrics:
149 continue
150 actual = metrics[metric_name]
151 if metric_name in ["max_drawdown", "consecutive_losses"]:
152 if actual > target_value:
153 passed = False
154 break
155 else:
156 if actual < target_value:
157 passed = False
158 break
159 return passed, metrics
162def run_block_bootstrap_scenario(
163 indicator: BaseIndicator,
164 seed: int,
165 block_size: int | None = None
166) -> Any | None:
167 """Generate block bootstrap scenario on fly and run signal generation."""
168 if SHARED_DATA is None:
169 return None
171 # Replicate block bootstrap logic for SINGLE scenario
172 rng = np.random.default_rng(seed)
173 n = len(SHARED_DATA)
174 if block_size is None:
175 block_size = max(1, int(np.sqrt(n)))
177 n_blocks = n // block_size
178 indices_range = np.arange(block_size)
180 block_starts = rng.choice(n - block_size + 1, size=n_blocks, replace=True)
181 full_indices = (block_starts[:, None] + indices_range).ravel()
183 # Use iloc for speed (returns copy by default for fancy indexing)
184 scenario_data = SHARED_DATA.iloc[full_indices]
186 # Run indicator
187 try:
188 if hasattr(indicator, "_compile_if_needed"):
189 indicator._compile_if_needed()
191 signals = indicator.generate_signals_fast(scenario_data)
193 # Extract signal array to reduce IPC
194 signal_arr = np.asarray(signals, dtype=np.float32)
196 # Calculate returns for GPU engine
197 close_prices = scenario_data["close"].values
198 returns = (close_prices[1:] / close_prices[:-1]) - 1
200 # Extract OHLC for SL/TP calculations
201 ohlc = scenario_data[["open", "high", "low", "close"]].values.astype(np.float32)
203 return signal_arr, returns.astype(np.float32), ohlc
204 except Exception:
205 return None