Coverage for src / monte_neo / cli / menu / custom_test.py: 0%
92 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"""Custom indicator testing workflow."""
3from __future__ import annotations
5import importlib.util
6import inspect
7import sys
8from pathlib import Path
9from typing import TYPE_CHECKING
11import questionary
12from rich.console import Console
14from monte_neo.cli.styles import CUSTOM_STYLE
15from monte_neo.core.generator import GeneratorConfig
16from monte_neo.indicators.base import BaseIndicator
17from monte_neo.metrics.calculator import MetricsCalculator
18from monte_neo.monte_carlo.engine import MonteCarloEngine
19from monte_neo.utils.logger import get_logger
21if TYPE_CHECKING:
22 from monte_neo.cli.menu.main import InteractiveMenu
24logger = get_logger(__name__)
25console = Console()
28def test_custom_indicator_workflow(menu: InteractiveMenu) -> None:
29 """Workflow for testing custom user indicators."""
30 console.print("\n[bold cyan]🧪 Test Custom Formula[/]\n")
32 # 1. Select Data
33 files = menu.storage.list_files()
34 if not files:
35 console.print("[yellow]⚠ No data available. Download data first.[/]\n")
36 return
38 file_choices = [f"{f['symbol']}_{f['timeframe']}" for f in files]
39 selected_data = questionary.select("Select data for testing:", choices=file_choices, style=CUSTOM_STYLE).ask()
40 if not selected_data:
41 return
43 symbol, timeframe = selected_data.split("_")
44 data = menu.storage.load(symbol, timeframe)
45 menu._last_data = data
47 # 2. Select Indicator File
48 indicators_dir = Path("user_indicators")
49 if not indicators_dir.exists():
50 indicators_dir.mkdir()
51 # Create template if not exists (should be done elsewhere, but safety check)
53 py_files = list(indicators_dir.glob("*.py"))
54 if not py_files:
55 console.print("[yellow]⚠ No python files found in user_indicators/ directory.[/]\n")
56 return
58 file_map = {f.name: f for f in py_files}
59 selected_file = questionary.select(
60 "Select indicator file:",
61 choices=list(file_map.keys()),
62 style=CUSTOM_STYLE
63 ).ask()
65 if not selected_file:
66 return
68 file_path = file_map[selected_file]
70 # 3. Load Indicator Class
71 indicator_class = _load_indicator_class(file_path)
72 if not indicator_class:
73 console.print("[red]❌ No valid BaseIndicator subclass found in the selected file.[/]")
74 return
76 console.print(f"[green]✓ Loaded indicator: {indicator_class.__name__}[/]")
78 # 4. Configure & Run
79 iterations = questionary.select(
80 "Number of MC iterations:",
81 choices=[
82 {"name": "1,000 (fast)", "value": 1000},
83 {"name": "10,000 (standard)", "value": 10000},
84 {"name": "100,000 (thorough)", "value": 100000},
85 ],
86 style=CUSTOM_STYLE
87 ).ask() or 1000
89 if not questionary.confirm("Start comprehensive validation?", style=CUSTOM_STYLE).ask():
90 return
92 # Setup Engine
93 config = GeneratorConfig(
94 max_iterations=iterations,
95 target_metrics=menu._target_metrics,
96 use_mc_shuffling=True,
97 use_mc_noise=True,
98 use_mc_sensitivity=True,
99 use_mc_walk_forward=True,
100 use_mc_block_bootstrap=True,
101 use_sequential_mc=True,
102 )
104 # Manually configure MonteCarloEngine
105 from monte_neo.monte_carlo.types import MCConfig
106 mc_config = MCConfig(
107 iterations=iterations,
108 n_workers=4, # Auto-detect in real app
109 use_shuffling=True,
110 use_noise=True,
111 use_sensitivity=True,
112 use_walk_forward=True,
113 use_block_bootstrap=True,
114 use_sequential=True,
115 pass_threshold=0.80,
116 )
118 engine = MonteCarloEngine(mc_config)
119 metrics_calc = MetricsCalculator()
121 # Instantiate Indicator
122 indicator = indicator_class()
124 console.print(f"\n[bold]🚀 Running Validation for {indicator.name}...[/]")
126 # Create a result object structure similar to generation result
127 try:
128 mc_result = engine.run(data, indicator, metrics_calc, menu._target_metrics, interactive=True)
130 # Display Final Certificate if passed
132 # We need to construct a "GenerationResult" like object or just reuse the display logic
133 # For simplicity, let's create a simple object to pass to show_generation_result logic
134 # OR just reuse _print_trust_certificate directly if we import it.
136 from monte_neo.cli.menu.results import _print_trust_certificate
138 class MockResult:
139 def __init__(self, ind, mc_res):
140 self.indicator = ind
141 self.success = mc_res.passed
142 self.mc_pass_rate = mc_res.pass_rate
143 self.mc_details = {"step_results": [
144 {
145 "method": s.method_name,
146 "passed": s.passed,
147 "rate": s.pass_rate,
148 "advice": s.advice
149 } for s in mc_res.step_results
150 ]}
151 self.elapsed_time = mc_res.elapsed_time
152 self.iterations_tried = iterations
153 self.final_metrics = {} # We could calculate baseline metrics here
154 self.parameters = ind.get_parameters()
156 mock_res = MockResult(indicator, mc_result)
158 if mock_res.success:
159 _print_trust_certificate(mock_res)
160 else:
161 console.print("\n[bold red]❌ Validation Failed. See advice above to improve your indicator.[/]")
163 # Chart
164 if questionary.confirm("Show chart?", style=CUSTOM_STYLE).ask():
165 from monte_neo.visualization.charts import ChartGenerator
166 chart_gen = ChartGenerator()
167 signals = indicator.generate_signals(data)
168 chart_gen.plot_with_signals(data, signals, title=f"Test: {indicator.name}")
170 except Exception as e:
171 console.print(f"[red]Error during validation: {e}[/]")
172 logger.exception("Validation error")
175def _load_indicator_class(file_path: Path) -> type[BaseIndicator] | None:
176 """Load the first BaseIndicator subclass found in the file."""
177 spec = importlib.util.spec_from_file_location("custom_indicator", file_path)
178 if not spec or not spec.loader:
179 return None
181 module = importlib.util.module_from_spec(spec)
182 sys.modules["custom_indicator"] = module
183 try:
184 spec.loader.exec_module(module)
185 except Exception as e:
186 console.print(f"[red]Error loading module: {e}[/]")
187 return None
189 for name, obj in inspect.getmembers(module):
190 if (
191 inspect.isclass(obj)
192 and issubclass(obj, BaseIndicator)
193 and obj is not BaseIndicator
194 ):
195 return obj
197 return None