Skip to main content

quantwave_core/indicators/
price_transform.rs

1use crate::indicators::metadata::IndicatorMetadata;
2use crate::traits::Next;
3
4pub use crate::indicators::incremental::price_transform::{AVGPRICE, MEDPRICE, TYPPRICE, WCLPRICE};
5
6/// (Open + Close) / 2
7///
8/// Based on John Ehlers' "Every Little Bit Helps" (2023).
9/// Used to reduce noise in technical indicators by averaging the open and close.
10#[derive(Debug, Clone, Default)]
11pub struct OC2;
12
13impl OC2 {
14    pub fn new() -> Self {
15        Self
16    }
17}
18
19impl Next<(f64, f64)> for OC2 {
20    type Output = f64;
21    fn next(&mut self, input: (f64, f64)) -> Self::Output {
22        (input.0 + input.1) / 2.0
23    }
24}
25
26pub const AVGPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
27    name: "Average Price (AVGPRICE)",
28    description: "The simple average of the Open, High, Low, and Close prices for a given period.",
29    usage: "Use as a smoothed price input for other indicators. It provides a more balanced view of the period's price action than the Close price alone.",
30    keywords: &["price-transform", "classic", "smoothing"],
31    ehlers_summary: "Average Price is the arithmetic mean of the four key price points in a bar. In technical analysis, using Average Price instead of Close can help filter out erratic price spikes and provide a more stable foundation for trend-following algorithms. — TA-Lib Documentation",
32    params: &[],
33    formula_source: "https://www.tradingview.com/support/solutions/43000502588-average-price-avgprice/",
34    formula_latex: r#"
35\[
36AVGPRICE = \frac{Open + High + Low + Close}{4}
37\]
38"#,
39    gold_standard_file: "avgprice.json",
40    category: "Classic",
41};
42
43pub const MEDPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
44    name: "Median Price (MEDPRICE)",
45    description: "The midpoint between the High and Low prices for a given period.",
46    usage: "Use to identify the central tendency of a bar's range. It is the basis for many oscillators and trend-following indicators like the Bill Williams Alligator.",
47    keywords: &["price-transform", "classic", "midpoint"],
48    ehlers_summary: "Median Price represents the 50% retracement level of the current period's range. By focusing on the High-Low midpoint, it removes the 'bias' of the closing price, which can often be manipulated by end-of-day positioning. — TA-Lib Documentation",
49    params: &[],
50    formula_source: "https://www.tradingview.com/support/solutions/43000502589-median-price-medprice/",
51    formula_latex: r#"
52\[
53MEDPRICE = \frac{High + Low}{2}
54\]
55"#,
56    gold_standard_file: "medprice.json",
57    category: "Classic",
58};
59
60pub const TYPPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
61    name: "Typical Price (TYPPRICE)",
62    description: "An average of the High, Low, and Close prices.",
63    usage: "Use as the primary price input for the Money Flow Index (MFI) and Commodity Channel Index (CCI). It provides a representative price level for the entire bar.",
64    keywords: &["price-transform", "classic"],
65    ehlers_summary: "Typical Price is a simple average of the High, Low, and Close. It is widely used in indicators that measure the relationship between price and volume, as it offers a more comprehensive view of the day's activity than the Close price alone. — StockCharts ChartSchool",
66    params: &[],
67    formula_source: "https://www.investopedia.com/terms/t/typicalprice.asp",
68    formula_latex: r#"
69\[
70TYPPRICE = \frac{High + Low + Close}{3}
71\]
72"#,
73    gold_standard_file: "typprice.json",
74    category: "Classic",
75};
76
77pub const WCLPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
78    name: "Weighted Close Price (WCLPRICE)",
79    description: "An average of the High, Low, and Close prices, with double weight given to the Close price.",
80    usage: "Use to emphasize the importance of the closing price while still accounting for the total range of the bar.",
81    keywords: &["price-transform", "classic", "weighted"],
82    ehlers_summary: "Weighted Close Price gives additional significance to the Close, reflecting the widely held belief that the closing price is the most important data point in a trading session. It provides a more nuanced input for smoothing algorithms. — TA-Lib Documentation",
83    params: &[],
84    formula_source: "https://www.tradingview.com/support/solutions/43000502590-weighted-close-wclprice/",
85    formula_latex: r#"
86\[
87WCLPRICE = \frac{High + Low + 2 \times Close}{4}
88\]
89"#,
90    gold_standard_file: "wclprice.json",
91    category: "Classic",
92};
93
94pub const OC2_METADATA: IndicatorMetadata = IndicatorMetadata {
95    name: "Open-Close Average (OC2)",
96    description: "A simple average of the Open and Close prices.",
97    usage: "Use to reduce noise in technical indicators. Based on John Ehlers' recent research, averaging the open and close can significantly improve signal-to-noise ratios in DSP-based indicators.",
98    keywords: &["price-transform", "ehlers", "smoothing", "dsp"],
99    ehlers_summary: "In his 2023 paper 'Every Little Bit Helps', John Ehlers demonstrates that using the average of the Open and Close as an input can enhance the performance of various filters and oscillators by providing a cleaner signal with reduced aliasing. — John Ehlers",
100    params: &[],
101    formula_source: "Every Little Bit Helps (John Ehlers, 2023)",
102    formula_latex: r#"
103\[
104OC2 = \frac{Open + Close}{2}
105\]
106"#,
107    gold_standard_file: "oc2.json",
108    category: "Ehlers DSP",
109};
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use crate::traits::Next;
115    use proptest::prelude::*;
116
117    proptest! {
118        #[test]
119        fn test_avgprice_parity(
120            o in prop::collection::vec(0.1..100.0, 1..100),
121            h in prop::collection::vec(0.1..100.0, 1..100),
122            l in prop::collection::vec(0.1..100.0, 1..100),
123            c in prop::collection::vec(0.1..100.0, 1..100)
124        ) {
125            let len = o.len().min(h.len()).min(l.len()).min(c.len());
126            if len == 0 { return Ok(()); }
127
128            let mut avgprice = AVGPRICE::new();
129            let streaming_results: Vec<f64> = (0..len).map(|i| avgprice.next((o[i], h[i], l[i], c[i]))).collect();
130            let batch_results = talib_rs::price_transform::avgprice(&o[..len], &h[..len], &l[..len], &c[..len]).unwrap_or_else(|_| vec![f64::NAN; len]);
131
132            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
133                if s.is_nan() {
134                    assert!(b.is_nan());
135                } else {
136                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
137                }
138            }
139        }
140
141        #[test]
142        fn test_medprice_parity(
143            h in prop::collection::vec(0.1..100.0, 1..100),
144            l in prop::collection::vec(0.1..100.0, 1..100)
145        ) {
146            let len = h.len().min(l.len());
147            if len == 0 { return Ok(()); }
148
149            let mut medprice = MEDPRICE::new();
150            let streaming_results: Vec<f64> = (0..len).map(|i| medprice.next((h[i], l[i]))).collect();
151            let batch_results = talib_rs::price_transform::medprice(&h[..len], &l[..len]).unwrap_or_else(|_| vec![f64::NAN; len]);
152
153            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
154                if s.is_nan() {
155                    assert!(b.is_nan());
156                } else {
157                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
158                }
159            }
160        }
161
162        #[test]
163        fn test_oc2_parity(
164            o in prop::collection::vec(0.1..100.0, 1..100),
165            c in prop::collection::vec(0.1..100.0, 1..100)
166        ) {
167            let len = o.len().min(c.len());
168            if len == 0 { return Ok(()); }
169
170            let mut oc2 = OC2::new();
171            let streaming_results: Vec<f64> = (0..len).map(|i| oc2.next((o[i], c[i]))).collect();
172            let batch_results: Vec<f64> = (0..len).map(|i| (o[i] + c[i]) / 2.0).collect();
173
174            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
175                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
176            }
177        }
178    }
179}