wickra_core/indicators/
close_vs_open.rs1use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6#[derive(Debug, Clone, Default)]
32pub struct CloseVsOpen {
33 has_emitted: bool,
34}
35
36impl CloseVsOpen {
37 pub const fn new() -> Self {
39 Self { has_emitted: false }
40 }
41}
42
43impl Indicator for CloseVsOpen {
44 type Input = Candle;
45 type Output = f64;
46
47 #[inline]
48 fn update(&mut self, candle: Candle) -> Option<f64> {
49 self.has_emitted = true;
50 let out = if candle.open == 0.0 {
51 0.0
53 } else {
54 (candle.close - candle.open) / candle.open
55 };
56 Some(out)
57 }
58
59 fn reset(&mut self) {
60 self.has_emitted = false;
61 }
62
63 #[inline]
64 fn warmup_period(&self) -> usize {
65 1
66 }
67
68 #[inline]
69 fn is_ready(&self) -> bool {
70 self.has_emitted
71 }
72
73 #[inline]
74 fn name(&self) -> &'static str {
75 "CloseVsOpen"
76 }
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 use crate::traits::BatchExt;
83 use approx::assert_relative_eq;
84
85 fn candle(open: f64, high: f64, low: f64, close: f64, ts: i64) -> Candle {
86 Candle::new(open, high, low, close, 1.0, ts).unwrap()
87 }
88
89 #[test]
90 fn reference_value() {
91 let mut cvo = CloseVsOpen::new();
93 assert_relative_eq!(
94 cvo.update(candle(100.0, 103.0, 99.0, 102.0, 0)).unwrap(),
95 0.02,
96 epsilon = 1e-12
97 );
98 }
99
100 #[test]
101 fn negative_body_is_negative() {
102 let mut cvo = CloseVsOpen::new();
103 assert_relative_eq!(
105 cvo.update(candle(100.0, 101.0, 97.0, 98.0, 0)).unwrap(),
106 -0.02,
107 epsilon = 1e-12
108 );
109 }
110
111 #[test]
112 fn zero_open_yields_zero() {
113 let mut cvo = CloseVsOpen::new();
115 assert_relative_eq!(
116 cvo.update(candle(0.0, 1.0, 0.0, 0.5, 0)).unwrap(),
117 0.0,
118 epsilon = 1e-12
119 );
120 }
121
122 #[test]
123 fn name_metadata() {
124 let cvo = CloseVsOpen::new();
125 assert_eq!(cvo.name(), "CloseVsOpen");
126 }
127
128 #[test]
129 fn emits_from_first_candle() {
130 let mut cvo = CloseVsOpen::new();
131 assert_eq!(cvo.warmup_period(), 1);
132 assert!(!cvo.is_ready());
133 assert!(cvo.update(candle(10.0, 11.0, 9.0, 10.0, 0)).is_some());
134 assert!(cvo.is_ready());
135 }
136
137 #[test]
138 fn reset_clears_state() {
139 let mut cvo = CloseVsOpen::new();
140 cvo.update(candle(10.0, 11.0, 9.0, 10.0, 0));
141 assert!(cvo.is_ready());
142 cvo.reset();
143 assert!(!cvo.is_ready());
144 }
145
146 #[test]
147 fn batch_equals_streaming() {
148 let candles: Vec<Candle> = (0..40)
149 .map(|i| {
150 let base = 100.0 + f64::from(i);
151 candle(base, base + 2.0, base - 2.0, base + 1.0, i64::from(i))
152 })
153 .collect();
154 let mut a = CloseVsOpen::new();
155 let mut b = CloseVsOpen::new();
156 assert_eq!(
157 a.batch(&candles),
158 candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
159 );
160 }
161}