fin_primitives/signals/indicators/
direction_changes.rs1use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6use std::collections::VecDeque;
7
8pub struct DirectionChanges {
28 name: String,
29 period: usize,
30 closes: VecDeque<Decimal>,
31}
32
33impl DirectionChanges {
34 pub fn new(name: impl Into<String>, period: usize) -> Result<Self, FinError> {
39 if period < 2 {
40 return Err(FinError::InvalidPeriod(period));
41 }
42 Ok(Self {
43 name: name.into(),
44 period,
45 closes: VecDeque::with_capacity(period + 1),
46 })
47 }
48}
49
50impl Signal for DirectionChanges {
51 fn name(&self) -> &str { &self.name }
52 fn period(&self) -> usize { self.period }
53 fn is_ready(&self) -> bool { self.closes.len() > self.period }
54
55 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
56 self.closes.push_back(bar.close);
57 if self.closes.len() > self.period + 1 {
58 self.closes.pop_front();
59 }
60 if self.closes.len() <= self.period {
61 return Ok(SignalValue::Unavailable);
62 }
63
64 let closes: Vec<&Decimal> = self.closes.iter().collect();
65 let mut count = 0u32;
66 for i in 1..closes.len() - 1 {
67 let prev_move = *closes[i] - *closes[i - 1];
68 let curr_move = *closes[i + 1] - *closes[i];
69 let reversal = (prev_move > Decimal::ZERO && curr_move < Decimal::ZERO)
71 || (prev_move < Decimal::ZERO && curr_move > Decimal::ZERO);
72 if reversal {
73 count += 1;
74 }
75 }
76
77 Ok(SignalValue::Scalar(Decimal::from(count)))
78 }
79
80 fn reset(&mut self) {
81 self.closes.clear();
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::ohlcv::OhlcvBar;
89 use crate::signals::Signal;
90 use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
91 use rust_decimal_macros::dec;
92
93 fn bar(c: &str) -> OhlcvBar {
94 let p = Price::new(c.parse().unwrap()).unwrap();
95 OhlcvBar {
96 symbol: Symbol::new("X").unwrap(),
97 open: p, high: p, low: p, close: p,
98 volume: Quantity::zero(),
99 ts_open: NanoTimestamp::new(0),
100 ts_close: NanoTimestamp::new(1),
101 tick_count: 1,
102 }
103 }
104
105 #[test]
106 fn test_dc_invalid_period() {
107 assert!(DirectionChanges::new("dc", 0).is_err());
108 assert!(DirectionChanges::new("dc", 1).is_err());
109 }
110
111 #[test]
112 fn test_dc_unavailable_before_warm_up() {
113 let mut s = DirectionChanges::new("dc", 3).unwrap();
114 for _ in 0..3 {
115 assert_eq!(s.update_bar(&bar("100")).unwrap(), SignalValue::Unavailable);
116 }
117 assert!(!s.is_ready());
118 }
119
120 #[test]
121 fn test_dc_trending_series_zero() {
122 let mut s = DirectionChanges::new("dc", 3).unwrap();
123 s.update_bar(&bar("100")).unwrap();
125 s.update_bar(&bar("101")).unwrap();
126 s.update_bar(&bar("102")).unwrap();
127 let v = s.update_bar(&bar("103")).unwrap();
128 assert_eq!(v, SignalValue::Scalar(dec!(0)));
129 }
130
131 #[test]
132 fn test_dc_alternating_series_max() {
133 let mut s = DirectionChanges::new("dc", 4).unwrap();
134 s.update_bar(&bar("100")).unwrap();
136 s.update_bar(&bar("102")).unwrap();
137 s.update_bar(&bar("100")).unwrap();
138 s.update_bar(&bar("102")).unwrap();
139 let v = s.update_bar(&bar("100")).unwrap();
140 if let SignalValue::Scalar(r) = v {
144 assert!(r >= dec!(1), "alternating series should have many reversals: {r}");
145 } else {
146 panic!("expected Scalar");
147 }
148 }
149
150 #[test]
151 fn test_dc_non_negative() {
152 let mut s = DirectionChanges::new("dc", 5).unwrap();
153 let prices = ["100", "102", "101", "103", "102", "104"];
154 for p in &prices {
155 if let SignalValue::Scalar(v) = s.update_bar(&bar(p)).unwrap() {
156 assert!(v >= dec!(0), "direction changes must be non-negative: {v}");
157 }
158 }
159 }
160
161 #[test]
162 fn test_dc_reset() {
163 let mut s = DirectionChanges::new("dc", 3).unwrap();
164 for p in &["100", "101", "102", "103"] {
165 s.update_bar(&bar(p)).unwrap();
166 }
167 assert!(s.is_ready());
168 s.reset();
169 assert!(!s.is_ready());
170 }
171}