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    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        // Every bar closes 1.5 above its open -> Qstick converges to 1.5.
126        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}