Skip to main content

indicators/
registry.rs

1//! Indicator registry — create indicators by name at runtime.
2//!
3//! Mirrors `indicators/registry.py` and `indicators/factory.py`:
4//! - `IndicatorRegistry` ↔ `class IndicatorRegistry`
5//! - `register!` macro ↔ `@register_indicator` decorator
6//! - `IndicatorFactory::create(name, params)` ↔ `IndicatorFactory.create(name, **params)`
7//!
8//! # Usage
9//!
10//! ```rust,ignore
11//! use crate::registry::REGISTRY;
12//!
13//! // list what's available
14//! let names = REGISTRY.list();
15//!
16//! // create by name with typed params map
17//! let params = [("period", "20")].into();
18//! let indicator = REGISTRY.create("sma", params).unwrap();
19//! let output = indicator.calculate(&candles).unwrap();
20//! ```
21
22use std::collections::HashMap;
23use std::sync::{OnceLock, RwLock};
24
25use crate::error::IndicatorError;
26use crate::indicator::Indicator;
27
28// ── Factory fn type ───────────────────────────────────────────────────────────
29
30/// A function that constructs a `Box<dyn Indicator>` from a string param map.
31///
32/// Mirrors Python's `indicator_cls(name=name, params=params)` call in
33/// `IndicatorRegistry.create()`.
34pub type IndicatorFactory =
35    fn(params: &HashMap<String, String>) -> Result<Box<dyn Indicator>, IndicatorError>;
36
37// ── Registry ──────────────────────────────────────────────────────────────────
38
39/// Runtime registry mapping indicator names to their factory functions.
40///
41/// Analogous to `IndicatorRegistry._indicators: dict[str, type[Indicator]]`
42/// in Python.
43pub struct IndicatorRegistry {
44    entries: RwLock<HashMap<String, IndicatorFactory>>,
45}
46
47impl IndicatorRegistry {
48    pub fn new_uninit() -> Self {
49        // RwLock::new is not const-stable yet; we use OnceLock wrapping below.
50        // This constructor is intentionally left as a marker — use `REGISTRY`.
51        Self {
52            entries: RwLock::new(HashMap::new()),
53        }
54    }
55
56    /// Register an indicator factory under `name` (lowercased).
57    ///
58    /// Mirrors `IndicatorRegistry.register(indicator_cls)`.
59    pub fn register(&self, name: &str, factory: IndicatorFactory) {
60        let mut map = self.entries.write().expect("registry write lock poisoned");
61        map.insert(name.to_ascii_lowercase(), factory);
62    }
63
64    /// List all registered indicator names.
65    ///
66    /// Mirrors `IndicatorRegistry.list() -> list[str]`.
67    pub fn list(&self) -> Vec<String> {
68        let map = self.entries.read().expect("registry read lock poisoned");
69        map.keys().cloned().collect()
70    }
71
72    /// Look up a factory by name (case-insensitive).
73    ///
74    /// Mirrors `IndicatorRegistry.get(name)`.
75    pub fn get(&self, name: &str) -> Option<IndicatorFactory> {
76        let map = self.entries.read().expect("registry read lock poisoned");
77        map.get(&name.to_ascii_lowercase()).copied()
78    }
79
80    /// Create an indicator instance by name.
81    ///
82    /// Mirrors `IndicatorRegistry.create(name, **params)` and
83    /// `IndicatorFactory.create(name, **params)`.
84    ///
85    /// # Errors
86    /// - `IndicatorError::UnknownIndicator` if `name` is not registered.
87    /// - Propagates construction errors from the factory.
88    pub fn create(
89        &self,
90        name: &str,
91        params: &HashMap<String, String>,
92    ) -> Result<Box<dyn Indicator>, IndicatorError> {
93        let factory = self
94            .get(name)
95            .ok_or_else(|| IndicatorError::UnknownIndicator {
96                name: name.to_string(),
97            })?;
98        factory(params)
99    }
100
101    /// Check whether an indicator name is registered.
102    ///
103    /// Mirrors `indicator_registry.get(name) is not None` in `IndicatorFactory.validate_config()`.
104    pub fn contains(&self, name: &str) -> bool {
105        self.get(name).is_some()
106    }
107}
108
109// ── Global singleton ──────────────────────────────────────────────────────────
110
111/// Global indicator registry — the single source of truth for runtime creation.
112///
113/// Populate it once at startup via `REGISTRY.register(...)` or the `register_all!`
114/// helper in each module's `mod.rs`.
115///
116/// Mirrors `indicator_registry = IndicatorRegistry()` in Python.
117pub static REGISTRY: OnceLock<IndicatorRegistry> = OnceLock::new();
118
119/// Get (or lazily init) the global registry.
120pub fn registry() -> &'static IndicatorRegistry {
121    REGISTRY.get_or_init(|| {
122        let reg = IndicatorRegistry {
123            entries: RwLock::new(HashMap::new()),
124        };
125        // Register all built-in indicators.
126        crate::trend::register_all(&reg);
127        crate::momentum::register_all(&reg);
128        crate::volatility::register_all(&reg);
129        crate::volume::register_all(&reg);
130        crate::signal::register_all(&reg);
131        crate::regime::register_all(&reg);
132        reg
133    })
134}
135
136// ── Param helpers ─────────────────────────────────────────────────────────────
137
138/// Parse a `usize` from the params map with a default fallback.
139///
140/// Mirrors `self.params.get("period", 14)` in Python.
141pub fn param_usize<S: ::std::hash::BuildHasher>(
142    params: &HashMap<String, String, S>,
143    key: &str,
144    default: usize,
145) -> Result<usize, IndicatorError> {
146    match params.get(key) {
147        None => Ok(default),
148        Some(s) => s
149            .parse::<usize>()
150            .map_err(|_| IndicatorError::InvalidParameter {
151                name: key.to_string(),
152                value: s.parse::<f64>().unwrap_or(f64::NAN),
153            }),
154    }
155}
156
157/// Parse an `f64` from the params map with a default fallback.
158pub fn param_f64<S: ::std::hash::BuildHasher>(
159    params: &HashMap<String, String, S>,
160    key: &str,
161    default: f64,
162) -> Result<f64, IndicatorError> {
163    match params.get(key) {
164        None => Ok(default),
165        Some(s) => s
166            .parse::<f64>()
167            .map_err(|_| IndicatorError::InvalidParameter {
168                name: key.to_string(),
169                value: f64::NAN,
170            }),
171    }
172}
173
174/// Parse a `String` param with a default fallback.
175pub fn param_str<'a, S: ::std::hash::BuildHasher>(params: &'a HashMap<String, String, S>, key: &str, default: &'a str) -> &'a str {
176    params.get(key).map_or(default, String::as_str)
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182
183    fn dummy_factory(_p: &HashMap<String, String>) -> Result<Box<dyn Indicator>, IndicatorError> {
184        // Stub: real indicators will provide a real factory.
185        Err(IndicatorError::UnknownIndicator {
186            name: "dummy".into(),
187        })
188    }
189
190    #[test]
191    fn registry_register_and_list() {
192        let reg = IndicatorRegistry {
193            entries: RwLock::new(HashMap::new()),
194        };
195        reg.register("sma", dummy_factory);
196        reg.register("ema", dummy_factory);
197        let mut names = reg.list();
198        names.sort();
199        assert_eq!(names, vec!["ema", "sma"]);
200    }
201
202    #[test]
203    fn registry_unknown_returns_error() {
204        let reg = IndicatorRegistry {
205            entries: RwLock::new(HashMap::new()),
206        };
207        let err = reg
208            .create("no_such_indicator", &HashMap::new())
209            .unwrap_err();
210        assert!(matches!(err, IndicatorError::UnknownIndicator { .. }));
211    }
212
213    #[test]
214    fn param_usize_default() {
215        let params = HashMap::new();
216        assert_eq!(param_usize(&params, "period", 14).unwrap(), 14);
217    }
218
219    #[test]
220    fn param_usize_override() {
221        let params = [("period".to_string(), "20".to_string())].into();
222        assert_eq!(param_usize(&params, "period", 14).unwrap(), 20);
223    }
224
225    #[test]
226    fn param_usize_bad_value() {
227        let params = [("period".to_string(), "abc".to_string())].into();
228        assert!(param_usize(&params, "period", 14).is_err());
229    }
230}