Skip to main content

wickra_core/indicators/
qstick.rs

1//! Qstick — Tushar Chande's measure of buying vs. selling pressure.
2
3use crate::error::Result;
4use crate::indicators::sma::Sma;
5use crate::ohlcv::Candle;
6use crate::traits::Indicator;
7
8/// Qstick: the simple moving average of the body `close - open` over `period`
9/// bars.
10///
11/// Positive values indicate a run of bars that closed above their open (net
12/// buying pressure); negative values indicate net selling pressure. A zero
13/// crossing is read as a shift in short-term sentiment.
14///
15/// ```text
16/// Qstick = SMA(close - open, period)
17/// ```
18///
19/// Reference: Tushar Chande, *The New Technical Trader*, 1994.
20///
21/// # Example
22///
23/// ```
24/// use wickra_core::{Candle, Indicator, Qstick};
25///
26/// let mut indicator = Qstick::new(5).unwrap();
27/// let mut last = None;
28/// for i in 0..20 {
29///     let base = 100.0 + f64::from(i);
30///     let candle =
31///         Candle::new(base, base + 2.0, base - 1.0, base + 1.0, 1.0, i64::from(i)).unwrap();
32///     last = indicator.update(candle);
33/// }
34/// assert!(last.is_some());
35/// ```
36#[derive(Debug, Clone)]
37pub struct Qstick {
38    period: usize,
39    sma: Sma,
40}
41
42impl Qstick {
43    /// Construct a Qstick with the given averaging period.
44    ///
45    /// # Errors
46    ///
47    /// Returns [`Error::PeriodZero`](crate::error::Error::PeriodZero) if `period == 0`.
48    pub fn new(period: usize) -> Result<Self> {
49        Ok(Self {
50            period,
51            sma: Sma::new(period)?,
52        })
53    }
54
55    /// Configured averaging period.
56    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        // Every bar closes 1.5 above its open -> Qstick converges to 1.5.
130        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}