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 = H + 2·L + C   if C  < O   (down bar — the low is weighted)
22///     2·H + L + C   if C  > O   (up bar — the high is weighted)
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 = 2·H + L + C = 430.
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 - 107.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    #[inline]
62    fn update(&mut self, candle: Candle) -> Option<DemarkPivotsOutput> {
63        let open = candle.open;
64        let high = candle.high;
65        let low = candle.low;
66        let close = candle.close;
67        let x = if close < open {
68            high + 2.0 * low + close
69        } else if close > open {
70            2.0 * high + low + close
71        } else {
72            high + low + 2.0 * close
73        };
74        let pp = x / 4.0;
75        let half = x / 2.0;
76        let out = DemarkPivotsOutput {
77            pp,
78            r1: half - low,
79            s1: half - high,
80        };
81        self.ready = true;
82        Some(out)
83    }
84
85    fn reset(&mut self) {
86        self.ready = false;
87    }
88
89    #[inline]
90    fn warmup_period(&self) -> usize {
91        1
92    }
93
94    #[inline]
95    fn is_ready(&self) -> bool {
96        self.ready
97    }
98
99    #[inline]
100    fn name(&self) -> &'static str {
101        "DemarkPivots"
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use crate::traits::BatchExt;
109
110    #[test]
111    fn down_bar_uses_h_plus_2l_plus_c() {
112        // O=110, H=120, L=80, C=100 (close < open) -> X = 120 + 2·80 + 100 = 380.
113        let cd = Candle::new(110.0, 120.0, 80.0, 100.0, 1.0, 0).unwrap();
114        let lv = DemarkPivots::new().update(cd).unwrap();
115        assert!((lv.pp - 95.0).abs() < 1e-12);
116        assert!((lv.r1 - (190.0 - 80.0)).abs() < 1e-12);
117        assert!((lv.s1 - (190.0 - 120.0)).abs() < 1e-12);
118    }
119
120    #[test]
121    fn up_bar_uses_2h_plus_l_plus_c() {
122        // O=100, H=120, L=80, C=110 (close > open) -> X = 2·120 + 80 + 110 = 430.
123        let cd = Candle::new(100.0, 120.0, 80.0, 110.0, 1.0, 0).unwrap();
124        let lv = DemarkPivots::new().update(cd).unwrap();
125        assert!((lv.pp - 107.5).abs() < 1e-12);
126        assert!((lv.r1 - (215.0 - 80.0)).abs() < 1e-12);
127        assert!((lv.s1 - (215.0 - 120.0)).abs() < 1e-12);
128    }
129
130    #[test]
131    fn doji_uses_h_plus_l_plus_2c() {
132        // O = C = 100, H=120, L=80 -> X = 120 + 80 + 200 = 400.
133        let cd = Candle::new(100.0, 120.0, 80.0, 100.0, 1.0, 0).unwrap();
134        let lv = DemarkPivots::new().update(cd).unwrap();
135        assert!((lv.pp - 100.0).abs() < 1e-12);
136    }
137
138    #[test]
139    fn ordering_resistance_above_pivot_above_support() {
140        let cd = Candle::new(100.0, 120.0, 80.0, 110.0, 1.0, 0).unwrap();
141        let lv = DemarkPivots::new().update(cd).unwrap();
142        assert!(lv.r1 >= lv.pp);
143        assert!(lv.pp >= lv.s1);
144    }
145
146    #[test]
147    fn constant_series_collapses_levels() {
148        let cd = Candle::new(50.0, 50.0, 50.0, 50.0, 1.0, 0).unwrap();
149        let lv = DemarkPivots::new().update(cd).unwrap();
150        assert_eq!(lv.pp, 50.0);
151        assert_eq!(lv.r1, 50.0);
152        assert_eq!(lv.s1, 50.0);
153    }
154
155    #[test]
156    fn warmup_and_ready() {
157        let mut p = DemarkPivots::new();
158        assert!(!p.is_ready());
159        assert_eq!(p.warmup_period(), 1);
160        let cd = Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0).unwrap();
161        p.update(cd);
162        assert!(p.is_ready());
163    }
164
165    #[test]
166    fn reset_clears_state() {
167        let mut p = DemarkPivots::new();
168        let cd = Candle::new(10.0, 11.0, 9.0, 10.0, 1.0, 0).unwrap();
169        p.update(cd);
170        p.reset();
171        assert!(!p.is_ready());
172    }
173
174    #[test]
175    fn batch_equals_streaming() {
176        let candles: Vec<Candle> = (0..40)
177            .map(|i| {
178                let base = f64::from(i);
179                Candle::new(base, base + 2.0, base - 0.5, base + 1.0, 1.0, i64::from(i)).unwrap()
180            })
181            .collect();
182        let mut a = DemarkPivots::new();
183        let mut b = DemarkPivots::new();
184        assert_eq!(
185            a.batch(&candles),
186            candles.iter().map(|x| b.update(*x)).collect::<Vec<_>>()
187        );
188    }
189
190    #[test]
191    fn accessors_and_metadata() {
192        let p = DemarkPivots::new();
193        assert_eq!(p.warmup_period(), 1);
194        assert_eq!(p.name(), "DemarkPivots");
195    }
196}