Add rqalpha-style scheduler primitives
This commit is contained in:
201
crates/fidc-core/src/scheduler.rs
Normal file
201
crates/fidc-core/src/scheduler.rs
Normal file
@@ -0,0 +1,201 @@
|
||||
use chrono::{Datelike, NaiveDate};
|
||||
|
||||
use crate::calendar::TradingCalendar;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum ScheduleStage {
|
||||
OpenAuction,
|
||||
OnDay,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ScheduleFrequency {
|
||||
Daily,
|
||||
Weekly {
|
||||
weekday: Option<u32>,
|
||||
tradingday: Option<i32>,
|
||||
},
|
||||
Monthly {
|
||||
tradingday: i32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ScheduleRule {
|
||||
pub name: String,
|
||||
pub stage: ScheduleStage,
|
||||
pub frequency: ScheduleFrequency,
|
||||
}
|
||||
|
||||
impl ScheduleRule {
|
||||
pub fn daily(name: impl Into<String>, stage: ScheduleStage) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
stage,
|
||||
frequency: ScheduleFrequency::Daily,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn weekly_by_weekday(name: impl Into<String>, weekday: u32, stage: ScheduleStage) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
stage,
|
||||
frequency: ScheduleFrequency::Weekly {
|
||||
weekday: Some(weekday),
|
||||
tradingday: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn weekly_by_tradingday(
|
||||
name: impl Into<String>,
|
||||
tradingday: i32,
|
||||
stage: ScheduleStage,
|
||||
) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
stage,
|
||||
frequency: ScheduleFrequency::Weekly {
|
||||
weekday: None,
|
||||
tradingday: Some(tradingday),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn monthly(name: impl Into<String>, tradingday: i32, stage: ScheduleStage) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
stage,
|
||||
frequency: ScheduleFrequency::Monthly { tradingday },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Scheduler<'a> {
|
||||
calendar: &'a TradingCalendar,
|
||||
}
|
||||
|
||||
impl<'a> Scheduler<'a> {
|
||||
pub fn new(calendar: &'a TradingCalendar) -> Self {
|
||||
Self { calendar }
|
||||
}
|
||||
|
||||
pub fn triggered_rules<'r>(
|
||||
&self,
|
||||
date: NaiveDate,
|
||||
stage: ScheduleStage,
|
||||
rules: &'r [ScheduleRule],
|
||||
) -> Vec<&'r ScheduleRule> {
|
||||
rules
|
||||
.iter()
|
||||
.filter(|rule| rule.stage == stage && self.matches(date, rule))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn matches(&self, date: NaiveDate, rule: &ScheduleRule) -> bool {
|
||||
match &rule.frequency {
|
||||
ScheduleFrequency::Daily => true,
|
||||
ScheduleFrequency::Weekly {
|
||||
weekday,
|
||||
tradingday,
|
||||
} => {
|
||||
if let Some(weekday) = weekday {
|
||||
return date.weekday().number_from_monday() == *weekday;
|
||||
}
|
||||
tradingday
|
||||
.and_then(|nth| nth_date_in_period(&self.week_dates(date), nth))
|
||||
.is_some_and(|matched| matched == date)
|
||||
}
|
||||
ScheduleFrequency::Monthly { tradingday } => {
|
||||
nth_date_in_period(&self.month_dates(date), *tradingday)
|
||||
.is_some_and(|matched| matched == date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn week_dates(&self, date: NaiveDate) -> Vec<NaiveDate> {
|
||||
let iso = date.iso_week();
|
||||
self.calendar
|
||||
.iter()
|
||||
.filter(|candidate| candidate.iso_week() == iso)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn month_dates(&self, date: NaiveDate) -> Vec<NaiveDate> {
|
||||
self.calendar
|
||||
.iter()
|
||||
.filter(|candidate| {
|
||||
candidate.year() == date.year() && candidate.month() == date.month()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn nth_date_in_period(period: &[NaiveDate], nth: i32) -> Option<NaiveDate> {
|
||||
if nth == 0 || period.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if nth > 0 {
|
||||
period.get((nth - 1) as usize).copied()
|
||||
} else {
|
||||
let idx = period.len().checked_sub((-nth) as usize)?;
|
||||
period.get(idx).copied()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{ScheduleRule, ScheduleStage, Scheduler};
|
||||
use crate::calendar::TradingCalendar;
|
||||
|
||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||
NaiveDate::from_ymd_opt(year, month, day).expect("valid date")
|
||||
}
|
||||
|
||||
fn sample_calendar() -> TradingCalendar {
|
||||
TradingCalendar::new(vec![
|
||||
d(2025, 1, 30),
|
||||
d(2025, 1, 31),
|
||||
d(2025, 2, 3),
|
||||
d(2025, 2, 4),
|
||||
d(2025, 2, 7),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scheduler_matches_daily_weekly_and_monthly_rules() {
|
||||
let calendar = sample_calendar();
|
||||
let scheduler = Scheduler::new(&calendar);
|
||||
let rules = vec![
|
||||
ScheduleRule::daily("daily", ScheduleStage::OnDay),
|
||||
ScheduleRule::weekly_by_weekday("friday", 5, ScheduleStage::OnDay),
|
||||
ScheduleRule::weekly_by_tradingday(
|
||||
"last_trading_day_of_week",
|
||||
-1,
|
||||
ScheduleStage::OnDay,
|
||||
),
|
||||
ScheduleRule::monthly("first_trading_day_of_month", 1, ScheduleStage::OnDay),
|
||||
];
|
||||
|
||||
let jan_31 = scheduler
|
||||
.triggered_rules(d(2025, 1, 31), ScheduleStage::OnDay, &rules)
|
||||
.into_iter()
|
||||
.map(|rule| rule.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(jan_31.contains(&"daily"));
|
||||
assert!(jan_31.contains(&"friday"));
|
||||
assert!(jan_31.contains(&"last_trading_day_of_week"));
|
||||
assert!(!jan_31.contains(&"first_trading_day_of_month"));
|
||||
|
||||
let feb_3 = scheduler
|
||||
.triggered_rules(d(2025, 2, 3), ScheduleStage::OnDay, &rules)
|
||||
.into_iter()
|
||||
.map(|rule| rule.name.as_str())
|
||||
.collect::<Vec<_>>();
|
||||
assert!(feb_3.contains(&"daily"));
|
||||
assert!(feb_3.contains(&"first_trading_day_of_month"));
|
||||
assert!(!feb_3.contains(&"friday"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user