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