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, PoisonError, 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        // Poison recovery: entries are plain fn-pointer inserts, so a panic in
61        // another thread can't leave the map in a half-written state.
62        let mut map = self.entries.write().unwrap_or_else(PoisonError::into_inner);
63        map.insert(name.to_ascii_lowercase(), factory);
64    }
65
66    /// List all registered indicator names.
67    ///
68    /// Mirrors `IndicatorRegistry.list() -> list[str]`.
69    pub fn list(&self) -> Vec<String> {
70        let map = self.entries.read().unwrap_or_else(PoisonError::into_inner);
71        map.keys().cloned().collect()
72    }
73
74    /// Look up a factory by name (case-insensitive).
75    ///
76    /// Mirrors `IndicatorRegistry.get(name)`.
77    pub fn get(&self, name: &str) -> Option<IndicatorFactory> {
78        let map = self.entries.read().unwrap_or_else(PoisonError::into_inner);
79        map.get(&name.to_ascii_lowercase()).copied()
80    }
81
82    /// Create an indicator instance by name.
83    ///
84    /// Mirrors `IndicatorRegistry.create(name, **params)` and
85    /// `IndicatorFactory.create(name, **params)`.
86    ///
87    /// # Errors
88    /// - `IndicatorError::UnknownIndicator` if `name` is not registered.
89    /// - Propagates construction errors from the factory.
90    pub fn create(
91        &self,
92        name: &str,
93        params: &HashMap<String, String>,
94    ) -> Result<Box<dyn Indicator>, IndicatorError> {
95        let factory = self
96            .get(name)
97            .ok_or_else(|| IndicatorError::UnknownIndicator {
98                name: name.to_string(),
99            })?;
100        factory(params)
101    }
102
103    /// Check whether an indicator name is registered.
104    ///
105    /// Mirrors `indicator_registry.get(name) is not None` in `IndicatorFactory.validate_config()`.
106    pub fn contains(&self, name: &str) -> bool {
107        self.get(name).is_some()
108    }
109}
110
111// ── Global singleton ──────────────────────────────────────────────────────────
112
113/// Global indicator registry — the single source of truth for runtime creation.
114///
115/// Populate it once at startup via `REGISTRY.register(...)` or the `register_all!`
116/// helper in each module's `mod.rs`.
117///
118/// Mirrors `indicator_registry = IndicatorRegistry()` in Python.
119pub static REGISTRY: OnceLock<IndicatorRegistry> = OnceLock::new();
120
121/// Get (or lazily init) the global registry.
122pub fn registry() -> &'static IndicatorRegistry {
123    REGISTRY.get_or_init(|| {
124        let reg = IndicatorRegistry {
125            entries: RwLock::new(HashMap::new()),
126        };
127        // Register all built-in indicators.
128        crate::trend::register_all(&reg);
129        crate::momentum::register_all(&reg);
130        crate::volatility::register_all(&reg);
131        crate::volume::register_all(&reg);
132        crate::signal::register_all(&reg);
133        crate::regime::register_all(&reg);
134        reg
135    })
136}
137
138// ── Param helpers ─────────────────────────────────────────────────────────────
139
140/// Parse a `usize` from the params map with a default fallback.
141///
142/// Mirrors `self.params.get("period", 14)` in Python.
143pub fn param_usize<S: ::std::hash::BuildHasher>(
144    params: &HashMap<String, String, S>,
145    key: &str,
146    default: usize,
147) -> Result<usize, IndicatorError> {
148    match params.get(key) {
149        None => Ok(default),
150        Some(s) => s
151            .parse::<usize>()
152            .map_err(|_| IndicatorError::InvalidParameter {
153                name: key.to_string(),
154                value: s.parse::<f64>().unwrap_or(f64::NAN),
155            }),
156    }
157}
158
159/// Parse an `f64` from the params map with a default fallback.
160pub fn param_f64<S: ::std::hash::BuildHasher>(
161    params: &HashMap<String, String, S>,
162    key: &str,
163    default: f64,
164) -> Result<f64, IndicatorError> {
165    match params.get(key) {
166        None => Ok(default),
167        Some(s) => s
168            .parse::<f64>()
169            .map_err(|_| IndicatorError::InvalidParameter {
170                name: key.to_string(),
171                value: f64::NAN,
172            }),
173    }
174}
175
176/// Parse a `String` param with a default fallback.
177pub fn param_str<'a, S: ::std::hash::BuildHasher>(
178    params: &'a HashMap<String, String, S>,
179    key: &str,
180    default: &'a str,
181) -> &'a str {
182    params.get(key).map_or(default, String::as_str)
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    fn dummy_factory(_p: &HashMap<String, String>) -> Result<Box<dyn Indicator>, IndicatorError> {
190        // Stub: real indicators will provide a real factory.
191        Err(IndicatorError::UnknownIndicator {
192            name: "dummy".into(),
193        })
194    }
195
196    #[test]
197    fn registry_register_and_list() {
198        let reg = IndicatorRegistry {
199            entries: RwLock::new(HashMap::new()),
200        };
201        reg.register("sma", dummy_factory);
202        reg.register("ema", dummy_factory);
203        let mut names = reg.list();
204        names.sort();
205        assert_eq!(names, vec!["ema", "sma"]);
206    }
207
208    #[test]
209    fn registry_unknown_returns_error() {
210        let reg = IndicatorRegistry {
211            entries: RwLock::new(HashMap::new()),
212        };
213        let err = reg
214            .create("no_such_indicator", &HashMap::new())
215            .unwrap_err();
216        assert!(matches!(err, IndicatorError::UnknownIndicator { .. }));
217    }
218
219    #[test]
220    fn param_usize_default() {
221        let params = HashMap::new();
222        assert_eq!(param_usize(&params, "period", 14).unwrap(), 14);
223    }
224
225    #[test]
226    fn param_usize_override() {
227        let params = [("period".to_string(), "20".to_string())].into();
228        assert_eq!(param_usize(&params, "period", 14).unwrap(), 20);
229    }
230
231    #[test]
232    fn param_usize_bad_value() {
233        let params = [("period".to_string(), "abc".to_string())].into();
234        assert!(param_usize(&params, "period", 14).is_err());
235    }
236}