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

1"""Custom indicator testing workflow.""" 

2 

3from __future__ import annotations 

4 

5import importlib.util 

6import inspect 

7import sys 

8from pathlib import Path 

9from typing import TYPE_CHECKING 

10 

11import questionary 

12from rich.console import Console 

13 

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 

20 

21if TYPE_CHECKING: 

22 from monte_neo.cli.menu.main import InteractiveMenu 

23 

24logger = get_logger(__name__) 

25console = Console() 

26 

27 

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") 

31 

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 

37 

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 

42 

43 symbol, timeframe = selected_data.split("_") 

44 data = menu.storage.load(symbol, timeframe) 

45 menu._last_data = data 

46 

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) 

52 

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 

57 

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() 

64 

65 if not selected_file: 

66 return 

67 

68 file_path = file_map[selected_file] 

69 

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 

75 

76 console.print(f"[green]✓ Loaded indicator: {indicator_class.__name__}[/]") 

77 

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 

88 

89 if not questionary.confirm("Start comprehensive validation?", style=CUSTOM_STYLE).ask(): 

90 return 

91 

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 ) 

103 

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 ) 

117 

118 engine = MonteCarloEngine(mc_config) 

119 metrics_calc = MetricsCalculator() 

120 

121 # Instantiate Indicator 

122 indicator = indicator_class() 

123 

124 console.print(f"\n[bold]🚀 Running Validation for {indicator.name}...[/]") 

125 

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) 

129 

130 # Display Final Certificate if passed 

131 

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. 

135 

136 from monte_neo.cli.menu.results import _print_trust_certificate 

137 

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() 

155 

156 mock_res = MockResult(indicator, mc_result) 

157 

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.[/]") 

162 

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}") 

169 

170 except Exception as e: 

171 console.print(f"[red]Error during validation: {e}[/]") 

172 logger.exception("Validation error") 

173 

174 

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 

180 

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 

188 

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 

196 

197 return None