fin_primitives/signals/indicators/
range_midpoint_position.rs1use crate::error::FinError;
4use crate::signals::{BarInput, Signal, SignalValue};
5use rust_decimal::Decimal;
6use std::collections::VecDeque;
7
8pub struct RangeMidpointPosition {
29 name: String,
30 period: usize,
31 highs: VecDeque<Decimal>,
32 lows: VecDeque<Decimal>,
33 closes: VecDeque<Decimal>,
34}
35
36impl RangeMidpointPosition {
37 pub fn new(name: impl Into<String>, period: usize) -> Result<Self, FinError> {
42 if period == 0 {
43 return Err(FinError::InvalidPeriod(period));
44 }
45 Ok(Self {
46 name: name.into(),
47 period,
48 highs: VecDeque::with_capacity(period),
49 lows: VecDeque::with_capacity(period),
50 closes: VecDeque::with_capacity(period),
51 })
52 }
53}
54
55impl Signal for RangeMidpointPosition {
56 fn name(&self) -> &str { &self.name }
57 fn period(&self) -> usize { self.period }
58 fn is_ready(&self) -> bool { self.closes.len() >= self.period }
59
60 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
61 self.highs.push_back(bar.high);
62 self.lows.push_back(bar.low);
63 self.closes.push_back(bar.close);
64
65 if self.highs.len() > self.period {
66 self.highs.pop_front();
67 self.lows.pop_front();
68 self.closes.pop_front();
69 }
70
71 if self.closes.len() < self.period {
72 return Ok(SignalValue::Unavailable);
73 }
74
75 let period_high = self.highs.iter().copied().fold(Decimal::MIN, Decimal::max);
76 let period_low = self.lows.iter().copied().fold(Decimal::MAX, Decimal::min);
77 let close = *self.closes.back().unwrap();
78
79 let range = period_high - period_low;
80 if range.is_zero() {
81 return Ok(SignalValue::Unavailable);
82 }
83
84 let midpoint = (period_high + period_low)
85 .checked_div(Decimal::TWO)
86 .ok_or(FinError::ArithmeticOverflow)?;
87 let half_range = range
88 .checked_div(Decimal::TWO)
89 .ok_or(FinError::ArithmeticOverflow)?;
90
91 let pos = (close - midpoint)
92 .checked_div(half_range)
93 .ok_or(FinError::ArithmeticOverflow)?;
94
95 Ok(SignalValue::Scalar(pos.max(Decimal::NEGATIVE_ONE).min(Decimal::ONE)))
97 }
98
99 fn reset(&mut self) {
100 self.highs.clear();
101 self.lows.clear();
102 self.closes.clear();
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::ohlcv::OhlcvBar;
110 use crate::signals::Signal;
111 use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
112 use rust_decimal_macros::dec;
113
114 fn bar(h: &str, l: &str, c: &str) -> OhlcvBar {
115 let hp = Price::new(h.parse().unwrap()).unwrap();
116 let lp = Price::new(l.parse().unwrap()).unwrap();
117 let cp = Price::new(c.parse().unwrap()).unwrap();
118 OhlcvBar {
119 symbol: Symbol::new("X").unwrap(),
120 open: lp, high: hp, low: lp, close: cp,
121 volume: Quantity::zero(),
122 ts_open: NanoTimestamp::new(0),
123 ts_close: NanoTimestamp::new(1),
124 tick_count: 1,
125 }
126 }
127
128 #[test]
129 fn test_rmp_invalid_period() {
130 assert!(RangeMidpointPosition::new("rmp", 0).is_err());
131 }
132
133 #[test]
134 fn test_rmp_unavailable_before_period() {
135 let mut s = RangeMidpointPosition::new("rmp", 3).unwrap();
136 assert_eq!(s.update_bar(&bar("110","90","100")).unwrap(), SignalValue::Unavailable);
137 assert_eq!(s.update_bar(&bar("112","88","100")).unwrap(), SignalValue::Unavailable);
138 assert!(!s.is_ready());
139 }
140
141 #[test]
142 fn test_rmp_close_at_midpoint_gives_zero() {
143 let mut s = RangeMidpointPosition::new("rmp", 2).unwrap();
145 s.update_bar(&bar("110","90","100")).unwrap();
146 if let SignalValue::Scalar(v) = s.update_bar(&bar("110","90","100")).unwrap() {
147 assert!(v.abs() < dec!(0.001), "close at midpoint should give 0: {v}");
148 } else {
149 panic!("expected Scalar");
150 }
151 }
152
153 #[test]
154 fn test_rmp_close_at_high_gives_positive() {
155 let mut s = RangeMidpointPosition::new("rmp", 2).unwrap();
156 s.update_bar(&bar("110","90","100")).unwrap();
157 if let SignalValue::Scalar(v) = s.update_bar(&bar("110","90","110")).unwrap() {
158 assert!(v > dec!(0), "close at high should give positive: {v}");
159 } else {
160 panic!("expected Scalar");
161 }
162 }
163
164 #[test]
165 fn test_rmp_in_range_negative_one_to_one() {
166 let mut s = RangeMidpointPosition::new("rmp", 3).unwrap();
167 for (h,l,c) in &[("110","90","100"),("115","85","95"),("112","88","110"),("108","92","89")] {
168 if let SignalValue::Scalar(v) = s.update_bar(&bar(h,l,c)).unwrap() {
169 assert!(v >= dec!(-1) && v <= dec!(1), "position out of [-1,1]: {v}");
170 }
171 }
172 }
173
174 #[test]
175 fn test_rmp_reset() {
176 let mut s = RangeMidpointPosition::new("rmp", 2).unwrap();
177 s.update_bar(&bar("110","90","100")).unwrap();
178 s.update_bar(&bar("110","90","100")).unwrap();
179 assert!(s.is_ready());
180 s.reset();
181 assert!(!s.is_ready());
182 }
183}