1use std::collections::{HashMap, HashSet};
12use std::fmt;
13
14use crate::indicator::{Indicator, IndicatorOutput};
15use crate::model::Bar;
16
17pub trait GraphIndicator: Send + Sync {
20 fn name(&self) -> &str;
21 fn dependencies(&self) -> &[String];
24 fn warmup_period(&self) -> usize {
25 0
26 }
27 fn reset(&mut self);
28 fn compute(
29 &mut self,
30 bar: &Bar,
31 deps: &HashMap<String, IndicatorOutput>,
32 ) -> Option<IndicatorOutput>;
33}
34
35pub struct Leaf<I: Indicator> {
38 inner: I,
39 dependencies: Vec<String>,
40}
41
42impl<I: Indicator> Leaf<I> {
43 pub fn new(inner: I) -> Self {
44 Self {
45 inner,
46 dependencies: Vec::new(),
47 }
48 }
49}
50
51impl<I: Indicator> GraphIndicator for Leaf<I> {
52 fn name(&self) -> &str {
53 self.inner.name()
54 }
55 fn dependencies(&self) -> &[String] {
56 &self.dependencies
57 }
58 fn warmup_period(&self) -> usize {
59 self.inner.warmup_period()
60 }
61 fn reset(&mut self) {
62 self.inner.reset()
63 }
64 fn compute(
65 &mut self,
66 bar: &Bar,
67 _deps: &HashMap<String, IndicatorOutput>,
68 ) -> Option<IndicatorOutput> {
69 self.inner.on_bar(bar)
70 }
71}
72
73type ComputeFn = Box<
74 dyn FnMut(&Bar, &HashMap<String, IndicatorOutput>) -> Option<IndicatorOutput> + Send + Sync,
75>;
76
77pub struct ComposedNode {
80 name: String,
81 dependencies: Vec<String>,
82 warmup_period: usize,
83 compute_fn: ComputeFn,
84 reset_fn: Box<dyn FnMut() + Send + Sync>,
85}
86
87impl ComposedNode {
88 pub fn new(
89 name: impl Into<String>,
90 dependencies: Vec<String>,
91 warmup_period: usize,
92 compute_fn: impl FnMut(&Bar, &HashMap<String, IndicatorOutput>) -> Option<IndicatorOutput>
93 + Send
94 + Sync
95 + 'static,
96 ) -> Self {
97 Self {
98 name: name.into(),
99 dependencies,
100 warmup_period,
101 compute_fn: Box::new(compute_fn),
102 reset_fn: Box::new(|| {}),
103 }
104 }
105
106 pub fn with_reset(mut self, reset_fn: impl FnMut() + Send + Sync + 'static) -> Self {
109 self.reset_fn = Box::new(reset_fn);
110 self
111 }
112}
113
114impl GraphIndicator for ComposedNode {
115 fn name(&self) -> &str {
116 &self.name
117 }
118 fn dependencies(&self) -> &[String] {
119 &self.dependencies
120 }
121 fn warmup_period(&self) -> usize {
122 self.warmup_period
123 }
124 fn reset(&mut self) {
125 (self.reset_fn)()
126 }
127 fn compute(
128 &mut self,
129 bar: &Bar,
130 deps: &HashMap<String, IndicatorOutput>,
131 ) -> Option<IndicatorOutput> {
132 (self.compute_fn)(bar, deps)
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq)]
138pub enum GraphError {
139 DuplicateNode(String),
140 MissingDependency { node: String, dependency: String },
141 CycleDetected,
142}
143
144impl fmt::Display for GraphError {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 match self {
147 GraphError::DuplicateNode(name) => write!(f, "duplicate node name: {}", name),
148 GraphError::MissingDependency { node, dependency } => write!(
149 f,
150 "node '{}' depends on unknown node '{}'",
151 node, dependency
152 ),
153 GraphError::CycleDetected => write!(f, "dependency cycle detected"),
154 }
155 }
156}
157
158impl std::error::Error for GraphError {}
159
160pub struct CompositionGraph {
162 nodes: HashMap<String, Box<dyn GraphIndicator>>,
163 order: Vec<String>,
164 effective_warmup: HashMap<String, usize>,
165}
166
167impl CompositionGraph {
168 pub fn build(nodes: Vec<Box<dyn GraphIndicator>>) -> Result<Self, GraphError> {
173 let mut by_name = HashMap::with_capacity(nodes.len());
174 for node in nodes {
175 let name = node.name().to_string();
176 if by_name.insert(name.clone(), node).is_some() {
177 return Err(GraphError::DuplicateNode(name));
178 }
179 }
180
181 for (name, node) in &by_name {
182 for dep in node.dependencies() {
183 if !by_name.contains_key(dep) {
184 return Err(GraphError::MissingDependency {
185 node: name.clone(),
186 dependency: dep.clone(),
187 });
188 }
189 }
190 }
191
192 let order = topological_order(&by_name)?;
193
194 let mut effective_warmup: HashMap<String, usize> = HashMap::with_capacity(order.len());
195 for name in &order {
196 let node = &by_name[name];
197 let dep_warmup = node
198 .dependencies()
199 .iter()
200 .map(|dep| effective_warmup[dep])
201 .max()
202 .unwrap_or(0);
203 effective_warmup.insert(name.clone(), node.warmup_period() + dep_warmup);
204 }
205
206 Ok(Self {
207 nodes: by_name,
208 order,
209 effective_warmup,
210 })
211 }
212
213 pub fn order(&self) -> &[String] {
216 &self.order
217 }
218
219 pub fn effective_warmup(&self, name: &str) -> Option<usize> {
222 self.effective_warmup.get(name).copied()
223 }
224
225 pub fn on_bar(&mut self, bar: &Bar) -> HashMap<String, Option<IndicatorOutput>> {
228 let mut outputs: HashMap<String, Option<IndicatorOutput>> =
229 HashMap::with_capacity(self.order.len());
230 let mut resolved: HashMap<String, IndicatorOutput> = HashMap::new();
231
232 for name in self.order.clone() {
233 let node = self.nodes.get_mut(&name).expect("node in order exists");
234 let deps: HashMap<String, IndicatorOutput> = node
235 .dependencies()
236 .iter()
237 .filter_map(|dep| resolved.get(dep).map(|o| (dep.clone(), o.clone())))
238 .collect();
239
240 let output = node.compute(bar, &deps);
241 if let Some(o) = &output {
242 resolved.insert(name.clone(), o.clone());
243 }
244 outputs.insert(name, output);
245 }
246
247 outputs
248 }
249
250 pub fn reset(&mut self) {
251 for node in self.nodes.values_mut() {
252 node.reset();
253 }
254 }
255}
256
257fn topological_order(
260 nodes: &HashMap<String, Box<dyn GraphIndicator>>,
261) -> Result<Vec<String>, GraphError> {
262 let mut remaining_deps: HashMap<&str, HashSet<&str>> = nodes
263 .iter()
264 .map(|(name, node)| {
265 (
266 name.as_str(),
267 node.dependencies().iter().map(|d| d.as_str()).collect(),
268 )
269 })
270 .collect();
271
272 let mut order = Vec::with_capacity(nodes.len());
273 loop {
274 let ready: Vec<&str> = remaining_deps
275 .iter()
276 .filter(|(_, deps)| deps.is_empty())
277 .map(|(name, _)| *name)
278 .collect();
279
280 if ready.is_empty() {
281 break;
282 }
283
284 let mut ready = ready;
285 ready.sort_unstable();
286 for name in ready {
287 remaining_deps.remove(name);
288 order.push(name.to_string());
289 }
290
291 for deps in remaining_deps.values_mut() {
292 for done in &order {
293 deps.remove(done.as_str());
294 }
295 }
296 }
297
298 if order.len() != nodes.len() {
299 return Err(GraphError::CycleDetected);
300 }
301
302 Ok(order)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308 use crate::indicator::atr::Atr;
309
310 fn sample_bars() -> Vec<Bar> {
311 (0..20)
312 .map(|i| {
313 let c = 100.0 + i as f64;
314 Bar::new(i * 60, c, c + 2.0, c - 2.0, c, 100.0)
315 })
316 .collect()
317 }
318
319 #[test]
320 fn test_graph_orders_dependencies_before_dependents() {
321 let atr_leaf = Box::new(Leaf::new(Atr::new(5, 5)));
322 let derived = Box::new(ComposedNode::new(
323 "double_atr",
324 vec!["atr".to_string()],
325 0,
326 |_bar, deps| deps.get("atr").map(|o| IndicatorOutput::new(o.value * 2.0)),
327 ));
328
329 let graph = CompositionGraph::build(vec![atr_leaf, derived]).unwrap();
330 assert_eq!(
331 graph.order(),
332 &["atr".to_string(), "double_atr".to_string()]
333 );
334 assert_eq!(
336 graph.effective_warmup("double_atr"),
337 graph.effective_warmup("atr")
338 );
339 }
340
341 #[test]
342 fn test_graph_propagates_dependency_output_same_bar() {
343 let atr_leaf = Box::new(Leaf::new(Atr::new(3, 3)));
344 let derived = Box::new(ComposedNode::new(
345 "double_atr",
346 vec!["atr".to_string()],
347 0,
348 |_bar, deps| deps.get("atr").map(|o| IndicatorOutput::new(o.value * 2.0)),
349 ));
350
351 let mut graph = CompositionGraph::build(vec![atr_leaf, derived]).unwrap();
352 let bars = sample_bars();
353 let mut last_outputs = HashMap::new();
354 for bar in &bars {
355 last_outputs = graph.on_bar(bar);
356 }
357
358 let atr_value = last_outputs["atr"].as_ref().unwrap().value;
359 let derived_value = last_outputs["double_atr"].as_ref().unwrap().value;
360 assert!((derived_value - atr_value * 2.0).abs() < 1e-9);
361 }
362
363 #[test]
364 fn test_graph_rejects_missing_dependency() {
365 let derived = Box::new(ComposedNode::new(
366 "double_atr",
367 vec!["missing_atr".to_string()],
368 0,
369 |_bar, _deps| None,
370 ));
371 let err = match CompositionGraph::build(vec![derived]) {
372 Err(e) => e,
373 Ok(_) => panic!("Expected missing dependency error"),
374 };
375 assert_eq!(
376 err,
377 GraphError::MissingDependency {
378 node: "double_atr".to_string(),
379 dependency: "missing_atr".to_string(),
380 }
381 );
382 }
383
384 #[test]
385 fn test_graph_rejects_cycle() {
386 let a = Box::new(ComposedNode::new("a", vec!["b".to_string()], 0, |_, _| {
387 None
388 }));
389 let b = Box::new(ComposedNode::new("b", vec!["a".to_string()], 0, |_, _| {
390 None
391 }));
392 let err = match CompositionGraph::build(vec![a, b]) {
393 Err(e) => e,
394 Ok(_) => panic!("Expected cycle error"),
395 };
396 assert_eq!(err, GraphError::CycleDetected);
397 }
398
399 #[test]
400 fn test_graph_rejects_duplicate_node_name() {
401 let a1 = Box::new(ComposedNode::new("a", vec![], 0, |_, _| None));
402 let a2 = Box::new(ComposedNode::new("a", vec![], 0, |_, _| None));
403 let err = match CompositionGraph::build(vec![a1, a2]) {
404 Err(e) => e,
405 Ok(_) => panic!("Expected duplicate node error"),
406 };
407 assert_eq!(err, GraphError::DuplicateNode("a".to_string()));
408 }
409}