use crate::basic_indicators::single::{max, mean, min};
pub fn peaks(prices: &[f64], period: usize, closest_neighbor: usize) -> Vec<(f64, usize)> {
if period == 0 {
panic!("Period ({}) must be greater than 0", period)
};
let length = prices.len();
if period > length {
panic!(
"Period ({}) cannot be longer than length of prices ({})",
period, length
)
};
let mut peaks: Vec<(f64, usize)> = Vec::new();
let mut last_peak_idx: usize = 0;
let mut last_peak: f64 = 0.0;
for i in 0..=length - period {
let window = &prices[i..i + period];
let peak = max(window);
let local_idx = window.iter().rposition(|&x| x == peak).unwrap();
let idx = i + local_idx;
if last_peak_idx != 0 {
if idx <= last_peak_idx + closest_neighbor {
if peak < last_peak {
last_peak_idx = idx;
} else if peak > last_peak {
peaks.pop();
peaks.push((peak, idx));
last_peak_idx = idx;
last_peak = peak;
}
} else if !peaks.contains(&(peak, idx)) {
peaks.push((peak, idx));
last_peak_idx = idx;
last_peak = peak;
}
} else {
peaks.push((peak, idx));
last_peak_idx = idx;
last_peak = peak;
}
}
peaks
}
pub fn valleys(prices: &[f64], period: usize, closest_neighbor: usize) -> Vec<(f64, usize)> {
if period == 0 {
panic!("Period ({}) must be greater than 0", period)
};
let length = prices.len();
if period > length {
panic!(
"Period ({}) cannot be longer than length of prices ({})",
period, length
)
};
let mut valleys: Vec<(f64, usize)> = Vec::new();
let mut last_valley_idx: usize = 0;
let mut last_valley: f64 = 0.0;
for i in 0..=length - period {
let window = &prices[i..i + period];
let valley = min(window);
let local_idx = window.iter().rposition(|&x| x == valley).unwrap();
let idx = i + local_idx;
if last_valley_idx != 0 {
if idx <= last_valley_idx + closest_neighbor {
if valley > last_valley {
last_valley_idx = idx;
} else if valley < last_valley {
valleys.pop();
valleys.push((valley, idx));
last_valley_idx = idx;
last_valley = valley;
}
} else if !valleys.contains(&(valley, idx)) {
valleys.push((valley, idx));
last_valley_idx = idx;
last_valley = valley;
}
} else {
valleys.push((valley, idx));
last_valley_idx = idx;
last_valley = valley;
}
}
valleys
}
fn get_trend_line(p: &[(f64, usize)]) -> (f64, f64) {
let length = p.len() as f64;
let mean_x = p.iter().map(|&(_, x)| x as f64).sum::<f64>() / length;
let mean_y = p.iter().map(|&(y, _)| y).sum::<f64>() / length;
let (num, den) = p.iter().fold((0.0, 0.0), |(num, den), &(y, x)| {
let x = x as f64;
let dx = x - mean_x;
(num + dx * (y - mean_y), den + dx * dx)
});
let slope = num / den;
let intercept = mean_y - (slope * mean_x);
(slope, intercept)
}
#[inline]
pub fn peak_trend(prices: &[f64], period: usize) -> (f64, f64) {
let peaks = peaks(prices, period, 1);
get_trend_line(&peaks)
}
#[inline]
pub fn valley_trend(prices: &[f64], period: usize) -> (f64, f64) {
let valleys = valleys(prices, period, 1);
get_trend_line(&valleys)
}
#[inline]
pub fn overall_trend(prices: &[f64]) -> (f64, f64) {
let indexed_prices: Vec<(f64, usize)> =
prices.iter().enumerate().map(|(i, &y)| (y, i)).collect();
get_trend_line(&indexed_prices)
}
#[derive(Copy, Clone, Debug)]
pub struct TrendBreakConfig {
pub max_outliers: usize,
pub soft_adj_r_squared_minimum: f64,
pub hard_adj_r_squared_minimum: f64,
pub soft_rmse_multiplier: f64,
pub hard_rmse_multiplier: f64,
pub soft_durbin_watson_min: f64,
pub soft_durbin_watson_max: f64,
pub hard_durbin_watson_min: f64,
pub hard_durbin_watson_max: f64,
}
impl Default for TrendBreakConfig {
fn default() -> Self {
Self {
max_outliers: 1,
soft_adj_r_squared_minimum: 0.25,
hard_adj_r_squared_minimum: 0.05,
soft_rmse_multiplier: 1.3,
hard_rmse_multiplier: 2.0,
soft_durbin_watson_min: 1.0,
soft_durbin_watson_max: 3.0,
hard_durbin_watson_min: 0.7,
hard_durbin_watson_max: 3.3,
}
}
}
pub fn break_down_trends(
prices: &[f64],
trend_break_config: TrendBreakConfig,
) -> Vec<(usize, usize, f64, f64)> {
if prices.is_empty() {
panic!("Prices cannot be empty");
};
let mut outliers: Vec<usize> = Vec::new();
let mut trends: Vec<(usize, usize, f64, f64)> = Vec::new();
let mut current_slope = 0.0;
let mut current_intercept = 0.0;
let mut start_index: usize = 0;
let mut end_index: usize = 1;
let mut indexed_points: Vec<(f64, usize)> = Vec::new();
let mut previous_rmse = f64::MAX;
for (index, &price) in prices.iter().enumerate() {
indexed_points.push((price, index));
if index == 0 {
continue;
}
if index > end_index {
let current_trend = get_trend_line(&indexed_points);
let (adjusted_r_squared, rmse, durbin_watson) =
goodness_of_fit(&indexed_points, ¤t_trend);
let soft_break = (adjusted_r_squared < trend_break_config.soft_adj_r_squared_minimum)
&& (rmse > trend_break_config.soft_rmse_multiplier * previous_rmse)
&& (durbin_watson < trend_break_config.soft_durbin_watson_min
|| durbin_watson > trend_break_config.soft_durbin_watson_max);
let hard_break = adjusted_r_squared < trend_break_config.hard_adj_r_squared_minimum
|| rmse > trend_break_config.hard_rmse_multiplier * previous_rmse
|| (durbin_watson < trend_break_config.hard_durbin_watson_min
|| durbin_watson > trend_break_config.hard_durbin_watson_max);
if soft_break || hard_break {
if outliers.len() < trend_break_config.max_outliers {
outliers.push(index);
indexed_points.pop();
continue;
};
trends.push((start_index, end_index, current_slope, current_intercept));
start_index = end_index;
end_index = index;
indexed_points = (start_index..=index).map(|x| (prices[x], x)).collect();
let current_trend = get_trend_line(&indexed_points);
current_slope = current_trend.0;
current_intercept = current_trend.1;
if indexed_points.len() > 2 {
(_, previous_rmse, _) = goodness_of_fit(&indexed_points, ¤t_trend);
} else {
previous_rmse = f64::MAX;
};
outliers.clear();
} else {
previous_rmse = rmse;
current_slope = current_trend.0;
current_intercept = current_trend.1;
}
}
end_index = index;
}
trends.push((start_index, end_index, current_slope, current_intercept));
trends
}
fn goodness_of_fit(indexed_points: &[(f64, usize)], trend: &(f64, f64)) -> (f64, f64, f64) {
let n = indexed_points.len();
if n < 2 {
return (0.0, 0.0, 2.0); }
let trend_line: Vec<f64> = indexed_points
.iter()
.map(|&(_, x)| trend.1 + trend.0 * x as f64)
.collect();
let observed_prices: Vec<f64> = indexed_points.iter().map(|&(y, _)| y).collect();
let observed_mean = mean(&observed_prices);
let (sum_sq_residuals, total_squares) = (0..n).fold((0.0, 0.0), |(ssr, tss), i| {
let resid = observed_prices[i] - trend_line[i];
let total = observed_prices[i] - observed_mean;
(ssr + resid.powi(2), tss + total.powi(2))
});
let degrees_of_freedom = ((n as f64) - 2.0).max(1.0);
let r_squared = if total_squares > 1e-10 {
(1.0 - (sum_sq_residuals / total_squares)).max(0.0)
} else {
0.0
};
let adjusted_r_squared = if n > 2 {
1.0 - ((1.0 - r_squared) * ((n - 1) as f64) / degrees_of_freedom)
} else {
r_squared
};
let durbin_watson = if n > 1 {
let dw_num = (1..n).fold(0.0, |acc, i| {
let diff =
(observed_prices[i] - trend_line[i]) - (observed_prices[i - 1] - trend_line[i - 1]);
acc + diff.powi(2)
});
if sum_sq_residuals > 1e-10 {
dw_num / sum_sq_residuals
} else {
2.0
}
} else {
2.0
};
let rmse = (sum_sq_residuals / n as f64).sqrt();
(adjusted_r_squared, rmse, durbin_watson)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn peaks_single_peak() {
let highs = vec![101.26, 102.57, 102.32, 100.69];
assert_eq!(vec![(102.57, 1)], peaks(&highs, 4_usize, 1usize));
}
#[test]
fn peaks_multiple_peaks() {
let highs = vec![101.26, 102.57, 102.32, 100.69, 100.83, 101.73, 102.01];
assert_eq!(
vec![(102.57, 1), (102.01, 6)],
peaks(&highs, 4_usize, 1usize)
);
}
#[test]
fn peaks_multiple_peaks_same_period() {
let highs = vec![101.26, 102.57, 102.57, 100.69, 100.83, 101.73, 102.01];
assert_eq!(
vec![(102.57, 2), (102.01, 6)],
peaks(&highs, 4_usize, 1usize)
);
}
#[test]
#[should_panic]
fn peaks_panic() {
let highs = vec![101.26, 102.57, 102.57, 100.69, 100.83, 101.73, 102.01];
peaks(&highs, 40_usize, 1usize);
}
#[test]
fn valleys_single_valley() {
let lows = vec![100.08, 98.75, 100.14, 98.98, 99.07, 100.1, 99.96];
assert_eq!(vec![(98.75, 1)], valleys(&lows, 7_usize, 1usize));
}
#[test]
fn valleys_multiple_valleys() {
let lows = vec![100.08, 98.75, 100.14, 98.98, 99.07, 100.1, 99.96];
assert_eq!(
vec![(98.75, 1), (98.98, 3)],
valleys(&lows, 4_usize, 1usize)
);
}
#[test]
fn valleys_multiple_valleys_same_period() {
let lows = vec![98.75, 98.75, 100.14, 98.98, 99.07, 100.1, 99.96];
assert_eq!(
vec![(98.75, 1), (98.98, 3)],
valleys(&lows, 4_usize, 1usize)
);
}
#[test]
#[should_panic]
fn valleys_panic() {
let lows = vec![98.75, 98.75, 100.14, 98.98, 99.07, 100.1, 99.96];
valleys(&lows, 40_usize, 1usize);
}
#[test]
fn peaks_trend() {
let highs = vec![101.26, 102.57, 102.32, 100.69, 100.83, 101.73, 102.01];
assert_eq!(
(-0.11199999999999762, 102.68199999999999),
peak_trend(&highs, 4_usize)
);
}
#[test]
fn valleys_trend() {
let lows = vec![100.08, 98.75, 100.14, 98.98, 99.07, 100.1, 99.96];
assert_eq!((0.11500000000000199, 98.635), valley_trend(&lows, 4_usize));
}
#[test]
fn overall_trends() {
let prices = vec![100.2, 100.46, 100.53, 100.38, 100.19];
assert_eq!((-0.010000000000000852, 100.372), overall_trend(&prices));
}
#[test]
fn break_down_trends_std_dev() {
let prices = vec![100.2, 100.46, 100.53, 100.38, 100.19];
let trend_break_config = TrendBreakConfig {
max_outliers: 1,
soft_adj_r_squared_minimum: 0.5,
hard_adj_r_squared_minimum: 0.25,
soft_rmse_multiplier: 1.2,
hard_rmse_multiplier: 2.0,
soft_durbin_watson_min: 1.0,
soft_durbin_watson_max: 3.0,
hard_durbin_watson_min: 0.5,
hard_durbin_watson_max: 3.5,
};
let trend_break_down = break_down_trends(&prices, trend_break_config);
assert_eq!(
vec![
(0, 2, 0.16499999999999915, 100.23166666666665),
(2, 4, -0.1700000000000017, 100.87666666666668)
],
trend_break_down
);
}
}