use crate::core::utils::RoundToNthDigit;
use chrono::{Datelike, NaiveDate};
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct PeriodWinRates {
pub week: f64,
pub month: f64,
pub quarter: f64,
pub year: f64,
}
impl Default for PeriodWinRates {
fn default() -> Self {
PeriodWinRates {
week: 0.0,
month: 0.0,
quarter: 0.0,
year: 0.0,
}
}
}
#[inline]
fn date_key_to_naive_date(dk: i32) -> NaiveDate {
let y = dk / 10000;
let m = (dk / 100) % 100;
let d = dk % 100;
NaiveDate::from_ymd_opt(y, m as u32, d as u32)
.unwrap_or_else(|| NaiveDate::from_ymd_opt(1970, 1, 1).unwrap())
}
#[inline]
fn week_key(nd: NaiveDate) -> (i32, u32) {
let iw = nd.iso_week();
(iw.year(), iw.week())
}
#[inline]
fn month_key(nd: NaiveDate) -> (i32, u32) {
(nd.year(), nd.month())
}
#[inline]
fn quarter_key(nd: NaiveDate) -> (i32, u32) {
let q = (nd.month() - 1) / 3 + 1;
(nd.year(), q)
}
#[inline]
fn year_key(nd: NaiveDate) -> i32 {
nd.year()
}
fn period_win_count<K, F>(date_keys: &[i32], returns: &[f64], key_fn: F) -> (usize, usize)
where
K: PartialEq,
F: Fn(NaiveDate) -> K,
{
if date_keys.is_empty() {
return (0, 0);
}
let mut wins = 0usize;
let mut total = 0usize;
let mut current_key = key_fn(date_key_to_naive_date(date_keys[0]));
let mut period_sum = returns[0];
for i in 1..date_keys.len() {
let nd = date_key_to_naive_date(date_keys[i]);
let key = key_fn(nd);
if key != current_key {
total += 1;
if period_sum > 0.0 {
wins += 1;
}
current_key = key;
period_sum = returns[i];
} else {
period_sum += returns[i];
}
}
total += 1;
if period_sum > 0.0 {
wins += 1;
}
(wins, total)
}
fn year_win_rate(date_keys: &[i32], returns: &[f64], yearly_days: i64) -> f64 {
if date_keys.is_empty() {
return 0.0;
}
let min_days = yearly_days / 2;
let mut wins = 0usize;
let mut total = 0usize;
let mut current_year = year_key(date_key_to_naive_date(date_keys[0]));
let mut period_sum = returns[0];
let mut period_count = 1usize;
let flush = |wins: &mut usize, total: &mut usize, sum: f64, count: usize| {
if count as i64 >= min_days {
*total += 1;
if sum > 0.0 {
*wins += 1;
}
}
};
for i in 1..date_keys.len() {
let nd = date_key_to_naive_date(date_keys[i]);
let year = year_key(nd);
if year != current_year {
flush(&mut wins, &mut total, period_sum, period_count);
current_year = year;
period_sum = returns[i];
period_count = 1;
} else {
period_sum += returns[i];
period_count += 1;
}
}
flush(&mut wins, &mut total, period_sum, period_count);
if total == 0 {
return 0.0;
}
(wins as f64 / total as f64).round_to_4_digit()
}
pub fn period_win_rates(date_keys: &[i32], returns: &[f64], yearly_days: i64) -> PeriodWinRates {
if date_keys.is_empty() {
return PeriodWinRates::default();
}
let rate = |wins: usize, total: usize| -> f64 {
if total == 0 {
0.0
} else {
(wins as f64 / total as f64).round_to_4_digit()
}
};
let (ww, wt) = period_win_count(date_keys, returns, week_key);
let (mw, mt) = period_win_count(date_keys, returns, month_key);
let (qw, qt) = period_win_count(date_keys, returns, quarter_key);
PeriodWinRates {
week: rate(ww, wt),
month: rate(mw, mt),
quarter: rate(qw, qt),
year: year_win_rate(date_keys, returns, yearly_days),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_returns_default() {
let result = period_win_rates(&[], &[], 252);
assert_eq!(result, PeriodWinRates::default());
}
#[test]
fn single_positive_day() {
let result = period_win_rates(&[20240101], &[0.01], 252);
assert_eq!(result.week, 1.0);
assert_eq!(result.month, 1.0);
assert_eq!(result.quarter, 1.0);
assert_eq!(result.year, 0.0); }
#[test]
fn single_negative_day() {
let result = period_win_rates(&[20240101], &[-0.01], 252);
assert_eq!(result.week, 0.0);
assert_eq!(result.month, 0.0);
assert_eq!(result.quarter, 0.0);
assert_eq!(result.year, 0.0);
}
#[test]
fn two_weeks_opposite_returns() {
let date_keys = [20240101, 20240102, 20240103, 20240108, 20240109, 20240110];
let returns = [0.01, 0.01, 0.01, -0.01, -0.01, -0.01];
let result = period_win_rates(&date_keys, &returns, 252);
assert_eq!(result.week, 0.5); }
#[test]
fn year_filter_excludes_short_year() {
let mut date_keys = vec![20231229i32]; let mut returns = vec![-0.01f64];
for i in 1..=200i32 {
let month = ((i - 1) / 28) + 1;
let day = ((i - 1) % 28) + 1;
if month <= 12 {
let dk = 20240000 + month * 100 + day;
date_keys.push(dk);
returns.push(0.01);
}
}
let result = period_win_rates(&date_keys, &returns, 252);
assert_eq!(result.year, 1.0);
}
#[test]
fn year_filter_includes_sufficient_year() {
let yearly_days = 10i64;
let min_days = (yearly_days / 2) as usize;
let date_keys: Vec<i32> = (1..=min_days as i32)
.map(|d| 20240100 + d) .collect();
let returns: Vec<f64> = vec![0.01; min_days];
let result = period_win_rates(&date_keys, &returns, yearly_days);
assert_eq!(result.year, 1.0); }
#[test]
fn year_filter_excludes_exactly_below_threshold() {
let yearly_days = 10i64;
let below_min = (yearly_days / 2 - 1) as usize;
let date_keys: Vec<i32> = (1..=below_min as i32).map(|d| 20240100 + d).collect();
let returns: Vec<f64> = vec![0.01; below_min];
let result = period_win_rates(&date_keys, &returns, yearly_days);
assert_eq!(result.year, 0.0); }
#[test]
fn quarter_mixed() {
let date_keys = [
20240102, 20240103, 20240401, 20240402, 20240701, 20240702, ];
let returns = [0.01, 0.01, -0.01, -0.01, 0.01, 0.01];
let result = period_win_rates(&date_keys, &returns, 252);
assert_eq!(result.quarter, 0.6667);
}
#[test]
fn month_two_months_one_win() {
let date_keys = [20240102, 20240103, 20240201, 20240202];
let returns = [0.01, 0.01, -0.01, -0.01];
let result = period_win_rates(&date_keys, &returns, 252);
assert_eq!(result.month, 0.5);
}
#[test]
fn period_zero_sum_is_loss() {
let date_keys = [20240101, 20240102];
let returns = [0.01, -0.01];
let result = period_win_rates(&date_keys, &returns, 252);
assert_eq!(result.week, 0.0);
assert_eq!(result.month, 0.0);
}
}