Skip to main content

wickra_core/indicators/
demark_pivots.rs

1//! `DeMark` Pivot Points.
2
3use crate::ohlcv::Candle;
4use crate::traits::Indicator;
5
6/// `DeMark` Pivot Points output: a single resistance, pivot and support.
7#[derive(Debug, Clone, Copy, PartialEq)]
8pub struct DemarkPivotsOutput {
9    /// Pivot Point: `X / 4` where `X` is the conditional sum (see [`DemarkPivots`]).
10    pub pp: f64,
11    /// Resistance 1: `X / 2 − L`.
12    pub r1: f64,
13    /// Support 1: `X / 2 − H`.
14    pub s1: f64,
15}
16
17/// `DeMark` Pivot Points — Tom `DeMark`'s conditional pivot formulation, derived
18/// from a sum `X` that depends on whether the bar closed up, down or flat.
19///
20/// ```text
21/// X = 2·H + L + C   if C  < O   (down bar)
22///     H + 2·L + C   if C  > O   (up bar)
23///     H + L + 2·C   if C == O   (doji)
24///
25/// PP = X / 4
26/// R1 = X / 2 − L
27/// S1 = X / 2 − H
28/// ```
29///
30/// Unlike the classic pivots, only one resistance and one support are
31/// produced; `DeMark`'s intent is a tighter, condition-sensitive set rather than
32/// a multi-tier fan. The branching means a bar's open carries information that
33/// other pivot variants discard.
34///
35/// # Example
36///
37/// ```
38/// use wickra_core::{Candle, DemarkPivots, Indicator};
39///
40/// // Up bar: O=100, H=120, L=80, C=110 -> X = H + 2·L + C = 390.
41/// let up = Candle::new(100.0, 120.0, 80.0, 110.0, 1.0, 0).unwrap();
42/// let lv = DemarkPivots::new().update(up).unwrap();
43/// assert!((lv.pp - 97.5).abs() < 1e-9);
44/// ```
45#[derive(Debug, Clone, Default)]
46pub struct DemarkPivots {
47    ready: bool,
48}
49
50impl DemarkPivots {
51    /// Construct a new `DeMark` Pivot Points indicator.
52    pub const fn new() -> Self {
53        Self { ready: false }
54    }
55}
56
57impl Indicator for DemarkPivots {
58    type Input = Candle;
59    type Output = DemarkPivotsOutput;
60
61    fn update(&mut self, candle: Candle) -> Option<DemarkPivotsOutput> {
62        let open = candle.open;
63        let high = candle.high;
64        let low = candle.low;
65        let close = candle.close;
66        let x = if close < open {
67            2.0 * high + low + close
68        } else if close > open {
69            high + 2.0 * low + close
70        } else {
71            high + low + 2.0 * close
72        };
73        let pp = x / 4.0;
74        let half = x / 2.0;
75        let out = DemarkPivotsOutput {
76            pp,
77            r1: half - low,
78            s1: half - high,
79        };
80        self.ready = true;
81        Some(out)
82    }
83
84    fn reset(&mut self) {
85        self.ready = false;
86    }
87
88    fn warmup_period(&self) -> usize {
89        1
90    }
91
92    fn is_ready(&self) -> bool {
93        self.ready
94    }
95
96    fn name(&self) -> &'static str {
97        "DemarkPivots"
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::traits::BatchExt;
105
106    #[test]
107    fn down_bar_uses_2h_plus_l_plus_c() {
108        // O=110, H=120, L=80, C=100 (close < open) -> X = 2·120 + 80 + 100 = 420.
109        let cd = Candle::new(110.0, 120.0, 80.0, 100.0, 1.0, 0).unwrap();
110        let lv = DemarkPivots::new().update(cd).unwrap();
111        assert!((lv.pp - 105.0).abs() < 1e-12);
112        assert!((lv.r1 - (210.0 - 80.0)).abs() < 1e-12);
113        assert!((lv.s1 - (210.0 - 120.0)).abs() < 1e-12);
114    }
115
116    #[test]
117    fn up_bar_uses_h_plus_2l_plus_c() {
118        // O=100, H=120, L=80, C=110 (close > open) -> X = 120 + 160 + 110 = 390.
119        let cd = Candle::new(100.0, 120.0, 80.0, 110.0, 1.0, 0).unwrap();
120        let lv = DemarkPivots::new().update(cd).unwrap();
121        assert!((lv.pp - 97.5).abs() < 1e-12);
122        assert!((lv.r1 - (195.0 - 80.0)).abs() < 1e-12);
123        assert!((lv.s1 - (195.0 - 120.0)).abs() < 1e-12);
124    }
125
126    #[test]
127    fn doji_uses_h_plus_l_plus_2c() {
128        // O = C = 100, H=120, L=80 -> X = 120 + 80 + 200 = 400.
129        let cd = Candle::new(100.0, 120.0, 80.0, 100.0, 1.0, 0).unwrap();
130        let lv = DemarkPivots::new().update(cd).unwrap();
131        assert!((lv.pp - 100.0).abs() < 1e-12);
132    }
133
134    #[test]
135    fn ordering_resistance_above_pivot_above_support() {
136        let cd = Candle::new(100.0, 120.0, 80.0, 110.0, 1.0, 0).unwrap();
137        let lv = DemarkPivots::new().update(cd).unwrap();
138        assert!(lv.r1 >= lv.pp);
139        assert!(lv.pp >= lv.s1);
140    }
141
142    #[test]
143    fn constant_series_collapses_levels() {
144        let cd = Candle::new(50.0, 50.0, 50.0, 50.0, 1.0, 0).unwrap();
145        let lv = DemarkPivots::new().update(cd).unwrap();
146        assert_eq!(lv.pp, 50.0);
147        assert_eq!(lv.r1, 50.0);
148        assert_eq!(lv.s1, 50.0);
149    }
150
151    #[test]
152    fn warmup_and_ready() {
153        let mut p = DemarkPivots::new();
154        assert!(!p.is_ready());
155        assert_eq!(p.warmup_period(), 1);
156        let cd = Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0).unwrap();
157        p.update(cd);
158        assert!(p.is_ready());
159    }
160
161    #[test]
162    fn reset_clears_state() {
163        let mut p = DemarkPivots::new();
164        let cd = Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0).unwrap();
165        p.update(cd);
166        p.reset();
167        assert!(!p.is_ready());
168    }
169
170    #[test]
171    fn batch_equals_streaming() {
172        let candles: Vec<Candle> = (0..40)
173            .map(|i| {
174                let base = f64::from(i);
175                Candle::new(base, base + 2.0, base - 0.5, base + 1.0, 1.0, i64::from(i)).unwrap()
176            })
177            .collect();
178        let mut a = DemarkPivots::new();
179        let mut b = DemarkPivots::new();
180        assert_eq!(
181            a.batch(&candles),
182            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
183        );
184    }
185
186    #[test]
187    fn accessors_and_metadata() {
188        let p = DemarkPivots::new();
189        assert_eq!(p.warmup_period(), 1);
190        assert_eq!(p.name(), "DemarkPivots");
191    }
192}