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, TrueRangeSmoothing};
10use super::bbtrend::BbTrend;
11use super::bollinger::{BollingerBands, VarianceConvention};
12use super::bop::BalanceOfPowerEngine;
13use super::bos_choch::BosChochEngine;
14use super::buy_sell_pressure::BuySellPressureEstimator;
15use super::candle_story::{CandleStoryConfig, CandleStoryEngine};
16use super::cci::Cci;
17use super::chaikin_osc::ChaikinOscillatorEngine;
18use super::chande_kroll::ChandeKrollStop;
19use super::chandelier_exit::ChandelierExitEngine;
20use super::chandelier_flip_radar::ChandelierFlipRadarEngine;
21use super::choppiness::ChoppinessIndexEngine;
22use super::connors_rsi::ConnorsRsiEngine;
23use super::coppock::CoppockCurveEngine;
24use super::dpo::DpoEngine;
25use super::efficiency::LegEfficiencyEngine;
26use super::envelope::EnvelopeEngine;
27use super::eom::EomEngine;
28use super::fisher_transform::FisherTransform;
29use super::force_index::ElderForceIndex;
30use super::kst::KstEngine;
31use super::liquidity_fvg::LiquidityFvgEngine;
32use super::liquidity_sweeps::LiquiditySweepEngine;
33use super::lsma::LsmaEngine;
34use super::macd::Macd;
35use super::market_structure_breaks::MarketStructureBreaksEngine;
36use super::mass_index::MassIndexEngine;
37use super::mcginley::McGinleyDynamicEngine;
38use super::mfi::Mfi;
39use super::midas::{MidasCurveEngine, MidasMode};
40use super::momentum_indicators::{
41 AwesomeOscillatorEngine, CmoEngine, ElderRayEngine, PpoEngine, RocEngine, StochasticEngine,
42 UltimateOscillatorEngine,
43};
44use super::money_flow_profile::MoneyFlowProfileEngine;
45use super::moving_averages::{
46 DemaEngine, EmaEngine, HmaEngine, KamaEngine, SmaEngine, VwmaEngine, WmaEngine,
47};
48use super::multi_factor::MultiFactorMarketScore;
49use super::nvi_pvi::{NviEngine, PviEngine};
50use super::order_block::OrderBlockEngine;
51pub use super::params::{ParamValue, TypedParams};
52use super::pivot_sets::{PivotSetType, PivotSetsEngine};
53use super::pivots_structure::PivotStructureEngine;
54use super::pmo::PriceMomentumOscillator;
55use super::pvt::PriceVolumeTrend;
56use super::rci::RciEngine;
57use super::relative_volatility::{RelativeVolatilityIndex, RelativeVolatilityVariant};
58use super::rsi::{Rsi, RsiSmoothing};
59use super::rvat::RelativeVolumeAtTime;
60use super::rvi::RviEngine;
61use super::smi::StochasticMomentumIndex;
62use super::smoothing::EmaInit;
63use super::stoch_rsi::StochRsi;
64use super::t3::T3;
65use super::tema::TemaEngine;
66use super::trend_quality::TrendQualityScoreEngine;
67use super::trend_structural::{
68 AroonEngine, DmiEngine, IchimokuEngine, ParabolicSarEngine, SupertrendEngine,
69};
70use super::trix::Trix;
71use super::tsi::Tsi;
72use super::twap::{AnchoredTwap, TwapAnchor, TwapWeighting};
73use super::ulcer::UlcerIndexEngine;
74use super::vidya::Vidya;
75use super::vix_fix::WilliamsVixFix;
76use super::volatility_indicators::{
77 DonchianChannelEngine, GarmanKlassVolatilityEngine, HistoricalVolatilityEngine,
78 KeltnerChannelEngine, TrueRangeEngine,
79};
80use super::volatility_regime::VolatilityRegimeDetector;
81use super::volume_flow::{CvdEngine, KlingerVolumeForceEngine};
82use super::volume_flow_hires::HiResVolumeFlowEngine;
83use super::volume_indicators::{AccDistEngine, CmfEngine, ObvEngine, RvolEngine, VolumeEngine};
84use super::volume_profile::VolumeProfileEngine;
85use super::volume_profile_extended::ExtendedVolumeProfileEngine;
86use super::volume_profile_persistent::PersistentVolumeProfileEngine;
87use super::vortex::VortexEngine;
88use super::vwap::Vwap;
89use super::wavetrend::WaveTrendEngine;
90use super::williams_r::WilliamsR;
91use super::zigzag::ZigZagEngine;
92use super::zigzag_advanced::{AdvancedZigZagEngine, ZigZagDeviationMode};
93use super::zscore::ZScoreEngine;
94use super::Indicator;
95
96#[derive(Debug, Clone, PartialEq)]
97#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
98pub struct IndicatorCatalogEntry {
99 pub name: &'static str,
100 pub description: &'static str,
101 pub default_params: HashMap<String, f64>,
102}
103
104pub fn catalog() -> Vec<IndicatorCatalogEntry> {
105 vec![
106 IndicatorCatalogEntry {
107 name: "rsi",
108 description: "Relative Strength Index (build_typed accepts smoothing=wilder|ema for the up/down averages; wilder is the default)",
109 default_params: [
110 ("rsi_len".to_string(), 14.0),
111 ("avg_len".to_string(), 3.0),
112 ("sig_len".to_string(), 3.0),
113 ("overbought".to_string(), 70.0),
114 ("oversold".to_string(), 30.0),
115 ]
116 .into(),
117 },
118 IndicatorCatalogEntry {
119 name: "macd",
120 description: "Moving Average Convergence Divergence",
121 default_params: [
122 ("fast_len".to_string(), 12.0),
123 ("slow_len".to_string(), 26.0),
124 ("signal_len".to_string(), 9.0),
125 ]
126 .into(),
127 },
128 IndicatorCatalogEntry {
129 name: "bollinger",
130 description: "Bollinger Bands (build_typed accepts variance=population|sample for the standard-deviation divisor; population is the default)",
131 default_params: [("len".to_string(), 20.0), ("mult".to_string(), 2.0)].into(),
132 },
133 IndicatorCatalogEntry {
134 name: "adx",
135 description: "Average Directional Index",
136 default_params: [
137 ("di_len".to_string(), 14.0),
138 ("adx_smooth".to_string(), 14.0),
139 ("level_weak".to_string(), 20.0),
140 ]
141 .into(),
142 },
143 IndicatorCatalogEntry {
144 name: "stoch_rsi",
145 description: "Stochastic RSI",
146 default_params: [
147 ("rsi_len".to_string(), 14.0),
148 ("stoch_len".to_string(), 14.0),
149 ("k_len".to_string(), 3.0),
150 ("d_len".to_string(), 3.0),
151 ("overbought".to_string(), 80.0),
152 ("oversold".to_string(), 20.0),
153 ]
154 .into(),
155 },
156 IndicatorCatalogEntry {
157 name: "cci",
158 description: "Commodity Channel Index",
159 default_params: [
160 ("cci_len".to_string(), 20.0),
161 ("overbought".to_string(), 100.0),
162 ("oversold".to_string(), -100.0),
163 ]
164 .into(),
165 },
166 IndicatorCatalogEntry {
167 name: "mfi",
168 description: "Money Flow Index",
169 default_params: [
170 ("mfi_len".to_string(), 14.0),
171 ("overbought".to_string(), 80.0),
172 ("oversold".to_string(), 20.0),
173 ]
174 .into(),
175 },
176 IndicatorCatalogEntry {
177 name: "twap",
178 description: "Anchored Time Weighted Average Price (no volume; build_typed accepts weighting=per_bar|by_duration and anchor=continuous|daily|manual_timestamp)",
179 default_params: [("day_start_offset".to_string(), 0.0)].into(),
180 },
181 IndicatorCatalogEntry {
182 name: "relative_volatility",
183 description: "Relative Volatility Index (the RSI construction applied to the standard deviation; build_typed accepts variant=close|high_low). Unrelated to \"rvi\", the Relative Vigor Index",
184 default_params: [
185 ("stdev_len".to_string(), 10.0),
186 ("smooth_len".to_string(), 14.0),
187 ]
188 .into(),
189 },
190 IndicatorCatalogEntry {
191 name: "bbtrend",
192 description: "BBTrend (how far a short Bollinger set has moved out of a long one, in percent of the short basis; build_typed accepts variance=population|sample for both sets)",
193 default_params: [
194 ("short_len".to_string(), 20.0),
195 ("long_len".to_string(), 50.0),
196 ("mult".to_string(), 2.0),
197 ]
198 .into(),
199 },
200 IndicatorCatalogEntry {
201 name: "pvt",
202 description: "Price Volume Trend (volume weighted by the relative price change, accumulated)",
203 default_params: [].into(),
204 },
205 IndicatorCatalogEntry {
206 name: "pmo",
207 description: "Price Momentum Oscillator (twice-smoothed one-bar return with alpha = 2/length, scaled by ten)",
208 default_params: [
209 ("length_1".to_string(), 35.0),
210 ("length_2".to_string(), 20.0),
211 ("signal_len".to_string(), 10.0),
212 ]
213 .into(),
214 },
215 IndicatorCatalogEntry {
216 name: "rvat",
217 description: "Relative Volume at Time (volume against the same time of day on previous days, regular and cumulative)",
218 default_params: [
219 ("days".to_string(), 10.0),
220 ("day_start_offset".to_string(), 0.0),
221 ("bar_seconds".to_string(), 60.0),
222 ]
223 .into(),
224 },
225 IndicatorCatalogEntry {
226 name: "smi",
227 description: "Stochastic Momentum Index (double-smoothed position of the close relative to the midpoint of the high-low range)",
228 default_params: [
229 ("len".to_string(), 10.0),
230 ("smooth_1".to_string(), 3.0),
231 ("smooth_2".to_string(), 3.0),
232 ("signal_len".to_string(), 3.0),
233 ]
234 .into(),
235 },
236 IndicatorCatalogEntry {
237 name: "rci",
238 description: "Rank Correlation Index (rank of prices against rank of time, scaled by 100)",
239 default_params: [("len".to_string(), 9.0)].into(),
240 },
241 IndicatorCatalogEntry {
242 name: "ulcer_index",
243 description: "Ulcer Index (root mean square of the percentage drawdowns below the running high)",
244 default_params: [("len".to_string(), 14.0)].into(),
245 },
246 IndicatorCatalogEntry {
247 name: "t3",
248 description: "Tillson T3 (weighted combination of six chained EMAs; v is a shape factor, not volume)",
249 default_params: [("period".to_string(), 5.0), ("v".to_string(), 0.7)].into(),
250 },
251 IndicatorCatalogEntry {
252 name: "vidya",
253 description: "Variable Index Dynamic Average (CMO-scaled exponential smoothing)",
254 default_params: [("cmo_len".to_string(), 9.0), ("ema_len".to_string(), 12.0)].into(),
255 },
256 IndicatorCatalogEntry {
257 name: "trix",
258 description: "TRIX (rate of change of a triple-smoothed price series, in percent)",
259 default_params: [("len".to_string(), 15.0), ("signal_len".to_string(), 9.0)].into(),
260 },
261 IndicatorCatalogEntry {
262 name: "efi",
263 description: "Elder's Force Index (price change times volume, EMA-smoothed)",
264 default_params: [("ema_len".to_string(), 13.0)].into(),
265 },
266 IndicatorCatalogEntry {
267 name: "atr",
268 description: "Average True Range (build_typed accepts smoothing=rma|sma|ema|wma for the true-range average; rma is the default and the signal line stays Wilder-smoothed)",
269 default_params: [("atr_len".to_string(), 14.0), ("sig_len".to_string(), 20.0)].into(),
270 },
271 IndicatorCatalogEntry {
272 name: "chande_kroll",
273 description: "Chande Kroll Stop (window extremes minus/plus a Wilder-smoothed ATR, passed through a second extreme window)",
274 default_params: [
275 ("atr_len".to_string(), 10.0),
276 ("stop_len".to_string(), 9.0),
277 ("mult".to_string(), 3.0),
278 ]
279 .into(),
280 },
281 IndicatorCatalogEntry {
282 name: "chandelier_exit",
283 description: "Chandelier Exit (ATR trailing stop with direction flip)",
284 default_params: [("length".to_string(), 22.0), ("atr_mult".to_string(), 3.0)].into(),
285 },
286 IndicatorCatalogEntry {
287 name: "chandelier_flip_radar",
288 description: "Chandelier Exit ratchet extended with adaptive multiplier, body-filtered weak flips, and bull/bear trap detection (use ChandelierFlipRadarEngine::new directly for use_close_extremes=false or simple_adaptive=true; this f64-only entry uses the documented defaults for both)",
289 default_params: [
290 ("length".to_string(), 30.0),
291 ("atr_mult".to_string(), 4.5),
292 ("body_filter_atr".to_string(), 0.80),
293 ("danger_dist_atr".to_string(), 0.35),
294 ("warn_dist_atr".to_string(), 0.75),
295 ]
296 .into(),
297 },
298 IndicatorCatalogEntry {
299 name: "midas",
300 description: "MIDAS launch-anchored curve with Topfinder/Bottomfinder projection (build_typed with mode=topfinder|bottomfinder)",
301 default_params: [("maturity_bars".to_string(), 20.0)].into(),
302 },
303 IndicatorCatalogEntry {
304 name: "hires_volume_flow",
305 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)",
306 default_params: [("window_len".to_string(), 20.0)].into(),
307 },
308 IndicatorCatalogEntry {
309 name: "extended_volume_profile",
310 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)",
311 default_params: [("lookback".to_string(), 70.0), ("num_bins".to_string(), 30.0)].into(),
312 },
313 IndicatorCatalogEntry {
314 name: "persistent_volume_profile",
315 description: "Fixed-price-grid volume profile with real bin lifecycle (birth/growth/expiry across updates) and a per-bin absorption profile",
316 default_params: [("lookback".to_string(), 70.0), ("bin_width".to_string(), 1.0)].into(),
317 },
318 IndicatorCatalogEntry {
319 name: "trend_relationship",
320 description: "Adaptive trend relationship between two configurable smoothers (build via build_typed with fast_kind/slow_kind params: ema|sma|rma|alma|jma)",
321 default_params: [("fast_len".to_string(), 9.0), ("slow_len".to_string(), 21.0)].into(),
322 },
323 IndicatorCatalogEntry {
324 name: "williams_r",
325 description: "Williams %R",
326 default_params: [
327 ("wpr_len".to_string(), 14.0),
328 ("overbought".to_string(), 80.0),
329 ("oversold".to_string(), 20.0),
330 ]
331 .into(),
332 },
333 IndicatorCatalogEntry {
334 name: "tsi",
335 description: "True Strength Index",
336 default_params: [
337 ("long_len".to_string(), 25.0),
338 ("short_len".to_string(), 13.0),
339 ("sig_len".to_string(), 7.0),
340 ]
341 .into(),
342 },
343 IndicatorCatalogEntry {
344 name: "fisher_transform",
345 description: "Fisher Transform",
346 default_params: [
347 ("fish_len".to_string(), 10.0),
348 ("overbought".to_string(), 1.5),
349 ("oversold".to_string(), -1.5),
350 ]
351 .into(),
352 },
353 IndicatorCatalogEntry {
354 name: "order_block",
355 description: "Order block detection from displacement candles (ATR-filtered)",
356 default_params: [
357 ("atr_len".to_string(), 14.0),
358 ("min_disp".to_string(), 1.0),
359 ]
360 .into(),
361 },
362 IndicatorCatalogEntry {
363 name: "liquidity_fvg",
364 description: "Fair value gap (imbalance) detection",
365 default_params: [("lookback".to_string(), 20.0)].into(),
366 },
367 IndicatorCatalogEntry {
368 name: "market_structure_breaks",
369 description: "Break of structure / change of character (BOS/CHoCH) detection",
370 default_params: [("lookback".to_string(), 5.0)].into(),
371 },
372 IndicatorCatalogEntry {
373 name: "pivots_structure",
374 description: "Swing pivot detection with a rolling structure score",
375 default_params: [
376 ("left_bars".to_string(), 5.0),
377 ("right_bars".to_string(), 5.0),
378 ("score_window".to_string(), 10.0),
379 ]
380 .into(),
381 },
382 IndicatorCatalogEntry {
383 name: "volume_profile",
384 description: "Price/volume distribution profile with point-of-control",
385 default_params: [
386 ("lookback".to_string(), 70.0),
387 ("num_bins".to_string(), 30.0),
388 ]
389 .into(),
390 },
391 IndicatorCatalogEntry {
392 name: "money_flow_profile",
393 description: "Volume-by-price profile binned by dollar volume (volume x price) instead of raw volume, plus an aggregate bull/bear flow-bias percentage",
394 default_params: [
395 ("lookback".to_string(), 200.0),
396 ("rows".to_string(), 25.0),
397 ("va_pct".to_string(), 0.70),
398 ]
399 .into(),
400 },
401 IndicatorCatalogEntry {
402 name: "vwap",
403 description: "Rolling Volume Weighted Average Price with sigma bands and slope",
404 default_params: [
405 ("window".to_string(), 390.0),
406 ("slope_lookback".to_string(), 20.0),
407 ]
408 .into(),
409 },
410 IndicatorCatalogEntry {
411 name: "vix_fix",
412 description: "Williams Vix Fix volatility spike detector",
413 default_params: [
414 ("pd".to_string(), 22.0),
415 ("bband_len".to_string(), 20.0),
416 ("mult".to_string(), 2.0),
417 ]
418 .into(),
419 },
420 IndicatorCatalogEntry {
421 name: "candle_story",
422 description: "Normalised candle classification — reports every pattern it recognises",
423 default_params: [
424 ("pin_wick_min".to_string(), 0.55),
425 ("pin_close_pos".to_string(), 0.65),
426 ("marubozu_body_min".to_string(), 0.82),
427 ("doji_body_max".to_string(), 0.08),
428 ("hammer_wick_body_min".to_string(), 2.0),
429 ("min_range_atr".to_string(), 0.5),
430 ("atr_len".to_string(), 14.0),
431 ("trend_lookback".to_string(), 10.0),
432 ]
433 .into(),
434 },
435 IndicatorCatalogEntry {
436 name: "efficiency",
437 description: "Kaufman-style leg efficiency ratio",
438 default_params: [("len".to_string(), 14.0)].into(),
439 },
440 IndicatorCatalogEntry {
441 name: "volume",
442 description: "Volume and Average Volume",
443 default_params: [("ma_period".to_string(), 20.0)].into(),
444 },
445 IndicatorCatalogEntry {
446 name: "rvol",
447 description: "Relative Volume vs Moving Average",
448 default_params: [("period".to_string(), 20.0)].into(),
449 },
450 IndicatorCatalogEntry {
451 name: "obv",
452 description: "On-Balance Volume",
453 default_params: HashMap::new(),
454 },
455 IndicatorCatalogEntry {
456 name: "cmf",
457 description: "Chaikin Money Flow",
458 default_params: [("period".to_string(), 20.0)].into(),
459 },
460 IndicatorCatalogEntry {
461 name: "acc_dist",
462 description: "Accumulation / Distribution Line",
463 default_params: HashMap::new(),
464 },
465 IndicatorCatalogEntry {
466 name: "true_range",
467 description: "True Range in price units",
468 default_params: HashMap::new(),
469 },
470 IndicatorCatalogEntry {
471 name: "keltner",
472 description: "Keltner Channels",
473 default_params: [
474 ("ema_period".to_string(), 20.0),
475 ("atr_period".to_string(), 10.0),
476 ("multiplier".to_string(), 2.0),
477 ]
478 .into(),
479 },
480 IndicatorCatalogEntry {
481 name: "donchian",
482 description: "Donchian Channels",
483 default_params: [("period".to_string(), 20.0)].into(),
484 },
485 IndicatorCatalogEntry {
486 name: "historical_volatility",
487 description: "Annualized Historical Volatility",
488 default_params: [("period".to_string(), 20.0)].into(),
489 },
490 IndicatorCatalogEntry {
491 name: "garman_klass",
492 description: "Garman-Klass Volatility Estimator",
493 default_params: [("period".to_string(), 20.0)].into(),
494 },
495 IndicatorCatalogEntry {
496 name: "sma",
497 description: "Simple Moving Average",
498 default_params: [("period".to_string(), 20.0)].into(),
499 },
500 IndicatorCatalogEntry {
501 name: "ema",
502 description: "Exponential Moving Average (build_typed accepts init=first_sample|sma for the seed; first_sample is the default)",
503 default_params: [("period".to_string(), 20.0)].into(),
504 },
505 IndicatorCatalogEntry {
506 name: "wma",
507 description: "Weighted Moving Average",
508 default_params: [("period".to_string(), 20.0)].into(),
509 },
510 IndicatorCatalogEntry {
511 name: "vwma",
512 description: "Volume-Weighted Moving Average",
513 default_params: [("period".to_string(), 20.0)].into(),
514 },
515 IndicatorCatalogEntry {
516 name: "hma",
517 description: "Hull Moving Average",
518 default_params: [("period".to_string(), 20.0)].into(),
519 },
520 IndicatorCatalogEntry {
521 name: "dema",
522 description: "Double Exponential Moving Average",
523 default_params: [("period".to_string(), 20.0)].into(),
524 },
525 IndicatorCatalogEntry {
526 name: "kama",
527 description: "Kaufman's Adaptive Moving Average",
528 default_params: [
529 ("period".to_string(), 10.0),
530 ("fast_period".to_string(), 2.0),
531 ("slow_period".to_string(), 30.0),
532 ]
533 .into(),
534 },
535 IndicatorCatalogEntry {
536 name: "dmi",
537 description: "Directional Movement Index (+DI / -DI)",
538 default_params: [("period".to_string(), 14.0)].into(),
539 },
540 IndicatorCatalogEntry {
541 name: "aroon",
542 description: "Aroon Indicator (Up, Down, Oscillator)",
543 default_params: [("period".to_string(), 14.0)].into(),
544 },
545 IndicatorCatalogEntry {
546 name: "parabolic_sar",
547 description: "Parabolic SAR",
548 default_params: [("step".to_string(), 0.02), ("max_step".to_string(), 0.20)].into(),
549 },
550 IndicatorCatalogEntry {
551 name: "supertrend",
552 description: "Supertrend ATR Trailing Stop",
553 default_params: [
554 ("period".to_string(), 10.0),
555 ("multiplier".to_string(), 3.0),
556 ]
557 .into(),
558 },
559 IndicatorCatalogEntry {
560 name: "ichimoku",
561 description: "Ichimoku Kinko Hyo Cloud",
562 default_params: [
563 ("tenkan_p".to_string(), 9.0),
564 ("kijun_p".to_string(), 26.0),
565 ("senkou_b_p".to_string(), 52.0),
566 ]
567 .into(),
568 },
569 IndicatorCatalogEntry {
570 name: "stochastic",
571 description: "Classic Stochastic Oscillator",
572 default_params: [
573 ("k_period".to_string(), 14.0),
574 ("d_period".to_string(), 3.0),
575 ]
576 .into(),
577 },
578 IndicatorCatalogEntry {
579 name: "roc",
580 description: "Rate of Change / Momentum",
581 default_params: [("period".to_string(), 12.0)].into(),
582 },
583 IndicatorCatalogEntry {
584 name: "ultimate_oscillator",
585 description: "Ultimate Oscillator",
586 default_params: [
587 ("period1".to_string(), 7.0),
588 ("period2".to_string(), 14.0),
589 ("period3".to_string(), 28.0),
590 ]
591 .into(),
592 },
593 IndicatorCatalogEntry {
594 name: "awesome_oscillator",
595 description: "Awesome Oscillator",
596 default_params: [
597 ("fast_period".to_string(), 5.0),
598 ("slow_period".to_string(), 34.0),
599 ]
600 .into(),
601 },
602 IndicatorCatalogEntry {
603 name: "ppo",
604 description: "Percentage Price Oscillator",
605 default_params: [
606 ("fast_period".to_string(), 12.0),
607 ("slow_period".to_string(), 26.0),
608 ("signal_period".to_string(), 9.0),
609 ]
610 .into(),
611 },
612 IndicatorCatalogEntry {
613 name: "wavetrend",
614 description: "WaveTrend Oscillator (wt1, wt2)",
615 default_params: [
616 ("n1".to_string(), 10.0),
617 ("n2".to_string(), 21.0),
618 ("ob_level".to_string(), 60.0),
619 ("os_level".to_string(), -60.0),
620 ]
621 .into(),
622 },
623 IndicatorCatalogEntry {
624 name: "cmo",
625 description: "Chande Momentum Oscillator",
626 default_params: [("period".to_string(), 14.0)].into(),
627 },
628 IndicatorCatalogEntry {
629 name: "elder_ray",
630 description: "Elder Ray Index (Bull/Bear Power)",
631 default_params: [("period".to_string(), 13.0)].into(),
632 },
633 IndicatorCatalogEntry {
634 name: "anchored_vwap",
635 description: "Anchored VWAP Engine",
636 default_params: [("mult1".to_string(), 1.0), ("mult2".to_string(), 2.0)].into(),
637 },
638 IndicatorCatalogEntry {
639 name: "cvd",
640 description: "Cumulative Volume Delta (Aufteilung aus der Bar-Form geschätzt, \
646 nicht aus klassifizierten Abschlüssen)",
647 default_params: [].into(),
648 },
649 IndicatorCatalogEntry {
650 name: "klinger",
651 description: "Klinger Volume Force Oscillator",
652 default_params: [
653 ("fast_len".to_string(), 34.0),
654 ("slow_len".to_string(), 55.0),
655 ("signal_len".to_string(), 13.0),
656 ]
657 .into(),
658 },
659 IndicatorCatalogEntry {
660 name: "zigzag",
661 description: "ZigZag Swing Leg Engine",
662 default_params: [
663 ("depth".to_string(), 12.0),
664 ("deviation_pct".to_string(), 5.0),
665 ]
666 .into(),
667 },
668 IndicatorCatalogEntry {
669 name: "zigzag_advanced",
670 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",
671 default_params: [
672 ("depth".to_string(), 3.0),
673 ("backstep".to_string(), 2.0),
674 ("deviation_pct".to_string(), 1.0),
675 ("atr_len".to_string(), 14.0),
676 ]
677 .into(),
678 },
679 IndicatorCatalogEntry {
680 name: "pivot_sets",
681 description: "Multi-Pivot Set Engine",
682 default_params: [].into(),
683 },
684 IndicatorCatalogEntry {
685 name: "tema",
686 description: "Triple Exponential Moving Average",
687 default_params: [("period".to_string(), 14.0)].into(),
688 },
689 IndicatorCatalogEntry {
690 name: "lsma",
691 description: "Least Squares Moving Average / Linear Regression",
692 default_params: [("period".to_string(), 25.0)].into(),
693 },
694 IndicatorCatalogEntry {
695 name: "mcginley",
696 description: "McGinley Dynamic Moving Average",
697 default_params: [("period".to_string(), 14.0)].into(),
698 },
699 IndicatorCatalogEntry {
700 name: "envelope",
701 description: "Moving Average Envelopes",
702 default_params: [("period".to_string(), 20.0), ("percent".to_string(), 2.5)].into(),
703 },
704 IndicatorCatalogEntry {
705 name: "choppiness",
706 description: "Choppiness Index",
707 default_params: [("period".to_string(), 14.0)].into(),
708 },
709 IndicatorCatalogEntry {
710 name: "vortex",
711 description: "Vortex Indicator (+VI, -VI)",
712 default_params: [("period".to_string(), 14.0)].into(),
713 },
714 IndicatorCatalogEntry {
715 name: "alligator",
716 description: "Williams Alligator (Jaw, Teeth, Lips)",
717 default_params: [].into(),
718 },
719 IndicatorCatalogEntry {
720 name: "connors_rsi",
721 description: "Connors RSI",
722 default_params: [
723 ("rsi_len".to_string(), 3.0),
724 ("streak_len".to_string(), 2.0),
725 ("rank_len".to_string(), 100.0),
726 ]
727 .into(),
728 },
729 IndicatorCatalogEntry {
730 name: "coppock",
731 description: "Coppock Curve",
732 default_params: [].into(),
733 },
734 IndicatorCatalogEntry {
735 name: "dpo",
736 description: "Detrended Price Oscillator",
737 default_params: [("period".to_string(), 21.0)].into(),
738 },
739 IndicatorCatalogEntry {
740 name: "kst",
741 description: "Know Sure Thing Oscillator",
742 default_params: [].into(),
743 },
744 IndicatorCatalogEntry {
745 name: "mass_index",
746 description: "Mass Index Reversal Detector",
747 default_params: [("period".to_string(), 25.0)].into(),
748 },
749 IndicatorCatalogEntry {
750 name: "rvi",
751 description: "Relative Vigor Index",
752 default_params: [("period".to_string(), 10.0)].into(),
753 },
754 IndicatorCatalogEntry {
755 name: "bop",
756 description: "Balance of Power",
757 default_params: [("period".to_string(), 14.0)].into(),
758 },
759 IndicatorCatalogEntry {
760 name: "eom",
761 description: "Ease of Movement",
762 default_params: [
763 ("period".to_string(), 14.0),
764 ("volume_divisor".to_string(), 10000.0),
765 ]
766 .into(),
767 },
768 IndicatorCatalogEntry {
769 name: "nvi",
770 description: "Negative Volume Index",
771 default_params: [].into(),
772 },
773 IndicatorCatalogEntry {
774 name: "pvi",
775 description: "Positive Volume Index",
776 default_params: [].into(),
777 },
778 IndicatorCatalogEntry {
779 name: "chaikin_oscillator",
780 description: "Chaikin Oscillator",
781 default_params: [
782 ("fast_len".to_string(), 3.0),
783 ("slow_len".to_string(), 10.0),
784 ]
785 .into(),
786 },
787 IndicatorCatalogEntry {
788 name: "bos_choch",
789 description: "BOS and CHoCH Market Structure Engine",
790 default_params: [("pivot_len".to_string(), 5.0)].into(),
791 },
792 IndicatorCatalogEntry {
793 name: "liquidity_sweeps",
794 description: "Liquidity Sweeps and EQH/EQL Detector",
795 default_params: [
796 ("pivot_len".to_string(), 5.0),
797 ("tolerance_pct".to_string(), 0.2),
798 ]
799 .into(),
800 },
801 IndicatorCatalogEntry {
802 name: "liquidity_pools",
803 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)",
804 default_params: [
805 ("pivot_len".to_string(), 5.0),
806 ("tolerance_pct".to_string(), 0.2),
807 ]
808 .into(),
809 },
810 IndicatorCatalogEntry {
811 name: "wyckoff",
812 description: "Wyckoff accumulation/distribution state machine: range-lock, Phases A-E, Spring/UTAD, SOS/SOW/LPS/LPSY, sequence validation and Cause/Quality scoring",
813 default_params: [
814 ("range_lookback".to_string(), 20.0),
815 ("range_atr_max".to_string(), 3.0),
816 ("min_range_bars".to_string(), 6.0),
817 ]
818 .into(),
819 },
820 IndicatorCatalogEntry {
821 name: "trend_quality",
822 description: "Trend Quality Score Engine",
823 default_params: [("period".to_string(), 14.0)].into(),
824 },
825 IndicatorCatalogEntry {
826 name: "buy_sell_pressure",
827 description: "Buy/Sell Pressure Estimator",
828 default_params: [("period".to_string(), 14.0)].into(),
829 },
830 IndicatorCatalogEntry {
831 name: "volatility_regime",
832 description: "Volatility Regime & Squeeze Detector",
833 default_params: [
834 ("period".to_string(), 20.0),
835 ("bb_mult".to_string(), 2.0),
836 ("kc_mult".to_string(), 1.5),
837 ]
838 .into(),
839 },
840 IndicatorCatalogEntry {
841 name: "zscore",
842 description: "Rolling Z-Score Engine",
843 default_params: [("period".to_string(), 20.0)].into(),
844 },
845 IndicatorCatalogEntry {
846 name: "multi_factor",
847 description: "Multi-Factor Composite Market Score",
848 default_params: [("period".to_string(), 14.0)].into(),
849 },
850 ]
851}
852
853#[derive(Debug, Clone, Copy, PartialEq)]
863#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
864pub enum OutputRange {
865 Bounded { min: f64, max: f64 },
867 Centered { center: f64 },
870 NonNegative,
872 Unbounded,
874}
875
876pub fn output_range(name: &str) -> OutputRange {
883 const PROZENT: OutputRange = OutputRange::Bounded {
887 min: 0.0,
888 max: 100.0,
889 };
890
891 match name {
892 "adx"
893 | "choppiness"
894 | "connors_rsi"
895 | "efficiency"
896 | "mfi"
897 | "relative_volatility"
898 | "rsi"
899 | "stoch_rsi"
900 | "stochastic"
901 | "ultimate_oscillator"
902 | "williams_r" => PROZENT,
903
904 "aroon" | "cmo" | "dmi" | "rci" | "tsi" => OutputRange::Bounded {
909 min: -100.0,
910 max: 100.0,
911 },
912
913 "bop" | "cmf" => OutputRange::Bounded {
915 min: -1.0,
916 max: 1.0,
917 },
918
919 "awesome_oscillator" | "cci" | "chaikin_oscillator" | "coppock" | "dpo" | "efi"
921 | "elder_ray" | "eom" | "fisher_transform" | "klinger" | "kst" | "macd" | "ppo" | "roc"
922 | "bbtrend" | "pmo" | "smi" | "trix" | "wavetrend" | "zscore" => {
923 OutputRange::Centered { center: 0.0 }
924 }
925
926 "atr"
928 | "garman_klass"
929 | "historical_volatility"
930 | "mass_index"
931 | "rvat"
932 | "rvol"
933 | "true_range"
934 | "ulcer_index"
935 | "vix_fix"
936 | "volume"
937 | "vortex" => OutputRange::NonNegative,
938
939 _ => OutputRange::Unbounded,
940 }
941}
942
943pub fn threshold_params(name: &str) -> &'static [&'static str] {
949 match name {
950 "cci" | "fisher_transform" | "mfi" | "rsi" | "stoch_rsi" | "williams_r" => {
951 &["oversold", "overbought"]
952 }
953 "adx" => &["level_weak"],
954 "wavetrend" => &["os_level", "ob_level"],
955 _ => &[],
956 }
957}
958
959impl IndicatorCatalogEntry {
960 pub fn output_range(&self) -> OutputRange {
962 output_range(self.name)
963 }
964
965 pub fn thresholds(&self) -> Vec<f64> {
970 let mut werte: Vec<f64> = threshold_params(self.name)
971 .iter()
972 .filter_map(|p| self.default_params.get(*p).copied())
973 .filter(|v| v.is_finite())
974 .collect();
975 werte.sort_by(|a, b| a.partial_cmp(b).expect("filtered to finite values"));
976 werte
977 }
978}
979
980#[derive(Debug, Clone, PartialEq)]
981#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
982pub enum RegistryError {
983 UnknownIndicator(String),
984 InvalidParameter {
985 parameter: String,
986 value: f64,
987 reason: String,
988 },
989 UnsupportedParameterType {
992 parameter: String,
993 type_name: String,
994 },
995 InvalidEnumValue {
997 parameter: String,
998 value: String,
999 reason: String,
1000 },
1001 IncompatibleParameter {
1005 parameter: String,
1006 indicator: String,
1007 reason: String,
1008 },
1009}
1010
1011impl std::fmt::Display for RegistryError {
1012 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1013 match self {
1014 RegistryError::UnknownIndicator(name) => write!(f, "Unknown indicator: {}", name),
1015 RegistryError::InvalidParameter {
1016 parameter,
1017 value,
1018 reason,
1019 } => write!(
1020 f,
1021 "Invalid parameter '{}' (value {}): {}",
1022 parameter, value, reason
1023 ),
1024 RegistryError::UnsupportedParameterType {
1025 parameter,
1026 type_name,
1027 } => write!(
1028 f,
1029 "Parameter '{}' has unsupported type '{}' for this indicator",
1030 parameter, type_name
1031 ),
1032 RegistryError::InvalidEnumValue {
1033 parameter,
1034 value,
1035 reason,
1036 } => write!(
1037 f,
1038 "Invalid value '{}' for parameter '{}': {}",
1039 value, parameter, reason
1040 ),
1041 RegistryError::IncompatibleParameter {
1042 parameter,
1043 indicator,
1044 reason,
1045 } => write!(
1046 f,
1047 "Parameter '{}' is not compatible with indicator '{}': {}",
1048 parameter, indicator, reason
1049 ),
1050 }
1051 }
1052}
1053
1054impl std::error::Error for RegistryError {}
1055
1056fn get_usize_p(
1057 params: &HashMap<String, f64>,
1058 name: &str,
1059 default: usize,
1060 min: usize,
1061 max: usize,
1062) -> Result<usize, RegistryError> {
1063 if let Some(&val) = params.get(name) {
1064 if !val.is_finite() || val.fract() != 0.0 || val < (min as f64) || val > (max as f64) {
1065 return Err(RegistryError::InvalidParameter {
1066 parameter: name.to_string(),
1067 value: val,
1068 reason: format!(
1069 "Value must be a whole, finite number between {} and {}",
1070 min, max
1071 ),
1072 });
1073 }
1074 Ok(val as usize)
1075 } else {
1076 Ok(default)
1077 }
1078}
1079
1080fn get_f64_p(
1081 params: &HashMap<String, f64>,
1082 name: &str,
1083 default: f64,
1084 min: f64,
1085 max: f64,
1086) -> Result<f64, RegistryError> {
1087 if let Some(&val) = params.get(name) {
1088 if !val.is_finite() || val < min || val > max {
1089 return Err(RegistryError::InvalidParameter {
1090 parameter: name.to_string(),
1091 value: val,
1092 reason: format!("Value must be a finite number between {} and {}", min, max),
1093 });
1094 }
1095 Ok(val)
1096 } else {
1097 Ok(default)
1098 }
1099}
1100
1101fn get_usize_p_aliased(
1106 params: &HashMap<String, f64>,
1107 canonical: &str,
1108 legacy_alias: &str,
1109 default: usize,
1110 min: usize,
1111 max: usize,
1112) -> Result<usize, RegistryError> {
1113 match (params.get(canonical), params.get(legacy_alias)) {
1114 (Some(&canonical_val), Some(&alias_val)) if canonical_val != alias_val => {
1115 Err(RegistryError::InvalidParameter {
1116 parameter: canonical.to_string(),
1117 value: canonical_val,
1118 reason: format!(
1119 "conflicting values for '{canonical}' ({canonical_val}) and legacy alias \
1120 '{legacy_alias}' ({alias_val}); set only one"
1121 ),
1122 })
1123 }
1124 (Some(_), _) => get_usize_p(params, canonical, default, min, max),
1125 (None, Some(_)) => get_usize_p(params, legacy_alias, default, min, max),
1126 (None, None) => Ok(default),
1127 }
1128}
1129
1130fn ensure_less(
1131 parameter: &str,
1132 value: f64,
1133 upper_parameter: &str,
1134 upper_value: f64,
1135) -> Result<(), RegistryError> {
1136 if value < upper_value {
1137 return Ok(());
1138 }
1139
1140 Err(RegistryError::InvalidParameter {
1141 parameter: parameter.to_string(),
1142 value,
1143 reason: format!("{} must be smaller than {}", parameter, upper_parameter),
1144 })
1145}
1146
1147pub fn build_checked(
1148 name: &str,
1149 params: &HashMap<String, f64>,
1150) -> Result<Box<dyn Indicator>, RegistryError> {
1151 match name.to_lowercase().as_str() {
1152 "rsi" => Ok(Box::new(build_rsi(params, RsiSmoothing::Wilder)?)),
1153 "macd" => {
1154 let fast_len = get_usize_p(params, "fast_len", 12, 1, 10000)?;
1155 let slow_len = get_usize_p(params, "slow_len", 26, 1, 10000)?;
1156 let signal_len = get_usize_p(params, "signal_len", 9, 1, 10000)?;
1157 ensure_less("fast_len", fast_len as f64, "slow_len", slow_len as f64)?;
1158 Ok(Box::new(Macd::new(fast_len, slow_len, signal_len)))
1159 }
1160 "bollinger" | "bb" => Ok(Box::new(build_bollinger(
1161 params,
1162 VarianceConvention::Population,
1163 )?)),
1164 "adx" => {
1165 let di_len = get_usize_p(params, "di_len", 14, 1, 10000)?;
1166 let adx_smooth = get_usize_p(params, "adx_smooth", 14, 1, 10000)?;
1167 let level_weak = get_f64_p(params, "level_weak", 20.0, 0.0, 100.0)?;
1168 Ok(Box::new(Adx::new(di_len, adx_smooth, 3, level_weak)))
1169 }
1170 "stoch_rsi" | "srsi" => {
1171 let rsi_len = get_usize_p(params, "rsi_len", 14, 1, 10000)?;
1172 let stoch_len = get_usize_p(params, "stoch_len", 14, 1, 10000)?;
1173 let k_len = get_usize_p(params, "k_len", 3, 1, 10000)?;
1174 let d_len = get_usize_p(params, "d_len", 3, 1, 10000)?;
1175 let overbought = get_f64_p(params, "overbought", 80.0, 0.0, 100.0)?;
1176 let oversold = get_f64_p(params, "oversold", 20.0, 0.0, 100.0)?;
1177 ensure_less("oversold", oversold, "overbought", overbought)?;
1178 Ok(Box::new(StochRsi::new(
1179 rsi_len, stoch_len, k_len, d_len, 50.0, overbought, oversold, 5, true, 50, 50, 4,
1180 10.0,
1181 )))
1182 }
1183 "cci" => {
1184 let cci_len = get_usize_p(params, "cci_len", 20, 1, 10000)?;
1185 let overbought = get_f64_p(params, "overbought", 100.0, -1000.0, 1000.0)?;
1186 let oversold = get_f64_p(params, "oversold", -100.0, -1000.0, 1000.0)?;
1187 ensure_less("oversold", oversold, "overbought", overbought)?;
1188 Ok(Box::new(Cci::new(
1189 cci_len, 3, 3, 5, oversold, overbought, true, 100, 4, 25.0,
1190 )))
1191 }
1192 "mfi" => {
1193 let mfi_len = get_usize_p(params, "mfi_len", 14, 1, 10000)?;
1194 let overbought = get_f64_p(params, "overbought", 80.0, 0.0, 100.0)?;
1195 let oversold = get_f64_p(params, "oversold", 20.0, 0.0, 100.0)?;
1196 ensure_less("oversold", oversold, "overbought", overbought)?;
1197 Ok(Box::new(Mfi::new(
1198 mfi_len, 3, 3, 50.0, overbought, oversold, 5, true,
1199 )))
1200 }
1201 "twap" => {
1202 let offset = get_f64_p(params, "day_start_offset", 0.0, -86_400.0, 86_400.0)?;
1203 Ok(Box::new(AnchoredTwap::new(
1204 TwapAnchor::Daily {
1205 start_offset_seconds: offset as i64,
1206 },
1207 crate::model::Source::Close,
1208 TwapWeighting::PerBar,
1209 )))
1210 }
1211 "relative_volatility" => Ok(Box::new(build_relative_volatility(
1212 params,
1213 RelativeVolatilityVariant::Close,
1214 )?)),
1215 "bbtrend" => Ok(Box::new(build_bbtrend(
1216 params,
1217 VarianceConvention::Population,
1218 )?)),
1219 "pvt" => Ok(Box::new(PriceVolumeTrend::new())),
1220 "pmo" => {
1221 let length_1 = get_usize_p(params, "length_1", 35, 1, 10000)?;
1222 let length_2 = get_usize_p(params, "length_2", 20, 1, 10000)?;
1223 let signal_len = get_usize_p(params, "signal_len", 10, 1, 10000)?;
1224 Ok(Box::new(PriceMomentumOscillator::new(
1225 length_1, length_2, signal_len,
1226 )))
1227 }
1228 "rvat" => {
1229 let days = get_usize_p(params, "days", 10, 1, 1000)?;
1230 let day_start_offset = get_f64_p(params, "day_start_offset", 0.0, -86_400.0, 86_400.0)?;
1231 let bar_seconds = get_f64_p(params, "bar_seconds", 60.0, 1.0, 86_400.0)?;
1232 Ok(Box::new(RelativeVolumeAtTime::new(
1233 days,
1234 day_start_offset as i64,
1235 bar_seconds as i64,
1236 )))
1237 }
1238 "smi" => {
1239 let len = get_usize_p(params, "len", 10, 1, 10000)?;
1240 let smooth_1 = get_usize_p(params, "smooth_1", 3, 1, 10000)?;
1241 let smooth_2 = get_usize_p(params, "smooth_2", 3, 1, 10000)?;
1242 let signal_len = get_usize_p(params, "signal_len", 3, 1, 10000)?;
1243 Ok(Box::new(StochasticMomentumIndex::new(
1244 len, smooth_1, smooth_2, signal_len,
1245 )))
1246 }
1247 "rci" => {
1248 let len = get_usize_p(params, "len", 9, 2, 10000)?;
1249 Ok(Box::new(RciEngine::new(len)))
1250 }
1251 "ulcer_index" => {
1252 let len = get_usize_p(params, "len", 14, 1, 10000)?;
1253 Ok(Box::new(UlcerIndexEngine::new(len)))
1254 }
1255 "t3" => {
1256 let period = get_usize_p(params, "period", 5, 1, 10000)?;
1257 let v = get_f64_p(params, "v", 0.7, 0.0, 1.0)?;
1258 Ok(Box::new(T3::new(period, v)))
1259 }
1260 "vidya" => {
1261 let cmo_len = get_usize_p(params, "cmo_len", 9, 1, 10000)?;
1262 let ema_len = get_usize_p(params, "ema_len", 12, 1, 10000)?;
1263 Ok(Box::new(Vidya::new(cmo_len, ema_len)))
1264 }
1265 "trix" => {
1266 let len = get_usize_p(params, "len", 15, 1, 10000)?;
1267 let signal_len = get_usize_p(params, "signal_len", 9, 1, 10000)?;
1268 Ok(Box::new(Trix::new(len, signal_len)))
1269 }
1270 "efi" => {
1271 let ema_len = get_usize_p(params, "ema_len", 13, 1, 10000)?;
1272 Ok(Box::new(ElderForceIndex::new(ema_len)))
1273 }
1274 "atr" => Ok(Box::new(build_atr(params, TrueRangeSmoothing::Rma)?)),
1275 "chande_kroll" | "cks" => {
1276 let atr_len = get_usize_p(params, "atr_len", 10, 1, 10000)?;
1277 let stop_len = get_usize_p(params, "stop_len", 9, 1, 10000)?;
1278 let mult = get_f64_p(params, "mult", 3.0, 0.01, 100.0)?;
1279 Ok(Box::new(ChandeKrollStop::new(atr_len, stop_len, mult)))
1280 }
1281 "chandelier_exit" | "ce" => {
1282 let length = get_usize_p(params, "length", 22, 1, 10000)?;
1283 let atr_mult = get_f64_p(params, "atr_mult", 3.0, 0.01, 100.0)?;
1284 Ok(Box::new(ChandelierExitEngine::new(length, atr_mult)))
1285 }
1286 "chandelier_flip_radar" | "chfr" => {
1287 let length = get_usize_p(params, "length", 30, 1, 10000)?;
1288 let atr_mult = get_f64_p(params, "atr_mult", 4.5, 0.01, 100.0)?;
1289 let body_filter_atr = get_f64_p(params, "body_filter_atr", 0.80, 0.0, 100.0)?;
1290 let danger_dist_atr = get_f64_p(params, "danger_dist_atr", 0.35, 0.0, 100.0)?;
1291 let warn_dist_atr = get_f64_p(params, "warn_dist_atr", 0.75, 0.0, 100.0)?;
1292 Ok(Box::new(ChandelierFlipRadarEngine::new(
1297 length,
1298 atr_mult,
1299 true,
1300 false,
1301 body_filter_atr,
1302 danger_dist_atr,
1303 warn_dist_atr,
1304 )))
1305 }
1306 "midas" => {
1307 let maturity_bars = get_usize_p(params, "maturity_bars", 20, 1, 10000)?;
1310 Ok(Box::new(MidasCurveEngine::new(
1311 MidasMode::Topfinder,
1312 crate::model::Source::Hlc3,
1313 maturity_bars as u32,
1314 )))
1315 }
1316 "trend_relationship" => {
1317 let fast_len = get_usize_p(params, "fast_len", 9, 1, 10000)?;
1320 let slow_len = get_usize_p(params, "slow_len", 21, 1, 10000)?;
1321 ensure_less("fast_len", fast_len as f64, "slow_len", slow_len as f64)?;
1322 Ok(Box::new(
1323 super::trend_relationship::AdaptiveTrendRelationship::new(
1324 super::smoothing::SmootherKind::Ema,
1325 fast_len,
1326 super::smoothing::SmootherKind::Ema,
1327 slow_len,
1328 ),
1329 ))
1330 }
1331 "williams_r" | "wpr" => {
1332 let wpr_len = get_usize_p(params, "wpr_len", 14, 1, 10000)?;
1333 let overbought = get_f64_p(params, "overbought", 80.0, 0.0, 100.0)?;
1334 let oversold = get_f64_p(params, "oversold", 20.0, 0.0, 100.0)?;
1335 ensure_less("oversold", oversold, "overbought", overbought)?;
1336 Ok(Box::new(WilliamsR::new(
1337 wpr_len, 3, 3, 50.0, overbought, oversold, 5, true, 50, 4, 10.0,
1338 )))
1339 }
1340 "tsi" => {
1341 let long_len = get_usize_p(params, "long_len", 25, 1, 10000)?;
1342 let short_len = get_usize_p(params, "short_len", 13, 1, 10000)?;
1343 let sig_len = get_usize_p(params, "sig_len", 7, 1, 10000)?;
1344 ensure_less("short_len", short_len as f64, "long_len", long_len as f64)?;
1345 Ok(Box::new(Tsi::new(
1346 long_len, short_len, sig_len, 0.0, 25.0, -25.0, 5, true, 50, 25, 4, 5.0,
1347 )))
1348 }
1349 "fisher_transform" | "fisher" => {
1350 let fish_len = get_usize_p(params, "fish_len", 10, 1, 10000)?;
1351 let overbought = get_f64_p(params, "overbought", 1.5, -100.0, 100.0)?;
1352 let oversold = get_f64_p(params, "oversold", -1.5, -100.0, 100.0)?;
1353 ensure_less("oversold", oversold, "overbought", overbought)?;
1354 Ok(Box::new(FisherTransform::new(
1355 fish_len, 2, 3, 0.0, overbought, oversold, 5, true, 40, 4, 0.5,
1356 )))
1357 }
1358 "order_block" | "ob" => {
1359 let atr_len = get_usize_p(params, "atr_len", 14, 1, 10000)?;
1360 let min_disp = get_f64_p(params, "min_disp", 1.0, 0.01, 100.0)?;
1361 Ok(Box::new(OrderBlockEngine::new(atr_len, min_disp)))
1362 }
1363 "liquidity_fvg" | "fvg" | "smc" => {
1364 let lookback = get_usize_p(params, "lookback", 20, 1, 10000)?;
1365 Ok(Box::new(LiquidityFvgEngine::new(lookback)))
1366 }
1367 "market_structure_breaks" | "bos" | "choch" => {
1368 let lookback = get_usize_p(params, "lookback", 5, 1, 10000)?;
1369 Ok(Box::new(MarketStructureBreaksEngine::new(lookback)))
1370 }
1371 "pivots_structure" | "pivots" => {
1372 let left_bars = get_usize_p(params, "left_bars", 5, 1, 10000)?;
1373 let right_bars = get_usize_p(params, "right_bars", 5, 1, 10000)?;
1374 let score_window = get_usize_p(params, "score_window", 10, 1, 10000)?;
1375 Ok(Box::new(PivotStructureEngine::new(
1376 left_bars,
1377 right_bars,
1378 score_window,
1379 )))
1380 }
1381 "volume_profile" | "vp" => {
1382 let lookback = get_usize_p(params, "lookback", 70, 1, 10000)?;
1383 let num_bins = get_usize_p(params, "num_bins", 30, 1, 1000)?;
1384 Ok(Box::new(VolumeProfileEngine::new(lookback, num_bins)))
1385 }
1386 "money_flow_profile" | "mfp" => {
1387 let lookback = get_usize_p(params, "lookback", 200, 1, 10000)?;
1388 let rows = get_usize_p(params, "rows", 25, 1, 1000)?;
1389 let va_pct = get_f64_p(params, "va_pct", 0.70, 0.0, 1.0)?;
1390 Ok(Box::new(MoneyFlowProfileEngine::new(
1391 lookback, rows, va_pct,
1392 )))
1393 }
1394 "extended_volume_profile" | "vp_extended" => {
1395 let lookback = get_usize_p(params, "lookback", 70, 1, 10000)?;
1396 let num_bins = get_usize_p(params, "num_bins", 30, 1, 1000)?;
1397 Ok(Box::new(ExtendedVolumeProfileEngine::new(
1398 lookback, num_bins,
1399 )))
1400 }
1401 "persistent_volume_profile" | "vp_persistent" => {
1402 let lookback = get_usize_p(params, "lookback", 70, 1, 10000)?;
1403 let bin_width = get_f64_p(params, "bin_width", 1.0, 1e-6, 1_000_000.0)?;
1404 Ok(Box::new(PersistentVolumeProfileEngine::new(
1405 lookback, bin_width,
1406 )))
1407 }
1408 "vwap" => {
1409 let window = get_usize_p(params, "window", 390, 1, 10000)?;
1410 let slope_lookback = get_usize_p(params, "slope_lookback", 20, 1, 10000)?;
1411 Ok(Box::new(Vwap::new(window, slope_lookback)))
1412 }
1413 "vix_fix" | "wvf" => {
1414 let pd = get_usize_p(params, "pd", 22, 1, 10000)?;
1415 let bband_len = get_usize_p(params, "bband_len", 20, 1, 10000)?;
1416 let mult = get_f64_p(params, "mult", 2.0, 0.01, 100.0)?;
1417 Ok(Box::new(WilliamsVixFix::new(pd, bband_len, mult)))
1418 }
1419 "candle_story" | "pinbar" => {
1420 let d = CandleStoryConfig::default();
1421 Ok(Box::new(CandleStoryEngine::with_config(
1422 CandleStoryConfig {
1423 pin_wick_min: get_f64_p(params, "pin_wick_min", d.pin_wick_min, 0.1, 0.95)?,
1424 pin_close_pos: get_f64_p(params, "pin_close_pos", d.pin_close_pos, 0.5, 1.0)?,
1425 marubozu_body_min: get_f64_p(
1426 params,
1427 "marubozu_body_min",
1428 d.marubozu_body_min,
1429 0.5,
1430 1.0,
1431 )?,
1432 belt_hold_body_min: get_f64_p(
1433 params,
1434 "belt_hold_body_min",
1435 d.belt_hold_body_min,
1436 0.3,
1437 1.0,
1438 )?,
1439 belt_hold_open_wick_max: get_f64_p(
1440 params,
1441 "belt_hold_open_wick_max",
1442 d.belt_hold_open_wick_max,
1443 0.0,
1444 0.3,
1445 )?,
1446 doji_body_max: get_f64_p(params, "doji_body_max", d.doji_body_max, 0.0, 0.4)?,
1447 spinning_top_body_max: get_f64_p(
1448 params,
1449 "spinning_top_body_max",
1450 d.spinning_top_body_max,
1451 0.05,
1452 0.6,
1453 )?,
1454 hammer_wick_body_min: get_f64_p(
1455 params,
1456 "hammer_wick_body_min",
1457 d.hammer_wick_body_min,
1458 0.5,
1459 20.0,
1460 )?,
1461 hammer_opposite_max: get_f64_p(
1462 params,
1463 "hammer_opposite_max",
1464 d.hammer_opposite_max,
1465 0.0,
1466 5.0,
1467 )?,
1468 tweezer_tolerance: get_f64_p(
1469 params,
1470 "tweezer_tolerance",
1471 d.tweezer_tolerance,
1472 0.0,
1473 0.1,
1474 )?,
1475 min_range_atr: get_f64_p(params, "min_range_atr", d.min_range_atr, 0.0, 10.0)?,
1476 atr_len: get_usize_p(params, "atr_len", d.atr_len, 1, 10000)?,
1477 trend_lookback: get_usize_p(
1478 params,
1479 "trend_lookback",
1480 d.trend_lookback,
1481 1,
1482 10000,
1483 )?,
1484 trend_min_atr: get_f64_p(params, "trend_min_atr", d.trend_min_atr, 0.0, 100.0)?,
1485 },
1486 )))
1487 }
1488 "efficiency" | "leg_efficiency" | "er" => {
1489 let len = get_usize_p(params, "len", 14, 1, 10000)?;
1490 Ok(Box::new(LegEfficiencyEngine::new(len)))
1491 }
1492 "volume" => {
1493 let ma = get_usize_p(params, "ma_period", 20, 1, 10000)?;
1494 Ok(Box::new(VolumeEngine::new(ma)))
1495 }
1496 "rvol" => {
1497 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1498 Ok(Box::new(RvolEngine::new(p)))
1499 }
1500 "obv" => Ok(Box::new(ObvEngine::new())),
1501 "cmf" => {
1502 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1503 Ok(Box::new(CmfEngine::new(p)))
1504 }
1505 "acc_dist" => Ok(Box::new(AccDistEngine::new())),
1506 "true_range" => Ok(Box::new(TrueRangeEngine::new())),
1507 "keltner" => {
1508 let ema = get_usize_p_aliased(params, "ema_period", "ma_period", 20, 1, 10000)?;
1511 let atr = get_usize_p(params, "atr_period", 10, 1, 10000)?;
1512 let mult = get_f64_p(params, "multiplier", 2.0, 0.01, 100.0)?;
1513 Ok(Box::new(KeltnerChannelEngine::new(ema, atr, mult)))
1514 }
1515 "donchian" => {
1516 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1517 Ok(Box::new(DonchianChannelEngine::new(p)))
1518 }
1519 "historical_volatility" => {
1520 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1521 Ok(Box::new(HistoricalVolatilityEngine::new(p)))
1522 }
1523 "garman_klass" => {
1524 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1525 Ok(Box::new(GarmanKlassVolatilityEngine::new(p)))
1526 }
1527 "sma" => {
1528 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1529 Ok(Box::new(SmaEngine::new(p)))
1530 }
1531 "ema" => Ok(Box::new(build_ema(params, EmaInit::FirstSample)?)),
1532 "wma" => {
1533 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1534 Ok(Box::new(WmaEngine::new(p)))
1535 }
1536 "vwma" => {
1537 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1538 Ok(Box::new(VwmaEngine::new(p)))
1539 }
1540 "hma" => {
1541 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1542 Ok(Box::new(HmaEngine::new(p)))
1543 }
1544 "dema" => {
1545 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1546 Ok(Box::new(DemaEngine::new(p)))
1547 }
1548 "kama" => {
1549 let p = get_usize_p(params, "period", 10, 1, 10000)?;
1550 let fast = get_usize_p(params, "fast_period", 2, 1, 10000)?;
1551 let slow = get_usize_p(params, "slow_period", 30, 1, 10000)?;
1552 ensure_less("fast_period", fast as f64, "slow_period", slow as f64)?;
1553 Ok(Box::new(KamaEngine::new(p, fast, slow)))
1554 }
1555 "dmi" => {
1556 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1557 Ok(Box::new(DmiEngine::new(p)))
1558 }
1559 "aroon" => {
1560 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1561 Ok(Box::new(AroonEngine::new(p)))
1562 }
1563 "parabolic_sar" => {
1564 let step = get_f64_p(params, "step", 0.02, 0.001, 1.0)?;
1565 let max_step = get_f64_p(params, "max_step", 0.20, 0.001, 1.0)?;
1566 if step > max_step {
1567 return Err(RegistryError::InvalidParameter {
1568 parameter: "step".to_string(),
1569 value: step,
1570 reason: "step must not exceed max_step".to_string(),
1571 });
1572 }
1573 Ok(Box::new(ParabolicSarEngine::new(step, max_step)))
1574 }
1575 "supertrend" => {
1576 let p = get_usize_p(params, "period", 10, 1, 10000)?;
1577 let mult = get_f64_p(params, "multiplier", 3.0, 0.01, 100.0)?;
1578 Ok(Box::new(SupertrendEngine::new(p, mult)))
1579 }
1580 "ichimoku" => {
1581 let tenkan = get_usize_p(params, "tenkan_p", 9, 1, 10000)?;
1582 let kijun = get_usize_p(params, "kijun_p", 26, 1, 10000)?;
1583 let senkou_b = get_usize_p(params, "senkou_b_p", 52, 1, 10000)?;
1584 ensure_less("tenkan_p", tenkan as f64, "kijun_p", kijun as f64)?;
1585 ensure_less("kijun_p", kijun as f64, "senkou_b_p", senkou_b as f64)?;
1586 Ok(Box::new(IchimokuEngine::new(tenkan, kijun, senkou_b)))
1587 }
1588 "stochastic" => {
1589 let k = get_usize_p(params, "k_period", 14, 1, 10000)?;
1590 let d = get_usize_p(params, "d_period", 3, 1, 10000)?;
1591 Ok(Box::new(StochasticEngine::new(k, d)))
1592 }
1593 "roc" => {
1594 let p = get_usize_p(params, "period", 12, 1, 10000)?;
1595 Ok(Box::new(RocEngine::new(p)))
1596 }
1597 "ultimate_oscillator" => {
1598 let p1 = get_usize_p(params, "period1", 7, 1, 10000)?;
1599 let p2 = get_usize_p(params, "period2", 14, 1, 10000)?;
1600 let p3 = get_usize_p(params, "period3", 28, 1, 10000)?;
1601 ensure_less("period1", p1 as f64, "period2", p2 as f64)?;
1602 ensure_less("period2", p2 as f64, "period3", p3 as f64)?;
1603 Ok(Box::new(UltimateOscillatorEngine::new(p1, p2, p3)))
1604 }
1605 "awesome_oscillator" => {
1606 let fast = get_usize_p(params, "fast_period", 5, 1, 10000)?;
1607 let slow = get_usize_p(params, "slow_period", 34, 1, 10000)?;
1608 ensure_less("fast_period", fast as f64, "slow_period", slow as f64)?;
1609 Ok(Box::new(AwesomeOscillatorEngine::new(fast, slow)))
1610 }
1611 "ppo" => {
1612 let fast = get_usize_p(params, "fast_period", 12, 1, 10000)?;
1613 let slow = get_usize_p(params, "slow_period", 26, 1, 10000)?;
1614 let signal = get_usize_p(params, "signal_period", 9, 1, 10000)?;
1615 ensure_less("fast_period", fast as f64, "slow_period", slow as f64)?;
1616 Ok(Box::new(PpoEngine::new(fast, slow, signal)))
1617 }
1618 "wavetrend" | "wt" => {
1619 let n1 = get_usize_p(params, "n1", 10, 1, 10000)?;
1620 let n2 = get_usize_p(params, "n2", 21, 1, 10000)?;
1621 let ob = get_f64_p(params, "ob_level", 60.0, -100.0, 100.0)?;
1622 let os = get_f64_p(params, "os_level", -60.0, -100.0, 100.0)?;
1623 Ok(Box::new(WaveTrendEngine::new(n1, n2, ob, os)))
1624 }
1625 "cmo" => {
1626 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1627 Ok(Box::new(CmoEngine::new(p)))
1628 }
1629 "elder_ray" => {
1630 let p = get_usize_p(params, "period", 13, 1, 10000)?;
1631 Ok(Box::new(ElderRayEngine::new(p)))
1632 }
1633 "anchored_vwap" | "avwap" => {
1634 let m1 = get_f64_p(params, "mult1", 1.0, 0.01, 100.0)?;
1635 let m2 = get_f64_p(params, "mult2", 2.0, 0.01, 100.0)?;
1636 Ok(Box::new(AnchoredVwapEngine::new(
1637 VwapAnchorKind::Session,
1638 m1,
1639 m2,
1640 )))
1641 }
1642 "cvd" => Ok(Box::new(CvdEngine::new())),
1643 "hires_volume_flow" => {
1644 let window_len = get_usize_p(params, "window_len", 20, 2, 10000)?;
1645 Ok(Box::new(HiResVolumeFlowEngine::new(window_len)))
1646 }
1647 "klinger" | "kvo" => {
1648 let fast = get_usize_p(params, "fast_len", 34, 1, 10000)?;
1649 let slow = get_usize_p(params, "slow_len", 55, 1, 10000)?;
1650 let sig = get_usize_p(params, "signal_len", 13, 1, 10000)?;
1651 ensure_less("fast_len", fast as f64, "slow_len", slow as f64)?;
1652 Ok(Box::new(KlingerVolumeForceEngine::new(fast, slow, sig)))
1653 }
1654 "zigzag" => {
1655 let depth = get_usize_p(params, "depth", 12, 2, 10000)?;
1656 let dev = get_f64_p(params, "deviation_pct", 5.0, 0.01, 100.0)?;
1657 Ok(Box::new(ZigZagEngine::new(depth, dev)))
1658 }
1659 "zigzag_advanced" => {
1660 let depth = get_usize_p(params, "depth", 3, 1, 10000)?;
1663 let backstep = get_usize_p(params, "backstep", 2, 0, 10000)?;
1664 let deviation_pct = get_f64_p(params, "deviation_pct", 1.0, 0.001, 100.0)?;
1665 let atr_len = get_usize_p(params, "atr_len", 14, 1, 10000)?;
1666 Ok(Box::new(AdvancedZigZagEngine::new(
1667 depth,
1668 backstep,
1669 ZigZagDeviationMode::Percent(deviation_pct),
1670 atr_len,
1671 )))
1672 }
1673 "pivot_sets" | "multi_pivots" => Ok(Box::new(PivotSetsEngine::new(PivotSetType::Classic))),
1674 "tema" => {
1675 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1676 Ok(Box::new(TemaEngine::new(p)))
1677 }
1678 "lsma" => {
1679 let p = get_usize_p(params, "period", 25, 2, 10000)?;
1680 Ok(Box::new(LsmaEngine::new(p)))
1681 }
1682 "mcginley" => {
1683 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1684 Ok(Box::new(McGinleyDynamicEngine::new(p)))
1685 }
1686 "envelope" => {
1687 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1688 let pct = get_f64_p(params, "percent", 2.5, 0.01, 100.0)?;
1689 Ok(Box::new(EnvelopeEngine::new(p, pct)))
1690 }
1691 "choppiness" | "chop" => {
1692 let p = get_usize_p(params, "period", 14, 2, 10000)?;
1693 Ok(Box::new(ChoppinessIndexEngine::new(p)))
1694 }
1695 "vortex" | "vi" => {
1696 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1697 Ok(Box::new(VortexEngine::new(p)))
1698 }
1699 "alligator" => Ok(Box::new(AlligatorEngine::new())),
1700 "connors_rsi" => {
1701 let rsi_len = get_usize_p(params, "rsi_len", 3, 1, 10000)?;
1702 let streak_len = get_usize_p(params, "streak_len", 2, 1, 10000)?;
1703 let rank_len = get_usize_p(params, "rank_len", 100, 1, 10000)?;
1704 Ok(Box::new(ConnorsRsiEngine::new(
1705 rsi_len, streak_len, rank_len,
1706 )))
1707 }
1708 "coppock" => Ok(Box::new(CoppockCurveEngine::new())),
1709 "dpo" => {
1710 let p = get_usize_p(params, "period", 21, 2, 10000)?;
1711 Ok(Box::new(DpoEngine::new(p)))
1712 }
1713 "kst" => Ok(Box::new(KstEngine::new())),
1714 "mass_index" => {
1715 let p = get_usize_p(params, "period", 25, 1, 10000)?;
1716 Ok(Box::new(MassIndexEngine::new(p)))
1717 }
1718 "rvi" => {
1719 let p = get_usize_p(params, "period", 10, 1, 10000)?;
1720 Ok(Box::new(RviEngine::new(p)))
1721 }
1722 "bop" => {
1723 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1724 Ok(Box::new(BalanceOfPowerEngine::new(p)))
1725 }
1726 "eom" => {
1727 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1728 let div = get_f64_p(params, "volume_divisor", 10000.0, 1.0, 1e9)?;
1729 Ok(Box::new(EomEngine::new(p, div)))
1730 }
1731 "nvi" => Ok(Box::new(NviEngine::new())),
1732 "pvi" => Ok(Box::new(PviEngine::new())),
1733 "chaikin_oscillator" | "cho" => {
1734 let fast = get_usize_p(params, "fast_len", 3, 1, 10000)?;
1735 let slow = get_usize_p(params, "slow_len", 10, 1, 10000)?;
1736 ensure_less("fast_len", fast as f64, "slow_len", slow as f64)?;
1737 Ok(Box::new(ChaikinOscillatorEngine::new(fast, slow)))
1738 }
1739 "bos_choch" => {
1740 let pivot_len = get_usize_p(params, "pivot_len", 5, 2, 10000)?;
1741 Ok(Box::new(BosChochEngine::new(pivot_len)))
1742 }
1743 "liquidity_sweeps" | "sweeps" => {
1744 let p = get_usize_p(params, "pivot_len", 5, 2, 10000)?;
1745 let tol = get_f64_p(params, "tolerance_pct", 0.2, 0.01, 100.0)?;
1746 Ok(Box::new(LiquiditySweepEngine::new(p, tol)))
1747 }
1748 "liquidity_pools" => {
1749 let p = get_usize_p(params, "pivot_len", 5, 2, 10000)?;
1750 let tol = get_f64_p(params, "tolerance_pct", 0.2, 0.001, 100.0)?;
1751 Ok(Box::new(
1752 super::smart_money_structure::LiquidityPoolEngine::new(p, tol),
1753 ))
1754 }
1755 "wyckoff" => {
1756 let lookback = get_usize_p(params, "range_lookback", 20, 3, 10000)?;
1757 let atr_max = get_f64_p(params, "range_atr_max", 3.0, 0.1, 1000.0)?;
1758 let min_bars = get_usize_p(params, "min_range_bars", 6, 2, 10000)?;
1759 Ok(Box::new(super::wyckoff::WyckoffStateMachine::new(
1760 lookback, atr_max, min_bars,
1761 )))
1762 }
1763 "trend_quality" => {
1764 let p = get_usize_p(params, "period", 14, 2, 10000)?;
1765 Ok(Box::new(TrendQualityScoreEngine::new(p)))
1766 }
1767 "buy_sell_pressure" | "pressure" => {
1768 let p = get_usize_p(params, "period", 14, 1, 10000)?;
1769 Ok(Box::new(BuySellPressureEstimator::new(p)))
1770 }
1771 "volatility_regime" => {
1772 let p = get_usize_p(params, "period", 20, 1, 10000)?;
1773 let bb_mult = get_f64_p(params, "bb_mult", 2.0, 0.01, 100.0)?;
1774 let kc_mult = get_f64_p(params, "kc_mult", 1.5, 0.01, 100.0)?;
1775 Ok(Box::new(VolatilityRegimeDetector::new(p, bb_mult, kc_mult)))
1776 }
1777 "zscore" => {
1778 let p = get_usize_p(params, "period", 20, 2, 10000)?;
1779 Ok(Box::new(ZScoreEngine::new(p)))
1780 }
1781 "multi_factor" => {
1782 let p = get_usize_p(params, "period", 14, 2, 10000)?;
1783 Ok(Box::new(MultiFactorMarketScore::new(p)))
1784 }
1785 _ => Err(RegistryError::UnknownIndicator(name.to_string())),
1786 }
1787}
1788
1789pub fn build(name: &str, params: &HashMap<String, f64>) -> Option<Box<dyn Indicator>> {
1792 build_checked(name, params).ok()
1793}
1794
1795const RANGE_DEPENDENT_INDICATORS: &[&str] = &[
1809 "atr",
1810 "relative_volatility",
1811 "smi",
1812 "chande_kroll",
1813 "cks",
1814 "true_range",
1815 "adx",
1816 "dmi",
1817 "chandelier_exit",
1818 "chandelier_flip_radar",
1819 "chfr",
1820 "wyckoff",
1821 "volume_profile",
1822 "vp",
1823 "money_flow_profile",
1824 "mfp",
1825 "extended_volume_profile",
1826 "vp_extended",
1827 "persistent_volume_profile",
1828 "vp_persistent",
1829 "pivots_structure",
1830 "pivots",
1831 "pivot_sets",
1832 "multi_pivots",
1833 "zigzag",
1834 "zigzag_advanced",
1835 "liquidity_pools",
1836 "liquidity_sweeps",
1837 "sweeps",
1838 "liquidity_fvg",
1839 "fvg",
1840 "smc",
1841 "order_block",
1842 "ob",
1843 "ce",
1844 "bos_choch",
1845 "market_structure_breaks",
1846 "bos",
1847 "choch",
1848 "vix_fix",
1849 "wvf",
1850 "keltner",
1851 "donchian",
1852 "vortex",
1853 "vi",
1854 "choppiness",
1855 "chop",
1856 "mass_index",
1857 "supertrend",
1858 "parabolic_sar",
1859 "ichimoku",
1860 "aroon",
1861 "garman_klass",
1862 "hires_volume_flow",
1863 "cvd",
1864 "klinger",
1865 "kvo",
1866 "volatility_regime",
1867 "swing_structure",
1868];
1869
1870const SOURCE_FIXED_INDICATORS: &[&str] = &["efi"];
1875
1876pub fn build_typed(name: &str, params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
1877 if name.to_lowercase() == "midas" {
1878 return build_midas_typed(params);
1880 }
1881
1882 let source = match params.get("source") {
1883 None => None,
1884 Some(ParamValue::Source(s)) => Some(*s),
1885 Some(other) => {
1886 return Err(RegistryError::UnsupportedParameterType {
1887 parameter: "source".to_string(),
1888 type_name: other.type_name().to_string(),
1889 });
1890 }
1891 };
1892
1893 if let Some(s) = source {
1894 if s != crate::model::Source::Close
1895 && SOURCE_FIXED_INDICATORS.contains(&name.to_lowercase().as_str())
1896 {
1897 return Err(RegistryError::IncompatibleParameter {
1898 parameter: "source".to_string(),
1899 indicator: name.to_string(),
1900 reason: "price source is fixed to the close by contract; another source would \
1901 redefine the series under the same name"
1902 .to_string(),
1903 });
1904 }
1905
1906 if s != crate::model::Source::Close
1907 && RANGE_DEPENDENT_INDICATORS.contains(&name.to_lowercase().as_str())
1908 {
1909 return Err(RegistryError::IncompatibleParameter {
1910 parameter: "source".to_string(),
1911 indicator: name.to_string(),
1912 reason: "range/OHLC-dependent indicator; SourceMapped would collapse its true range to zero"
1913 .to_string(),
1914 });
1915 }
1916 }
1917
1918 let mut remaining = params.clone();
1922 remaining.remove("source");
1923
1924 let built = match name.to_lowercase().as_str() {
1925 "anchored_vwap" | "avwap" => build_anchored_vwap_typed(&remaining)?,
1926 "atr" => build_atr_typed(&remaining)?,
1927 "ema" => build_ema_typed(&remaining)?,
1928 "rsi" => build_rsi_typed(&remaining)?,
1929 "bbtrend" => build_bbtrend_typed(&remaining)?,
1930 "relative_volatility" => build_relative_volatility_typed(&remaining)?,
1931 "twap" => build_twap_typed(&remaining, source)?,
1932 "bollinger" | "bb" => build_bollinger_typed(&remaining)?,
1933 "pivot_sets" | "multi_pivots" => build_pivot_sets_typed(&remaining)?,
1934 "trend_relationship" => build_trend_relationship_typed(&remaining)?,
1935 "zigzag_advanced" => build_zigzag_advanced_typed(&remaining)?,
1936 _ => build_typed_by_flattening(name, &remaining)?,
1937 };
1938
1939 Ok(match source {
1940 Some(s) if s != crate::model::Source::Close => {
1941 Box::new(super::source_mapped::SourceMapped::new(built, s))
1942 }
1943 _ => built,
1944 })
1945}
1946
1947fn build_typed_by_flattening(
1951 name: &str,
1952 params: &TypedParams,
1953) -> Result<Box<dyn Indicator>, RegistryError> {
1954 build_checked(name, &flatten_typed(params)?)
1955}
1956
1957fn flatten_typed(params: &TypedParams) -> Result<HashMap<String, f64>, RegistryError> {
1962 let mut flat = HashMap::with_capacity(params.len());
1963 for (key, value) in params {
1964 match value.as_f64() {
1965 Some(v) => {
1966 flat.insert(key.clone(), v);
1967 }
1968 None => {
1969 return Err(RegistryError::UnsupportedParameterType {
1970 parameter: key.clone(),
1971 type_name: value.type_name().to_string(),
1972 });
1973 }
1974 }
1975 }
1976 Ok(flat)
1977}
1978
1979fn build_atr(
1981 params: &HashMap<String, f64>,
1982 smoothing: TrueRangeSmoothing,
1983) -> Result<Atr, RegistryError> {
1984 let atr_len = get_usize_p(params, "atr_len", 14, 1, 10000)?;
1985 let sig_len = get_usize_p(params, "sig_len", 20, 1, 10000)?;
1986 Ok(Atr::new(atr_len, sig_len).with_smoothing(smoothing))
1987}
1988
1989fn build_atr_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
1990 let smoothing = match get_enum_p(params, "smoothing")?.as_deref() {
1991 None | Some("rma") => TrueRangeSmoothing::Rma,
1992 Some("sma") => TrueRangeSmoothing::Sma,
1993 Some("ema") => TrueRangeSmoothing::Ema,
1994 Some("wma") => TrueRangeSmoothing::Wma,
1995 Some(other) => {
1996 return Err(RegistryError::InvalidEnumValue {
1997 parameter: "smoothing".to_string(),
1998 value: other.to_string(),
1999 reason: "expected one of rma|sma|ema|wma".to_string(),
2000 });
2001 }
2002 };
2003
2004 let mut numeric = params.clone();
2005 numeric.remove("smoothing");
2006 Ok(Box::new(build_atr(&flatten_typed(&numeric)?, smoothing)?))
2007}
2008
2009fn build_ema(params: &HashMap<String, f64>, init: EmaInit) -> Result<EmaEngine, RegistryError> {
2011 let period = get_usize_p(params, "period", 20, 1, 10000)?;
2012 Ok(EmaEngine::new(period).with_init(init))
2013}
2014
2015fn build_ema_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2016 let init = match get_enum_p(params, "init")?.as_deref() {
2017 None | Some("first_sample") => EmaInit::FirstSample,
2018 Some("sma") => EmaInit::Sma,
2019 Some(other) => {
2020 return Err(RegistryError::InvalidEnumValue {
2021 parameter: "init".to_string(),
2022 value: other.to_string(),
2023 reason: "expected one of first_sample|sma".to_string(),
2024 });
2025 }
2026 };
2027
2028 let mut numeric = params.clone();
2029 numeric.remove("init");
2030 Ok(Box::new(build_ema(&flatten_typed(&numeric)?, init)?))
2031}
2032
2033fn build_twap_typed(
2037 params: &TypedParams,
2038 source: Option<crate::model::Source>,
2039) -> Result<Box<dyn Indicator>, RegistryError> {
2040 let weighting = match get_enum_p(params, "weighting")?.as_deref() {
2041 None | Some("per_bar") => TwapWeighting::PerBar,
2042 Some("by_duration") => TwapWeighting::ByDuration,
2043 Some(other) => {
2044 return Err(RegistryError::InvalidEnumValue {
2045 parameter: "weighting".to_string(),
2046 value: other.to_string(),
2047 reason: "expected one of per_bar|by_duration".to_string(),
2048 });
2049 }
2050 };
2051
2052 let numeric = extract_numeric_subset(params, &["day_start_offset"])?;
2053 let offset = get_f64_p(&numeric, "day_start_offset", 0.0, -86_400.0, 86_400.0)? as i64;
2054
2055 let anchor = match get_enum_p(params, "anchor")?.as_deref() {
2056 None | Some("daily") => TwapAnchor::Daily {
2057 start_offset_seconds: offset,
2058 },
2059 Some("continuous") => TwapAnchor::Continuous,
2060 Some("manual_timestamp") => {
2061 let timestamp = get_timestamp_p(params, "anchor_timestamp")?.ok_or_else(|| {
2062 RegistryError::InvalidEnumValue {
2063 parameter: "anchor".to_string(),
2064 value: "manual_timestamp".to_string(),
2065 reason: "requires an accompanying 'anchor_timestamp' Timestamp parameter"
2066 .to_string(),
2067 }
2068 })?;
2069 TwapAnchor::ManualTimestamp(timestamp)
2070 }
2071 Some(other) => {
2072 return Err(RegistryError::InvalidEnumValue {
2073 parameter: "anchor".to_string(),
2074 value: other.to_string(),
2075 reason: "expected one of continuous|daily|manual_timestamp".to_string(),
2076 });
2077 }
2078 };
2079
2080 Ok(Box::new(AnchoredTwap::new(
2081 anchor,
2082 source.unwrap_or(crate::model::Source::Close),
2083 weighting,
2084 )))
2085}
2086
2087fn build_relative_volatility(
2090 params: &HashMap<String, f64>,
2091 variant: RelativeVolatilityVariant,
2092) -> Result<RelativeVolatilityIndex, RegistryError> {
2093 let stdev_len = get_usize_p(params, "stdev_len", 10, 2, 10000)?;
2094 let smooth_len = get_usize_p(params, "smooth_len", 14, 1, 10000)?;
2095 Ok(RelativeVolatilityIndex::new(stdev_len, smooth_len, variant))
2096}
2097
2098fn build_relative_volatility_typed(
2099 params: &TypedParams,
2100) -> Result<Box<dyn Indicator>, RegistryError> {
2101 let variant = match get_enum_p(params, "variant")?.as_deref() {
2102 None | Some("close") => RelativeVolatilityVariant::Close,
2103 Some("high_low") => RelativeVolatilityVariant::HighLow,
2104 Some(other) => {
2105 return Err(RegistryError::InvalidEnumValue {
2106 parameter: "variant".to_string(),
2107 value: other.to_string(),
2108 reason: "expected one of close|high_low".to_string(),
2109 });
2110 }
2111 };
2112
2113 let mut numeric = params.clone();
2114 numeric.remove("variant");
2115 Ok(Box::new(build_relative_volatility(
2116 &flatten_typed(&numeric)?,
2117 variant,
2118 )?))
2119}
2120
2121fn build_bbtrend(
2124 params: &HashMap<String, f64>,
2125 variance: VarianceConvention,
2126) -> Result<BbTrend, RegistryError> {
2127 let short_len = get_usize_p(params, "short_len", 20, 1, 10000)?;
2128 let long_len = get_usize_p(params, "long_len", 50, 1, 10000)?;
2129 let mult = get_f64_p(params, "mult", 2.0, 0.01, 100.0)?;
2130 ensure_less("short_len", short_len as f64, "long_len", long_len as f64)?;
2131 if variance == VarianceConvention::Sample && short_len < 2 {
2132 return Err(RegistryError::IncompatibleParameter {
2133 parameter: "variance".to_string(),
2134 indicator: "bbtrend".to_string(),
2135 reason: "sample variance divides by len - 1 and is undefined for len < 2".to_string(),
2136 });
2137 }
2138 Ok(BbTrend::new(short_len, long_len, mult, variance))
2139}
2140
2141fn build_bbtrend_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2142 let variance = match get_enum_p(params, "variance")?.as_deref() {
2143 None | Some("population") => VarianceConvention::Population,
2144 Some("sample") => VarianceConvention::Sample,
2145 Some(other) => {
2146 return Err(RegistryError::InvalidEnumValue {
2147 parameter: "variance".to_string(),
2148 value: other.to_string(),
2149 reason: "expected one of population|sample".to_string(),
2150 });
2151 }
2152 };
2153
2154 let mut numeric = params.clone();
2155 numeric.remove("variance");
2156 Ok(Box::new(build_bbtrend(
2157 &flatten_typed(&numeric)?,
2158 variance,
2159 )?))
2160}
2161
2162fn build_rsi(params: &HashMap<String, f64>, smoothing: RsiSmoothing) -> Result<Rsi, RegistryError> {
2165 let rsi_len = get_usize_p(params, "rsi_len", 14, 1, 10000)?;
2166 let avg_len = get_usize_p(params, "avg_len", 3, 1, 10000)?;
2167 let sig_len = get_usize_p(params, "sig_len", 3, 1, 10000)?;
2168 let overbought = get_f64_p(params, "overbought", 70.0, 0.0, 100.0)?;
2169 let oversold = get_f64_p(params, "oversold", 30.0, 0.0, 100.0)?;
2170 ensure_less("oversold", oversold, "overbought", overbought)?;
2171 Ok(Rsi::new(
2172 rsi_len, avg_len, sig_len, 50.0, overbought, oversold, 5, true, 100, 4, 10.0,
2173 )
2174 .with_smoothing(smoothing))
2175}
2176
2177fn build_rsi_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2178 let smoothing = match get_enum_p(params, "smoothing")?.as_deref() {
2179 None | Some("wilder") => RsiSmoothing::Wilder,
2180 Some("ema") => RsiSmoothing::Ema,
2181 Some(other) => {
2182 return Err(RegistryError::InvalidEnumValue {
2183 parameter: "smoothing".to_string(),
2184 value: other.to_string(),
2185 reason: "expected one of wilder|ema".to_string(),
2186 });
2187 }
2188 };
2189
2190 let mut numeric = params.clone();
2191 numeric.remove("smoothing");
2192 Ok(Box::new(build_rsi(&flatten_typed(&numeric)?, smoothing)?))
2193}
2194
2195fn build_bollinger(
2197 params: &HashMap<String, f64>,
2198 variance: VarianceConvention,
2199) -> Result<BollingerBands, RegistryError> {
2200 let len = get_usize_p(params, "len", 20, 1, 10000)?;
2201 let mult = get_f64_p(params, "mult", 2.0, 0.01, 100.0)?;
2202 if variance == VarianceConvention::Sample && len < 2 {
2203 return Err(RegistryError::IncompatibleParameter {
2204 parameter: "variance".to_string(),
2205 indicator: "bollinger".to_string(),
2206 reason: "sample variance divides by len - 1 and is undefined for len < 2".to_string(),
2207 });
2208 }
2209 Ok(BollingerBands::new(len, mult).with_variance(variance))
2210}
2211
2212fn build_bollinger_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2213 let variance = match get_enum_p(params, "variance")?.as_deref() {
2214 None | Some("population") => VarianceConvention::Population,
2215 Some("sample") => VarianceConvention::Sample,
2216 Some(other) => {
2217 return Err(RegistryError::InvalidEnumValue {
2218 parameter: "variance".to_string(),
2219 value: other.to_string(),
2220 reason: "expected one of population|sample".to_string(),
2221 });
2222 }
2223 };
2224
2225 let mut numeric = params.clone();
2226 numeric.remove("variance");
2227 Ok(Box::new(build_bollinger(
2228 &flatten_typed(&numeric)?,
2229 variance,
2230 )?))
2231}
2232
2233fn get_enum_p(params: &TypedParams, name: &str) -> Result<Option<String>, RegistryError> {
2236 match params.get(name) {
2237 None => Ok(None),
2238 Some(ParamValue::Enum(value)) => Ok(Some(value.to_lowercase())),
2239 Some(other) => Err(RegistryError::UnsupportedParameterType {
2240 parameter: name.to_string(),
2241 type_name: other.type_name().to_string(),
2242 }),
2243 }
2244}
2245
2246fn get_timestamp_p(params: &TypedParams, name: &str) -> Result<Option<i64>, RegistryError> {
2249 match params.get(name) {
2250 None => Ok(None),
2251 Some(ParamValue::Timestamp(value)) => Ok(Some(*value)),
2252 Some(other) => Err(RegistryError::UnsupportedParameterType {
2253 parameter: name.to_string(),
2254 type_name: other.type_name().to_string(),
2255 }),
2256 }
2257}
2258
2259fn extract_numeric_subset(
2262 params: &TypedParams,
2263 keys: &[&str],
2264) -> Result<HashMap<String, f64>, RegistryError> {
2265 let mut numeric = HashMap::new();
2266 for key in keys {
2267 if let Some(value) = params.get(*key) {
2268 match value.as_f64() {
2269 Some(v) => {
2270 numeric.insert((*key).to_string(), v);
2271 }
2272 None => {
2273 return Err(RegistryError::UnsupportedParameterType {
2274 parameter: (*key).to_string(),
2275 type_name: value.type_name().to_string(),
2276 });
2277 }
2278 }
2279 }
2280 }
2281 Ok(numeric)
2282}
2283
2284fn build_anchored_vwap_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2285 let anchor_kind = match get_enum_p(params, "anchor_kind")?.as_deref() {
2286 None | Some("session") => VwapAnchorKind::Session,
2287 Some("day") => VwapAnchorKind::Day,
2288 Some("week") => VwapAnchorKind::Week,
2289 Some("month") => VwapAnchorKind::Month,
2290 Some("external") => VwapAnchorKind::External,
2291 Some("manual_timestamp") => {
2292 let ts = get_timestamp_p(params, "anchor_timestamp")?.ok_or_else(|| {
2293 RegistryError::InvalidEnumValue {
2294 parameter: "anchor_kind".to_string(),
2295 value: "manual_timestamp".to_string(),
2296 reason: "requires an accompanying 'anchor_timestamp' Timestamp parameter"
2297 .to_string(),
2298 }
2299 })?;
2300 VwapAnchorKind::ManualTimestamp(ts)
2301 }
2302 Some(other) => {
2303 return Err(RegistryError::InvalidEnumValue {
2304 parameter: "anchor_kind".to_string(),
2305 value: other.to_string(),
2306 reason: "expected one of session|day|week|month|external|manual_timestamp"
2307 .to_string(),
2308 });
2309 }
2310 };
2311
2312 let zero_volume_policy = match get_enum_p(params, "zero_volume_policy")?.as_deref() {
2313 None | Some("equal_weight") => ZeroVolumePolicy::EqualWeight,
2314 Some("skip") => ZeroVolumePolicy::Skip,
2315 Some(other) => {
2316 return Err(RegistryError::InvalidEnumValue {
2317 parameter: "zero_volume_policy".to_string(),
2318 value: other.to_string(),
2319 reason: "expected one of equal_weight|skip".to_string(),
2320 });
2321 }
2322 };
2323
2324 let numeric = extract_numeric_subset(params, &["mult1", "mult2"])?;
2325 let m1 = get_f64_p(&numeric, "mult1", 1.0, 0.01, 100.0)?;
2326 let m2 = get_f64_p(&numeric, "mult2", 2.0, 0.01, 100.0)?;
2327
2328 Ok(Box::new(
2329 AnchoredVwapEngine::new(anchor_kind, m1, m2).with_zero_volume_policy(zero_volume_policy),
2330 ))
2331}
2332
2333fn build_pivot_sets_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2334 let pivot_type = match get_enum_p(params, "pivot_type")?.as_deref() {
2335 None | Some("classic") => PivotSetType::Classic,
2336 Some("fibonacci") => PivotSetType::Fibonacci,
2337 Some("camarilla") => PivotSetType::Camarilla,
2338 Some("woodie") => PivotSetType::Woodie,
2339 Some("demark") => PivotSetType::DeMark,
2340 Some("cpr") => PivotSetType::Cpr,
2341 Some(other) => {
2342 return Err(RegistryError::InvalidEnumValue {
2343 parameter: "pivot_type".to_string(),
2344 value: other.to_string(),
2345 reason: "expected one of classic|fibonacci|camarilla|woodie|demark|cpr".to_string(),
2346 });
2347 }
2348 };
2349
2350 Ok(Box::new(PivotSetsEngine::new(pivot_type)))
2351}
2352
2353fn parse_smoother_kind(
2354 params: &TypedParams,
2355 parameter_name: &str,
2356 default: super::smoothing::SmootherKind,
2357) -> Result<super::smoothing::SmootherKind, RegistryError> {
2358 use super::smoothing::SmootherKind;
2359
2360 match get_enum_p(params, parameter_name)?.as_deref() {
2361 None => Ok(default),
2362 Some("ema") => Ok(SmootherKind::Ema),
2363 Some("sma") => Ok(SmootherKind::Sma),
2364 Some("rma") => Ok(SmootherKind::Rma),
2365 Some("alma") => Ok(SmootherKind::Alma),
2366 Some("jma") => Ok(SmootherKind::Jma),
2367 Some(other) => Err(RegistryError::InvalidEnumValue {
2368 parameter: parameter_name.to_string(),
2369 value: other.to_string(),
2370 reason: "expected one of ema|sma|rma|alma|jma".to_string(),
2371 }),
2372 }
2373}
2374
2375fn build_midas_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2381 let mode = match get_enum_p(params, "mode")?.as_deref() {
2382 None | Some("topfinder") => MidasMode::Topfinder,
2383 Some("bottomfinder") => MidasMode::Bottomfinder,
2384 Some(other) => {
2385 return Err(RegistryError::InvalidEnumValue {
2386 parameter: "mode".to_string(),
2387 value: other.to_string(),
2388 reason: "expected one of topfinder|bottomfinder".to_string(),
2389 });
2390 }
2391 };
2392
2393 let source = match params.get("source") {
2394 None => crate::model::Source::Hlc3,
2395 Some(ParamValue::Source(s)) => *s,
2396 Some(other) => {
2397 return Err(RegistryError::UnsupportedParameterType {
2398 parameter: "source".to_string(),
2399 type_name: other.type_name().to_string(),
2400 });
2401 }
2402 };
2403
2404 let numeric = extract_numeric_subset(params, &["maturity_bars"])?;
2405 let maturity_bars = get_usize_p(&numeric, "maturity_bars", 20, 1, 10000)?;
2406
2407 Ok(Box::new(MidasCurveEngine::new(
2408 mode,
2409 source,
2410 maturity_bars as u32,
2411 )))
2412}
2413
2414fn build_zigzag_advanced_typed(params: &TypedParams) -> Result<Box<dyn Indicator>, RegistryError> {
2415 let numeric =
2416 extract_numeric_subset(params, &["depth", "backstep", "deviation_value", "atr_len"])?;
2417 let depth = get_usize_p(&numeric, "depth", 3, 1, 10000)?;
2418 let backstep = get_usize_p(&numeric, "backstep", 2, 0, 10000)?;
2419 let atr_len = get_usize_p(&numeric, "atr_len", 14, 1, 10000)?;
2420
2421 let deviation = match get_enum_p(params, "deviation_mode")?.as_deref() {
2422 None | Some("percent") => {
2423 let value = get_f64_p(&numeric, "deviation_value", 1.0, 0.001, 100.0)?;
2424 ZigZagDeviationMode::Percent(value)
2425 }
2426 Some("atr_multiple") => {
2427 let value = get_f64_p(&numeric, "deviation_value", 1.0, 0.001, 1000.0)?;
2428 ZigZagDeviationMode::AtrMultiple(value)
2429 }
2430 Some(other) => {
2431 return Err(RegistryError::InvalidEnumValue {
2432 parameter: "deviation_mode".to_string(),
2433 value: other.to_string(),
2434 reason: "expected one of percent|atr_multiple".to_string(),
2435 });
2436 }
2437 };
2438
2439 Ok(Box::new(AdvancedZigZagEngine::new(
2440 depth, backstep, deviation, atr_len,
2441 )))
2442}
2443
2444fn build_trend_relationship_typed(
2445 params: &TypedParams,
2446) -> Result<Box<dyn Indicator>, RegistryError> {
2447 use super::smoothing::SmootherKind;
2448 use super::trend_relationship::AdaptiveTrendRelationship;
2449
2450 let fast_kind = parse_smoother_kind(params, "fast_kind", SmootherKind::Ema)?;
2451 let slow_kind = parse_smoother_kind(params, "slow_kind", SmootherKind::Ema)?;
2452
2453 let numeric = extract_numeric_subset(params, &["fast_len", "slow_len"])?;
2454 let fast_len = get_usize_p(&numeric, "fast_len", 9, 1, 10000)?;
2455 let slow_len = get_usize_p(&numeric, "slow_len", 21, 1, 10000)?;
2456 ensure_less("fast_len", fast_len as f64, "slow_len", slow_len as f64)?;
2457
2458 Ok(Box::new(AdaptiveTrendRelationship::new(
2459 fast_kind, fast_len, slow_kind, slow_len,
2460 )))
2461}
2462
2463pub const CANONICAL_INDICATOR_NAMES: &[&str] = &[
2478 "rsi",
2479 "macd",
2480 "bollinger",
2481 "adx",
2482 "stoch_rsi",
2483 "cci",
2484 "mfi",
2485 "atr",
2486 "chande_kroll",
2487 "efi",
2488 "trix",
2489 "vidya",
2490 "t3",
2491 "ulcer_index",
2492 "rci",
2493 "smi",
2494 "rvat",
2495 "bbtrend",
2496 "relative_volatility",
2497 "twap",
2498 "pvt",
2499 "pmo",
2500 "chandelier_exit",
2501 "chandelier_flip_radar",
2502 "midas",
2503 "trend_relationship",
2504 "williams_r",
2505 "tsi",
2506 "fisher_transform",
2507 "order_block",
2508 "liquidity_fvg",
2509 "market_structure_breaks",
2510 "pivots_structure",
2511 "volume_profile",
2512 "money_flow_profile",
2513 "extended_volume_profile",
2514 "persistent_volume_profile",
2515 "vwap",
2516 "vix_fix",
2517 "candle_story",
2518 "efficiency",
2519 "volume",
2520 "rvol",
2521 "obv",
2522 "cmf",
2523 "acc_dist",
2524 "true_range",
2525 "keltner",
2526 "donchian",
2527 "historical_volatility",
2528 "garman_klass",
2529 "sma",
2530 "ema",
2531 "wma",
2532 "vwma",
2533 "hma",
2534 "dema",
2535 "kama",
2536 "dmi",
2537 "aroon",
2538 "parabolic_sar",
2539 "supertrend",
2540 "ichimoku",
2541 "stochastic",
2542 "roc",
2543 "ultimate_oscillator",
2544 "awesome_oscillator",
2545 "ppo",
2546 "wavetrend",
2547 "cmo",
2548 "elder_ray",
2549 "anchored_vwap",
2550 "cvd",
2551 "hires_volume_flow",
2552 "klinger",
2553 "zigzag",
2554 "zigzag_advanced",
2555 "pivot_sets",
2556 "tema",
2557 "lsma",
2558 "mcginley",
2559 "envelope",
2560 "choppiness",
2561 "vortex",
2562 "alligator",
2563 "connors_rsi",
2564 "coppock",
2565 "dpo",
2566 "kst",
2567 "mass_index",
2568 "rvi",
2569 "bop",
2570 "eom",
2571 "nvi",
2572 "pvi",
2573 "chaikin_oscillator",
2574 "bos_choch",
2575 "liquidity_sweeps",
2576 "liquidity_pools",
2577 "wyckoff",
2578 "trend_quality",
2579 "buy_sell_pressure",
2580 "volatility_regime",
2581 "zscore",
2582 "multi_factor",
2583];
2584
2585#[cfg(test)]
2586mod tests {
2587 use super::*;
2588 use std::collections::HashSet;
2589
2590 #[test]
2595 fn test_catalog_matches_canonical_indicator_names_exactly() {
2596 let catalog_names: HashSet<&str> = catalog().iter().map(|e| e.name).collect();
2597 let canonical: HashSet<&str> = CANONICAL_INDICATOR_NAMES.iter().copied().collect();
2598
2599 let missing_from_catalog: Vec<&&str> = canonical.difference(&catalog_names).collect();
2600 assert!(
2601 missing_from_catalog.is_empty(),
2602 "buildable but not discoverable via catalog(): {missing_from_catalog:?}"
2603 );
2604
2605 let extra_in_catalog: Vec<&&str> = catalog_names.difference(&canonical).collect();
2606 assert!(
2607 extra_in_catalog.is_empty(),
2608 "catalog() entries with no matching canonical build_checked arm: {extra_in_catalog:?}"
2609 );
2610
2611 assert_eq!(CANONICAL_INDICATOR_NAMES.len(), 105);
2612 assert_eq!(catalog().len(), 105);
2613 }
2614
2615 #[test]
2616 fn test_catalog_has_no_duplicate_names() {
2617 let names: Vec<&str> = catalog().iter().map(|e| e.name).collect();
2618 let unique: HashSet<&str> = names.iter().copied().collect();
2619 assert_eq!(
2620 names.len(),
2621 unique.len(),
2622 "catalog() contains a duplicate indicator name"
2623 );
2624 }
2625
2626 #[test]
2627 fn test_every_catalog_entry_builds_with_its_default_params() {
2628 for entry in catalog() {
2629 let built = build_checked(entry.name, &entry.default_params);
2630 assert!(
2631 built.is_ok(),
2632 "catalog entry '{}' failed to build with its own default params: {:?}",
2633 entry.name,
2634 built.err()
2635 );
2636 }
2637 }
2638
2639 #[test]
2642 fn test_catalog_does_not_contain_aliases() {
2643 let catalog_names: HashSet<&str> = catalog().iter().map(|e| e.name).collect();
2644 for alias in [
2645 "vp",
2646 "wvf",
2647 "ob",
2648 "fvg",
2649 "smc",
2650 "bos",
2651 "choch",
2652 "pivots",
2653 "pinbar",
2654 "leg_efficiency",
2655 "er",
2656 "vp_extended",
2657 "vp_persistent",
2658 ] {
2659 assert!(
2660 !catalog_names.contains(alias),
2661 "'{alias}' is an alias, not a canonical name, and must not be its own catalog entry"
2662 );
2663 }
2664 }
2665
2666 #[test]
2667 fn test_build_checked_valid_and_invalid_params() {
2668 let valid_params = HashMap::from([("period".to_string(), 14.0)]);
2669 assert!(build_checked("rsi", &valid_params).is_ok());
2670
2671 let zero_params = HashMap::from([("rsi_len".to_string(), 0.0)]);
2673 let err = match build_checked("rsi", &zero_params) {
2674 Err(e) => e,
2675 Ok(_) => panic!("Expected error for zero period"),
2676 };
2677 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2678
2679 let neg_params = HashMap::from([("period".to_string(), -5.0)]);
2681 let err = match build_checked("sma", &neg_params) {
2682 Err(e) => e,
2683 Ok(_) => panic!("Expected error for negative period"),
2684 };
2685 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2686
2687 let fractional_params = HashMap::from([("period".to_string(), 14.5)]);
2689 assert!(matches!(
2690 build_checked("sma", &fractional_params),
2691 Err(RegistryError::InvalidParameter { .. })
2692 ));
2693
2694 let huge_params = HashMap::from([("period".to_string(), 10_001.0)]);
2696 assert!(matches!(
2697 build_checked("sma", &huge_params),
2698 Err(RegistryError::InvalidParameter { .. })
2699 ));
2700
2701 let nan_params = HashMap::from([("period".to_string(), f64::NAN)]);
2703 let err = match build_checked("sma", &nan_params) {
2704 Err(e) => e,
2705 Ok(_) => panic!("Expected error for NaN period"),
2706 };
2707 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2708
2709 let reversed_rsi = HashMap::from([
2711 ("oversold".to_string(), 80.0),
2712 ("overbought".to_string(), 20.0),
2713 ]);
2714 assert!(matches!(
2715 build_checked("rsi", &reversed_rsi),
2716 Err(RegistryError::InvalidParameter { .. })
2717 ));
2718
2719 let reversed_ultimate = HashMap::from([
2720 ("period1".to_string(), 28.0),
2721 ("period2".to_string(), 14.0),
2722 ("period3".to_string(), 7.0),
2723 ]);
2724 assert!(matches!(
2725 build_checked("ultimate_oscillator", &reversed_ultimate),
2726 Err(RegistryError::InvalidParameter { .. })
2727 ));
2728
2729 let oversized_bins = HashMap::from([("num_bins".to_string(), 1_001.0)]);
2730 assert!(matches!(
2731 build_checked("volume_profile", &oversized_bins),
2732 Err(RegistryError::InvalidParameter { .. })
2733 ));
2734
2735 let inf_params = HashMap::from([("period".to_string(), f64::INFINITY)]);
2737 let err = match build_checked("sma", &inf_params) {
2738 Err(e) => e,
2739 Ok(_) => panic!("Expected error for Infinity period"),
2740 };
2741 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2742
2743 let macd_bad = HashMap::from([
2745 ("fast_len".to_string(), 30.0),
2746 ("slow_len".to_string(), 20.0),
2747 ]);
2748 let err = match build_checked("macd", &macd_bad) {
2749 Err(e) => e,
2750 Ok(_) => panic!("Expected error for fast >= slow"),
2751 };
2752 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2753
2754 let err = match build_checked("non_existent_ind", &HashMap::new()) {
2756 Err(e) => e,
2757 Ok(_) => panic!("Expected error for unknown indicator"),
2758 };
2759 assert_eq!(
2760 err,
2761 RegistryError::UnknownIndicator("non_existent_ind".to_string())
2762 );
2763 }
2764
2765 #[test]
2766 fn test_keltner_ema_period_is_the_canonical_catalog_key() {
2767 let ind = build_checked(
2772 "keltner",
2773 &HashMap::from([
2774 ("ema_period".to_string(), 5.0),
2775 ("atr_period".to_string(), 1.0),
2776 ]),
2777 )
2778 .unwrap();
2779 assert_eq!(ind.warmup_period(), 5);
2780
2781 let default_ind =
2782 build_checked("keltner", &HashMap::from([("atr_period".to_string(), 1.0)])).unwrap();
2783 assert_eq!(default_ind.warmup_period(), 20);
2784 }
2785
2786 #[test]
2787 fn test_keltner_ma_period_legacy_alias_still_works() {
2788 let ind = build_checked(
2789 "keltner",
2790 &HashMap::from([
2791 ("ma_period".to_string(), 5.0),
2792 ("atr_period".to_string(), 1.0),
2793 ]),
2794 )
2795 .unwrap();
2796 assert_eq!(ind.warmup_period(), 5);
2797 }
2798
2799 #[test]
2800 fn test_keltner_ema_period_invalid_value_rejected() {
2801 let err = match build_checked("keltner", &HashMap::from([("ema_period".to_string(), 0.0)]))
2802 {
2803 Err(e) => e,
2804 Ok(_) => panic!("Expected error for zero ema_period"),
2805 };
2806 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2807 }
2808
2809 #[test]
2810 fn test_keltner_conflicting_canonical_and_alias_rejected() {
2811 let err = match build_checked(
2812 "keltner",
2813 &HashMap::from([
2814 ("ema_period".to_string(), 5.0),
2815 ("ma_period".to_string(), 10.0),
2816 ]),
2817 ) {
2818 Err(e) => e,
2819 Ok(_) => panic!("Expected error for conflicting ema_period/ma_period"),
2820 };
2821 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2822
2823 assert!(build_checked(
2825 "keltner",
2826 &HashMap::from([
2827 ("ema_period".to_string(), 5.0),
2828 ("ma_period".to_string(), 5.0),
2829 ]),
2830 )
2831 .is_ok());
2832 }
2833
2834 #[test]
2835 fn test_build_typed_flattens_numeric_params() {
2836 let params: TypedParams = HashMap::from([("period".to_string(), ParamValue::Int(14))]);
2837 assert!(build_typed("rsi", ¶ms).is_ok());
2838
2839 let bool_params: TypedParams =
2840 HashMap::from([("period".to_string(), ParamValue::Bool(true))]);
2841 assert!(build_typed("rsi", &bool_params).is_ok());
2843 }
2844
2845 #[test]
2846 fn test_build_typed_rejects_non_numeric_params() {
2847 let params: TypedParams =
2848 HashMap::from([("period".to_string(), ParamValue::Enum("fast".to_string()))]);
2849 let err = match build_typed("rsi", ¶ms) {
2850 Err(e) => e,
2851 Ok(_) => panic!("Expected error for enum parameter"),
2852 };
2853 assert!(matches!(
2854 err,
2855 RegistryError::UnsupportedParameterType { .. }
2856 ));
2857 }
2858
2859 #[test]
2860 fn test_build_typed_propagates_indicator_validation_errors() {
2861 let params: TypedParams =
2862 HashMap::from([("period".to_string(), ParamValue::Float(f64::INFINITY))]);
2863 let err = match build_typed("sma", ¶ms) {
2864 Err(e) => e,
2865 Ok(_) => panic!("Expected error for infinite period"),
2866 };
2867 assert!(matches!(err, RegistryError::InvalidParameter { .. }));
2868 }
2869
2870 #[test]
2871 fn test_build_typed_anchored_vwap_native_enum_selection() {
2872 let defaulted = build_typed("anchored_vwap", &TypedParams::new());
2874 assert!(defaulted.is_ok());
2875
2876 let day_params: TypedParams = HashMap::from([(
2877 "anchor_kind".to_string(),
2878 ParamValue::Enum("day".to_string()),
2879 )]);
2880 assert!(build_typed("avwap", &day_params).is_ok());
2881
2882 let skip_zero_vol: TypedParams = HashMap::from([(
2883 "zero_volume_policy".to_string(),
2884 ParamValue::Enum("skip".to_string()),
2885 )]);
2886 assert!(build_typed("anchored_vwap", &skip_zero_vol).is_ok());
2887
2888 let manual_without_timestamp: TypedParams = HashMap::from([(
2889 "anchor_kind".to_string(),
2890 ParamValue::Enum("manual_timestamp".to_string()),
2891 )]);
2892 let err = match build_typed("anchored_vwap", &manual_without_timestamp) {
2893 Err(e) => e,
2894 Ok(_) => panic!("Expected error: manual_timestamp requires anchor_timestamp"),
2895 };
2896 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
2897
2898 let manual_with_timestamp: TypedParams = HashMap::from([
2899 (
2900 "anchor_kind".to_string(),
2901 ParamValue::Enum("manual_timestamp".to_string()),
2902 ),
2903 (
2904 "anchor_timestamp".to_string(),
2905 ParamValue::Timestamp(1_700_000_000),
2906 ),
2907 ]);
2908 assert!(build_typed("anchored_vwap", &manual_with_timestamp).is_ok());
2909
2910 let unknown_kind: TypedParams = HashMap::from([(
2911 "anchor_kind".to_string(),
2912 ParamValue::Enum("bogus".to_string()),
2913 )]);
2914 let err = match build_typed("anchored_vwap", &unknown_kind) {
2915 Err(e) => e,
2916 Ok(_) => panic!("Expected error for unknown anchor_kind"),
2917 };
2918 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
2919 }
2920
2921 #[test]
2922 fn test_build_typed_pivot_sets_native_enum_selection() {
2923 for kind in [
2924 "classic",
2925 "fibonacci",
2926 "camarilla",
2927 "woodie",
2928 "demark",
2929 "cpr",
2930 ] {
2931 let params: TypedParams =
2932 HashMap::from([("pivot_type".to_string(), ParamValue::Enum(kind.to_string()))]);
2933 assert!(
2934 build_typed("pivot_sets", ¶ms).is_ok(),
2935 "expected {kind} to build"
2936 );
2937 }
2938
2939 let unknown: TypedParams = HashMap::from([(
2940 "pivot_type".to_string(),
2941 ParamValue::Enum("bogus".to_string()),
2942 )]);
2943 let err = match build_typed("multi_pivots", &unknown) {
2944 Err(e) => e,
2945 Ok(_) => panic!("Expected error for unknown pivot_type"),
2946 };
2947 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
2948 }
2949
2950 #[test]
2951 fn test_build_typed_source_propagates_to_computation() {
2952 use crate::model::{Bar, Source};
2953
2954 let bars = [
2955 Bar::new(0, 10.0, 12.0, 8.0, 11.0, 100.0),
2956 Bar::new(60, 20.0, 22.0, 18.0, 21.0, 100.0),
2957 ];
2958
2959 let close_params: TypedParams = HashMap::from([("period".to_string(), ParamValue::Int(2))]);
2960 let mut close_sma = build_typed("sma", &close_params).unwrap();
2961
2962 let open_params: TypedParams = HashMap::from([
2963 ("period".to_string(), ParamValue::Int(2)),
2964 ("source".to_string(), ParamValue::Source(Source::Open)),
2965 ]);
2966 let mut open_sma = build_typed("sma", &open_params).unwrap();
2967
2968 let mut close_out = None;
2969 let mut open_out = None;
2970 for bar in &bars {
2971 close_out = close_sma.on_bar(bar);
2972 open_out = open_sma.on_bar(bar);
2973 }
2974
2975 assert_eq!(close_out.unwrap().value, (11.0 + 21.0) / 2.0);
2976 assert_eq!(open_out.unwrap().value, (10.0 + 20.0) / 2.0);
2977 }
2978
2979 #[test]
2980 fn test_build_typed_source_close_is_a_no_op() {
2981 let params: TypedParams = HashMap::from([(
2982 "source".to_string(),
2983 ParamValue::Source(crate::model::Source::Close),
2984 )]);
2985 assert!(build_typed("sma", ¶ms).is_ok());
2986 }
2987
2988 #[test]
2989 fn test_adx_with_defaults_matches_registry_default() {
2990 let mut via_struct = Adx::with_defaults();
2991 let mut via_registry = build_checked("adx", &HashMap::new()).unwrap();
2992
2993 let bars = crate::model::Bar::new(0, 100.0, 101.0, 99.0, 100.5, 1000.0);
2994 let mut struct_out = None;
2995 let mut registry_out = None;
2996 for i in 0..60 {
2997 let price = 100.0 + (i as f64 * 0.3).sin() * 5.0;
2998 let bar =
2999 crate::model::Bar::new(i, price, price + 1.0, price - 1.0, price + 0.5, 1000.0);
3000 struct_out = via_struct.on_bar(&bar);
3001 registry_out = via_registry.on_bar(&bar);
3002 }
3003 let _ = bars;
3004 assert_eq!(
3005 struct_out.map(|o| o.value),
3006 registry_out.map(|o| o.value),
3007 "Adx::with_defaults() must produce identical output to the registry's \"adx\" default"
3008 );
3009 }
3010
3011 #[test]
3012 fn test_atr_with_defaults_matches_registry_default() {
3013 let mut via_struct = Atr::with_defaults();
3014 let mut via_registry = build_checked("atr", &HashMap::new()).unwrap();
3015
3016 let mut struct_out = None;
3017 let mut registry_out = None;
3018 for i in 0..40 {
3019 let price = 100.0 + (i as f64 * 0.3).sin() * 5.0;
3020 let bar =
3021 crate::model::Bar::new(i, price, price + 1.0, price - 1.0, price + 0.5, 1000.0);
3022 struct_out = via_struct.on_bar(&bar);
3023 registry_out = via_registry.on_bar(&bar);
3024 }
3025 assert_eq!(
3026 struct_out.map(|o| o.value),
3027 registry_out.map(|o| o.value),
3028 "Atr::with_defaults() must produce identical output to the registry's \"atr\" default"
3029 );
3030 }
3031
3032 #[test]
3035 fn test_build_typed_rejects_non_close_source_on_efi() {
3036 let params: TypedParams = HashMap::from([(
3037 "source".to_string(),
3038 ParamValue::Source(crate::model::Source::Hlc3),
3039 )]);
3040 let err = match build_typed("efi", ¶ms) {
3041 Err(e) => e,
3042 Ok(_) => panic!("expected 'efi' to reject a non-Close source"),
3043 };
3044 assert!(
3045 matches!(err, RegistryError::IncompatibleParameter { .. }),
3046 "efi returned {err:?} instead of IncompatibleParameter"
3047 );
3048
3049 let close: TypedParams = HashMap::from([(
3050 "source".to_string(),
3051 ParamValue::Source(crate::model::Source::Close),
3052 )]);
3053 assert!(build_typed("efi", &close).is_ok());
3054 }
3055
3056 #[test]
3057 fn test_build_typed_rejects_non_close_source_on_range_dependent_indicators() {
3058 let params: TypedParams = HashMap::from([(
3059 "source".to_string(),
3060 ParamValue::Source(crate::model::Source::Open),
3061 )]);
3062 for name in [
3063 "atr",
3064 "wyckoff",
3065 "chandelier_exit",
3066 "ce",
3067 "extended_volume_profile",
3068 "zigzag_advanced",
3069 "keltner",
3070 "donchian",
3071 "garman_klass",
3072 ] {
3073 let err = match build_typed(name, ¶ms) {
3074 Err(e) => e,
3075 Ok(_) => panic!("expected '{name}' to reject a non-Close source"),
3076 };
3077 assert!(
3078 matches!(err, RegistryError::IncompatibleParameter { .. }),
3079 "'{name}' returned {err:?} instead of IncompatibleParameter"
3080 );
3081 }
3082
3083 let close_params: TypedParams = HashMap::from([(
3085 "source".to_string(),
3086 ParamValue::Source(crate::model::Source::Close),
3087 )]);
3088 assert!(build_typed("atr", &close_params).is_ok());
3089 }
3090
3091 #[test]
3092 fn test_build_typed_trend_relationship_native_smoother_kind_selection() {
3093 let params: TypedParams = HashMap::from([
3094 ("fast_kind".to_string(), ParamValue::Enum("jma".to_string())),
3095 (
3096 "slow_kind".to_string(),
3097 ParamValue::Enum("alma".to_string()),
3098 ),
3099 ("fast_len".to_string(), ParamValue::Int(5)),
3100 ("slow_len".to_string(), ParamValue::Int(20)),
3101 ]);
3102 assert!(build_typed("trend_relationship", ¶ms).is_ok());
3103
3104 let unknown: TypedParams = HashMap::from([(
3105 "fast_kind".to_string(),
3106 ParamValue::Enum("bogus".to_string()),
3107 )]);
3108 let err = match build_typed("trend_relationship", &unknown) {
3109 Err(e) => e,
3110 Ok(_) => panic!("Expected error for unknown fast_kind"),
3111 };
3112 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
3113 }
3114
3115 #[test]
3116 fn test_build_checked_trend_relationship_defaults_to_ema() {
3117 assert!(build_checked("trend_relationship", &HashMap::new()).is_ok());
3118 }
3119
3120 #[test]
3121 fn test_build_typed_midas_native_mode_and_source_selection() {
3122 let params: TypedParams = HashMap::from([
3123 (
3124 "mode".to_string(),
3125 ParamValue::Enum("bottomfinder".to_string()),
3126 ),
3127 (
3128 "source".to_string(),
3129 ParamValue::Source(crate::model::Source::Close),
3130 ),
3131 ]);
3132 assert!(build_typed("midas", ¶ms).is_ok());
3133
3134 let unknown: TypedParams =
3135 HashMap::from([("mode".to_string(), ParamValue::Enum("bogus".to_string()))]);
3136 let err = match build_typed("midas", &unknown) {
3137 Err(e) => e,
3138 Ok(_) => panic!("Expected error for unknown mode"),
3139 };
3140 assert!(matches!(err, RegistryError::InvalidEnumValue { .. }));
3141 }
3142
3143 #[test]
3144 fn test_build_checked_midas_defaults_to_topfinder() {
3145 assert!(build_checked("midas", &HashMap::new()).is_ok());
3146 }
3147}