Skip to main content

egui_charts/studies/factory/
mod.rs

1//! Indicator factory for dynamic indicator creation by name.
2//!
3//! The [`IndicatorFactory`] provides a registry of constructor functions
4//! keyed by indicator name strings. The built-in catalogue is
5//! registered under its canonical [`Indicator::name`] key and a category,
6//! and users can add their own at runtime. It is the catalogue of studies: a
7//! picker lists [`list`](IndicatorFactory::list), describes each entry with
8//! [`Indicator::desc`] and [`category`](IndicatorFactory::category), and
9//! creates it with [`create`](IndicatorFactory::create).
10//!
11//! # Example
12//!
13//! ```rust
14//! use egui_charts::studies::IndicatorFactory;
15//!
16//! let factory = IndicatorFactory::new(); // includes all built-ins
17//! let rsi = factory.create("RSI").unwrap();
18//! assert_eq!(rsi.name(), "RSI");
19//!
20//! // Register a custom entry
21//! let mut factory = factory;
22//! factory.register("MySMA", || Box::new(egui_charts::studies::SMA::new(42)));
23//! ```
24
25use crate::data::OrderFlowKind;
26use crate::studies::*;
27use std::collections::HashMap;
28
29/// Type alias for indicator constructor closures stored in the factory.
30type IndicatorConstructor = Box<dyn Fn() -> Box<dyn Indicator> + Send + Sync>;
31
32/// Register every listed built-in indicator type with the factory, under its category.
33///
34/// Each indicator is constructed through its [`Default`] implementation --
35/// the single canonical source of its conventional parameters -- and keyed by
36/// the [`Indicator::name`] it reports. Keying off `name()` keeps the registry
37/// in lock-step with the indicators themselves: a name can never drift from
38/// the value the rendering layer and legends display, and a new indicator is
39/// reachable through the factory the moment it is added to this list.
40macro_rules! register_builtins {
41    ($factory:expr, $($category:literal => [$($ty:ty),+ $(,)?]),+ $(,)?) => {
42        $($(
43            // Read the canonical name from a throwaway default instance so the
44            // key is always exactly what the indicator reports at runtime.
45            let key = <$ty as Default>::default().name().to_string();
46            $factory.constructors.insert(
47                key,
48                (Some($category), Box::new(|| Box::new(<$ty as Default>::default()))),
49            );
50        )+)+
51    };
52}
53
54/// A factory that creates [`Indicator`] instances by name.
55///
56/// On construction, catalogue studies are registered under their canonical
57/// [`Indicator::name`] (e.g. `"SMA"`, `"RSI"`, `"MACD"`, `"SuperTrend"`) using
58/// its conventional default parameters. Each name selects a distinct calculation
59/// or source range. Additional indicators can be registered at runtime with
60/// [`register`](Self::register).
61///
62/// [`create`](Self::create) and [`has`](Self::has) also resolve the names of seven exported
63/// studies the catalogue leaves out, so that a template, a copied study or a saved layout of
64/// one is recreated: `CVD`, `C2C Vol`, `HL2`, `HLC3`, `Price Channel`, `Ehlers Fisher` and
65/// `MA Combination`, which creates [`MACombination::ribbon`] for its inputs to reshape. They have
66/// no category and are not in [`list`](Self::list) or [`count`](Self::count).
67pub struct IndicatorFactory {
68    /// The category of each name, `None` for a built-in outside the catalogue, and its constructor.
69    constructors: HashMap<String, (Option<&'static str>, IndicatorConstructor)>,
70}
71
72impl IndicatorFactory {
73    /// The category of every indicator added with [`register`](Self::register).
74    pub const CUSTOM: &'static str = "Custom";
75    /// The category of the user's own studies, such as scripts the host compiles or imports;
76    /// a study browser lists them as the user's scripts.
77    pub const PERSONAL: &'static str = "Personal";
78    /// The category of studies the host obtains from other users.
79    pub const COMMUNITY: &'static str = "Community";
80    /// The category of the built-in volume and time price profiles.
81    pub const PROFILES: &'static str = "Profiles";
82    /// The category of the built-in pattern detectors.
83    pub const PATTERNS: &'static str = "Patterns";
84    /// The category of studies computed from trades, quotes or the order book.
85    pub const ORDER_FLOW: &'static str = "Order flow";
86
87    /// Create a factory with the built-in catalogue enabled by the current features.
88    pub fn new() -> Self {
89        let mut factory = Self {
90            constructors: HashMap::new(),
91        };
92
93        factory.register_builtin();
94        factory
95    }
96
97    /// Register every built-in indicator under its canonical name.
98    ///
99    /// Types use their defaults; automatic drawings select a construction kind and
100    /// stream studies select aggregation parameters. Names come from each study.
101    /// Exported studies outside the catalogue are registered first, without a category, so a
102    /// listed study of the same name would replace them.
103    fn register_builtin(&mut self) {
104        // Exported studies outside the catalogue, resolved by name so that saved copies load.
105        let unlisted: [fn() -> Box<dyn Indicator>; 7] = [
106            || Box::new(CVD::default()),
107            || Box::new(CloseToCloseVol::default()),
108            || Box::new(HL2::default()),
109            || Box::new(HLC3::default()),
110            || Box::new(PriceChannel::default()),
111            || Box::new(EhlersFisher::default()),
112            || Box::new(MACombination::ribbon()),
113        ];
114        for constructor in unlisted {
115            self.constructors.insert(
116                constructor().name().to_owned(),
117                (None, Box::new(constructor)),
118            );
119        }
120        register_builtins!(
121            self,
122            "Moving averages" => [
123                SMA,
124                EMA,
125                WMA,
126                DEMA,
127                TEMA,
128                HMA,
129                ALMA,
130                VWMA,
131                LSMA,
132                SWMA,
133                KAMA,
134                SmoothedMA,
135                GuppyMA,
136                McGinleyDynamic,
137                VIDYA,
138                ZLEMA,
139                HammingMA,
140                MultipleMA,
141                MAChannel,
142                EMACross,
143                MACross,
144                MixedMACross,
145                TripleMA,
146                MARibbon,
147            ],
148            "Momentum" => [
149                RSI,
150                MACD,
151                VolumeWeightedMACD,
152                Stochastic,
153                StochasticRSI,
154                WilliamsR,
155                CCI,
156                Momentum,
157                RateOfChange,
158                Aroon,
159                AwesomeOscillator,
160                RsiDivergence,
161                KnoxvilleDivergence,
162                TechnicalRatings,
163                UltimateOscillator,
164                ConnorsRSI,
165                CoppockCurve,
166                TRIX,
167                ElderRay,
168                FisherTransform,
169                ForceIndex,
170                MassIndex,
171                DetrendedPriceOscillator,
172                KDJ,
173                PrettyGoodOscillator,
174                ChandeMomentumOscillator,
175                TrueStrengthIndex,
176                StochasticMomentumIndex,
177                SchaffTrendCycle,
178                RelativeVigorIndex,
179                DoubleStochastic,
180                CenterOfGravity,
181                KnowSureThing,
182                RainbowOscillator,
183                UltimateMomentum,
184                PriceOscillator,
185                PercentageVolumeOscillator,
186                VolumeOscillator,
187                BalanceOfPower,
188                EaseOfMovement,
189                AcceleratorOscillator,
190                AroonOscillator,
191                BullBearPower,
192                AdvanceDeclineRatioBars,
193                PriceMomentumOscillator,
194                PringsSpecialK,
195                SMIErgodicIndicator,
196                SMIErgodicOscillator,
197                WoodiesCCI,
198            ],
199            "Trend" => [
200                ADX,
201                AdxBreakout,
202                SuperTrend,
203                IchimokuCloud,
204                ParabolicSAR,
205                DirectionalMovement,
206                VortexIndicator,
207                SqueezeMomentum,
208                TrendIntensityIndex,
209                TrendDetectionIndex,
210                Qstick,
211                ChoppinessIndex,
212                ElderImpulseSystem,
213                WilliamsAlligator,
214                ChopZone,
215                VolatilityStop,
216                ChandeKrollStop,
217            ],
218            "Volatility" => [
219                BollingerBands,
220                BollingerBandsWidth,
221                ATR,
222                TrueRange,
223                KeltnerChannels,
224                DonchianChannels,
225                StandardDeviation,
226                HistoricalVolatility,
227                ChaikinVolatility,
228                ChandelierExit,
229                AccelerationBands,
230                Envelopes,
231                PercentB,
232                OhlcVolatility,
233                ParkinsonVol,
234                ZeroTrendVol,
235                CommoditySelectionIndex,
236                AverageDailyRange,
237                BBTrend,
238                UlcerIndex,
239                RelativeVolatilityIndex,
240                Median,
241            ],
242            "Volume" => [
243                Volume,
244                UpDownVolume,
245                VolumeDelta,
246                CumulativeVolumeDelta,
247                OnBalanceVolume,
248                VolumeWeightedAvgPrice,
249                AnchoredVWAP,
250                VwapAutoAnchored,
251                TimeWeightedAvgPrice,
252                MoneyFlowIndex,
253                AccumulationDistribution,
254                ChaikinMoneyFlow,
255                VolumeRateOfChange,
256                KlingerOscillator,
257                NetVolume,
258                VolumePriceTrend,
259                PriceVolumeTrendStudy,
260                NegativeVolumeIndex,
261                PositiveVolumeIndex,
262                WilliamsAD,
263                MarketFacilitationIndex,
264                ChaikinOscillator,
265                Volume24h,
266                RelativeVolumeAtTime,
267            ],
268            "Profiles" => [
269                VolProfileVisible,
270                VolProfileFixed,
271                VolProfileSession,
272                VolProfilePeriodic,
273                VolProfileAnchored,
274                VolProfileAutoAnchored,
275                TimePriceOpportunity,
276            ],
277            "Support and resistance" => [
278                PivotPoints,
279                PivotPointsHighLow,
280                Reversal,
281                ElliottWave,
282                RisingWedge,
283                FallingWedge,
284                MissedPivots,
285                KeyLevels,
286                IntradayPivotPoints,
287                ProjectedPivotPoints,
288                WilliamsFractals,
289                ZigZag,
290            ],
291            "Statistics" => [
292                LinearRegression,
293                LinearRegressionChannel,
294                LinearRegressionSlope,
295                TrendStrengthIndex,
296                StandardError,
297                StandardErrorBands,
298                RankCorrelationIndex,
299                Ratio,
300                Spread,
301                Correlation,
302                RciRibbon,
303                MajorityRule,
304                AccumulationSwingIndex,
305                Seasonality,
306                LunarPhases,
307                Performance,
308            ],
309            "Price" => [
310                HigherIntervalCandles,
311                BollingerBars,
312                TypicalPrice,
313                MedianPrice,
314                WeightedClose,
315                OHLC4,
316                VisibleAveragePrice,
317            ],
318            "Market structure" => [
319                Week52HighLow,
320                Week52HighLowLevels,
321                TradingSessions,
322                Gaps,
323            ],
324            "Market breadth" => [
325                AdvanceDeclineRatio,
326                AdvanceDeclineLine,
327                CumulativeVolumeIndex,
328            ],
329            "Patterns" => [
330                CandlestickPatterns,
331            ],
332        );
333        for kind in [
334            OrderFlowKind::Spread,
335            OrderFlowKind::BookSize {
336                levels: 10,
337                distance: 0.0,
338            },
339            OrderFlowKind::TradeRate {
340                volume: false,
341                seconds: 60,
342                instant: 3,
343            },
344            OrderFlowKind::AggressorRate {
345                seconds: 60,
346                instant: 3,
347            },
348            OrderFlowKind::LevelsExecuted {
349                seconds: 0,
350                reversal: false,
351            },
352            OrderFlowKind::SizeDelta {
353                ranges: [[0.0, 9.0], [10.0, 99.0], [100.0, f64::MAX]],
354            },
355            OrderFlowKind::FlowSum { bars: 10 },
356            OrderFlowKind::FilteredCvd {
357                min: 100.0,
358                max: f64::MAX,
359            },
360            OrderFlowKind::LevelsTradedThrough { seconds: 0 },
361        ] {
362            let name = OrderFlowStudy::new(kind).name().to_owned();
363            self.constructors.insert(
364                name,
365                (
366                    Some(Self::ORDER_FLOW),
367                    Box::new(move || Box::new(OrderFlowStudy::new(kind))),
368                ),
369            );
370        }
371        for cumulative in [false, true] {
372            let name = OrderFlowStudy::execution_delta(cumulative)
373                .name()
374                .to_owned();
375            self.constructors.insert(
376                name,
377                (
378                    Some(Self::ORDER_FLOW),
379                    Box::new(move || Box::new(OrderFlowStudy::execution_delta(cumulative))),
380                ),
381            );
382        }
383        for family in PatternFamily::ALL {
384            let name = ChartPatterns::new(family).name().to_owned();
385            self.constructors.insert(
386                name,
387                (
388                    Some(Self::PATTERNS),
389                    Box::new(move || Box::new(ChartPatterns::new(family))),
390                ),
391            );
392        }
393        #[cfg(feature = "drawings")]
394        for kind in AutoDrawingKind::ALL {
395            let name = AutoDrawing::new(kind, 5.0, 10).name().to_owned();
396            self.constructors.insert(
397                name,
398                (
399                    Some("Support and resistance"),
400                    Box::new(move || Box::new(AutoDrawing::new(kind, 5.0, 10).with_atr(3.0, 10))),
401                ),
402            );
403        }
404    }
405
406    /// Register a named indicator constructor in the [`CUSTOM`](Self::CUSTOM) category.
407    ///
408    /// A constructor already registered under `name`, a built-in included, is replaced. Returns
409    /// `false`, registering nothing, when `name` is empty or only whitespace.
410    ///
411    /// # Arguments
412    /// * `name` -- The lookup key (e.g. `"MySMA"`).
413    /// * `constructor` -- A closure that returns a new boxed indicator.
414    pub fn register<F>(&mut self, name: impl Into<String>, constructor: F) -> bool
415    where
416        F: Fn() -> Box<dyn Indicator> + Send + Sync + 'static,
417    {
418        self.register_in(Self::CUSTOM, name, constructor)
419    }
420
421    /// Register a named indicator constructor in `category`, such as [`PERSONAL`](Self::PERSONAL)
422    /// for a script the user imported, [`COMMUNITY`](Self::COMMUNITY) or
423    /// [`PROFILES`](Self::PROFILES) or [`ORDER_FLOW`](Self::ORDER_FLOW).
424    ///
425    /// A constructor already registered under `name`, a built-in included, is replaced. Returns
426    /// `false`, registering nothing, when `name` is empty or only whitespace.
427    pub fn register_in<F>(
428        &mut self,
429        category: &'static str,
430        name: impl Into<String>,
431        constructor: F,
432    ) -> bool
433    where
434        F: Fn() -> Box<dyn Indicator> + Send + Sync + 'static,
435    {
436        let name = name.into();
437        if name.trim().is_empty() {
438            return false;
439        }
440        self.constructors
441            .insert(name, (Some(category), Box::new(constructor)));
442        true
443    }
444
445    /// Create an indicator by its registered name, or `None` if not found.
446    pub fn create(&self, name: &str) -> Option<Box<dyn Indicator>> {
447        self.constructors.get(name).map(|(_, f)| f())
448    }
449
450    /// The category of the indicator registered under `name`, such as `"Momentum"` or
451    /// `"Volume"` for a built-in and [`CUSTOM`](Self::CUSTOM) for one added with
452    /// [`register`](Self::register), or `None` if not found or left out of the catalogue.
453    pub fn category(&self, name: &str) -> Option<&'static str> {
454        self.constructors
455            .get(name)
456            .and_then(|(category, _)| *category)
457    }
458
459    /// The names a study picker offers, unordered: every registered name except the exported
460    /// studies outside the catalogue.
461    pub fn list(&self) -> Vec<String> {
462        self.constructors
463            .iter()
464            .filter(|(_, (category, _))| category.is_some())
465            .map(|(name, _)| name.clone())
466            .collect()
467    }
468
469    /// Whether [`create`](Self::create) resolves `name`, also one that [`list`](Self::list)
470    /// leaves out.
471    pub fn has(&self, name: &str) -> bool {
472        self.constructors.contains_key(name)
473    }
474
475    /// Remove the indicator registered under `name`.
476    ///
477    /// Returns `true` if it was present and removed, `false` otherwise.
478    pub fn unregister(&mut self, name: &str) -> bool {
479        self.constructors.remove(name).is_some()
480    }
481
482    /// Remove all registered indicators, including built-ins.
483    pub fn clear(&mut self) {
484        self.constructors.clear();
485    }
486
487    /// The number of names in [`list`](Self::list).
488    pub fn count(&self) -> usize {
489        self.constructors
490            .values()
491            .filter(|(category, _)| category.is_some())
492            .count()
493    }
494}
495
496impl Default for IndicatorFactory {
497    fn default() -> Self {
498        Self::new()
499    }
500}
501
502#[cfg(test)]
503mod builtin_properties;
504#[cfg(test)]
505mod tests;