kestrel_chartkit/
runner.rs1use crate::indicator::{Indicator, IndicatorOutput};
11use crate::model::{Bar, BarValidationError};
12
13#[derive(Debug, Clone, PartialEq)]
16pub struct TimestampedOutput {
17 pub timestamp: i64,
18 pub output: Option<IndicatorOutput>,
19}
20
21pub fn run_batch<I: Indicator + ?Sized>(indicator: &mut I, bars: &[Bar]) -> Vec<TimestampedOutput> {
25 indicator.reset();
26 bars.iter()
27 .map(|bar| TimestampedOutput {
28 timestamp: bar.timestamp,
29 output: indicator.on_bar(bar),
30 })
31 .collect()
32}
33
34pub fn run_batch_checked<I: Indicator + ?Sized>(
38 indicator: &mut I,
39 bars: &[Bar],
40) -> Result<Vec<TimestampedOutput>, (Vec<TimestampedOutput>, BarValidationError)> {
41 indicator.reset();
42 let mut series = Vec::with_capacity(bars.len());
43 for bar in bars {
44 match indicator.on_checked_bar(bar) {
45 Ok(output) => series.push(TimestampedOutput {
46 timestamp: bar.timestamp,
47 output,
48 }),
49 Err(err) => return Err((series, err)),
50 }
51 }
52 Ok(series)
53}
54
55#[cfg(test)]
56mod tests {
57 use super::*;
58 use crate::indicator::moving_averages::SmaEngine;
59
60 fn sample_bars(closes: &[f64]) -> Vec<Bar> {
62 closes
63 .iter()
64 .enumerate()
65 .map(|(i, &c)| {
66 let c = c + 100.0;
67 Bar::new((i as i64) * 60, c, c + 1.0, c - 1.0, c, 100.0)
68 })
69 .collect()
70 }
71
72 #[test]
73 fn test_run_batch_is_timestamp_aligned_and_warmup_aware() {
74 let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
75 let mut sma = SmaEngine::new(3);
76 let series = run_batch(&mut sma, &bars);
77
78 assert_eq!(series.len(), bars.len());
79 for (entry, bar) in series.iter().zip(&bars) {
80 assert_eq!(entry.timestamp, bar.timestamp);
81 }
82 assert!(series[0].output.is_none());
84 assert!(series[1].output.is_none());
85 assert!(series[2].output.is_some());
86 assert_eq!(series[2].output.as_ref().unwrap().value, 102.0);
87 assert_eq!(series[4].output.as_ref().unwrap().value, 104.0);
88 }
89
90 #[test]
91 fn test_run_batch_is_deterministic_and_resets_prior_state() {
92 let bars = sample_bars(&[1.0, 2.0, 3.0, 4.0, 5.0]);
93 let mut sma = SmaEngine::new(3);
94
95 let first = run_batch(&mut sma, &bars);
96 let second = run_batch(&mut sma, &bars);
98 assert_eq!(first, second);
99 }
100
101 #[test]
102 fn test_run_batch_checked_stops_at_invalid_bar() {
103 let mut bars = sample_bars(&[10.0, 20.0]);
104 bars.push(Bar::new(120, f64::NAN, 1.0, -1.0, 1.0, 100.0));
105 bars.push(Bar::new(180, 3.0, 4.0, 2.0, 3.0, 100.0));
106
107 let mut sma = SmaEngine::new(2);
108 let result = run_batch_checked(&mut sma, &bars);
109 let (partial, err) = result.unwrap_err();
110 assert_eq!(partial.len(), 2);
111 assert_eq!(err, BarValidationError::NonFiniteValue);
112 }
113}