use crate::error::{ChapatyResult, EnvError};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use strum::{Display, EnumString};
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct FilterConfig {
pub economic_news_policy: Option<EconomicCalendarPolicy>,
pub allowed_years: Option<BTreeSet<u16>>,
pub allowed_trading_hours: Option<BTreeMap<Weekday, Vec<TradingWindow>>>,
}
impl FilterConfig {
pub fn is_unrestricted(&self) -> bool {
self.economic_news_policy.is_none()
&& self.allowed_years.is_none()
&& self.allowed_trading_hours.is_none()
}
}
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize, Display, EnumString,
)]
#[strum(serialize_all = "snake_case")]
pub enum EconomicCalendarPolicy {
#[default]
Unrestricted,
OnlyWithEvents,
ExcludeEvents,
}
impl EconomicCalendarPolicy {
pub fn is_unrestricted(&self) -> bool {
matches!(self, Self::Unrestricted)
}
pub fn is_only_with_events(&self) -> bool {
matches!(self, Self::OnlyWithEvents)
}
pub fn is_exclude_events(&self) -> bool {
matches!(self, Self::ExcludeEvents)
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct TradingWindow {
start: u8,
end: u8,
}
impl TradingWindow {
pub fn new(start: u8, end: u8) -> ChapatyResult<Self> {
if start > 23 {
return Err(EnvError::InvalidTradingWindow {
start,
end,
msg: "start hour must be in the range 0-23".to_string(),
}
.into());
}
if end == 0 || end > 24 {
return Err(EnvError::InvalidTradingWindow {
start,
end,
msg: "end hour must be in the range 1-24".to_string(),
}
.into());
}
if start >= end {
return Err(EnvError::InvalidTradingWindow {
start,
end,
msg:
"start hour must be strictly less than end hour (wrapping not supported in MVP)"
.to_string(),
}
.into());
}
Ok(Self { start, end })
}
pub fn full_day() -> Self {
Self { start: 0, end: 24 }
}
pub fn start(&self) -> u8 {
self.start
}
pub fn end(&self) -> u8 {
self.end
}
}
#[derive(
Debug,
Clone,
Copy,
PartialEq,
Eq,
Hash,
Serialize,
Deserialize,
Display,
EnumString,
PartialOrd,
Ord,
)]
#[strum(serialize_all = "snake_case")]
pub enum Weekday {
Monday,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday,
}
impl From<chrono::Weekday> for Weekday {
fn from(weekday: chrono::Weekday) -> Self {
match weekday {
chrono::Weekday::Mon => Weekday::Monday,
chrono::Weekday::Tue => Weekday::Tuesday,
chrono::Weekday::Wed => Weekday::Wednesday,
chrono::Weekday::Thu => Weekday::Thursday,
chrono::Weekday::Fri => Weekday::Friday,
chrono::Weekday::Sat => Weekday::Saturday,
chrono::Weekday::Sun => Weekday::Sunday,
}
}
}
impl From<Weekday> for chrono::Weekday {
fn from(weekday: Weekday) -> Self {
match weekday {
Weekday::Monday => chrono::Weekday::Mon,
Weekday::Tuesday => chrono::Weekday::Tue,
Weekday::Wednesday => chrono::Weekday::Wed,
Weekday::Thursday => chrono::Weekday::Thu,
Weekday::Friday => chrono::Weekday::Fri,
Weekday::Saturday => chrono::Weekday::Sat,
Weekday::Sunday => chrono::Weekday::Sun,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_config_unrestricted_logic() {
let mut config = FilterConfig::default();
assert!(
config.is_unrestricted(),
"Default config should be unrestricted"
);
config.economic_news_policy = Some(EconomicCalendarPolicy::OnlyWithEvents);
assert!(
!config.is_unrestricted(),
"Config with policy should not be unrestricted"
);
let config = FilterConfig {
allowed_years: Some(BTreeSet::from([2023])),
..Default::default()
};
assert!(
!config.is_unrestricted(),
"Config with years should not be unrestricted"
);
let config = FilterConfig {
allowed_trading_hours: Some(BTreeMap::from([(Weekday::Monday, vec![])])),
..Default::default()
};
assert!(
!config.is_unrestricted(),
"Config with hours should not be unrestricted"
);
}
#[test]
fn test_economic_default() {
assert_eq!(
EconomicCalendarPolicy::default(),
EconomicCalendarPolicy::Unrestricted
);
}
#[test]
fn test_economic_policy_helpers() {
let policy = EconomicCalendarPolicy::Unrestricted;
assert!(policy.is_unrestricted());
assert!(!policy.is_only_with_events());
assert!(!policy.is_exclude_events());
let policy = EconomicCalendarPolicy::OnlyWithEvents;
assert!(!policy.is_unrestricted());
assert!(policy.is_only_with_events());
assert!(!policy.is_exclude_events());
let policy = EconomicCalendarPolicy::ExcludeEvents;
assert!(!policy.is_unrestricted());
assert!(!policy.is_only_with_events());
assert!(policy.is_exclude_events());
}
#[test]
fn test_trading_window_validation() {
assert!(TradingWindow::new(0, 1).is_ok());
assert!(TradingWindow::new(9, 17).is_ok());
assert!(TradingWindow::new(23, 24).is_ok());
assert!(TradingWindow::new(24, 25).is_err());
assert!(TradingWindow::new(9, 0).is_err());
assert!(TradingWindow::new(9, 25).is_err());
assert!(TradingWindow::new(10, 10).is_err());
assert!(TradingWindow::new(17, 9).is_err());
}
#[test]
fn test_trading_window_full_day() {
let window = TradingWindow::full_day();
assert_eq!(window.start, 0);
assert_eq!(window.end, 24);
}
#[test]
fn test_weekday_chrono_conversion() {
let days = vec![
(Weekday::Monday, chrono::Weekday::Mon),
(Weekday::Tuesday, chrono::Weekday::Tue),
(Weekday::Wednesday, chrono::Weekday::Wed),
(Weekday::Thursday, chrono::Weekday::Thu),
(Weekday::Friday, chrono::Weekday::Fri),
(Weekday::Saturday, chrono::Weekday::Sat),
(Weekday::Sunday, chrono::Weekday::Sun),
];
for (w, c) in days {
assert_eq!(chrono::Weekday::from(w), c);
assert_eq!(Weekday::from(c), w);
}
}
}