1use std::collections::HashMap;
2
3#[cfg(feature = "serde")]
4use serde::{Deserialize, Serialize};
5
6use super::adx::Adx;
7use super::alligator::AlligatorEngine;
8use super::anchored_vwap::{AnchoredVwapEngine, VwapAnchorKind, ZeroVolumePolicy};
9use super::atr::Atr;
10use super::bollinger::BollingerBands;
11use super::bop::BalanceOfPowerEngine;
12use super::bos_choch::BosChochEngine;
13use super::buy_sell_pressure::BuySellPressureEstimator;
14use super::candle_story::CandleStoryEngine;
15use super::cci::Cci;
16use super::chaikin_osc::ChaikinOscillatorEngine;
17use super::chandelier_exit::ChandelierExitEngine;
18use super::choppiness::ChoppinessIndexEngine;
19use super::connors_rsi::ConnorsRsiEngine;
20use super::coppock::CoppockCurveEngine;
21use super::dpo::DpoEngine;
22use super::efficiency::LegEfficiencyEngine;
23use super::envelope::EnvelopeEngine;
24use super::eom::EomEngine;
25use super::fisher_transform::FisherTransform;
26use super::kst::KstEngine;
27use super::liquidity_fvg::LiquidityFvgEngine;
28use super::liquidity_sweeps::LiquiditySweepEngine;
29use super::lsma::LsmaEngine;
30use super::macd::Macd;
31use super::market_structure_breaks::MarketStructureBreaksEngine;
32use super::mass_index::MassIndexEngine;
33use super::mcginley::McGinleyDynamicEngine;
34use super::mfi::Mfi;
35use super::midas::{MidasCurveEngine, MidasMode};
36use super::momentum_indicators::{
37 AwesomeOscillatorEngine, CmoEngine, ElderRayEngine, PpoEngine, RocEngine, StochasticEngine,
38 UltimateOscillatorEngine,
39};
40use super::moving_averages::{
41 DemaEngine, EmaEngine, HmaEngine, KamaEngine, SmaEngine, VwmaEngine, WmaEngine,
42};
43use super::multi_factor::MultiFactorMarketScore;
44use super::nvi_pvi::{NviEngine, PviEngine};
45use super::order_block::OrderBlockEngine;
46pub use super::params::{ParamValue, TypedParams};
47use super::pivot_sets::{PivotSetType, PivotSetsEngine};
48use super::pivots_structure::PivotStructureEngine;
49use super::rsi::Rsi;
50use super::rvi::RviEngine;
51use super::stoch_rsi::StochRsi;
52use super::tema::TemaEngine;
53use super::trend_quality::TrendQualityScoreEngine;
54use super::trend_structural::{
55 AroonEngine, DmiEngine, IchimokuEngine, ParabolicSarEngine, SupertrendEngine,
56};
57use super::tsi::Tsi;
58use super::vix_fix::WilliamsVixFix;
59use super::volatility_indicators::{
60 DonchianChannelEngine, GarmanKlassVolatilityEngine, HistoricalVolatilityEngine,
61 KeltnerChannelEngine, TrueRangeEngine,
62};
63use super::volatility_regime::VolatilityRegimeDetector;
64use super::volume_flow::{CvdEngine, KlingerVolumeForceEngine};
65use super::volume_flow_hires::HiResVolumeFlowEngine;
66use super::volume_indicators::{AccDistEngine, CmfEngine, ObvEngine, RvolEngine, VolumeEngine};
67use super::volume_profile::VolumeProfileEngine;
68use super::volume_profile_extended::ExtendedVolumeProfileEngine;
69use super::volume_profile_persistent::PersistentVolumeProfileEngine;
70use super::vortex::VortexEngine;
71use super::vwap::Vwap;
72use super::wavetrend::WaveTrendEngine;
73use super::williams_r::WilliamsR;
74use super::zigzag::ZigZagEngine;
75use super::zigzag_advanced::{AdvancedZigZagEngine, ZigZagDeviationMode};
76use super::zscore::ZScoreEngine;
77use super::Indicator;
78
79#[derive(Debug, Clone, PartialEq)]
80#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
81pub struct IndicatorCatalogEntry {
82 pub name: &'static str,
83 pub description: &'static str,
84 pub default_params: HashMap<String, f64>,
85}
86
87pub fn catalog() -> Vec<IndicatorCatalogEntry> {
88 vec![
89 IndicatorCatalogEntry {
90 name: "rsi",
91 description: "Relative Strength Index",
92 default_params: [
93 ("rsi_len".to_string(), 14.0),
94 ("avg_len".to_string(), 3.0),
95 ("sig_len".to_string(), 3.0),
96 ("overbought".to_string(), 70.0),
97 ("oversold".to_string(), 30.0),
98 ]
99 .into(),
100 },
101 IndicatorCatalogEntry {
102 name: "macd",
103 description: "Moving Average Convergence Divergence",
104 default_params: [
105 ("fast_len".to_string(), 12.0),
106 ("slow_len".to_string(), 26.0),
107 ("signal_len".to_string(), 9.0),
108 ]
109 .into(),
110 },
111 IndicatorCatalogEntry {
112 name: "bollinger",
113 description: "Bollinger Bands",
114 default_params: [("len".to_string(), 20.0), ("mult".to_string(), 2.0)].into(),
115 },
116 IndicatorCatalogEntry {
117 name: "adx",
118 description: "Average Directional Index",
119 default_params: [
120 ("di_len".to_string(), 14.0),
121 ("adx_smooth".to_string(), 14.0),
122 ("level_weak".to_string(), 20.0),
123 ]
124 .into(),
125 },
126 IndicatorCatalogEntry {
127 name: "stoch_rsi",
128 description: "Stochastic RSI",
129 default_params: [
130 ("rsi_len".to_string(), 14.0),
131 ("stoch_len".to_string(), 14.0),
132 ("k_len".to_string(), 3.0),
133 ("d_len".to_string(), 3.0),
134 ("overbought".to_string(), 80.0),
135 ("oversold".to_string(), 20.0),
136 ]
137 .into(),
138 },
139 IndicatorCatalogEntry {
140 name: "cci",
141 description: "Commodity Channel Index",
142 default_params: [
143 ("cci_len".to_string(), 20.0),
144 ("overbought".to_string(), 100.0),
145 ("oversold".to_string(), -100.0),
146 ]
147 .into(),
148 },
149 IndicatorCatalogEntry {
150 name: "mfi",
151 description: "Money Flow Index",
152 default_params: [
153 ("mfi_len".to_string(), 14.0),
154 ("overbought".to_string(), 80.0),
155 ("oversold".to_string(), 20.0),
156 ]
157 .into(),
158 },
159 IndicatorCatalogEntry {
160 name: "atr",
161 description: "Average True Range",
162 default_params: [("atr_len".to_string(), 14.0), ("sig_len".to_string(), 20.0)].into(),
163 },
164 IndicatorCatalogEntry {
165 name: "chandelier_exit",
166 description: "Chandelier Exit (ATR trailing stop with direction flip)",
167 default_params: [("length".to_string(), 22.0), ("atr_mult".to_string(), 3.0)].into(),
168 },
169 IndicatorCatalogEntry {
170 name: "midas",
171 description: "MIDAS launch-anchored curve with Topfinder/Bottomfinder projection (build_typed with mode=topfinder|bottomfinder)",
172 default_params: [("maturity_bars".to_string(), 20.0)].into(),
173 },
174 IndicatorCatalogEntry {
175 name: "hires_volume_flow",
176 description: "High-resolution volume flow with absorption detection (OHLC-estimated via this registry entry point; use HiResVolumeFlowEngine::on_bar_with_aggressor/on_intrabar_group directly for direct aggressor/intrabar-delta resolution)",
177 default_params: [("window_len".to_string(), 20.0)].into(),
178 },
179 IndicatorCatalogEntry {
180 name: "extended_volume_profile",
181 description: "Full-bin price/volume profile with HVN/LVN/AVN classification, a delta profile, and zone formation (use ExtendedVolumeProfileEngine::on_intrabar_group directly for intrabar-resolution distribution)",
182 default_params: [("lookback".to_string(), 70.0), ("num_bins".to_string(), 30.0)].into(),
183 },
184 IndicatorCatalogEntry {
185 name: "persistent_volume_profile",
186 description: "Fixed-price-grid volume profile with real bin lifecycle (birth/growth/expiry across updates) and a per-bin absorption profile",
187 default_params: [("lookback".to_string(), 70.0), ("bin_width".to_string(), 1.0)].into(),
188 },
189 IndicatorCatalogEntry {
190 name: "trend_relationship",
191 description: "Adaptive trend relationship between two configurable smoothers (build via build_typed with fast_kind/slow_kind params: ema|sma|rma|alma|jma)",
192 default_params: [("fast_len".to_string(), 9.0), ("slow_len".to_string(), 21.0)].into(),
193 },
194 IndicatorCatalogEntry {
195 name: "williams_r",
196 description: "Williams %R",
197 default_params: [
198 ("wpr_len".to_string(), 14.0),
199 ("overbought".to_string(), 80.0),
200 ("oversold".to_string(), 20.0),
201 ]
202 .into(),
203 },
204 IndicatorCatalogEntry {
205 name: "tsi",
206 description: "True Strength Index",
207 default_params: [
208 ("long_len".to_string(), 25.0),
209 ("short_len".to_string(), 13.0),
210 ("sig_len".to_string(), 7.0),
211 ]
212 .into(),
213 },
214 IndicatorCatalogEntry {
215 name: "fisher_transform",
216 description: "Fisher Transform",
217 default_params: [
218 ("fish_len".to_string(), 10.0),
219 ("overbought".to_string(), 1.5),
220 ("oversold".to_string(), -1.5),
221 ]
222 .into(),
223 },
224 IndicatorCatalogEntry {
225 name: "vwap",
226 description: "Rolling Volume Weighted Average Price with sigma bands and slope",
227 default_params: [
228 ("window".to_string(), 390.0),
229 ("slope_lookback".to_string(), 20.0),
230 ]
231 .into(),
232 },
233 IndicatorCatalogEntry {
234 name: "volume",
235 description: "Volume and Average Volume",
236 default_params: [("ma_period".to_string(), 20.0)].into(),
237 },
238 IndicatorCatalogEntry {
239 name: "rvol",
240 description: "Relative Volume vs Moving Average",
241 default_params: [("period".to_string(), 20.0)].into(),
242 },
243 IndicatorCatalogEntry {
244 name: "obv",
245 description: "On-Balance Volume",
246 default_params: HashMap::new(),
247 },
248 IndicatorCatalogEntry {
249 name: "cmf",
250 description: "Chaikin Money Flow",
251 default_params: [("period".to_string(), 20.0)].into(),
252 },
253 IndicatorCatalogEntry {
254 name: "acc_dist",
255 description: "Accumulation / Distribution Line",
256 default_params: HashMap::new(),
257 },
258 IndicatorCatalogEntry {
259 name: "true_range",
260 description: "True Range in price units",
261 default_params: HashMap::new(),
262 },
263 IndicatorCatalogEntry {
264 name: "keltner",
265 description: "Keltner Channels",
266 default_params: [
267 ("ema_period".to_string(), 20.0),
268 ("atr_period".to_string(), 10.0),
269 ("multiplier".to_string(), 2.0),
270 ]
271 .into(),
272 },
273 IndicatorCatalogEntry {
274 name: "donchian",
275 description: "Donchian Channels",
276 default_params: [("period".to_string(), 20.0)].into(),
277 },
278 IndicatorCatalogEntry {
279 name: "historical_volatility",
280 description: "Annualized Historical Volatility",
281 default_params: [("period".to_string(), 20.0)].into(),
282 },
283 IndicatorCatalogEntry {
284 name: "garman_klass",
285 description: "Garman-Klass Volatility Estimator",
286 default_params: [("period".to_string(), 20.0)].into(),
287 },
288 IndicatorCatalogEntry {
289 name: "sma",
290 description: "Simple Moving Average",
291 default_params: [("period".to_string(), 20.0)].into(),
292 },
293 IndicatorCatalogEntry {
294 name: "ema",
295 description: "Exponential Moving Average",
296 default_params: [("period".to_string(), 20.0)].into(),
297 },
298 IndicatorCatalogEntry {
299 name: "wma",
300 description: "Weighted Moving Average",
301 default_params: [("period".to_string(), 20.0)].into(),
302 },
303 IndicatorCatalogEntry {
304 name: "vwma",
305 description: "Volume-Weighted Moving Average",
306 default_params: [("period".to_string(), 20.0)].into(),
307 },
308 IndicatorCatalogEntry {
309 name: "hma",
310 description: "Hull Moving Average",
311 default_params: [("period".to_string(), 20.0)].into(),
312 },
313 IndicatorCatalogEntry {
314 name: "dema",
315 description: "Double Exponential Moving Average",
316 default_params: [("period".to_string(), 20.0)].into(),
317 },
318 IndicatorCatalogEntry {
319 name: "kama",
320 description: "Kaufman's Adaptive Moving Average",
321 default_params: [
322 ("period".to_string(), 10.0),
323 ("fast_period".to_string(), 2.0),
324 ("slow_period".to_string(), 30.0),
325 ]
326 .into(),
327 },
328 IndicatorCatalogEntry {
329 name: "dmi",
330 description: "Directional Movement Index (+DI / -DI)",
331 default_params: [("period".to_string(), 14.0)].into(),
332 },
333 IndicatorCatalogEntry {
334 name: "aroon",
335 description: "Aroon Indicator (Up, Down, Oscillator)",
336 default_params: [("period".to_string(), 14.0)].into(),
337 },
338 IndicatorCatalogEntry {
339 name: "parabolic_sar",
340 description: "Parabolic SAR",
341 default_params: [("step".to_string(), 0.02), ("max_step".to_string(), 0.20)].into(),
342 },
343 IndicatorCatalogEntry {
344 name: "supertrend",
345 description: "Supertrend ATR Trailing Stop",
346 default_params: [
347 ("period".to_string(), 10.0),
348 ("multiplier".to_string(), 3.0),
349 ]
350 .into(),
351 },
352 IndicatorCatalogEntry {
353 name: "ichimoku",
354 description: "Ichimoku Kinko Hyo Cloud",
355 default_params: [
356 ("tenkan_p".to_string(), 9.0),
357 ("kijun_p".to_string(), 26.0),
358 ("senkou_b_p".to_string(), 52.0),
359 ]
360 .into(),
361 },
362 IndicatorCatalogEntry {
363 name: "stochastic",
364 description: "Classic Stochastic Oscillator",
365 default_params: [
366 ("k_period".to_string(), 14.0),
367 ("d_period".to_string(), 3.0),
368 ]
369 .into(),
370 },
371 IndicatorCatalogEntry {
372 name: "roc",
373 description: "Rate of Change / Momentum",
374 default_params: [("period".to_string(), 12.0)].into(),
375 },
376 IndicatorCatalogEntry {
377 name: "ultimate_oscillator",
378 description: "Ultimate Oscillator",
379 default_params: [
380 ("period1".to_string(), 7.0),
381 ("period2".to_string(), 14.0),
382 ("period3".to_string(), 28.0),
383 ]
384 .into(),
385 },
386 IndicatorCatalogEntry {
387 name: "awesome_oscillator",
388 description: "Awesome Oscillator",
389 default_params: [
390 ("fast_period".to_string(), 5.0),
391 ("slow_period".to_string(), 34.0),
392 ]
393 .into(),
394 },
395 IndicatorCatalogEntry {
396 name: "ppo",
397 description: "Percentage Price Oscillator",
398 default_params: [
399 ("fast_period".to_string(), 12.0),
400 ("slow_period".to_string(), 26.0),
401 ("signal_period".to_string(), 9.0),
402 ]
403 .into(),
404 },
405 IndicatorCatalogEntry {
406 name: "wavetrend",
407 description: "WaveTrend Oscillator (wt1, wt2)",
408 default_params: [
409 ("n1".to_string(), 10.0),
410 ("n2".to_string(), 21.0),
411 ("ob_level".to_string(), 60.0),
412 ("os_level".to_string(), -60.0),
413 ]
414 .into(),
415 },
416 IndicatorCatalogEntry {
417 name: "cmo",
418 description: "Chande Momentum Oscillator",
419 default_params: [("period".to_string(), 14.0)].into(),
420 },
421 IndicatorCatalogEntry {
422 name: "elder_ray",
423 description: "Elder Ray Index (Bull/Bear Power)",
424 default_params: [("period".to_string(), 13.0)].into(),
425 },
426 IndicatorCatalogEntry {
427 name: "anchored_vwap",
428 description: "Anchored VWAP Engine",
429 default_params: [("mult1".to_string(), 1.0), ("mult2".to_string(), 2.0)].into(),
430 },
431 IndicatorCatalogEntry {
432 name: "cvd",
433 description: "Cumulative Volume Delta",
434 default_params: [].into(),
435 },
436 IndicatorCatalogEntry {
437 name: "klinger",
438 description: "Klinger Volume Force Oscillator",
439 default_params: [
440 ("fast_len".to_string(), 34.0),
441 ("slow_len".to_string(), 55.0),
442 ("signal_len".to_string(), 13.0),
443 ]
444 .into(),
445 },
446 IndicatorCatalogEntry {
447 name: "zigzag",
448 description: "ZigZag Swing Leg Engine",
449 default_params: [
450 ("depth".to_string(), 12.0),
451 ("deviation_pct".to_string(), 5.0),
452 ]
453 .into(),
454 },
455 IndicatorCatalogEntry {
456 name: "zigzag_advanced",
457 description: "ZigZag with backstep, running-leg/confirmation status, and ATR-mode deviation (build_typed with deviation_mode=percent|atr_multiple); AdvancedZigZagEngine::reduce/project_to_timeframe for recursive levels and HTF projection",
458 default_params: [
459 ("depth".to_string(), 3.0),
460 ("backstep".to_string(), 2.0),
461 ("deviation_pct".to_string(), 1.0),
462 ("atr_len".to_string(), 14.0),
463 ]
464 .into(),
465 },
466 IndicatorCatalogEntry {
467 name: "pivot_sets",
468 description: "Multi-Pivot Set Engine",
469 default_params: [].into(),
470 },
471 IndicatorCatalogEntry {
472 name: "tema",
473 description: "Triple Exponential Moving Average",
474 default_params: [("period".to_string(), 14.0)].into(),
475 },
476 IndicatorCatalogEntry {
477 name: "lsma",
478 description: "Least Squares Moving Average / Linear Regression",
479 default_params: [("period".to_string(), 25.0)].into(),
480 },
481 IndicatorCatalogEntry {
482 name: "mcginley",
483 description: "McGinley Dynamic Moving Average",
484 default_params: [("period".to_string(), 14.0)].into(),
485 },
486 IndicatorCatalogEntry {
487 name: "envelope",
488 description: "Moving Average Envelopes",
489 default_params: [("period".to_string(), 20.0), ("percent".to_string(), 2.5)].into(),
490 },
491 IndicatorCatalogEntry {
492 name: "choppiness",
493 description: "Choppiness Index",
494 default_params: [("period".to_string(), 14.0)].into(),
495 },
496 IndicatorCatalogEntry {
497 name: "vortex",
498 description: "Vortex Indicator (+VI, -VI)",
499 default_params: [("period".to_string(), 14.0)].into(),
500 },
501 IndicatorCatalogEntry {
502 name: "alligator",
503 description: "Williams Alligator (Jaw, Teeth, Lips)",
504 default_params: [].into(),
505 },
506 IndicatorCatalogEntry {
507 name: "connors_rsi",
508 description: "Connors RSI",
509 default_params: [
510 ("rsi_len".to_string(), 3.0),
511 ("streak_len".to_string(), 2.0),
512 ("rank_len".to_string(), 100.0),
513 ]
514 .into(),
515 },
516 IndicatorCatalogEntry {
517 name: "coppock",
518 description: "Coppock Curve",
519 default_params: [].into(),
520 },
521 IndicatorCatalogEntry {
522 name: "dpo",
523 description: "Detrended Price Oscillator",
524 default_params: [("period".to_string(), 21.0)].into(),
525 },
526 IndicatorCatalogEntry {
527 name: "kst",
528 description: "Know Sure Thing Oscillator",
529 default_params: [].into(),
530 },
531 IndicatorCatalogEntry {
532 name: "mass_index",
533 description: "Mass Index Reversal Detector",
534 default_params: [("period".to_string(), 25.0)].into(),
535 },
536 IndicatorCatalogEntry {
537 name: "rvi",
538 description: "Relative Vigor Index",
539 default_params: [("period".to_string(), 10.0)].into(),
540 },
541 IndicatorCatalogEntry {
542 name: "bop",
543 description: "Balance of Power",
544 default_params: [("period".to_string(), 14.0)].into(),
545 },
546 IndicatorCatalogEntry {
547 name: "eom",
548 description: "Ease of Movement",
549 default_params: [
550 ("period".to_string(), 14.0),
551 ("volume_divisor".to_string(), 10000.0),
552 ]
553 .into(),
554 },
555 IndicatorCatalogEntry {
556 name: "nvi",
557 description: "Negative Volume Index",
558 default_params: [].into(),
559 },
560 IndicatorCatalogEntry {
561 name: "pvi",
562 description: "Positive Volume Index",
563 default_params: [].into(),
564 },
565 IndicatorCatalogEntry {
566 name: "chaikin_oscillator",
567 description: "Chaikin Oscillator",
568 default_params: [
569 ("fast_len".to_string(), 3.0),
570 ("slow_len".to_string(), 10.0),
571 ]
572 .into(),
573 },
574 IndicatorCatalogEntry {
575 name: "bos_choch",
576 description: "BOS and CHoCH Market Structure Engine",
577 default_params: [("pivot_len".to_string(), 5.0)].into(),
578 },
579 IndicatorCatalogEntry {
580 name: "liquidity_sweeps",
581 description: "Liquidity Sweeps and EQH/EQL Detector",
582 default_params: [
583 ("pivot_len".to_string(), 5.0),
584 ("tolerance_pct".to_string(), 0.2),
585 ]
586 .into(),
587 },
588 IndicatorCatalogEntry {
589 name: "liquidity_pools",
590 description: "BSL/SSL liquidity pools with explicit stop-hunt/breakout/reclaim classification (see also FvgZoneTracker and SmartMoneyStructureLinker for FVG-fill tracking and cross-detector confluence)",
591 default_params: [
592 ("pivot_len".to_string(), 5.0),
593 ("tolerance_pct".to_string(), 0.2),
594 ]
595 .into(),
596 },
597 IndicatorCatalogEntry {
598 name: "wyckoff",
599 description: "Wyckoff accumulation/distribution state machine: range-lock, Phases A-E, Spring/UTAD, SOS/SOW/LPS/LPSY, sequence validation and Cause/Quality scoring",
600 default_params: [
601 ("range_lookback".to_string(), 20.0),
602 ("range_atr_max".to_string(), 3.0),
603 ("min_range_bars".to_string(), 6.0),
604 ]
605 .into(),
606 },
607 IndicatorCatalogEntry {
608 name: "trend_quality",
609 description: "Trend Quality Score Engine",
610 default_params: [("period".to_string(), 14.0)].into(),
611 },
612 IndicatorCatalogEntry {
613 name: "buy_sell_pressure",
614 description: "Buy/Sell Pressure Estimator",
615 default_params: [("period".to_string(), 14.0)].into(),
616 },
617 IndicatorCatalogEntry {
618 name: "volatility_regime",
619 description: "Volatility Regime & Squeeze Detector",
620 default_params: [
621 ("period".to_string(), 20.0),
622 ("bb_mult".to_string(), 2.0),
623 ("kc_mult".to_string(), 1.5),
624 ]
625 .into(),
626 },
627 IndicatorCatalogEntry {
628 name: "zscore",
629 description: "Rolling Z-Score Engine",
630 default_params: [("period".to_string(), 20.0)].into(),
631 },
632 IndicatorCatalogEntry {
633 name: "multi_factor",
634 description: "Multi-Factor Composite Market Score",
635 default_params: [("period".to_string(), 14.0)].into(),
636 },
637 ]
638}
639
640#[derive(Debug, Clone, PartialEq)]
641#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
642pub enum RegistryError {
643 UnknownIndicator(String),
644 InvalidParameter {
645 parameter: String,
646 value: f64,
647 reason: String,
648 },
649 UnsupportedParameterType {
652 parameter: String,
653 type_name: String,
654 },
655 InvalidEnumValue {
657 parameter: String,
658 value: String,
659 reason: String,
660 },
661 IncompatibleParameter {
665 parameter: String,
666 indicator: String,
667 reason: String,
668 },
669}
670
671impl std::fmt::Display for RegistryError {
672 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
673 match self {
674 RegistryError::UnknownIndicator(name) => write!(f, "Unknown indicator: {}", name),
675 RegistryError::InvalidParameter {
676 parameter,
677 value,
678 reason,
679 } => write!(
680 f,
681 "Invalid parameter '{}' (value {}): {}",
682 parameter, value, reason
683 ),
684 RegistryError::UnsupportedParameterType {
685 parameter,
686 type_name,
687 } => write!(
688 f,
689 "Parameter '{}' has unsupported type '{}' for this indicator",
690 parameter, type_name
691 ),
692 RegistryError::InvalidEnumValue {
693 parameter,
694 value,
695 reason,
696 } => write!(
697 f,
698 "Invalid value '{}' for parameter '{}': {}",
699 value, parameter, reason
700 ),
701 RegistryError::IncompatibleParameter {
702 parameter,
703 indicator,
704 reason,
705 } => write!(
706 f,
707 "Parameter '{}' is not compatible with indicator '{}': {}",
708 parameter, indicator, reason
709 ),
710 }
711 }
712}
713
714impl std::error::Error for RegistryError {}
715
716fn get_usize_p(
717 params: &HashMap<String, f64>,
718 name: &str,
719 default: usize,
720 min: usize,
721 max: usize,
722) -> Result<usize, RegistryError> {
723 if let Some(&val) = params.get(name) {
724 if !val.is_finite() || val.fract() != 0.0 || val < (min as f64) || val > (max as f64) {
725 return Err(RegistryError::InvalidParameter {
726 parameter: name.to_string(),
727 value: val,
728 reason: format!(
729 "Value must be a whole, finite number between {} and {}",
730 min, max
731 ),
732 });
733 }
734 Ok(val as usize)
735 } else {
736 Ok(default)
737 }
738}
739
740fn get_f64_p(
741 params: &HashMap<String, f64>,
742 name: &str,
743 default: f64,
744 min: f64,
745 max: f64,
746) -> Result<f64, RegistryError> {
747 if let Some(&val) = params.get(name) {
748 if !val.is_finite() || val < min || val > max {
749 return Err(RegistryError::InvalidParameter {
750 parameter: name.to_string(),
751 value: val,
752 reason: format!("Value must be a finite number between {} and {}", min, max),
753 });
754 }
755 Ok(val)
756 } else {
757 Ok(default)
758 }
759}
760
761fn ensure_less(
762 parameter: &str,
763 value: f64,
764 upper_parameter: &str,
765 upper_value: f64,
766) -> Result<(), RegistryError> {
767 if value < upper_value {
768 return Ok(());
769 }
770
771 Err(RegistryError::InvalidParameter {
772 parameter: parameter.to_string(),
773 value,
774 reason: format!("{} must be smaller than {}", parameter, upper_parameter),
775 })
776}
777
778pub fn build_checked(
779 name: &str,
780 params: &HashMap<String, f64>,
781) -> Result<Box<dyn Indicator>, RegistryError> {
782 match name.to_lowercase().as_str() {
783 "rsi" => {
784 let rsi_len = get_usize_p(params, "rsi_len", 14, 1, 10000)?;
785 let avg_len = get_usize_p(params, "avg_len", 3, 1, 10000)?;
786 let sig_len = get_usize_p(params, "sig_len", 3, 1, 10000)?;
787 let overbought = get_f64_p(params, "overbought", 70.0, 0.0, 100.0)?;
788 let oversold = get_f64_p(params, "oversold", 30.0, 0.0, 100.0)?;
789 ensure_less("oversold", oversold, "overbought", overbought)?;
790 Ok(Box::new(Rsi::new(
791 rsi_len, avg_len, sig_len, 50.0, overbought, oversold, 5, true, 100, 4, 10.0,
792 )))
793 }
794 "macd" => {
795 let fast_len = get_usize_p(params, "fast_len", 12, 1, 10000)?;
796 let slow_len = get_usize_p(params, "slow_len", 26, 1, 10000)?;
797 let signal_len = get_usize_p(params, "signal_len", 9, 1, 10000)?;
798 ensure_less("fast_len", fast_len as f64, "slow_len", slow_len as f64)?;
799 Ok(Box::new(Macd::new(fast_len, slow_len, signal_len)))
800 }
801 "bollinger" | "bb" => {
802 let len = get_usize_p(params, "len", 20, 1, 10000)?;
803 let mult = get_f64_p(params, "mult", 2.0, 0.01, 100.0)?;
804 Ok(Box::new(BollingerBands::new(len, mult)))
805 }
806 "adx" => {
807 let di_len = get_usize_p(params, "di_len", 14, 1, 10000)?;
808 let adx_smooth = get_usize_p(params, "adx_smooth", 14, 1, 10000)?;
809 let level_weak = get_f64_p(params, "level_weak", 20.0, 0.0, 100.0)?;
810 Ok(Box::new(Adx::new(di_len, adx_smooth, 3, level_weak)))
811 }
812 "stoch_rsi" | "srsi" => {
813 let rsi_len = get_usize_p(params, "rsi_len", 14, 1, 10000)?;
814 let stoch_len = get_usize_p(params, "stoch_len", 14, 1, 10000)?;
815 let k_len = get_usize_p(params, "k_len", 3, 1, 10000)?;
816 let d_len = get_usize_p(params, "d_len", 3, 1, 10000)?;
817 let overbought = get_f64_p(params, "overbought", 80.0, 0.0, 100.0)?;
818 let oversold = get_f64_p(params, "oversold", 20.0, 0.0, 100.0)?;
819 ensure_less("oversold", oversold, "overbought", overbought)?;
820 Ok(Box::new(StochRsi::new(
821 rsi_len, stoch_len, k_len, d_len, 50.0, overbought, oversold, 5, true, 50, 50, 4,
822 10.0,
823 )))
824 }
825 "cci" => {
826 let cci_len = get_usize_p(params, "cci_len", 20, 1, 10000)?;
827 let overbought = get_f64_p(params, "overbought", 100.0, -1000.0, 1000.0)?;
828 let oversold = get_f64_p(params, "oversold", -100.0, -1000.0, 1000.0)?;
829 ensure_less("oversold", oversold, "overbought", overbought)?;
830 Ok(Box::new(Cci::new(
831 cci_len, 3, 3, 5, oversold, overbought, true, 100, 4, 25.0,
832 )))
833 }
834 "mfi" => {
835 let mfi_len = get_usize_p(params, "mfi_len", 14, 1, 10000)?;
836 let overbought = get_f64_p(params, "overbought", 80.0, 0.0, 100.0)?;
837 let oversold = get_f64_p(params, "oversold", 20.0, 0.0, 100.0)?;
838 ensure_less("oversold", oversold, "overbought", overbought)?;
839 Ok(Box::new(Mfi::new(
840 mfi_len, 3, 3, 50.0, overbought, oversold, 5, true,
841 )))
842 }
843 "atr" => {
844 let atr_len = get_usize_p(params, "atr_len", 14, 1, 10000)?;
845 let sig_len = get_usize_p(params, "sig_len", 20, 1, 10000)?;
846 Ok(Box::new(Atr::new(atr_len, sig_len)))
847 }
848 "chandelier_exit" | "ce" => {
849 let length = get_usize_p(params, "length", 22, 1, 10000)?;
850 let atr_mult = get_f64_p(params, "atr_mult", 3.0, 0.01, 100.0)?;
851 Ok(Box::new(ChandelierExitEngine::new(length, atr_mult)))
852 }
853 "midas" => {
854 let maturity_bars = get_usize_p(params, "maturity_bars", 20, 1, 10000)?;
857 Ok(Box::new(MidasCurveEngine::new(
858 MidasMode::Topfinder,
859 crate::model::Source::Hlc3,
860 maturity_bars as u32,
861 )))
862 }
863 "trend_relationship" => {
864 let fast_len = get_usize_p(params, "fast_len", 9, 1, 10000)?;
867 let slow_len = get_usize_p(params, "slow_len", 21, 1, 10000)?;
868 ensure_less("fast_len", fast_len as f64, "slow_len", slow_len as f64)?;
869 Ok(Box::new(
870 super::trend_relationship::AdaptiveTrendRelationship::new(
871 super::smoothing::SmootherKind::Ema,
872 fast_len,
873 super::smoothing::SmootherKind::Ema,
874 slow_len,
875 ),
876 ))
877 }
878 "williams_r" | "wpr" => {
879 let wpr_len = get_usize_p(params, "wpr_len", 14, 1, 10000)?;
880 let overbought = get_f64_p(params, "overbought", 80.0, 0.0, 100.0)?;
881 let oversold = get_f64_p(params, "oversold", 20.0, 0.0, 100.0)?;
882 ensure_less("oversold", oversold, "overbought", overbought)?;
883 Ok(Box::new(WilliamsR::new(
884 wpr_len, 3, 3, 50.0, overbought, oversold, 5, true, 50, 4, 10.0,
885 )))
886 }
887 "tsi" => {
888 let long_len = get_usize_p(params, "long_len", 25, 1, 10000)?;
889 let short_len = get_usize_p(params, "short_len", 13, 1, 10000)?;
890 let sig_len = get_usize_p(params, "sig_len", 7, 1, 10000)?;
891 ensure_less("short_len", short_len as f64, "long_len", long_len as f64)?;
892 Ok(Box::new(Tsi::new(
893 long_len, short_len, sig_len, 0.0, 25.0, -25.0, 5, true, 50, 25, 4, 5.0,
894 )))
895 }
896 "fisher_transform" | "fisher" => {
897 let fish_len = get_usize_p(params, "fish_len", 10, 1, 10000)?;
898 let overbought = get_f64_p(params, "overbought", 1.5, -100.0, 100.0)?;
899 let oversold = get_f64_p(params, "oversold", -1.5, -100.0, 100.0)?;
900 ensure_less("oversold", oversold, "overbought", overbought)?;
901 Ok(Box::new(FisherTransform::new(
902 fish_len, 2, 3, 0.0, overbought, oversold, 5, true, 40, 4, 0.5,
903 )))
904 }
905 "order_block" | "ob" => {
906 let atr_len = get_usize_p(params, "atr_len", 14, 1, 10000)?;
907 let min_disp = get_f64_p(params, "min_disp", 1.0, 0.01, 100.0)?;
908 Ok(Box::new(OrderBlockEngine::new(atr_len, min_disp)))
909 }
910 "liquidity_fvg" | "fvg" | "smc" => {
911 let lookback = get_usize_p(params, "lookback", 20, 1, 10000)?;
912 Ok(Box::new(LiquidityFvgEngine::new(lookback)))
913 }
914 "market_structure_breaks" | "bos" | "choch" => {
915 let lookback = get_usize_p(params, "lookback", 5, 1, 10000)?;
916 Ok(Box::new(MarketStructureBreaksEngine::new(lookback)))
917 }
918 "pivots_structure" | "pivots" => {
919 let left_bars = get_usize_p(params, "left_bars", 5, 1, 10000)?;
920 let right_bars = get_usize_p(params, "right_bars", 5, 1, 10000)?;
921 let score_window = get_usize_p(params, "score_window", 10, 1, 10000)?;
922 Ok(Box::new(PivotStructureEngine::new(
923 left_bars,
924 right_bars,
925 score_window,
926 )))
927 }
928 "volume_profile" | "vp" => {
929 let lookback = get_usize_p(params, "lookback", 70, 1, 10000)?;
930 let num_bins = get_usize_p(params, "num_bins", 30, 1, 1000)?;
931 Ok(Box::new(VolumeProfileEngine::new(lookback, num_bins)))
932 }
933 "extended_volume_profile" | "vp_extended" => {
934 let lookback = get_usize_p(params, "lookback", 70, 1, 10000)?;
935 let num_bins = get_usize_p(params, "num_bins", 30, 1, 1000)?;
936 Ok(Box::new(ExtendedVolumeProfileEngine::new(
937 lookback, num_bins,
938 )))
939 }
940 "persistent_volume_profile" | "vp_persistent" => {
941 let lookback = get_usize_p(params, "lookback", 70, 1, 10000)?;
942 let bin_width = get_f64_p(params, "bin_width", 1.0, 1e-6, 1_000_000.0)?;
943 Ok(Box::new(PersistentVolumeProfileEngine::new(
944 lookback, bin_width,
945 )))
946 }
947 "vwap" => {
948 let window = get_usize_p(params, "window", 390, 1, 10000)?;
949 let slope_lookback = get_usize_p(params, "slope_lookback", 20, 1, 10000)?;
950 Ok(Box::new(Vwap::new(window, slope_lookback)))
951 }
952 "vix_fix" | "wvf" => {
953 let pd = get_usize_p(params, "pd", 22, 1, 10000)?;
954 let bband_len = get_usize_p(params, "bband_len", 20, 1, 10000)?;
955 let mult = get_f64_p(params, "mult", 2.0, 0.01, 100.0)?;
956 Ok(Box::new(WilliamsVixFix::new(pd, bband_len, mult)))
957 }
958 "candle_story" | "pinbar" => Ok(Box::new(CandleStoryEngine::new())),
959 "efficiency" | "leg_efficiency" | "er" => {
960 let len = get_usize_p(params, "len", 14, 1, 10000)?;
961 Ok(Box::new(LegEfficiencyEngine::new(len)))
962 }
963 "volume" => {
964 let ma = get_usize_p(params, "ma_period", 20, 1, 10000)?;
965 Ok(Box::new(VolumeEngine::new(ma)))
966 }
967 "rvol" => {
968 let p = get_usize_p(params, "period", 20, 1, 10000)?;
969 Ok(Box::new(RvolEngine::new(p)))
970 }
971 "obv" => Ok(Box::new(ObvEngine::new())),
972 "cmf" => {
973 let p = get_usize_p(params, "period", 20, 1, 10000)?;
974 Ok(Box::new(CmfEngine::new(p)))
975 }
976 "acc_dist" => Ok(Box::new(AccDistEngine::new())),
977 "true_range" => Ok(Box::new(TrueRangeEngine::new())),
978 "keltner" => {
979 let ma = get_usize_p(params, "ma_period", 20, 1, 10000)?;
980 let atr = get_usize_p(params, "atr_period", 10, 1, 10000)?;
981 let mult = get_f64_p(params, "multiplier", 2.0, 0.01, 100.0)?;
982 Ok(Box::new(KeltnerChannelEngine::new(ma, atr, mult)))
983 }
984 "donchian" => {
985 let p = get_usize_p(params, "period", 20, 1, 10000)?;
986 Ok(Box::new(DonchianChannelEngine::new(p)))
987 }
988 "historical_volatility" => {
989 let p = get_usize_p(params, "period", 20, 1, 10000)?;
990 Ok(Box::new(HistoricalVolatilityEngine::new(p)))
991 }
992 "garman_klass" => {
993 let p = get_usize_p(params, "period", 20, 1, 10000)?;
994 Ok(Box::new(GarmanKlassVolatilityEngine::new(p)))
995 }
996 "sma" => {
997 let p = get_usize_p(params, "period", 20, 1, 10000)?;
998 Ok(Box::new(SmaEngine::new(p)))
999 }
1000 "ema" => {
1001 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1002 Ok(Box::new(EmaEngine::new(p)))
1003 }
1004 "wma" => {
1005 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1006 Ok(Box::new(WmaEngine::new(p)))
1007 }
1008 "vwma" => {
1009 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1010 Ok(Box::new(VwmaEngine::new(p)))
1011 }
1012 "hma" => {
1013 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1014 Ok(Box::new(HmaEngine::new(p)))
1015 }
1016 "dema" => {
1017 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1018 Ok(Box::new(DemaEngine::new(p)))
1019 }
1020 "kama" => {
1021 let p = get_usize_p(params, "period", 10, 1, 10000)?;
1022 let fast = get_usize_p(params, "fast_period", 2, 1, 10000)?;
1023 let slow = get_usize_p(params, "slow_period", 30, 1, 10000)?;
1024 ensure_less("fast_period", fast as f64, "slow_period", slow as f64)?;
1025 Ok(Box::new(KamaEngine::new(p, fast, slow)))
1026 }
1027 "dmi" => {
1028 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1029 Ok(Box::new(DmiEngine::new(p)))
1030 }
1031 "aroon" => {
1032 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1033 Ok(Box::new(AroonEngine::new(p)))
1034 }
1035 "parabolic_sar" => {
1036 let step = get_f64_p(params, "step", 0.02, 0.001, 1.0)?;
1037 let max_step = get_f64_p(params, "max_step", 0.20, 0.001, 1.0)?;
1038 if step > max_step {
1039 return Err(RegistryError::InvalidParameter {
1040 parameter: "step".to_string(),
1041 value: step,
1042 reason: "step must not exceed max_step".to_string(),
1043 });
1044 }
1045 Ok(Box::new(ParabolicSarEngine::new(step, max_step)))
1046 }
1047 "supertrend" => {
1048 let p = get_usize_p(params, "period", 10, 1, 10000)?;
1049 let mult = get_f64_p(params, "multiplier", 3.0, 0.01, 100.0)?;
1050 Ok(Box::new(SupertrendEngine::new(p, mult)))
1051 }
1052 "ichimoku" => {
1053 let tenkan = get_usize_p(params, "tenkan_p", 9, 1, 10000)?;
1054 let kijun = get_usize_p(params, "kijun_p", 26, 1, 10000)?;
1055 let senkou_b = get_usize_p(params, "senkou_b_p", 52, 1, 10000)?;
1056 ensure_less("tenkan_p", tenkan as f64, "kijun_p", kijun as f64)?;
1057 ensure_less("kijun_p", kijun as f64, "senkou_b_p", senkou_b as f64)?;
1058 Ok(Box::new(IchimokuEngine::new(tenkan, kijun, senkou_b)))
1059 }
1060 "stochastic" => {
1061 let k = get_usize_p(params, "k_period", 14, 1, 10000)?;
1062 let d = get_usize_p(params, "d_period", 3, 1, 10000)?;
1063 Ok(Box::new(StochasticEngine::new(k, d)))
1064 }
1065 "roc" => {
1066 let p = get_usize_p(params, "period", 12, 1, 10000)?;
1067 Ok(Box::new(RocEngine::new(p)))
1068 }
1069 "ultimate_oscillator" => {
1070 let p1 = get_usize_p(params, "period1", 7, 1, 10000)?;
1071 let p2 = get_usize_p(params, "period2", 14, 1, 10000)?;
1072 let p3 = get_usize_p(params, "period3", 28, 1, 10000)?;
1073 ensure_less("period1", p1 as f64, "period2", p2 as f64)?;
1074 ensure_less("period2", p2 as f64, "period3", p3 as f64)?;
1075 Ok(Box::new(UltimateOscillatorEngine::new(p1, p2, p3)))
1076 }
1077 "awesome_oscillator" => {
1078 let fast = get_usize_p(params, "fast_period", 5, 1, 10000)?;
1079 let slow = get_usize_p(params, "slow_period", 34, 1, 10000)?;
1080 ensure_less("fast_period", fast as f64, "slow_period", slow as f64)?;
1081 Ok(Box::new(AwesomeOscillatorEngine::new(fast, slow)))
1082 }
1083 "ppo" => {
1084 let fast = get_usize_p(params, "fast_period", 12, 1, 10000)?;
1085 let slow = get_usize_p(params, "slow_period", 26, 1, 10000)?;
1086 let signal = get_usize_p(params, "signal_period", 9, 1, 10000)?;
1087 ensure_less("fast_period", fast as f64, "slow_period", slow as f64)?;
1088 Ok(Box::new(PpoEngine::new(fast, slow, signal)))
1089 }
1090 "wavetrend" | "wt" => {
1091 let n1 = get_usize_p(params, "n1", 10, 1, 10000)?;
1092 let n2 = get_usize_p(params, "n2", 21, 1, 10000)?;
1093 let ob = get_f64_p(params, "ob_level", 60.0, -100.0, 100.0)?;
1094 let os = get_f64_p(params, "os_level", -60.0, -100.0, 100.0)?;
1095 Ok(Box::new(WaveTrendEngine::new(n1, n2, ob, os)))
1096 }
1097 "cmo" => {
1098 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1099 Ok(Box::new(CmoEngine::new(p)))
1100 }
1101 "elder_ray" => {
1102 let p = get_usize_p(params, "period", 13, 1, 10000)?;
1103 Ok(Box::new(ElderRayEngine::new(p)))
1104 }
1105 "anchored_vwap" | "avwap" => {
1106 let m1 = get_f64_p(params, "mult1", 1.0, 0.01, 100.0)?;
1107 let m2 = get_f64_p(params, "mult2", 2.0, 0.01, 100.0)?;
1108 Ok(Box::new(AnchoredVwapEngine::new(
1109 VwapAnchorKind::Session,
1110 m1,
1111 m2,
1112 )))
1113 }
1114 "cvd" => Ok(Box::new(CvdEngine::new())),
1115 "hires_volume_flow" => {
1116 let window_len = get_usize_p(params, "window_len", 20, 2, 10000)?;
1117 Ok(Box::new(HiResVolumeFlowEngine::new(window_len)))
1118 }
1119 "klinger" | "kvo" => {
1120 let fast = get_usize_p(params, "fast_len", 34, 1, 10000)?;
1121 let slow = get_usize_p(params, "slow_len", 55, 1, 10000)?;
1122 let sig = get_usize_p(params, "signal_len", 13, 1, 10000)?;
1123 ensure_less("fast_len", fast as f64, "slow_len", slow as f64)?;
1124 Ok(Box::new(KlingerVolumeForceEngine::new(fast, slow, sig)))
1125 }
1126 "zigzag" => {
1127 let depth = get_usize_p(params, "depth", 12, 2, 10000)?;
1128 let dev = get_f64_p(params, "deviation_pct", 5.0, 0.01, 100.0)?;
1129 Ok(Box::new(ZigZagEngine::new(depth, dev)))
1130 }
1131 "zigzag_advanced" => {
1132 let depth = get_usize_p(params, "depth", 3, 1, 10000)?;
1135 let backstep = get_usize_p(params, "backstep", 2, 0, 10000)?;
1136 let deviation_pct = get_f64_p(params, "deviation_pct", 1.0, 0.001, 100.0)?;
1137 let atr_len = get_usize_p(params, "atr_len", 14, 1, 10000)?;
1138 Ok(Box::new(AdvancedZigZagEngine::new(
1139 depth,
1140 backstep,
1141 ZigZagDeviationMode::Percent(deviation_pct),
1142 atr_len,
1143 )))
1144 }
1145 "pivot_sets" | "multi_pivots" => Ok(Box::new(PivotSetsEngine::new(PivotSetType::Classic))),
1146 "tema" => {
1147 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1148 Ok(Box::new(TemaEngine::new(p)))
1149 }
1150 "lsma" => {
1151 let p = get_usize_p(params, "period", 25, 2, 10000)?;
1152 Ok(Box::new(LsmaEngine::new(p)))
1153 }
1154 "mcginley" => {
1155 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1156 Ok(Box::new(McGinleyDynamicEngine::new(p)))
1157 }
1158 "envelope" => {
1159 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1160 let pct = get_f64_p(params, "percent", 2.5, 0.01, 100.0)?;
1161 Ok(Box::new(EnvelopeEngine::new(p, pct)))
1162 }
1163 "choppiness" | "chop" => {
1164 let p = get_usize_p(params, "period", 14, 2, 10000)?;
1165 Ok(Box::new(ChoppinessIndexEngine::new(p)))
1166 }
1167 "vortex" | "vi" => {
1168 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1169 Ok(Box::new(VortexEngine::new(p)))
1170 }
1171 "alligator" => Ok(Box::new(AlligatorEngine::new())),
1172 "connors_rsi" => {
1173 let rsi_len = get_usize_p(params, "rsi_len", 3, 1, 10000)?;
1174 let streak_len = get_usize_p(params, "streak_len", 2, 1, 10000)?;
1175 let rank_len = get_usize_p(params, "rank_len", 100, 1, 10000)?;
1176 Ok(Box::new(ConnorsRsiEngine::new(
1177 rsi_len, streak_len, rank_len,
1178 )))
1179 }
1180 "coppock" => Ok(Box::new(CoppockCurveEngine::new())),
1181 "dpo" => {
1182 let p = get_usize_p(params, "period", 21, 2, 10000)?;
1183 Ok(Box::new(DpoEngine::new(p)))
1184 }
1185 "kst" => Ok(Box::new(KstEngine::new())),
1186 "mass_index" => {
1187 let p = get_usize_p(params, "period", 25, 1, 10000)?;
1188 Ok(Box::new(MassIndexEngine::new(p)))
1189 }
1190 "rvi" => {
1191 let p = get_usize_p(params, "period", 10, 1, 10000)?;
1192 Ok(Box::new(RviEngine::new(p)))
1193 }
1194 "bop" => {
1195 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1196 Ok(Box::new(BalanceOfPowerEngine::new(p)))
1197 }
1198 "eom" => {
1199 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1200 let div = get_f64_p(params, "volume_divisor", 10000.0, 1.0, 1e9)?;
1201 Ok(Box::new(EomEngine::new(p, div)))
1202 }
1203 "nvi" => Ok(Box::new(NviEngine::new())),
1204 "pvi" => Ok(Box::new(PviEngine::new())),
1205 "chaikin_oscillator" | "cho" => {
1206 let fast = get_usize_p(params, "fast_len", 3, 1, 10000)?;
1207 let slow = get_usize_p(params, "slow_len", 10, 1, 10000)?;
1208 ensure_less("fast_len", fast as f64, "slow_len", slow as f64)?;
1209 Ok(Box::new(ChaikinOscillatorEngine::new(fast, slow)))
1210 }
1211 "bos_choch" => {
1212 let pivot_len = get_usize_p(params, "pivot_len", 5, 2, 10000)?;
1213 Ok(Box::new(BosChochEngine::new(pivot_len)))
1214 }
1215 "liquidity_sweeps" | "sweeps" => {
1216 let p = get_usize_p(params, "pivot_len", 5, 2, 10000)?;
1217 let tol = get_f64_p(params, "tolerance_pct", 0.2, 0.01, 100.0)?;
1218 Ok(Box::new(LiquiditySweepEngine::new(p, tol)))
1219 }
1220 "liquidity_pools" => {
1221 let p = get_usize_p(params, "pivot_len", 5, 2, 10000)?;
1222 let tol = get_f64_p(params, "tolerance_pct", 0.2, 0.001, 100.0)?;
1223 Ok(Box::new(
1224 super::smart_money_structure::LiquidityPoolEngine::new(p, tol),
1225 ))
1226 }
1227 "wyckoff" => {
1228 let lookback = get_usize_p(params, "range_lookback", 20, 3, 10000)?;
1229 let atr_max = get_f64_p(params, "range_atr_max", 3.0, 0.1, 1000.0)?;
1230 let min_bars = get_usize_p(params, "min_range_bars", 6, 2, 10000)?;
1231 Ok(Box::new(super::wyckoff::WyckoffStateMachine::new(
1232 lookback, atr_max, min_bars,
1233 )))
1234 }
1235 "trend_quality" => {
1236 let p = get_usize_p(params, "period", 14, 2, 10000)?;
1237 Ok(Box::new(TrendQualityScoreEngine::new(p)))
1238 }
1239 "buy_sell_pressure" | "pressure" => {
1240 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1241 Ok(Box::new(BuySellPressureEstimator::new(p)))
1242 }
1243 "volatility_regime" => {
1244 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1245 let bb_mult = get_f64_p(params, "bb_mult", 2.0, 0.01, 100.0)?;
1246 let kc_mult = get_f64_p(params, "kc_mult", 1.5, 0.01, 100.0)?;
1247 Ok(Box::new(VolatilityRegimeDetector::new(p, bb_mult, kc_mult)))
1248 }
1249 "zscore" => {
1250 let p = get_usize_p(params, "period", 20, 2, 10000)?;
1251 Ok(Box::new(ZScoreEngine::new(p)))
1252 }
1253 "multi_factor" => {
1254 let p = get_usize_p(params, "period", 14, 2, 10000)?;
1255 Ok(Box::new(MultiFactorMarketScore::new(p)))
1256 }
1257 _ => Err(RegistryError::UnknownIndicator(name.to_string())),
1258 }
1259}
1260
1261pub fn build(name: &str, params: &HashMap<String, f64>) -> Option<Box<dyn Indicator>> {
1264 build_checked(name, params).ok()
1265}
1266
1267const RANGE_DEPENDENT_INDICATORS: &[&str] = &[
1281 "atr",
1282 "true_range",
1283 "adx",
1284 "dmi",
1285 "chandelier_exit",
1286 "wyckoff",
1287 "volume_profile",
1288 "vp",
1289 "extended_volume_profile",
1290 "vp_extended",
1291 "persistent_volume_profile",
1292 "vp_persistent",
1293 "pivots_structure",
1294 "pivots",
1295 "pivot_sets",
1296 "multi_pivots",
1297 "zigzag",
1298 "zigzag_advanced",
1299 "liquidity_pools",
1300 "liquidity_sweeps",
1301 "sweeps",
1302 "liquidity_fvg",
1303 "fvg",
1304 "smc",
1305 "order_block",
1306 "ob",
1307 "ce",
1308 "bos_choch",
1309 "market_structure_breaks",
1310 "bos",
1311 "choch",
1312 "vix_fix",
1313 "wvf",
1314 "keltner",
1315 "donchian",
1316 "vortex",
1317 "vi",
1318 "choppiness",
1319 "chop",
1320 "mass_index",
1321 "supertrend",
1322 "parabolic_sar",
1323 "ichimoku",
1324 "aroon",
1325 "garman_klass",
1326 "hires_volume_flow",
1327 "cvd",
1328 "klinger",
1329 "kvo",
1330 "volatility_regime",
1331 "swing_structure",
1332];
1333
1334pub fn build_typed(name: &str, params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
1335 if name.to_lowercase() == "midas" {
1336 return build_midas_typed(params);
1338 }
1339
1340 let source = match params.get("source") {
1341 None => None,
1342 Some(ParamValue::Source(s)) => Some(*s),
1343 Some(other) => {
1344 return Err(RegistryError::UnsupportedParameterType {
1345 parameter: "source".to_string(),
1346 type_name: other.type_name().to_string(),
1347 });
1348 }
1349 };
1350
1351 if let Some(s) = source {
1352 if s != crate::model::Source::Close
1353 && RANGE_DEPENDENT_INDICATORS.contains(&name.to_lowercase().as_str())
1354 {
1355 return Err(RegistryError::IncompatibleParameter {
1356 parameter: "source".to_string(),
1357 indicator: name.to_string(),
1358 reason: "range/OHLC-dependent indicator; SourceMapped would collapse its true range to zero"
1359 .to_string(),
1360 });
1361 }
1362 }
1363
1364 let mut remaining = params.clone();
1368 remaining.remove("source");
1369
1370 let built = match name.to_lowercase().as_str() {
1371 "anchored_vwap" | "avwap" => build_anchored_vwap_typed(&remaining)?,
1372 "pivot_sets" | "multi_pivots" => build_pivot_sets_typed(&remaining)?,
1373 "trend_relationship" => build_trend_relationship_typed(&remaining)?,
1374 "zigzag_advanced" => build_zigzag_advanced_typed(&remaining)?,
1375 _ => build_typed_by_flattening(name, &remaining)?,
1376 };
1377
1378 Ok(match source {
1379 Some(s) if s != crate::model::Source::Close => {
1380 Box::new(super::source_mapped::SourceMapped::new(built, s))
1381 }
1382 _ => built,
1383 })
1384}
1385
1386fn build_typed_by_flattening(
1390 name: &str,
1391 params: &TypedParams,
1392) -> Result<Box<dyn Indicator>, RegistryError> {
1393 let mut flat = HashMap::with_capacity(params.len());
1394 for (key, value) in params {
1395 match value.as_f64() {
1396 Some(v) => {
1397 flat.insert(key.clone(), v);
1398 }
1399 None => {
1400 return Err(RegistryError::UnsupportedParameterType {
1401 parameter: key.clone(),
1402 type_name: value.type_name().to_string(),
1403 });
1404 }
1405 }
1406 }
1407 build_checked(name, &flat)
1408}
1409
1410fn get_enum_p(params: &TypedParams, name: &str) -> Result<Option<String>, RegistryError> {
1413 match params.get(name) {
1414 None => Ok(None),
1415 Some(ParamValue::Enum(value)) => Ok(Some(value.to_lowercase())),
1416 Some(other) => Err(RegistryError::UnsupportedParameterType {
1417 parameter: name.to_string(),
1418 type_name: other.type_name().to_string(),
1419 }),
1420 }
1421}
1422
1423fn get_timestamp_p(params: &TypedParams, name: &str) -> Result<Option<i64>, RegistryError> {
1426 match params.get(name) {
1427 None => Ok(None),
1428 Some(ParamValue::Timestamp(value)) => Ok(Some(*value)),
1429 Some(other) => Err(RegistryError::UnsupportedParameterType {
1430 parameter: name.to_string(),
1431 type_name: other.type_name().to_string(),
1432 }),
1433 }
1434}
1435
1436fn extract_numeric_subset(
1439 params: &TypedParams,
1440 keys: &[&str],
1441) -> Result<HashMap<String, f64>, RegistryError> {
1442 let mut numeric = HashMap::new();
1443 for key in keys {
1444 if let Some(value) = params.get(*key) {
1445 match value.as_f64() {
1446 Some(v) => {
1447 numeric.insert((*key).to_string(), v);
1448 }
1449 None => {
1450 return Err(RegistryError::UnsupportedParameterType {
1451 parameter: (*key).to_string(),
1452 type_name: value.type_name().to_string(),
1453 });
1454 }
1455 }
1456 }
1457 }
1458 Ok(numeric)
1459}
1460
1461fn build_anchored_vwap_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
1462 let anchor_kind = match get_enum_p(params, "anchor_kind")?.as_deref() {
1463 None | Some("session") => VwapAnchorKind::Session,
1464 Some("day") => VwapAnchorKind::Day,
1465 Some("week") => VwapAnchorKind::Week,
1466 Some("month") => VwapAnchorKind::Month,
1467 Some("external") => VwapAnchorKind::External,
1468 Some("manual_timestamp") => {
1469 let ts = get_timestamp_p(params, "anchor_timestamp")?.ok_or_else(|| {
1470 RegistryError::InvalidEnumValue {
1471 parameter: "anchor_kind".to_string(),
1472 value: "manual_timestamp".to_string(),
1473 reason: "requires an accompanying 'anchor_timestamp' Timestamp parameter"
1474 .to_string(),
1475 }
1476 })?;
1477 VwapAnchorKind::ManualTimestamp(ts)
1478 }
1479 Some(other) => {
1480 return Err(RegistryError::InvalidEnumValue {
1481 parameter: "anchor_kind".to_string(),
1482 value: other.to_string(),
1483 reason: "expected one of session|day|week|month|external|manual_timestamp"
1484 .to_string(),
1485 });
1486 }
1487 };
1488
1489 let zero_volume_policy = match get_enum_p(params, "zero_volume_policy")?.as_deref() {
1490 None | Some("equal_weight") => ZeroVolumePolicy::EqualWeight,
1491 Some("skip") => ZeroVolumePolicy::Skip,
1492 Some(other) => {
1493 return Err(RegistryError::InvalidEnumValue {
1494 parameter: "zero_volume_policy".to_string(),
1495 value: other.to_string(),
1496 reason: "expected one of equal_weight|skip".to_string(),
1497 });
1498 }
1499 };
1500
1501 let numeric = extract_numeric_subset(params, &["mult1", "mult2"])?;
1502 let m1 = get_f64_p(&numeric, "mult1", 1.0, 0.01, 100.0)?;
1503 let m2 = get_f64_p(&numeric, "mult2", 2.0, 0.01, 100.0)?;
1504
1505 Ok(Box::new(
1506 AnchoredVwapEngine::new(anchor_kind, m1, m2).with_zero_volume_policy(zero_volume_policy),
1507 ))
1508}
1509
1510fn build_pivot_sets_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
1511 let pivot_type = match get_enum_p(params, "pivot_type")?.as_deref() {
1512 None | Some("classic") => PivotSetType::Classic,
1513 Some("fibonacci") => PivotSetType::Fibonacci,
1514 Some("camarilla") => PivotSetType::Camarilla,
1515 Some("woodie") => PivotSetType::Woodie,
1516 Some("demark") => PivotSetType::DeMark,
1517 Some("cpr") => PivotSetType::Cpr,
1518 Some(other) => {
1519 return Err(RegistryError::InvalidEnumValue {
1520 parameter: "pivot_type".to_string(),
1521 value: other.to_string(),
1522 reason: "expected one of classic|fibonacci|camarilla|woodie|demark|cpr".to_string(),
1523 });
1524 }
1525 };
1526
1527 Ok(Box::new(PivotSetsEngine::new(pivot_type)))
1528}
1529
1530fn parse_smoother_kind(
1531 params: &TypedParams,
1532 parameter_name: &str,
1533 default: super::smoothing::SmootherKind,
1534) -> Result<super::smoothing::SmootherKind, RegistryError> {
1535 use super::smoothing::SmootherKind;
1536
1537 match get_enum_p(params, parameter_name)?.as_deref() {
1538 None => Ok(default),
1539 Some("ema") => Ok(SmootherKind::Ema),
1540 Some("sma") => Ok(SmootherKind::Sma),
1541 Some("rma") => Ok(SmootherKind::Rma),
1542 Some("alma") => Ok(SmootherKind::Alma),
1543 Some("jma") => Ok(SmootherKind::Jma),
1544 Some(other) => Err(RegistryError::InvalidEnumValue {
1545 parameter: parameter_name.to_string(),
1546 value: other.to_string(),
1547 reason: "expected one of ema|sma|rma|alma|jma".to_string(),
1548 }),
1549 }
1550}
1551
1552fn build_midas_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
1558 let mode = match get_enum_p(params, "mode")?.as_deref() {
1559 None | Some("topfinder") => MidasMode::Topfinder,
1560 Some("bottomfinder") => MidasMode::Bottomfinder,
1561 Some(other) => {
1562 return Err(RegistryError::InvalidEnumValue {
1563 parameter: "mode".to_string(),
1564 value: other.to_string(),
1565 reason: "expected one of topfinder|bottomfinder".to_string(),
1566 });
1567 }
1568 };
1569
1570 let source = match params.get("source") {
1571 None => crate::model::Source::Hlc3,
1572 Some(ParamValue::Source(s)) => *s,
1573 Some(other) => {
1574 return Err(RegistryError::UnsupportedParameterType {
1575 parameter: "source".to_string(),
1576 type_name: other.type_name().to_string(),
1577 });
1578 }
1579 };
1580
1581 let numeric = extract_numeric_subset(params, &["maturity_bars"])?;
1582 let maturity_bars = get_usize_p(&numeric, "maturity_bars", 20, 1, 10000)?;
1583
1584 Ok(Box::new(MidasCurveEngine::new(
1585 mode,
1586 source,
1587 maturity_bars as u32,
1588 )))
1589}
1590
1591fn build_zigzag_advanced_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
1592 let numeric =
1593 extract_numeric_subset(params, &["depth", "backstep", "deviation_value", "atr_len"])?;
1594 let depth = get_usize_p(&numeric, "depth", 3, 1, 10000)?;
1595 let backstep = get_usize_p(&numeric, "backstep", 2, 0, 10000)?;
1596 let atr_len = get_usize_p(&numeric, "atr_len", 14, 1, 10000)?;
1597
1598 let deviation = match get_enum_p(params, "deviation_mode")?.as_deref() {
1599 None | Some("percent") => {
1600 let value = get_f64_p(&numeric, "deviation_value", 1.0, 0.001, 100.0)?;
1601 ZigZagDeviationMode::Percent(value)
1602 }
1603 Some("atr_multiple") => {
1604 let value = get_f64_p(&numeric, "deviation_value", 1.0, 0.001, 1000.0)?;
1605 ZigZagDeviationMode::AtrMultiple(value)
1606 }
1607 Some(other) => {
1608 return Err(RegistryError::InvalidEnumValue {
1609 parameter: "deviation_mode".to_string(),
1610 value: other.to_string(),
1611 reason: "expected one of percent|atr_multiple".to_string(),
1612 });
1613 }
1614 };
1615
1616 Ok(Box::new(AdvancedZigZagEngine::new(
1617 depth, backstep, deviation, atr_len,
1618 )))
1619}
1620
1621fn build_trend_relationship_typed(
1622 params: &TypedParams,
1623) -> Result<Box<dyn Indicator>, RegistryError> {
1624 use super::smoothing::SmootherKind;
1625 use super::trend_relationship::AdaptiveTrendRelationship;
1626
1627 let fast_kind = parse_smoother_kind(params, "fast_kind", SmootherKind::Ema)?;
1628 let slow_kind = parse_smoother_kind(params, "slow_kind", SmootherKind::Ema)?;
1629
1630 let numeric = extract_numeric_subset(params, &["fast_len", "slow_len"])?;
1631 let fast_len = get_usize_p(&numeric, "fast_len", 9, 1, 10000)?;
1632 let slow_len = get_usize_p(&numeric, "slow_len", 21, 1, 10000)?;
1633 ensure_less("fast_len", fast_len as f64, "slow_len", slow_len as f64)?;
1634
1635 Ok(Box::new(AdaptiveTrendRelationship::new(
1636 fast_kind, fast_len, slow_kind, slow_len,
1637 )))
1638}
1639
1640#[cfg(test)]
1647mod tests {
1648 use super::*;
1649
1650 #[test]
1651 fn test_build_checked_valid_and_invalid_params() {
1652 let valid_params = HashMap::from([("period".to_string(), 14.0)]);
1653 assert!(build_checked("rsi", &valid_params).is_ok());
1654
1655 let zero_params = HashMap::from([("rsi_len".to_string(), 0.0)]);
1657 let err = match build_checked("rsi", &zero_params) {
1658 Err(e) => e,
1659 Ok(_) => panic!("Expected error for zero period"),
1660 };
1661 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
1662
1663 let neg_params = HashMap::from([("period".to_string(), -5.0)]);
1665 let err = match build_checked("sma", &neg_params) {
1666 Err(e) => e,
1667 Ok(_) => panic!("Expected error for negative period"),
1668 };
1669 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
1670
1671 let fractional_params = HashMap::from([("period".to_string(), 14.5)]);
1673 assert!(matches!(
1674 build_checked("sma", &fractional_params),
1675 Err(RegistryError::InvalidParameter { .. })
1676 ));
1677
1678 let huge_params = HashMap::from([("period".to_string(), 10_001.0)]);
1680 assert!(matches!(
1681 build_checked("sma", &huge_params),
1682 Err(RegistryError::InvalidParameter { .. })
1683 ));
1684
1685 let nan_params = HashMap::from([("period".to_string(), f64::NAN)]);
1687 let err = match build_checked("sma", &nan_params) {
1688 Err(e) => e,
1689 Ok(_) => panic!("Expected error for NaN period"),
1690 };
1691 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
1692
1693 let reversed_rsi = HashMap::from([
1695 ("oversold".to_string(), 80.0),
1696 ("overbought".to_string(), 20.0),
1697 ]);
1698 assert!(matches!(
1699 build_checked("rsi", &reversed_rsi),
1700 Err(RegistryError::InvalidParameter { .. })
1701 ));
1702
1703 let reversed_ultimate = HashMap::from([
1704 ("period1".to_string(), 28.0),
1705 ("period2".to_string(), 14.0),
1706 ("period3".to_string(), 7.0),
1707 ]);
1708 assert!(matches!(
1709 build_checked("ultimate_oscillator", &reversed_ultimate),
1710 Err(RegistryError::InvalidParameter { .. })
1711 ));
1712
1713 let oversized_bins = HashMap::from([("num_bins".to_string(), 1_001.0)]);
1714 assert!(matches!(
1715 build_checked("volume_profile", &oversized_bins),
1716 Err(RegistryError::InvalidParameter { .. })
1717 ));
1718
1719 let inf_params = HashMap::from([("period".to_string(), f64::INFINITY)]);
1721 let err = match build_checked("sma", &inf_params) {
1722 Err(e) => e,
1723 Ok(_) => panic!("Expected error for Infinity period"),
1724 };
1725 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
1726
1727 let macd_bad = HashMap::from([
1729 ("fast_len".to_string(), 30.0),
1730 ("slow_len".to_string(), 20.0),
1731 ]);
1732 let err = match build_checked("macd", &macd_bad) {
1733 Err(e) => e,
1734 Ok(_) => panic!("Expected error for fast >= slow"),
1735 };
1736 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
1737
1738 let err = match build_checked("non_existent_ind", &HashMap::new()) {
1740 Err(e) => e,
1741 Ok(_) => panic!("Expected error for unknown indicator"),
1742 };
1743 assert_eq!(
1744 err,
1745 RegistryError::UnknownIndicator("non_existent_ind".to_string())
1746 );
1747 }
1748
1749 #[test]
1750 fn test_build_typed_flattens_numeric_params() {
1751 let params: TypedParams = HashMap::from([("period".to_string(), ParamValue::Int(14))]);
1752 assert!(build_typed("rsi", ¶ms).is_ok());
1753
1754 let bool_params: TypedParams =
1755 HashMap::from([("period".to_string(), ParamValue::Bool(true))]);
1756 assert!(build_typed("rsi", &bool_params).is_ok());
1758 }
1759
1760 #[test]
1761 fn test_build_typed_rejects_non_numeric_params() {
1762 let params: TypedParams =
1763 HashMap::from([("period".to_string(), ParamValue::Enum("fast".to_string()))]);
1764 let err = match build_typed("rsi", ¶ms) {
1765 Err(e) => e,
1766 Ok(_) => panic!("Expected error for enum parameter"),
1767 };
1768 assert!(matches!(
1769 err,
1770 RegistryError::UnsupportedParameterType { .. }
1771 ));
1772 }
1773
1774 #[test]
1775 fn test_build_typed_propagates_indicator_validation_errors() {
1776 let params: TypedParams =
1777 HashMap::from([("period".to_string(), ParamValue::Float(f64::INFINITY))]);
1778 let err = match build_typed("sma", ¶ms) {
1779 Err(e) => e,
1780 Ok(_) => panic!("Expected error for infinite period"),
1781 };
1782 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
1783 }
1784
1785 #[test]
1786 fn test_build_typed_anchored_vwap_native_enum_selection() {
1787 let defaulted = build_typed("anchored_vwap", &TypedParams::new());
1789 assert!(defaulted.is_ok());
1790
1791 let day_params: TypedParams = HashMap::from([(
1792 "anchor_kind".to_string(),
1793 ParamValue::Enum("day".to_string()),
1794 )]);
1795 assert!(build_typed("avwap", &day_params).is_ok());
1796
1797 let skip_zero_vol: TypedParams = HashMap::from([(
1798 "zero_volume_policy".to_string(),
1799 ParamValue::Enum("skip".to_string()),
1800 )]);
1801 assert!(build_typed("anchored_vwap", &skip_zero_vol).is_ok());
1802
1803 let manual_without_timestamp: TypedParams = HashMap::from([(
1804 "anchor_kind".to_string(),
1805 ParamValue::Enum("manual_timestamp".to_string()),
1806 )]);
1807 let err = match build_typed("anchored_vwap", &manual_without_timestamp) {
1808 Err(e) => e,
1809 Ok(_) => panic!("Expected error: manual_timestamp requires anchor_timestamp"),
1810 };
1811 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
1812
1813 let manual_with_timestamp: TypedParams = HashMap::from([
1814 (
1815 "anchor_kind".to_string(),
1816 ParamValue::Enum("manual_timestamp".to_string()),
1817 ),
1818 (
1819 "anchor_timestamp".to_string(),
1820 ParamValue::Timestamp(1_700_000_000),
1821 ),
1822 ]);
1823 assert!(build_typed("anchored_vwap", &manual_with_timestamp).is_ok());
1824
1825 let unknown_kind: TypedParams = HashMap::from([(
1826 "anchor_kind".to_string(),
1827 ParamValue::Enum("bogus".to_string()),
1828 )]);
1829 let err = match build_typed("anchored_vwap", &unknown_kind) {
1830 Err(e) => e,
1831 Ok(_) => panic!("Expected error for unknown anchor_kind"),
1832 };
1833 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
1834 }
1835
1836 #[test]
1837 fn test_build_typed_pivot_sets_native_enum_selection() {
1838 for kind in [
1839 "classic",
1840 "fibonacci",
1841 "camarilla",
1842 "woodie",
1843 "demark",
1844 "cpr",
1845 ] {
1846 let params: TypedParams =
1847 HashMap::from([("pivot_type".to_string(), ParamValue::Enum(kind.to_string()))]);
1848 assert!(
1849 build_typed("pivot_sets", ¶ms).is_ok(),
1850 "expected {kind} to build"
1851 );
1852 }
1853
1854 let unknown: TypedParams = HashMap::from([(
1855 "pivot_type".to_string(),
1856 ParamValue::Enum("bogus".to_string()),
1857 )]);
1858 let err = match build_typed("multi_pivots", &unknown) {
1859 Err(e) => e,
1860 Ok(_) => panic!("Expected error for unknown pivot_type"),
1861 };
1862 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
1863 }
1864
1865 #[test]
1866 fn test_build_typed_source_propagates_to_computation() {
1867 use crate::model::{Bar, Source};
1868
1869 let bars = [
1870 Bar::new(0, 10.0, 12.0, 8.0, 11.0, 100.0),
1871 Bar::new(60, 20.0, 22.0, 18.0, 21.0, 100.0),
1872 ];
1873
1874 let close_params: TypedParams = HashMap::from([("period".to_string(), ParamValue::Int(2))]);
1875 let mut close_sma = build_typed("sma", &close_params).unwrap();
1876
1877 let open_params: TypedParams = HashMap::from([
1878 ("period".to_string(), ParamValue::Int(2)),
1879 ("source".to_string(), ParamValue::Source(Source::Open)),
1880 ]);
1881 let mut open_sma = build_typed("sma", &open_params).unwrap();
1882
1883 let mut close_out = None;
1884 let mut open_out = None;
1885 for bar in &bars {
1886 close_out = close_sma.on_bar(bar);
1887 open_out = open_sma.on_bar(bar);
1888 }
1889
1890 assert_eq!(close_out.unwrap().value, (11.0 + 21.0) / 2.0);
1891 assert_eq!(open_out.unwrap().value, (10.0 + 20.0) / 2.0);
1892 }
1893
1894 #[test]
1895 fn test_build_typed_source_close_is_a_no_op() {
1896 let params: TypedParams = HashMap::from([(
1897 "source".to_string(),
1898 ParamValue::Source(crate::model::Source::Close),
1899 )]);
1900 assert!(build_typed("sma", ¶ms).is_ok());
1901 }
1902
1903 #[test]
1904 fn test_adx_with_defaults_matches_registry_default() {
1905 let mut via_struct = Adx::with_defaults();
1906 let mut via_registry = build_checked("adx", &HashMap::new()).unwrap();
1907
1908 let bars = crate::model::Bar::new(0, 100.0, 101.0, 99.0, 100.5, 1000.0);
1909 let mut struct_out = None;
1910 let mut registry_out = None;
1911 for i in 0..60 {
1912 let price = 100.0 + (i as f64 * 0.3).sin() * 5.0;
1913 let bar =
1914 crate::model::Bar::new(i, price, price + 1.0, price - 1.0, price + 0.5, 1000.0);
1915 struct_out = via_struct.on_bar(&bar);
1916 registry_out = via_registry.on_bar(&bar);
1917 }
1918 let _ = bars;
1919 assert_eq!(
1920 struct_out.map(|o| o.value),
1921 registry_out.map(|o| o.value),
1922 "Adx::with_defaults() must produce identical output to the registry's \"adx\" default"
1923 );
1924 }
1925
1926 #[test]
1927 fn test_atr_with_defaults_matches_registry_default() {
1928 let mut via_struct = Atr::with_defaults();
1929 let mut via_registry = build_checked("atr", &HashMap::new()).unwrap();
1930
1931 let mut struct_out = None;
1932 let mut registry_out = None;
1933 for i in 0..40 {
1934 let price = 100.0 + (i as f64 * 0.3).sin() * 5.0;
1935 let bar =
1936 crate::model::Bar::new(i, price, price + 1.0, price - 1.0, price + 0.5, 1000.0);
1937 struct_out = via_struct.on_bar(&bar);
1938 registry_out = via_registry.on_bar(&bar);
1939 }
1940 assert_eq!(
1941 struct_out.map(|o| o.value),
1942 registry_out.map(|o| o.value),
1943 "Atr::with_defaults() must produce identical output to the registry's \"atr\" default"
1944 );
1945 }
1946
1947 #[test]
1948 fn test_build_typed_rejects_non_close_source_on_range_dependent_indicators() {
1949 let params: TypedParams = HashMap::from([(
1950 "source".to_string(),
1951 ParamValue::Source(crate::model::Source::Open),
1952 )]);
1953 for name in [
1954 "atr",
1955 "wyckoff",
1956 "chandelier_exit",
1957 "ce",
1958 "extended_volume_profile",
1959 "zigzag_advanced",
1960 "keltner",
1961 "donchian",
1962 "garman_klass",
1963 ] {
1964 let err = match build_typed(name, ¶ms) {
1965 Err(e) => e,
1966 Ok(_) => panic!("expected '{name}' to reject a non-Close source"),
1967 };
1968 assert!(
1969 matches!(err, RegistryError::IncompatibleParameter { .. }),
1970 "'{name}' returned {err:?} instead of IncompatibleParameter"
1971 );
1972 }
1973
1974 let close_params: TypedParams = HashMap::from([(
1976 "source".to_string(),
1977 ParamValue::Source(crate::model::Source::Close),
1978 )]);
1979 assert!(build_typed("atr", &close_params).is_ok());
1980 }
1981
1982 #[test]
1983 fn test_build_typed_trend_relationship_native_smoother_kind_selection() {
1984 let params: TypedParams = HashMap::from([
1985 ("fast_kind".to_string(), ParamValue::Enum("jma".to_string())),
1986 (
1987 "slow_kind".to_string(),
1988 ParamValue::Enum("alma".to_string()),
1989 ),
1990 ("fast_len".to_string(), ParamValue::Int(5)),
1991 ("slow_len".to_string(), ParamValue::Int(20)),
1992 ]);
1993 assert!(build_typed("trend_relationship", ¶ms).is_ok());
1994
1995 let unknown: TypedParams = HashMap::from([(
1996 "fast_kind".to_string(),
1997 ParamValue::Enum("bogus".to_string()),
1998 )]);
1999 let err = match build_typed("trend_relationship", &unknown) {
2000 Err(e) => e,
2001 Ok(_) => panic!("Expected error for unknown fast_kind"),
2002 };
2003 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
2004 }
2005
2006 #[test]
2007 fn test_build_checked_trend_relationship_defaults_to_ema() {
2008 assert!(build_checked("trend_relationship", &HashMap::new()).is_ok());
2009 }
2010
2011 #[test]
2012 fn test_build_typed_midas_native_mode_and_source_selection() {
2013 let params: TypedParams = HashMap::from([
2014 (
2015 "mode".to_string(),
2016 ParamValue::Enum("bottomfinder".to_string()),
2017 ),
2018 (
2019 "source".to_string(),
2020 ParamValue::Source(crate::model::Source::Close),
2021 ),
2022 ]);
2023 assert!(build_typed("midas", ¶ms).is_ok());
2024
2025 let unknown: TypedParams =
2026 HashMap::from([("mode".to_string(), ParamValue::Enum("bogus".to_string()))]);
2027 let err = match build_typed("midas", &unknown) {
2028 Err(e) => e,
2029 Ok(_) => panic!("Expected error for unknown mode"),
2030 };
2031 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
2032 }
2033
2034 #[test]
2035 fn test_build_checked_midas_defaults_to_topfinder() {
2036 assert!(build_checked("midas", &HashMap::new()).is_ok());
2037 }
2038}