wickra_core/indicators/
qstick.rs1use crate::error::Result;
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8#[derive(Debug, Clone)]
37pub struct Qstick {
38 period: usize,
39 sma: Sma,
40}
41
42impl Qstick {
43 pub fn new(period: usize) -> Result<Self> {
49 Ok(Self {
50 period,
51 sma: Sma::new(period)?,
52 })
53 }
54
55 pub const fn period(&self) -> usize {
57 self.period
58 }
59}
60
61impl Indicator for Qstick {
62 type Input = Candle;
63 type Output = f64;
64
65 #[inline]
66 fn update(&mut self, candle: Candle) -> Option<f64> {
67 self.sma.update(candle.close - candle.open)
68 }
69
70 fn reset(&mut self) {
71 self.sma.reset();
72 }
73
74 #[inline]
75 fn warmup_period(&self) -> usize {
76 self.period
77 }
78
79 #[inline]
80 fn is_ready(&self) -> bool {
81 self.sma.is_ready()
82 }
83
84 #[inline]
85 fn name(&self) -> &'static str {
86 "Qstick"
87 }
88}
89
90#[cfg(test)]
91mod tests {
92 use super::*;
93 use crate::error::Error;
94 use crate::traits::BatchExt;
95 use approx::assert_relative_eq;
96
97 fn candle(open: f64, close: f64, ts: i64) -> Candle {
98 let high = open.max(close) + 1.0;
99 let low = open.min(close) - 1.0;
100 Candle::new(open, high, low, close, 1.0, ts).unwrap()
101 }
102
103 #[test]
104 fn rejects_zero_period() {
105 assert!(matches!(Qstick::new(0), Err(Error::PeriodZero)));
106 }
107
108 #[test]
109 fn accessors_and_metadata() {
110 let q = Qstick::new(5).unwrap();
111 assert_eq!(q.period(), 5);
112 assert_eq!(q.warmup_period(), 5);
113 assert_eq!(q.name(), "Qstick");
114 assert!(!q.is_ready());
115 }
116
117 #[test]
118 fn warmup_emits_first_value_at_period() {
119 let mut q = Qstick::new(3).unwrap();
120 let candles: Vec<Candle> = (0..3).map(|i| candle(10.0, 11.0, i)).collect();
121 let out = q.batch(&candles);
122 assert!(out[0].is_none());
123 assert!(out[1].is_none());
124 assert!(out[2].is_some());
125 }
126
127 #[test]
128 fn constant_bodies_yield_the_body() {
129 let mut q = Qstick::new(4).unwrap();
131 let candles: Vec<Candle> = (0..10).map(|i| candle(10.0, 11.5, i)).collect();
132 let out = q.batch(&candles);
133 assert_relative_eq!(out.last().unwrap().unwrap(), 1.5, epsilon = 1e-12);
134 }
135
136 #[test]
137 fn selling_pressure_is_negative() {
138 let mut q = Qstick::new(3).unwrap();
139 let candles: Vec<Candle> = (0..6).map(|i| candle(11.0, 10.0, i)).collect();
140 let last = q.batch(&candles).last().unwrap().unwrap();
141 assert!(last < 0.0, "qstick {last} should be negative");
142 }
143
144 #[test]
145 fn reset_clears_state() {
146 let mut q = Qstick::new(3).unwrap();
147 let candles: Vec<Candle> = (0..6).map(|i| candle(10.0, 11.0, i)).collect();
148 q.batch(&candles);
149 assert!(q.is_ready());
150 q.reset();
151 assert!(!q.is_ready());
152 }
153
154 #[test]
155 fn batch_equals_streaming() {
156 let candles: Vec<Candle> = (0..40_i64)
157 .map(|i| {
158 candle(
159 100.0 + (i as f64 * 0.3).sin(),
160 100.0 + (i as f64 * 0.4).cos(),
161 i,
162 )
163 })
164 .collect();
165 let mut a = Qstick::new(7).unwrap();
166 let mut b = Qstick::new(7).unwrap();
167 assert_eq!(
168 a.batch(&candles),
169 candles.iter().map(|c| b.update(*c)).collect::<Vec<_>>()
170 );
171 }
172}