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;