#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use crate::finance::Date;
use crate::model::Bar;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MonthlyReturn {
pub year: i32,
pub month: u32,
pub return_pct: f64,
pub complete: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct MonthStatistics {
pub month: u32,
pub samples: usize,
pub mean_return_pct: f64,
pub stdev_return_pct: Option<f64>,
pub positive_share: f64,
}
#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct SeasonalityReport {
pub monthly_returns: Vec<MonthlyReturn>,
pub months: Vec<MonthStatistics>,
pub as_of: i64,
pub completed_months: usize,
}
pub fn monthly_seasonality(bars: &[Bar]) -> SeasonalityReport {
let mut monthly_returns = Vec::new();
let as_of = bars.last().map(|bar| bar.timestamp).unwrap_or(0);
let mut month_closes: Vec<((i32, u32), f64)> = Vec::new();
for bar in bars {
if !bar.close.is_finite() || bar.close <= 0.0 {
continue;
}
let date = date_of(bar.timestamp);
let key = (date.year, date.month);
match month_closes.last_mut() {
Some((last_key, close)) if *last_key == key => *close = bar.close,
_ => month_closes.push((key, bar.close)),
}
}
for index in 1..month_closes.len() {
let ((year, month), close) = month_closes[index];
let previous_close = month_closes[index - 1].1;
if previous_close <= 0.0 {
continue;
}
monthly_returns.push(MonthlyReturn {
year,
month,
return_pct: 100.0 * (close / previous_close - 1.0),
complete: index + 1 < month_closes.len(),
});
}
let mut months = Vec::new();
for month in 1..=12u32 {
let returns: Vec<f64> = monthly_returns
.iter()
.filter(|entry| entry.month == month && entry.complete)
.map(|entry| entry.return_pct)
.collect();
if returns.is_empty() {
continue;
}
let samples = returns.len();
let mean = returns.iter().sum::<f64>() / samples as f64;
let stdev = (samples >= 2).then(|| {
let variance = returns
.iter()
.map(|value| {
let diff = value - mean;
diff * diff
})
.sum::<f64>()
/ (samples - 1) as f64;
variance.sqrt()
});
let positive = returns.iter().filter(|value| **value > 0.0).count();
months.push(MonthStatistics {
month,
samples,
mean_return_pct: mean,
stdev_return_pct: stdev,
positive_share: positive as f64 / samples as f64,
});
}
let completed_months = monthly_returns
.iter()
.filter(|entry| entry.complete)
.count();
SeasonalityReport {
monthly_returns,
months,
as_of,
completed_months,
}
}
fn date_of(timestamp: i64) -> Date {
let epoch = Date::new(1970, 1, 1).expect("the epoch is a valid date");
epoch.add_days(timestamp.div_euclid(86_400))
}