Skip to main content

quantedge_ta/
price_source.rs

1use crate::{Ohlcv, Price};
2
3use std::fmt::{Debug, Display};
4
5/// Price source extracted from an [`Ohlcv`] bar before feeding into an
6/// indicator.
7///
8/// Each indicator is configured with a `PriceSource` that determines which
9/// value (or derived value) to compute on.
10#[derive(PartialEq, Eq, Hash, Clone, Copy, Default, Debug)]
11pub enum PriceSource {
12    /// Opening price.
13    Open,
14    /// Highest price.
15    High,
16    /// Closing price.
17    #[default]
18    Close,
19    /// Lowest price.
20    Low,
21    /// Median price: `(high + low) / 2`.
22    HL2,
23    /// Typical price: `(high + low + close) / 3`.
24    HLC3,
25    /// Average price: `(open + high + low + close) / 4`.
26    OHLC4,
27    /// Weighted close: `(high + low + close + close) / 4`.
28    HLCC4,
29    /// True range: `max(high - low, |high - prev_close|, |low - prev_close|)`.
30    ///
31    /// On the first bar (no previous close), falls back to `high - low`.
32    TrueRange,
33}
34
35impl Display for PriceSource {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        write!(f, "{self:?}")
38    }
39}
40
41impl PriceSource {
42    #[inline]
43    pub(crate) fn extract(self, ohlcv: &impl Ohlcv, prev_close: Option<Price>) -> Price {
44        match self {
45            Self::Open => ohlcv.open(),
46            Self::High => ohlcv.high(),
47            Self::Close => ohlcv.close(),
48            Self::Low => ohlcv.low(),
49            Self::HL2 => f64::midpoint(ohlcv.high(), ohlcv.low()),
50            Self::HLC3 => (ohlcv.high() + ohlcv.low() + ohlcv.close()) / 3.0,
51            Self::OHLC4 => (ohlcv.open() + ohlcv.high() + ohlcv.low() + ohlcv.close()) / 4.0,
52            Self::HLCC4 => (ohlcv.high() + ohlcv.low() + ohlcv.close() + ohlcv.close()) / 4.0,
53            Self::TrueRange => {
54                let hl = ohlcv.high() - ohlcv.low();
55
56                match prev_close {
57                    Some(prev_close) => {
58                        let hc = (ohlcv.high() - prev_close).abs();
59                        let lc = (ohlcv.low() - prev_close).abs();
60                        hl.max(hc).max(lc)
61                    }
62                    None => hl,
63                }
64            }
65        }
66    }
67}
68
69#[cfg(test)]
70#[allow(clippy::float_cmp)]
71mod tests {
72    use super::*;
73    use crate::test_util::{Bar, assert_approx};
74
75    fn bar() -> Bar {
76        Bar::new(10.0, 30.0, 5.0, 20.0)
77    }
78
79    #[test]
80    fn extract_open() {
81        assert_eq!(PriceSource::Open.extract(&bar(), None), 10.0);
82    }
83
84    #[test]
85    fn extract_high() {
86        assert_eq!(PriceSource::High.extract(&bar(), None), 30.0);
87    }
88
89    #[test]
90    fn extract_low() {
91        assert_eq!(PriceSource::Low.extract(&bar(), None), 5.0);
92    }
93
94    #[test]
95    fn extract_close() {
96        assert_eq!(PriceSource::Close.extract(&bar(), None), 20.0);
97    }
98
99    #[test]
100    fn extract_hl2() {
101        // (30 + 5) / 2 = 17.5
102        assert_eq!(PriceSource::HL2.extract(&bar(), None), 17.5);
103    }
104
105    #[test]
106    fn extract_hlc3() {
107        // (30 + 5 + 20) / 3 = 18.333...
108        let result = PriceSource::HLC3.extract(&bar(), None);
109        assert_approx!(result, 55.0 / 3.0);
110    }
111
112    #[test]
113    fn extract_ohlc4() {
114        // (10 + 30 + 5 + 20) / 4 = 16.25
115        assert_eq!(PriceSource::OHLC4.extract(&bar(), None), 16.25);
116    }
117
118    #[test]
119    fn extract_hlcc4() {
120        // (30 + 5 + 20 + 20) / 4 = 18.75
121        assert_eq!(PriceSource::HLCC4.extract(&bar(), None), 18.75);
122    }
123
124    // TrueRange: max(high - low, |high - prev_close|, |low - prev_close|)
125
126    #[test]
127    fn true_range_without_prev_close_falls_back_to_hl() {
128        // No previous bar, returns high - low = 25
129        assert_eq!(PriceSource::TrueRange.extract(&bar(), None), 25.0);
130    }
131
132    #[test]
133    fn true_range_hl_wins() {
134        // prev_close inside the bar range: hl dominates
135        // hl = 25, |30 - 15| = 15, |5 - 15| = 10
136        let b = bar();
137        assert_eq!(PriceSource::TrueRange.extract(&b, Some(15.0)), 25.0);
138    }
139
140    #[test]
141    fn true_range_high_vs_prev_close_wins() {
142        // Gap up: prev_close far below low
143        // hl = 25, |30 - (-10)| = 40, |5 - (-10)| = 15
144        let b = bar();
145        assert_eq!(PriceSource::TrueRange.extract(&b, Some(-10.0)), 40.0);
146    }
147
148    #[test]
149    fn true_range_low_vs_prev_close_wins() {
150        // Gap down: prev_close far above high
151        // hl = 25, |30 - 50| = 20, |5 - 50| = 45
152        let b = bar();
153        assert_eq!(PriceSource::TrueRange.extract(&b, Some(50.0)), 45.0);
154    }
155}