diff --git a/README.md b/README.md index 9ea6f06..e493010 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ encouraged. - cargo test - cargo bench - cargo run --example file_json -- + ## todo - handle div by zero scenarios - allow other numeric types rather than just f64 diff --git a/src/lib.rs b/src/lib.rs index b37d49b..ce40cfd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod momentum; pub mod smooth; +pub mod statistic; pub mod trend; pub mod volatility; pub mod volume; diff --git a/src/smooth.rs b/src/smooth.rs index 595e0c8..df8c2af 100644 --- a/src/smooth.rs +++ b/src/smooth.rs @@ -9,6 +9,8 @@ use std::collections::VecDeque; use std::f64::consts::PI; use std::iter; +use crate::statistic::distribution::_std_dev; + /// Moving average types pub enum MaMode { SMA, @@ -278,13 +280,6 @@ pub fn hull(data: &[f64], window: usize) -> impl Iterator + '_ { .into_iter() } -pub(crate) fn std_dev(data: &[f64], window: usize) -> impl Iterator + '_ { - data.windows(window).map(move |w| { - let mean = w.iter().sum::() / window as f64; - (w.iter().map(|x| (x - mean).powi(2)).sum::() / window as f64).sqrt() - }) -} - /// Volatility Index Dynamic Average (VIDYA) /// /// A type of moving average that uses a combination of short-term and long-term @@ -301,7 +296,7 @@ pub(crate) fn std_dev(data: &[f64], window: usize) -> impl Iterator /// ``` pub fn vidya(data: &[f64], window: usize) -> impl Iterator + '_ { let alpha = 2.0 / (window + 1) as f64; - let std5 = std_dev(data, 5).collect::>(); + let std5 = _std_dev(data, 5).collect::>(); let std20 = sma(&std5, 20).collect::>(); let offset = (5 - 1) + (20 - 1); iter::repeat(f64::NAN).take(offset).chain( diff --git a/src/statistic/distribution.rs b/src/statistic/distribution.rs new file mode 100644 index 0000000..dbe2731 --- /dev/null +++ b/src/statistic/distribution.rs @@ -0,0 +1,206 @@ +//! Distribution Functions +//! +//! Provides functions which describe the distribution of a dataset. This can relate to the +//! shape, centre, or dispersion of the +//! distribution[[1]](https://en.wikipedia.org/wiki/Probability_distribution) +use std::cmp::Ordering; +use std::iter; + +pub(crate) fn _std_dev(data: &[f64], window: usize) -> impl Iterator + '_ { + data.windows(window).map(move |w| { + let mean = w.iter().sum::() / window as f64; + (w.iter().fold(0.0, |acc, x| acc + (x - mean).powi(2)) / window as f64).sqrt() + }) +} + +/// Standard Deviation +/// +/// A measure of the amount of variation of a random variable expected about its mean. +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Standard_deviation) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::distribution; +/// +/// distribution::std_dev(&vec![1.0,2.0,3.0,4.0,5.0], 3).collect::>(); +/// ``` +pub fn std_dev(data: &[f64], window: usize) -> impl Iterator + '_ { + iter::repeat(f64::NAN) + .take(window - 1) + .chain(_std_dev(data, window)) +} + +/// Kurtosis +/// +/// A measure of the "tailedness" of the probability distribution of a real-valued random +/// variable. Computes the standard unbiased estimator. +/// +/// ```math +/// \frac{(n+1)\,n\,(n-1)}{(n-2)\,(n-3)} \; \frac{\sum_{i=1}^n (x_i - \bar{x})^4}{\left(\sum_{i=1}^n (x_i - \bar{x})^2\right)^2} - 3\,\frac{(n-1)^2}{(n-2)\,(n-3)} \\[6pt] +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Kurtosis) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::distribution; +/// +/// distribution::kurtosis(&vec![1.0,2.0,3.0,4.0,5.0], 3).collect::>(); +/// ``` +pub fn kurtosis(data: &[f64], window: usize) -> impl Iterator + '_ { + let adj1 = ((window + 1) * window * (window - 1)) as f64 / ((window - 2) * (window - 3)) as f64; + let adj2 = 3.0 * (window - 1).pow(2) as f64 / ((window - 2) * (window - 3)) as f64; + iter::repeat(f64::NAN) + .take(window - 1) + .chain(data.windows(window).map(move |w| { + let mean = w.iter().sum::() / window as f64; + let k4 = w.iter().fold(0.0, |acc, x| acc + (x - mean).powi(4)); + let k2 = w.iter().fold(0.0, |acc, x| acc + (x - mean).powi(2)); + adj1 * k4 / k2.powi(2) - adj2 + })) +} + +/// Skew +/// +/// The degree of asymmetry observed in a probability distribution. When data points on a +/// bell curve are not distributed symmetrically to the left and right sides of the median, +/// the bell curve is skewed. Distributions can be positive and right-skewed, or negative +/// and left-skewed. +/// +/// Computes unbiased adjusted Fisher–Pearson standardized moment coefficient G1 +/// +/// ```math +/// g_1 = \frac{m_3}{m_2^{3/2}} +/// = \frac{\tfrac{1}{n} \sum_{i=1}^n (x_i-\overline{x})^3}{\left[\tfrac{1}{n} \sum_{i=1}^n (x_i-\overline{x})^2 \right]^{3/2}}, +/// +/// G_1 = \frac{k_3}{k_2^{3/2}} = \frac{n^2}{(n-1)(n-2)}\; b_1 = \frac{\sqrt{n(n-1)}}{n-2}\; g_1 +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Skewness) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::distribution; +/// +/// distribution::skew(&vec![1.0,2.0,3.0,4.0,5.0], 3).collect::>(); +/// ``` +pub fn skew(data: &[f64], window: usize) -> impl Iterator + '_ { + iter::repeat(f64::NAN) + .take(window - 1) + .chain(data.windows(window).map(move |w| { + let mean = w.iter().sum::() / window as f64; + let m3 = w.iter().fold(0.0, |acc, x| acc + (x - mean).powi(3)) / window as f64; + let m2 = (w.iter().fold(0.0, |acc, x| acc + (x - mean).powi(2)) / window as f64) + .powf(3.0 / 2.0); + ((window * (window - 1)) as f64).sqrt() / (window - 2) as f64 * m3 / m2 + })) +} + +fn quickselect(data: &[f64], k: usize) -> Option { + // https://rust-lang-nursery.github.io/rust-cookbook/science/mathematics/statistics.html + // TODO: implement median of medians to get pivot + let part = match data.len() { + 0 => None, + _ => { + let (pivot_slice, tail) = data.split_at(1); + let pivot = pivot_slice[0]; + let (left, right) = tail.iter().fold((vec![], vec![]), |mut splits, next| { + { + let (ref mut left, ref mut right) = &mut splits; + if next < &pivot { + left.push(*next); + } else { + right.push(*next); + } + } + splits + }); + + Some((left, pivot, right)) + } + }; + + match part { + None => None, + Some((left, pivot, right)) => { + let pivot_idx = left.len(); + + match pivot_idx.cmp(&k) { + Ordering::Equal => Some(pivot), + Ordering::Greater => quickselect(&left, k), + Ordering::Less => quickselect(&right, k - (pivot_idx + 1)), + } + } + } +} + +/// Median +/// +/// The value separating the higher half from the lower half of a data sample, a population, or a +/// probability distribution within a window. +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::distribution; +/// +/// distribution::median(&vec![1.0,2.0,3.0,4.0,5.0], 3).collect::>(); +/// ``` +pub fn median(data: &[f64], window: usize) -> impl Iterator + '_ { + iter::repeat(f64::NAN) + .take(window - 1) + .chain(data.windows(window).map(move |w| match window { + even if even % 2 == 0 => { + let fst_med = quickselect(w, (even / 2) - 1); + let snd_med = quickselect(w, even / 2); + + match (fst_med, snd_med) { + (Some(fst), Some(snd)) => (fst + snd) * 0.5, + _ => f64::NAN, + } + } + odd => quickselect(w, odd / 2).unwrap(), + })) +} + +/// Quantile +/// +/// Partition a finite set of values into q subsets of (nearly) equal sizes. 50th percentile is +/// equivalent to median. +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::distribution; +/// +/// distribution::quantile(&vec![1.0,2.0,3.0,4.0,5.0], 3, 90.0).collect::>(); +/// ``` +pub fn quantile(data: &[f64], window: usize, q: f64) -> impl Iterator + '_ { + iter::repeat(f64::NAN) + .take(window - 1) + .chain(data.windows(window).map(move |w| { + let pos = (window - 1) as f64 * (q.clamp(0.0, 100.0) / 100.0); + match pos { + exact if exact.fract() == 0.0 => quickselect(w, exact as usize).unwrap(), + _ => { + let lower = quickselect(w, pos.floor() as usize); + let upper = quickselect(w, pos.ceil() as usize); + + match (lower, upper) { + (Some(l), Some(u)) => l * (pos.ceil() - pos) + u * (pos - pos.floor()), + _ => f64::NAN, + } + } + } + })) +} diff --git a/src/statistic/mod.rs b/src/statistic/mod.rs new file mode 100644 index 0000000..e53c83a --- /dev/null +++ b/src/statistic/mod.rs @@ -0,0 +1,2 @@ +pub mod distribution; +pub mod regression; diff --git a/src/statistic/regression.rs b/src/statistic/regression.rs new file mode 100644 index 0000000..760b15a --- /dev/null +++ b/src/statistic/regression.rs @@ -0,0 +1,213 @@ +//! Regression functions +//! +//! Provides functions for estimating the relationships between a dependent variable +//! and one or more independent variables. Potentially useful for forecasting and +//! determing causal relationships.[[1]](https://en.wikipedia.org/wiki/Regression_analysis) +use std::iter; + +/// Mean Squared Error +/// +/// Measures average squared difference between estimated and actual values. +/// +/// ```math +/// MSE = \frac{1}{T}\sum_{t=1}^{T} (X(t) - \hat{X}(t))^2 +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Mean_squared_error) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::regression; +/// +/// regression::mse(&vec![1.0,2.0,3.0,4.0,5.0], &vec![1.0,2.0,3.0,4.0,5.0]).collect::>(); +/// ``` +pub fn mse<'a>(data: &'a [f64], estimate: &'a [f64]) -> impl Iterator + 'a { + data.iter() + .enumerate() + .zip(estimate) + .scan(0.0, |state, ((cnt, observe), est)| { + *state += (observe - est).powi(2).max(0.0); + Some(*state / (cnt + 1) as f64) + }) +} + +/// Root Mean Squared Error +/// +/// Square root of MSE, but normalises it to same units as input. +/// +/// ```math +/// RMSE = \sqrt{\frac{1}{n} \sum_{i=1}^{n} (Y_i – \hat{Y}_i)^2} +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Root_mean_square_deviation) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::regression; +/// +/// regression::rmse(&vec![1.0,2.0,3.0,4.0,5.0], &vec![1.0,2.0,3.0,4.0,5.0]).collect::>(); +/// ``` +pub fn rmse<'a>(data: &'a [f64], estimate: &'a [f64]) -> impl Iterator + 'a { + data.iter() + .enumerate() + .zip(estimate) + .scan(0.0, |state, ((cnt, observe), est)| { + *state += (observe - est).powi(2).max(0.0); + Some((*state / (cnt + 1) as f64).sqrt()) + }) +} + +/// Mean Absolute Error +/// +/// Measures the average magnitude of the errors in a set of predictions. Less sensitive to +/// outliers compared to MSE and RMSE. +/// +/// ```math +/// MAE = \frac{1}{n} \sum_{i=1}^{n} |Y_i – \hat{Y}_i| +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Mean_absolute_error) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::regression; +/// +/// regression::mae(&vec![1.0,2.0,3.0,4.0,5.0], &vec![1.0,2.0,3.0,4.0,5.0]).collect::>(); +/// ``` +pub fn mae<'a>(data: &'a [f64], estimate: &'a [f64]) -> impl Iterator + 'a { + data.iter() + .enumerate() + .zip(estimate) + .scan(0.0, |state, ((cnt, observe), est)| { + *state += (observe - est).max(0.0); + Some(*state / (cnt + 1) as f64) + }) +} + +/// Mean Absolute Percentage Error +/// +/// Measures the error as a percentage of the actual value. +/// +/// ```math +/// MAPE = \frac{100%}{n} \sum_{i=1}^{n} \left|\frac{Y_i – \hat{Y}_i}{Y_i}\right| +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Mean_absolute_percentage_error) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::regression; +/// +/// regression::mape(&vec![1.0,2.0,3.0,4.0,5.0], &vec![1.0,2.0,3.0,4.0,5.0]).collect::>(); +/// ``` +pub fn mape<'a>(data: &'a [f64], estimate: &'a [f64]) -> impl Iterator + 'a { + data.iter() + .enumerate() + .zip(estimate) + .scan(0.0, |state, ((cnt, observe), est)| { + *state += ((observe - est) / observe).max(0.0); + Some(100.0 * *state / (cnt + 1) as f64) + }) +} + +/// Symmetric Mean Absolute Percentage Error +/// +/// Similar to MAPE but attempts to limit the overweighting of negative errors in MAPE. Computed so +/// range is bound to between 0 and 100. +/// +/// ```math +/// SMAPE = \frac{100}{n} \sum_{t=1}^n \frac{|F_t-A_t|}{|A_t|+|F_t|} +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Symmetric_mean_absolute_percentage_error) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::regression; +/// +/// regression::mape(&vec![1.0,2.0,3.0,4.0,5.0], &vec![1.0,2.0,3.0,4.0,5.0]).collect::>(); +/// ``` +pub fn smape<'a>(data: &'a [f64], estimate: &'a [f64]) -> impl Iterator + 'a { + data.iter() + .enumerate() + .zip(estimate) + .scan(0.0, |state, ((cnt, observe), est)| { + *state += ((observe - est).abs() / (observe.abs() + est.abs())).max(0.0); + Some(100.0 * *state / (cnt + 1) as f64) + }) +} + +/// Mean Directional Accuracy +/// +/// Measure of prediction accuracy of a forecasting method in statistics. It compares the forecast +/// direction (upward or downward) to the actual realized direction. +/// +/// ```math +/// MDA = \frac{1}{N}\sum_t \mathbf{1}_{\sgn(A_t - A_{t-1}) = \sgn(F_t - A_{t-1})} +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Mean_directional_accuracy) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::regression; +/// +/// regression::mda(&vec![1.0,2.0,3.0,4.0,5.0], &vec![1.0,2.0,3.0,4.0,5.0]).collect::>(); +/// ``` +pub fn mda<'a>(data: &'a [f64], estimate: &'a [f64]) -> impl Iterator + 'a { + data[1..].iter().enumerate().zip(&estimate[1..]).scan( + (0.0, data[0]), + |state, ((cnt, observe), est)| { + let dir = ((observe - state.0).signum() == (est - state.0).signum()) as u8 as f64; + *state = (state.0 + dir, *observe); + Some(state.0 / (cnt + 1) as f64) + }, + ) +} + +/// Mean Absolute Deviation +/// +/// A measure of variability that indicates the average distance between observations and +/// their mean. An alternative to Standard Deviation. +/// +/// ```math +/// \frac{1}{n} \sum_{i=1}^n |x_i-m(X)| +/// ``` +/// +/// ## Sources +/// +/// [[1]](https://en.wikipedia.org/wiki/Average_absolute_deviation) +/// +/// # Examples +/// +/// ``` +/// use traquer::statistic::regression; +/// +/// regression::mad(&vec![1.0,2.0,3.0,4.0,5.0], 3).collect::>(); +/// ``` +pub fn mad(data: &[f64], window: usize) -> impl Iterator + '_ { + iter::repeat(f64::NAN) + .take(window - 1) + .chain(data.windows(window).map(move |w| { + let mean = w.iter().sum::() / window as f64; + w.iter().fold(0.0, |acc, x| acc + (x - mean).abs()) / window as f64 + })) +} diff --git a/src/volatility.rs b/src/volatility.rs index 8b57925..48668b8 100644 --- a/src/volatility.rs +++ b/src/volatility.rs @@ -10,6 +10,7 @@ use std::iter; use itertools::izip; use crate::smooth; +use crate::statistic::distribution::{_std_dev as unpadded_std_dev, std_dev as padded_std_dev}; /// Mass Index /// @@ -254,9 +255,7 @@ pub fn std_dev( deviations: Option, ) -> impl Iterator + '_ { let devs = deviations.unwrap_or(2.0); - iter::repeat(f64::NAN) - .take(window - 1) - .chain(smooth::std_dev(data, window).map(move |x| x * devs)) + padded_std_dev(data, window).map(move |x| x * devs) } /// Bollinger Bands @@ -516,7 +515,7 @@ pub fn relative_vol( smoothing: usize, ) -> impl Iterator + '_ { let (gain, loss): (Vec, Vec) = izip!( - smooth::std_dev(close, window), + unpadded_std_dev(close, window), &close[window - 1..], &close[window - 2..close.len() - 1] ) diff --git a/tests/stat_dist_test.rs b/tests/stat_dist_test.rs new file mode 100644 index 0000000..e60ab18 --- /dev/null +++ b/tests/stat_dist_test.rs @@ -0,0 +1,230 @@ +use traquer::statistic::distribution::*; + +mod common; + +#[test] +fn test_kurtosis() { + let stats = common::test_data(); + let result = kurtosis(&stats.close, 16).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 1.3103030757850638, + 1.102747994753904, + 3.06719774124363, + 2.193365559939223, + -0.9756569944812274, + -0.5481498668335418, + -0.2243968276571402, + -0.9521890050850512, + -0.37669821534673487, + -0.2913451763689965, + -0.7833694623815677, + -0.5970746750197158, + -0.6755548198272634, + -0.6448793229503962, + -0.9854966855493053, + -1.1611158440747498, + -1.00010972265145, + -0.5654849882747519, + -0.9958264091354074, + ], + result[16 - 1..] + ); +} + +#[test] +fn test_skew() { + let stats = common::test_data(); + let result = skew(&stats.close, 16).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 1.3535582408038451, + 1.298528601661446, + 1.6254206574818177, + 1.162585198346388, + 0.19295159682513383, + 0.40411386820995304, + 0.6117924727238208, + 0.285232085638137, + 0.4564095498787098, + 0.45766697953883545, + 0.20396596725267968, + 0.11996720728103474, + 0.18545745211753306, + 0.27852019344431006, + 0.24824251094586902, + 0.15953878421297624, + 0.1407199604842263, + 0.242851303974619, + 0.2493021518683219, + ], + result[16 - 1..] + ); +} + +#[test] +fn test_std_dev() { + let stats = common::test_data(); + let result = std_dev(&stats.close, 16).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 6.625812863289285, + 6.8379387368587805, + 6.44773730777228, + 4.49258751006369, + 3.278515396116506, + 3.049319751315024, + 3.0080629244983026, + 2.444736991480583, + 2.244285265237126, + 2.2178709754584722, + 2.0662406383473115, + 1.9310349413348022, + 2.0064465134124934, + 2.3225764208542694, + 2.45508524578238, + 2.720542235887045, + 2.8841957743336115, + 3.2473568576978904, + 3.0590540332135254 + ], + result[15..] + ); +} + +#[test] +fn test_quantile_even() { + let stats = common::test_data(); + let result = quantile(&stats.close, 16, 90.0).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 58.77499961853027, + 58.77499961853027, + 54.10000038146973, + 49.885000228881836, + 48.44000053405762, + 47.275001525878906, + 47.275001525878906, + 46.57000160217285, + 45.96500015258789, + 45.790000915527344, + 45.790000915527344, + 45.21500015258789, + 45.65500068664551, + 46.079999923706055, + 46.459999084472656, + 47.170000076293945, + 47.98500061035156, + 48.83500099182129, + 49.34749984741211, + ], + result[16 - 1..] + ); + assert_eq!( + quantile(&stats.close, 16, 50.0).collect::>()[16 - 1..], + median(&stats.close, 16).collect::>()[16 - 1..] + ); +} + +#[test] +fn test_quantile_odd() { + let stats = common::test_data(); + let result = quantile(&stats.close, 15, 90.0).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 58.97999954223633, + 58.97999954223633, + 54.83000030517577, + 49.99800033569336, + 48.61600036621094, + 47.332001495361325, + 47.332001495361325, + 46.65400161743164, + 46.002000427246095, + 45.69000091552734, + 45.258000183105466, + 45.258000183105466, + 45.06999969482422, + 45.70000076293945, + 46.119999694824216, + 46.49599914550781, + 47.27600021362305, + 48.04200057983398, + 48.948001098632815, + 49.358000183105474, + ], + result[15 - 1..] + ); + assert_eq!( + quantile(&stats.close, 15, 50.0).collect::>()[15 - 1..], + median(&stats.close, 15).collect::>()[15 - 1..] + ); +} + +#[test] +fn test_median_even() { + let stats = common::test_data(); + let result = median(&stats.close, 16).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 46.07500076293945, + 46.060001373291016, + 45.875, + 45.38999938964844, + 44.76499938964844, + 43.39999961853027, + 42.545000076293945, + 42.545000076293945, + 42.36000061035156, + 42.36000061035156, + 42.36000061035156, + 42.36000061035156, + 42.36000061035156, + 42.635000228881836, + 42.635000228881836, + 42.98500061035156, + 43.795000076293945, + 44.93499946594238, + 45.65500068664551, + ], + result[16 - 1..] + ); +} + +#[test] +fn test_median_odd() { + let stats = common::test_data(); + let result = median(&stats.close, 15).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 46.150001525878906, + 46.150001525878906, + 45.970001220703125, + 45.779998779296875, + 45.0, + 44.529998779296875, + 42.27000045776367, + 42.27000045776367, + 42.27000045776367, + 42.27000045776367, + 42.27000045776367, + 42.27000045776367, + 42.27000045776367, + 42.45000076293945, + 42.45000076293945, + 42.81999969482422, + 43.150001525878906, + 44.439998626708984, + 45.43000030517578, + 45.880001068115234, + ], + result[15 - 1..] + ); +} diff --git a/tests/stat_regression_test.rs b/tests/stat_regression_test.rs new file mode 100644 index 0000000..93480c8 --- /dev/null +++ b/tests/stat_regression_test.rs @@ -0,0 +1,80 @@ +use traquer::statistic::regression::*; + +mod common; + +#[test] +fn test_mse() { + let stats = common::test_data(); + let result = mse(&stats.close, &stats.high).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 25.0, + 14.789798693847843, + 41.80790510457397, + 40.38594878463846, + 39.460837932740105, + 34.710045583599516, + 29.867181340247043, + 28.391596172716163, + 26.01725305209887, + 23.46881768005551, + 22.169915400281422, + 20.839098146821925, + 19.719941450262784, + 18.547974124469743, + 17.479982548520397, + 16.663108238694804, + 15.741748930536286, + 14.981608931136886, + 14.201950559754838, + 13.926978256834264, + 13.27716494489245, + 12.74354854569736, + 12.218715978471902, + 11.71110695762521, + 11.266317181275808, + 11.09299774745215, + 10.880636212498993, + 10.610342022379752, + 10.374113601077939, + 10.16976333630737, + 9.924286942142286, + 9.641181117938686, + 9.777435853090013, + 9.585688507460445, + ], + result + ); +} + +#[test] +fn test_mad() { + let stats = common::test_data(); + let result = mad(&stats.close, 16).collect::>(); + assert_eq!(stats.close.len(), result.len()); + assert_eq!( + vec![ + 5.039296746253967, + 5.229140520095825, + 4.5250003933906555, + 3.3781254291534424, + 2.8657814264297485, + 2.695000171661377, + 2.6033595204353333, + 2.09703129529953, + 1.829843819141388, + 1.8025001883506775, + 1.7024999856948853, + 1.5822653472423553, + 1.6441404223442078, + 1.947343647480011, + 2.1082810163497925, + 2.407577931880951, + 2.541874885559082, + 2.770625114440918, + 2.6209373474121094, + ], + result[16 - 1..] + ); +}