Skip to main content

quantwave_core/indicators/
volume.rs

1use crate::indicators::metadata::{IndicatorMetadata, ParamDef};
2#[allow(unused_imports)]
3use crate::traits::Next;
4
5pub use crate::indicators::incremental::simple::OBV;
6pub use crate::indicators::incremental::volume_ta::{AD, ADOSC};
7impl Default for AD {
8    fn default() -> Self {
9        Self::new()
10    }
11}
12impl Default for OBV {
13    fn default() -> Self {
14        Self::new()
15    }
16}
17
18pub const AD_METADATA: IndicatorMetadata = IndicatorMetadata {
19    name: "Accumulation/Distribution Line (AD)",
20    description: "A volume-based indicator designed to measure the cumulative flow of money into and out of a security.",
21    usage: "Use to confirm price trends or identify potential reversals through divergences. Rising AD confirms an uptrend; falling AD confirms a downtrend.",
22    keywords: &[
23        "volume",
24        "momentum",
25        "classic",
26        "accumulation",
27        "distribution",
28    ],
29    ehlers_summary: "Developed by Marc Chaikin, the AD line uses the relationship between price and volume to determine whether a security is being accumulated or distributed. It is calculated by multiplying the Money Flow Multiplier by the period's volume and adding it to a cumulative total. — StockCharts ChartSchool",
30    params: &[],
31    formula_source: "https://www.investopedia.com/terms/a/accumulationdistributioncurve.asp",
32    formula_latex: r#"
33\[
34\text{MFM} = \frac{(Close - Low) - (High - Close)}{High - Low} \\ \text{MFV} = \text{MFM} \times Volume \\ AD_t = AD_{t-1} + \text{MFV}
35\]
36"#,
37    gold_standard_file: "ad.json",
38    category: "Classic",
39};
40
41pub const ADOSC_METADATA: IndicatorMetadata = IndicatorMetadata {
42    name: "Chaikin Oscillator (ADOSC)",
43    description: "An indicator that measures the momentum of the Accumulation/Distribution Line using the difference between two exponential moving averages.",
44    usage: "Use to anticipate changes in the AD Line. Positive values indicate increasing buying pressure, while negative values indicate increasing selling pressure.",
45    keywords: &["volume", "oscillator", "momentum", "classic"],
46    ehlers_summary: "Marc Chaikin developed this oscillator to identify momentum shifts in the AD Line. By applying EMAs of different lengths to the AD Line, it highlights changes in money flow before they become apparent in the cumulative total, providing an early warning system for trend exhaustion. — StockCharts ChartSchool",
47    params: &[
48        ParamDef {
49            name: "fastperiod",
50            default: "3",
51            description: "Fast EMA period",
52        },
53        ParamDef {
54            name: "slowperiod",
55            default: "10",
56            description: "Slow EMA period",
57        },
58    ],
59    formula_source: "https://www.investopedia.com/terms/c/chaikinoscillator.asp",
60    formula_latex: r#"
61\[
62ADOSC = EMA(AD, 3) - EMA(AD, 10)
63\]
64"#,
65    gold_standard_file: "adosc.json",
66    category: "Classic",
67};
68
69pub const OBV_METADATA: IndicatorMetadata = IndicatorMetadata {
70    name: "On-Balance Volume (OBV)",
71    description: "A momentum indicator that uses volume flow to predict changes in stock price.",
72    usage: "Use to identify accumulation by institutions. When price is flat but OBV is rising, a breakout to the upside is likely. Conversely, when price is flat but OBV is falling, a breakdown is likely.",
73    keywords: &[
74        "volume",
75        "momentum",
76        "classic",
77        "accumulation",
78        "distribution",
79    ],
80    ehlers_summary: "Introduced by Joe Granville in his 1963 book 'Granville's New Key to Stock Market Profits', OBV is one of the oldest and most respected volume indicators. It operates on the principle that volume precedes price, and that institutional money flow leaves a detectable trail in the volume data before the price move occurs. — StockCharts ChartSchool",
81    params: &[],
82    formula_source: "https://www.investopedia.com/terms/o/onbalancevolume.asp",
83    formula_latex: r#"
84\[
85OBV_t = OBV_{t-1} + \begin{cases} Volume & \text{if } Close_t > Close_{t-1} \\ 0 & \text{if } Close_t = Close_{t-1} \\ -Volume & \text{if } Close_t < Close_{t-1} \end{cases}
86\]
87"#,
88    gold_standard_file: "obv.json",
89    category: "Classic",
90};
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95    use crate::traits::Next;
96    use proptest::prelude::*;
97
98    proptest! {
99        #[test]
100        fn test_ad_parity(
101            h in prop::collection::vec(10.0..100.0, 1..100),
102            l in prop::collection::vec(10.0..100.0, 1..100),
103            c in prop::collection::vec(10.0..100.0, 1..100),
104            v in prop::collection::vec(1.0..1000.0, 1..100)
105        ) {
106            let len = h.len().min(l.len()).min(c.len()).min(v.len());
107            if len == 0 { return Ok(()); }
108            let mut high = Vec::with_capacity(len);
109            let mut low = Vec::with_capacity(len);
110            let mut close = Vec::with_capacity(len);
111            let mut volume = Vec::with_capacity(len);
112            for i in 0..len {
113                let v_h: f64 = h[i];
114                let v_l: f64 = l[i];
115                let v_c: f64 = c[i];
116                let v_v: f64 = v[i];
117                high.push(v_h.max(v_l).max(v_c));
118                low.push(v_h.min(v_l).min(v_c));
119                close.push(v_c);
120                volume.push(v_v);
121            }
122
123            let mut ad = AD::new();
124            let streaming_results: Vec<f64> = (0..len).map(|i| ad.next((high[i], low[i], close[i], volume[i]))).collect();
125            let batch_results = talib_rs::volume::ad(&high, &low, &close, &volume).unwrap_or_else(|_| vec![f64::NAN; len]);
126
127            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
128                if s.is_nan() {
129                    assert!(b.is_nan());
130                } else {
131                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
132                }
133            }
134        }
135
136        #[test]
137        fn test_obv_parity(
138            c in prop::collection::vec(10.0..100.0, 1..100),
139            v in prop::collection::vec(1.0..1000.0, 1..100)
140        ) {
141            let len = c.len().min(v.len());
142            if len == 0 { return Ok(()); }
143            let close = c[..len].to_vec();
144            let volume = v[..len].to_vec();
145
146            let mut obv = OBV::new();
147            let streaming_results: Vec<f64> = (0..len).map(|i| obv.next((close[i], volume[i]))).collect();
148            let batch_results = talib_rs::volume::obv(&close, &volume).unwrap_or_else(|_| vec![f64::NAN; len]);
149
150            for (s, b) in streaming_results.iter().zip(batch_results.iter()) {
151                if s.is_nan() {
152                    assert!(b.is_nan());
153                } else {
154                    approx::assert_relative_eq!(s, b, epsilon = 1e-6);
155                }
156            }
157        }
158    }
159}