finance_solution/stocks/ta/
rvol.rs1use crate::stocks::ta::common::opt_cell;
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 let mut st = crate::stocks::ta::state::RvolState::new(v.params)?;
192 let rvol = st.push_bars(volume)?;
193 Ok(RvolSeries {
194 rvol,
195 params: v.params,
196 })
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn constant_is_one() {
205 let v = vec![500.0; 30];
206 let s = rvol(&v, RvolParams::days_20()).unwrap();
207 assert!((s.rvol[19].unwrap() - 1.0).abs() < 1e-12);
208 }
209
210 #[test]
211 fn lookback_one_is_unity_when_positive() {
212 let v = [10.0, 20.0, 5.0];
213 let s = rvol(&v, RvolParams::new(1)).unwrap();
214 assert!((s.rvol[0].unwrap() - 1.0).abs() < 1e-12);
215 assert!((s.rvol[1].unwrap() - 1.0).abs() < 1e-12);
216 }
217
218 #[test]
219 fn zero_mean_window_is_none() {
220 let v = vec![0.0; 25];
221 let s = rvol(&v, RvolParams::days_20()).unwrap();
222 assert!(s.rvol[19].is_none());
223 }
224
225 #[test]
226 fn spike_matches_doc_formula() {
227 let mut vol = vec![1_000.0; 20];
228 vol.push(2_000.0);
229 let s = rvol(&vol, RvolParams::days_20()).unwrap();
230 assert!((s.rvol[20].unwrap() - 2000.0 / 1050.0).abs() < 1e-12);
232 }
233
234 #[test]
235 fn negative_volume_err() {
236 assert!(rvol(&[1.0, -1.0], RvolParams::new(2)).is_err());
237 }
238}