1use std::collections::HashMap;
23use std::sync::{OnceLock, RwLock};
24
25use crate::error::IndicatorError;
26use crate::indicator::Indicator;
27
28pub type IndicatorFactory =
35 fn(params: &HashMap<String, String>) -> Result<Box<dyn Indicator>, IndicatorError>;
36
37pub struct IndicatorRegistry {
44 entries: RwLock<HashMap<String, IndicatorFactory>>,
45}
46
47impl IndicatorRegistry {
48 pub fn new_uninit() -> Self {
49 Self {
52 entries: RwLock::new(HashMap::new()),
53 }
54 }
55
56 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 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 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 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 pub fn contains(&self, name: &str) -> bool {
105 self.get(name).is_some()
106 }
107}
108
109pub static REGISTRY: OnceLock<IndicatorRegistry> = OnceLock::new();
118
119pub fn registry() -> &'static IndicatorRegistry {
121 REGISTRY.get_or_init(|| {
122 let reg = IndicatorRegistry {
123 entries: RwLock::new(HashMap::new()),
124 };
125 crate::trend::register_all(®);
127 crate::momentum::register_all(®);
128 crate::volatility::register_all(®);
129 crate::volume::register_all(®);
130 crate::signal::register_all(®);
131 crate::regime::register_all(®);
132 reg
133 })
134}
135
136pub 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
157pub 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
174pub 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 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(¶ms, "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(¶ms, "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(¶ms, "period", 14).is_err());
229 }
230}