Skip to main content

quantwave_core/indicators/
price_transform.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2use crate::traits::Next;
3
4talib_4_in_1_out!(AVGPRICE, talib_rs::price_transform::avgprice);
5impl Default for AVGPRICE {
6    fn default() -> Self {
7        Self::new()
8    }
9}
10talib_2_in_1_out!(MEDPRICE, talib_rs::price_transform::medprice);
11impl Default for MEDPRICE {
12    fn default() -> Self {
13        Self::new()
14    }
15}
16talib_3_in_1_out!(TYPPRICE, talib_rs::price_transform::typprice);
17impl Default for TYPPRICE {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22talib_3_in_1_out!(WCLPRICE, talib_rs::price_transform::wclprice);
23impl Default for WCLPRICE {
24    fn default() -> Self {
25        Self::new()
26    }
27}
28
29/// (Open + Close) / 2
30/// 
31/// Based on John Ehlers' "Every Little Bit Helps" (2023).
32/// Used to reduce noise in technical indicators by averaging the open and close.
33#[derive(Debug, Clone, Default)]
34pub struct OC2;
35
36impl OC2 {
37    pub fn new() -> Self {
38        Self
39    }
40}
41
42impl Next<(f64, f64)> for OC2 {
43    type Output = f64;
44    fn next(&mut self, input: (f64, f64)) -> Self::Output {
45        (input.0 + input.1) / 2.0
46    }
47}
48
49pub const AVGPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
50    name: "Average Price (AVGPRICE)",
51    description: "The simple average of the Open, High, Low, and Close prices for a given period.",
52    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.",
53    keywords: &["price-transform", "classic", "smoothing"],
54    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",
55    params: &[],
56    formula_source: "https://www.tradingview.com/support/solutions/43000502588-average-price-avgprice/",
57    formula_latex: r#"
58\[
59AVGPRICE = \frac{Open + High + Low + Close}{4}
60\]
61"#,
62    gold_standard_file: "avgprice.json",
63    category: "Classic",
64};
65
66pub const MEDPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
67    name: "Median Price (MEDPRICE)",
68    description: "The midpoint between the High and Low prices for a given period.",
69    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.",
70    keywords: &["price-transform", "classic", "midpoint"],
71    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",
72    params: &[],
73    formula_source: "https://www.tradingview.com/support/solutions/43000502589-median-price-medprice/",
74    formula_latex: r#"
75\[
76MEDPRICE = \frac{High + Low}{2}
77\]
78"#,
79    gold_standard_file: "medprice.json",
80    category: "Classic",
81};
82
83pub const TYPPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
84    name: "Typical Price (TYPPRICE)",
85    description: "An average of the High, Low, and Close prices.",
86    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.",
87    keywords: &["price-transform", "classic"],
88    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",
89    params: &[],
90    formula_source: "https://www.investopedia.com/terms/t/typicalprice.asp",
91    formula_latex: r#"
92\[
93TYPPRICE = \frac{High + Low + Close}{3}
94\]
95"#,
96    gold_standard_file: "typprice.json",
97    category: "Classic",
98};
99
100pub const WCLPRICE_METADATA: IndicatorMetadata = IndicatorMetadata {
101    name: "Weighted Close Price (WCLPRICE)",
102    description: "An average of the High, Low, and Close prices, with double weight given to the Close price.",
103    usage: "Use to emphasize the importance of the closing price while still accounting for the total range of the bar.",
104    keywords: &["price-transform", "classic", "weighted"],
105    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",
106    params: &[],
107    formula_source: "https://www.tradingview.com/support/solutions/43000502590-weighted-close-wclprice/",
108    formula_latex: r#"
109\[
110WCLPRICE = \frac{High + Low + 2 \times Close}{4}
111\]
112"#,
113    gold_standard_file: "wclprice.json",
114    category: "Classic",
115};
116
117pub const OC2_METADATA: IndicatorMetadata = IndicatorMetadata {
118    name: "Open-Close Average (OC2)",
119    description: "A simple average of the Open and Close prices.",
120    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.",
121    keywords: &["price-transform", "ehlers", "smoothing", "dsp"],
122    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",
123    params: &[],
124    formula_source: "Every Little Bit Helps (John Ehlers, 2023)",
125    formula_latex: r#"
126\[
127OC2 = \frac{Open + Close}{2}
128\]
129"#,
130    gold_standard_file: "oc2.json",
131    category: "Ehlers DSP",
132};
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::traits::Next;
138    use proptest::prelude::*;
139
140    proptest! {
141        #[test]
142        fn test_avgprice_parity(
143            o in prop::collection::vec(0.1..100.0, 1..100),
144            h in prop::collection::vec(0.1..100.0, 1..100),
145            l in prop::collection::vec(0.1..100.0, 1..100),
146            c in prop::collection::vec(0.1..100.0, 1..100)
147        ) {
148            let len = o.len().min(h.len()).min(l.len()).min(c.len());
149            if len == 0 { return Ok(()); }
150
151            let mut avgprice = AVGPRICE::new();
152            let streaming_results: Vec<f64> = (0..len).map(|i| avgprice.next((o[i], h[i], l[i], c[i]))).collect();
153            let batch_results = talib_rs::price_transform::avgprice(&o[..len], &h[..len], &l[..len], &c[..len]).unwrap_or_else(|_| vec![f64::NAN; len]);
154
155            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
156                if s.is_nan() {
157                    assert!(b.is_nan());
158                } else {
159                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
160                }
161            }
162        }
163
164        #[test]
165        fn test_medprice_parity(
166            h in prop::collection::vec(0.1..100.0, 1..100),
167            l in prop::collection::vec(0.1..100.0, 1..100)
168        ) {
169            let len = h.len().min(l.len());
170            if len == 0 { return Ok(()); }
171
172            let mut medprice = MEDPRICE::new();
173            let streaming_results: Vec<f64> = (0..len).map(|i| medprice.next((h[i], l[i]))).collect();
174            let batch_results = talib_rs::price_transform::medprice(&h[..len], &l[..len]).unwrap_or_else(|_| vec![f64::NAN; len]);
175
176            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
177                if s.is_nan() {
178                    assert!(b.is_nan());
179                } else {
180                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
181                }
182            }
183        }
184
185        #[test]
186        fn test_oc2_parity(
187            o in prop::collection::vec(0.1..100.0, 1..100),
188            c in prop::collection::vec(0.1..100.0, 1..100)
189        ) {
190            let len = o.len().min(c.len());
191            if len == 0 { return Ok(()); }
192
193            let mut oc2 = OC2::new();
194            let streaming_results: Vec<f64> = (0..len).map(|i| oc2.next((o[i], c[i]))).collect();
195            let batch_results: Vec<f64> = (0..len).map(|i| (o[i] + c[i]) / 2.0).collect();
196
197            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
198                approx::assert_relative_eq!(s, b, epsilon = 1e-10);
199            }
200        }
201    }
202}