fin_primitives/signals/indicators/
open_range_position.rs1use crate::error::FinError;
7use crate::signals::{BarInput, Signal, SignalValue};
8use rust_decimal::Decimal;
9
10pub struct OpenRangePosition {
36 name: String,
37 period: usize,
38 ema: Option<Decimal>,
39 k: Decimal,
40}
41
42impl OpenRangePosition {
43 pub fn new(name: impl Into<String>, period: usize) -> Result<Self, FinError> {
48 if period == 0 {
49 return Err(FinError::InvalidPeriod(period));
50 }
51 #[allow(clippy::cast_possible_truncation)]
52 let k = Decimal::from(2u32) / (Decimal::from(period as u32) + Decimal::ONE);
53 Ok(Self { name: name.into(), period, ema: None, k })
54 }
55}
56
57impl crate::signals::Signal for OpenRangePosition {
58 fn name(&self) -> &str {
59 &self.name
60 }
61
62 fn period(&self) -> usize {
63 self.period
64 }
65
66 fn is_ready(&self) -> bool {
67 self.ema.is_some()
68 }
69
70 fn update(&mut self, bar: &BarInput) -> Result<SignalValue, FinError> {
71 let range = bar.range();
72 let raw = if range.is_zero() {
73 Decimal::ZERO
74 } else {
75 (bar.open - bar.low)
76 .checked_div(range)
77 .ok_or(FinError::ArithmeticOverflow)?
78 };
79
80 let ema = match self.ema {
81 None => {
82 self.ema = Some(raw);
83 raw
84 }
85 Some(prev) => {
86 let next = raw * self.k + prev * (Decimal::ONE - self.k);
87 self.ema = Some(next);
88 next
89 }
90 };
91
92 Ok(SignalValue::Scalar(ema))
93 }
94
95 fn reset(&mut self) {
96 self.ema = None;
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::ohlcv::OhlcvBar;
104 use crate::signals::Signal;
105 use crate::types::{NanoTimestamp, Price, Quantity, Symbol};
106 use rust_decimal_macros::dec;
107
108 fn bar(open: &str, high: &str, low: &str, close: &str) -> OhlcvBar {
109 OhlcvBar {
110 symbol: Symbol::new("X").unwrap(),
111 open: Price::new(open.parse().unwrap()).unwrap(),
112 high: Price::new(high.parse().unwrap()).unwrap(),
113 low: Price::new(low.parse().unwrap()).unwrap(),
114 close: Price::new(close.parse().unwrap()).unwrap(),
115 volume: Quantity::zero(),
116 ts_open: NanoTimestamp::new(0),
117 ts_close: NanoTimestamp::new(1),
118 tick_count: 1,
119 }
120 }
121
122 #[test]
123 fn test_orp_invalid_period() {
124 assert!(OpenRangePosition::new("orp", 0).is_err());
125 }
126
127 #[test]
128 fn test_orp_ready_after_first_bar() {
129 let mut orp = OpenRangePosition::new("orp", 5).unwrap();
130 orp.update_bar(&bar("100", "110", "90", "105")).unwrap();
131 assert!(orp.is_ready());
132 }
133
134 #[test]
135 fn test_orp_open_at_low_zero() {
136 let mut orp = OpenRangePosition::new("orp", 5).unwrap();
137 let v = orp.update_bar(&bar("90", "110", "90", "105")).unwrap();
139 assert_eq!(v, SignalValue::Scalar(dec!(0)));
140 }
141
142 #[test]
143 fn test_orp_open_at_high_one() {
144 let mut orp = OpenRangePosition::new("orp", 5).unwrap();
145 let v = orp.update_bar(&bar("110", "110", "90", "100")).unwrap();
147 assert_eq!(v, SignalValue::Scalar(dec!(1)));
148 }
149
150 #[test]
151 fn test_orp_open_at_midpoint_half() {
152 let mut orp = OpenRangePosition::new("orp", 5).unwrap();
153 let v = orp.update_bar(&bar("100", "110", "90", "105")).unwrap();
155 assert_eq!(v, SignalValue::Scalar(dec!(0.5)));
156 }
157
158 #[test]
159 fn test_orp_flat_bar_zero() {
160 let mut orp = OpenRangePosition::new("orp", 5).unwrap();
161 let v = orp.update_bar(&bar("100", "100", "100", "100")).unwrap();
162 assert_eq!(v, SignalValue::Scalar(dec!(0)));
163 }
164
165 #[test]
166 fn test_orp_reset() {
167 let mut orp = OpenRangePosition::new("orp", 5).unwrap();
168 orp.update_bar(&bar("100", "110", "90", "105")).unwrap();
169 assert!(orp.is_ready());
170 orp.reset();
171 assert!(!orp.is_ready());
172 }
173}