use crate::stocks::ta::common::{opt_cell, validate_positive_volume};
use crate::util::error::FinanceResult;
use crate::util::primitives::PeriodLength;
use crate::{columns_with_strings, print_table_locale_opt};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct RvolParams {
pub lookback: usize,
}
impl RvolParams {
pub const fn new(lookback: usize) -> Self {
Self { lookback }
}
pub const fn days_20() -> Self {
Self { lookback: 20 }
}
pub const fn days_50() -> Self {
Self { lookback: 50 }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct ValidatedRvol {
params: RvolParams,
}
impl ValidatedRvol {
pub fn new(params: RvolParams) -> FinanceResult<Self> {
PeriodLength::new(params.lookback)?;
Ok(Self { params })
}
pub fn params(self) -> RvolParams {
self.params
}
pub fn compute(self, volume: &[f64]) -> FinanceResult<RvolSeries> {
rvol_validated(volume, self)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct RvolSeries {
pub rvol: Vec<Option<f64>>,
pub params: RvolParams,
}
impl RvolSeries {
pub fn last(&self) -> Option<f64> {
self.rvol.iter().rev().find_map(|x| *x)
}
}
#[derive(Clone, Debug)]
pub struct RvolSolution {
series: RvolSeries,
volume: Vec<f64>,
formula: String,
symbolic_formula: String,
}
impl RvolSolution {
pub fn series(&self) -> &RvolSeries {
&self.series
}
pub fn formula(&self) -> &str {
&self.formula
}
pub fn symbolic_formula(&self) -> &str {
&self.symbolic_formula
}
pub fn print_table(&self) {
self.print_table_locale_opt(None, None);
}
pub fn print_table_locale(&self, locale: &num_format::Locale, precision: usize) {
self.print_table_locale_opt(Some(locale), Some(precision));
}
fn print_table_locale_opt(
&self,
locale: Option<&num_format::Locale>,
precision: Option<usize>,
) {
let columns = columns_with_strings(&[
("period", "i", true),
("volume", "f", true),
("rvol", "f", true),
]);
let data = self
.volume
.iter()
.enumerate()
.map(|(i, v)| vec![i.to_string(), v.to_string(), opt_cell(self.series.rvol[i])])
.collect();
print_table_locale_opt(&columns, data, locale, precision);
}
}
pub fn rvol(volume: &[f64], params: RvolParams) -> FinanceResult<RvolSeries> {
ValidatedRvol::new(params)?.compute(volume)
}
pub fn rvol_solution(volume: &[f64], params: RvolParams) -> FinanceResult<RvolSolution> {
let series = rvol(volume, params)?;
let formula = format!(
"rvol[i] = volume[i] / mean(volume[i-{}+1 ..= i])",
params.lookback
);
let symbolic = "rvol = volume / sma(volume, lookback)".to_string();
Ok(RvolSolution {
series,
volume: volume.to_vec(),
formula,
symbolic_formula: symbolic,
})
}
fn rvol_validated(volume: &[f64], v: ValidatedRvol) -> FinanceResult<RvolSeries> {
validate_positive_volume(volume)?;
let lb = v.params.lookback;
let n = volume.len();
let mut out = vec![None; n];
if n < lb {
return Ok(RvolSeries {
rvol: out,
params: v.params,
});
}
let mut sum: f64 = volume[..lb].iter().sum();
let mean0 = sum / lb as f64;
if mean0 > 0.0 {
out[lb - 1] = Some(volume[lb - 1] / mean0);
}
for i in lb..n {
sum += volume[i] - volume[i - lb];
let mean = sum / lb as f64;
if mean > 0.0 {
out[i] = Some(volume[i] / mean);
}
}
Ok(RvolSeries {
rvol: out,
params: v.params,
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn constant_is_one() {
let v = vec![500.0; 30];
let s = rvol(&v, RvolParams::days_20()).unwrap();
assert!((s.rvol[19].unwrap() - 1.0).abs() < 1e-12);
}
#[test]
fn lookback_one_is_unity_when_positive() {
let v = [10.0, 20.0, 5.0];
let s = rvol(&v, RvolParams::new(1)).unwrap();
assert!((s.rvol[0].unwrap() - 1.0).abs() < 1e-12);
assert!((s.rvol[1].unwrap() - 1.0).abs() < 1e-12);
}
#[test]
fn zero_mean_window_is_none() {
let v = vec![0.0; 25];
let s = rvol(&v, RvolParams::days_20()).unwrap();
assert!(s.rvol[19].is_none());
}
#[test]
fn spike_matches_doc_formula() {
let mut vol = vec![1_000.0; 20];
vol.push(2_000.0);
let s = rvol(&vol, RvolParams::days_20()).unwrap();
assert!((s.rvol[20].unwrap() - 2000.0 / 1050.0).abs() < 1e-12);
}
#[test]
fn negative_volume_err() {
assert!(rvol(&[1.0, -1.0], RvolParams::new(2)).is_err());
}
}