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