finance_solution/stocks/ta/
rvol.rs1use crate::stocks::ta::common::{opt_cell, validate_positive_volume};
51use crate::util::error::FinanceResult;
52use crate::util::primitives::PeriodLength;
53use crate::{columns_with_strings, print_table_locale_opt};
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
57pub struct RvolParams {
58 pub lookback: usize,
59}
60
61impl RvolParams {
62 pub const fn new(lookback: usize) -> Self {
63 Self { lookback }
64 }
65
66 pub const fn days_20() -> Self {
68 Self { lookback: 20 }
69 }
70
71 pub const fn days_50() -> Self {
73 Self { lookback: 50 }
74 }
75}
76
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
79pub struct ValidatedRvol {
80 params: RvolParams,
81}
82
83impl ValidatedRvol {
84 pub fn new(params: RvolParams) -> FinanceResult<Self> {
85 PeriodLength::new(params.lookback)?;
86 Ok(Self { params })
87 }
88
89 pub fn params(self) -> RvolParams {
90 self.params
91 }
92
93 pub fn compute(self, volume: &[f64]) -> FinanceResult<RvolSeries> {
94 rvol_validated(volume, self)
95 }
96}
97
98#[derive(Clone, Debug, PartialEq)]
99pub struct RvolSeries {
100 pub rvol: Vec<Option<f64>>,
101 pub params: RvolParams,
102}
103
104impl RvolSeries {
105 pub fn last(&self) -> Option<f64> {
106 self.rvol.iter().rev().find_map(|x| *x)
107 }
108}
109
110#[derive(Clone, Debug)]
111pub struct RvolSolution {
112 series: RvolSeries,
113 volume: Vec<f64>,
114 formula: String,
115 symbolic_formula: String,
116}
117
118impl RvolSolution {
119 pub fn series(&self) -> &RvolSeries {
120 &self.series
121 }
122 pub fn formula(&self) -> &str {
123 &self.formula
124 }
125 pub fn symbolic_formula(&self) -> &str {
126 &self.symbolic_formula
127 }
128
129 pub fn print_table(&self) {
136 self.print_table_locale_opt(None, None);
137 }
138
139 pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
140 self.print_table_locale_opt(Some(locale), Some(precision));
141 }
142
143 fn print_table_locale_opt(
144 &self,
145 locale: Option<&num_format::Locale>,
146 precision: Option<usize>,
147 ) {
148 let columns = columns_with_strings(&[
149 ("period", "i", true),
150 ("volume", "f", true),
151 ("rvol", "f", true),
152 ]);
153 let data = self
154 .volume
155 .iter()
156 .enumerate()
157 .map(|(i, v)| vec![i.to_string(), v.to_string(), opt_cell(self.series.rvol[i])])
158 .collect();
159 print_table_locale_opt(&columns, data, locale, precision);
160 }
161}
162
163pub fn rvol(volume: &[f64], params: RvolParams) -> FinanceResult<RvolSeries> {
164 ValidatedRvol::new(params)?.compute(volume)
165}
166
167pub fn rvol_solution(volume: &[f64], params: RvolParams) -> FinanceResult<RvolSolution> {
176 let series = rvol(volume, params)?;
177 let formula = format!(
178 "rvol[i] = volume[i] / mean(volume[i-{}+1 ..= i])",
179 params.lookback
180 );
181 let symbolic = "rvol = volume / sma(volume, lookback)".to_string();
182 Ok(RvolSolution {
183 series,
184 volume: volume.to_vec(),
185 formula,
186 symbolic_formula: symbolic,
187 })
188}
189
190fn rvol_validated(volume: &[f64], v: ValidatedRvol) -> FinanceResult<RvolSeries> {
191 validate_positive_volume(volume)?;
192 let lb = v.params.lookback;
193 let n = volume.len();
194 let mut out = vec![None; n];
195 if n < lb {
196 return Ok(RvolSeries {
197 rvol: out,
198 params: v.params,
199 });
200 }
201 let mut sum: f64 = volume[..lb].iter().sum();
202 let mean0 = sum / lb as f64;
203 if mean0 > 0.0 {
204 out[lb - 1] = Some(volume[lb - 1] / mean0);
205 }
206 for i in lb..n {
207 sum += volume[i] - volume[i - lb];
208 let mean = sum / lb as f64;
209 if mean > 0.0 {
210 out[i] = Some(volume[i] / mean);
211 }
212 }
213 Ok(RvolSeries {
214 rvol: out,
215 params: v.params,
216 })
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 #[test]
224 fn constant_is_one() {
225 let v = vec![500.0; 30];
226 let s = rvol(&v, RvolParams::days_20()).unwrap();
227 assert!((s.rvol[19].unwrap() - 1.0).abs() < 1e-12);
228 }
229
230 #[test]
231 fn lookback_one_is_unity_when_positive() {
232 let v = [10.0, 20.0, 5.0];
233 let s = rvol(&v, RvolParams::new(1)).unwrap();
234 assert!((s.rvol[0].unwrap() - 1.0).abs() < 1e-12);
235 assert!((s.rvol[1].unwrap() - 1.0).abs() < 1e-12);
236 }
237
238 #[test]
239 fn zero_mean_window_is_none() {
240 let v = vec![0.0; 25];
241 let s = rvol(&v, RvolParams::days_20()).unwrap();
242 assert!(s.rvol[19].is_none());
243 }
244
245 #[test]
246 fn spike_matches_doc_formula() {
247 let mut vol = vec![1_000.0; 20];
248 vol.push(2_000.0);
249 let s = rvol(&vol, RvolParams::days_20()).unwrap();
250 assert!((s.rvol[20].unwrap() - 2000.0 / 1050.0).abs() < 1e-12);
252 }
253
254 #[test]
255 fn negative_volume_err() {
256 assert!(rvol(&[1.0, -1.0], RvolParams::new(2)).is_err());
257 }
258}