Add rqalpha-style scheduler time rules

This commit is contained in:
boris
2026-04-23 06:34:07 -07:00
parent fae09afb86
commit 5265f82fef
10 changed files with 557 additions and 233 deletions

View File

@@ -1,11 +1,57 @@
use chrono::{Datelike, NaiveDate};
use chrono::{Datelike, NaiveDate, NaiveTime, Timelike};
use crate::calendar::TradingCalendar;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScheduleStage {
BeforeTrading,
OpenAuction,
OnDay,
AfterTrading,
Settlement,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScheduleTimeRule {
BeforeTrading,
MinuteOfDay(u32),
}
impl ScheduleTimeRule {
pub fn before_trading() -> Self {
Self::BeforeTrading
}
pub fn market_open(hour: i32, minute: i32) -> Self {
let mut minutes_since_midnight = 9 * 60 + 31 + hour * 60 + minute;
if minutes_since_midnight > 11 * 60 + 30 {
minutes_since_midnight += 90;
}
Self::MinuteOfDay(minutes_since_midnight.max(0) as u32)
}
pub fn market_close(hour: i32, minute: i32) -> Self {
let mut minutes_since_midnight = 15 * 60 - hour * 60 - minute;
if minutes_since_midnight < 13 * 60 {
minutes_since_midnight -= 90;
}
Self::MinuteOfDay(minutes_since_midnight.max(0) as u32)
}
pub fn physical_time(hour: u32, minute: u32) -> Self {
Self::MinuteOfDay(hour.saturating_mul(60).saturating_add(minute))
}
pub fn from_time(time: NaiveTime) -> Self {
Self::physical_time(time.hour(), time.minute())
}
pub fn minute_of_day(&self) -> Option<u32> {
match self {
Self::BeforeTrading => None,
Self::MinuteOfDay(value) => Some(*value),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -25,6 +71,7 @@ pub struct ScheduleRule {
pub name: String,
pub stage: ScheduleStage,
pub frequency: ScheduleFrequency,
pub time_rule: Option<ScheduleTimeRule>,
}
impl ScheduleRule {
@@ -33,6 +80,7 @@ impl ScheduleRule {
name: name.into(),
stage,
frequency: ScheduleFrequency::Daily,
time_rule: None,
}
}
@@ -44,6 +92,7 @@ impl ScheduleRule {
weekday: Some(weekday),
tradingday: None,
},
time_rule: None,
}
}
@@ -59,6 +108,7 @@ impl ScheduleRule {
weekday: None,
tradingday: Some(tradingday),
},
time_rule: None,
}
}
@@ -67,8 +117,14 @@ impl ScheduleRule {
name: name.into(),
stage,
frequency: ScheduleFrequency::Monthly { tradingday },
time_rule: None,
}
}
pub fn with_time_rule(mut self, time_rule: ScheduleTimeRule) -> Self {
self.time_rule = Some(time_rule);
self
}
}
pub struct Scheduler<'a> {
@@ -85,10 +141,24 @@ impl<'a> Scheduler<'a> {
date: NaiveDate,
stage: ScheduleStage,
rules: &'r [ScheduleRule],
) -> Vec<&'r ScheduleRule> {
self.triggered_rules_at(date, stage, default_stage_time(stage), rules)
}
pub fn triggered_rules_at<'r>(
&self,
date: NaiveDate,
stage: ScheduleStage,
current_time: Option<NaiveTime>,
rules: &'r [ScheduleRule],
) -> Vec<&'r ScheduleRule> {
rules
.iter()
.filter(|rule| rule.stage == stage && self.matches(date, rule))
.filter(|rule| {
rule.stage == stage
&& self.matches(date, rule)
&& self.matches_time(stage, current_time, rule.time_rule.as_ref())
})
.collect()
}
@@ -129,6 +199,33 @@ impl<'a> Scheduler<'a> {
})
.collect()
}
fn matches_time(
&self,
stage: ScheduleStage,
current_time: Option<NaiveTime>,
time_rule: Option<&ScheduleTimeRule>,
) -> bool {
let Some(time_rule) = time_rule else {
return true;
};
match time_rule {
ScheduleTimeRule::BeforeTrading => stage == ScheduleStage::BeforeTrading,
ScheduleTimeRule::MinuteOfDay(expected) => current_time
.map(|value| value.hour() * 60 + value.minute())
.is_some_and(|current| current == *expected),
}
}
}
pub fn default_stage_time(stage: ScheduleStage) -> Option<NaiveTime> {
match stage {
ScheduleStage::BeforeTrading => Some(NaiveTime::from_hms_opt(9, 0, 0).expect("valid time")),
ScheduleStage::OpenAuction => Some(NaiveTime::from_hms_opt(9, 31, 0).expect("valid time")),
ScheduleStage::OnDay => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")),
ScheduleStage::AfterTrading => Some(NaiveTime::from_hms_opt(15, 0, 0).expect("valid time")),
ScheduleStage::Settlement => Some(NaiveTime::from_hms_opt(15, 1, 0).expect("valid time")),
}
}
fn nth_date_in_period(period: &[NaiveDate], nth: i32) -> Option<NaiveDate> {
@@ -145,9 +242,9 @@ fn nth_date_in_period(period: &[NaiveDate], nth: i32) -> Option<NaiveDate> {
#[cfg(test)]
mod tests {
use chrono::NaiveDate;
use chrono::{NaiveDate, NaiveTime};
use super::{ScheduleRule, ScheduleStage, Scheduler};
use super::{ScheduleRule, ScheduleStage, ScheduleTimeRule, Scheduler};
use crate::calendar::TradingCalendar;
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -198,4 +295,80 @@ mod tests {
assert!(feb_3.contains(&"first_trading_day_of_month"));
assert!(!feb_3.contains(&"friday"));
}
#[test]
fn scheduler_matches_before_trading_and_minute_level_rules() {
let calendar = sample_calendar();
let scheduler = Scheduler::new(&calendar);
let rules = vec![
ScheduleRule::daily("before_trading", ScheduleStage::BeforeTrading)
.with_time_rule(ScheduleTimeRule::before_trading()),
ScheduleRule::daily("market_open", ScheduleStage::OpenAuction)
.with_time_rule(ScheduleTimeRule::market_open(0, 0)),
ScheduleRule::daily("physical_time", ScheduleStage::OnDay)
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
ScheduleRule::daily("market_close", ScheduleStage::AfterTrading)
.with_time_rule(ScheduleTimeRule::market_close(0, 0)),
];
let before = scheduler
.triggered_rules_at(
d(2025, 2, 3),
ScheduleStage::BeforeTrading,
Some(NaiveTime::from_hms_opt(9, 0, 0).unwrap()),
&rules,
)
.into_iter()
.map(|rule| rule.name.as_str())
.collect::<Vec<_>>();
assert_eq!(before, vec!["before_trading"]);
let market_open = scheduler
.triggered_rules_at(
d(2025, 2, 3),
ScheduleStage::OpenAuction,
Some(NaiveTime::from_hms_opt(9, 31, 0).unwrap()),
&rules,
)
.into_iter()
.map(|rule| rule.name.as_str())
.collect::<Vec<_>>();
assert_eq!(market_open, vec!["market_open"]);
let intraday = scheduler
.triggered_rules_at(
d(2025, 2, 3),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(10, 18, 0).unwrap()),
&rules,
)
.into_iter()
.map(|rule| rule.name.as_str())
.collect::<Vec<_>>();
assert_eq!(intraday, vec!["physical_time"]);
let close = scheduler
.triggered_rules_at(
d(2025, 2, 3),
ScheduleStage::AfterTrading,
Some(NaiveTime::from_hms_opt(15, 0, 0).unwrap()),
&rules,
)
.into_iter()
.map(|rule| rule.name.as_str())
.collect::<Vec<_>>();
assert_eq!(close, vec!["market_close"]);
let mismatch = scheduler
.triggered_rules_at(
d(2025, 2, 3),
ScheduleStage::OnDay,
Some(NaiveTime::from_hms_opt(10, 17, 0).unwrap()),
&rules,
)
.into_iter()
.map(|rule| rule.name.as_str())
.collect::<Vec<_>>();
assert!(mismatch.is_empty());
}
}