Add rqalpha-style scheduler primitives
This commit is contained in:
@@ -9,6 +9,7 @@ use crate::events::{AccountEvent, FillEvent, OrderEvent, OrderSide, OrderStatus,
|
|||||||
use crate::metrics::{BacktestMetrics, compute_backtest_metrics};
|
use crate::metrics::{BacktestMetrics, compute_backtest_metrics};
|
||||||
use crate::portfolio::{CashReceivable, HoldingSummary, PortfolioState};
|
use crate::portfolio::{CashReceivable, HoldingSummary, PortfolioState};
|
||||||
use crate::rules::EquityRuleHooks;
|
use crate::rules::EquityRuleHooks;
|
||||||
|
use crate::scheduler::{ScheduleRule, ScheduleStage, Scheduler};
|
||||||
use crate::strategy::{Strategy, StrategyContext};
|
use crate::strategy::{Strategy, StrategyContext};
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
@@ -124,6 +125,8 @@ where
|
|||||||
F: FnMut(&BacktestDayProgress),
|
F: FnMut(&BacktestDayProgress),
|
||||||
{
|
{
|
||||||
let mut portfolio = PortfolioState::new(self.config.initial_cash);
|
let mut portfolio = PortfolioState::new(self.config.initial_cash);
|
||||||
|
let scheduler_calendar = self.data.calendar().clone();
|
||||||
|
let scheduler = Scheduler::new(&scheduler_calendar);
|
||||||
let execution_dates = self
|
let execution_dates = self
|
||||||
.data
|
.data
|
||||||
.calendar()
|
.calendar()
|
||||||
@@ -202,8 +205,17 @@ where
|
|||||||
data: &self.data,
|
data: &self.data,
|
||||||
portfolio: &portfolio,
|
portfolio: &portfolio,
|
||||||
};
|
};
|
||||||
|
let schedule_rules = self.strategy.schedule_rules();
|
||||||
self.strategy.before_trading(&daily_context)?;
|
self.strategy.before_trading(&daily_context)?;
|
||||||
let auction_decision = self.strategy.open_auction(&daily_context)?;
|
let mut auction_decision = collect_scheduled_decisions(
|
||||||
|
&mut self.strategy,
|
||||||
|
&scheduler,
|
||||||
|
execution_date,
|
||||||
|
ScheduleStage::OpenAuction,
|
||||||
|
&schedule_rules,
|
||||||
|
&daily_context,
|
||||||
|
)?;
|
||||||
|
auction_decision.merge_from(self.strategy.open_auction(&daily_context)?);
|
||||||
let mut report = self.broker.execute(
|
let mut report = self.broker.execute(
|
||||||
execution_date,
|
execution_date,
|
||||||
&mut portfolio,
|
&mut portfolio,
|
||||||
@@ -211,7 +223,7 @@ where
|
|||||||
&auction_decision,
|
&auction_decision,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let decision = decision_slot
|
let mut decision = decision_slot
|
||||||
.map(|(decision_idx, decision_date)| {
|
.map(|(decision_idx, decision_date)| {
|
||||||
self.strategy.on_day(&StrategyContext {
|
self.strategy.on_day(&StrategyContext {
|
||||||
execution_date,
|
execution_date,
|
||||||
@@ -223,6 +235,20 @@ where
|
|||||||
})
|
})
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
decision.merge_from(collect_scheduled_decisions(
|
||||||
|
&mut self.strategy,
|
||||||
|
&scheduler,
|
||||||
|
execution_date,
|
||||||
|
ScheduleStage::OnDay,
|
||||||
|
&schedule_rules,
|
||||||
|
&StrategyContext {
|
||||||
|
execution_date,
|
||||||
|
decision_date,
|
||||||
|
decision_index,
|
||||||
|
data: &self.data,
|
||||||
|
portfolio: &portfolio,
|
||||||
|
},
|
||||||
|
)?);
|
||||||
|
|
||||||
let intraday_report =
|
let intraday_report =
|
||||||
self.broker
|
self.broker
|
||||||
@@ -635,6 +661,21 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn collect_scheduled_decisions<S: Strategy>(
|
||||||
|
strategy: &mut S,
|
||||||
|
scheduler: &Scheduler<'_>,
|
||||||
|
execution_date: NaiveDate,
|
||||||
|
stage: ScheduleStage,
|
||||||
|
rules: &[ScheduleRule],
|
||||||
|
ctx: &StrategyContext<'_>,
|
||||||
|
) -> Result<crate::strategy::StrategyDecision, BacktestError> {
|
||||||
|
let mut combined = crate::strategy::StrategyDecision::default();
|
||||||
|
for rule in scheduler.triggered_rules(execution_date, stage, rules) {
|
||||||
|
combined.merge_from(strategy.on_scheduled(ctx, rule)?);
|
||||||
|
}
|
||||||
|
Ok(combined)
|
||||||
|
}
|
||||||
|
|
||||||
mod date_format {
|
mod date_format {
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use serde::Serializer;
|
use serde::Serializer;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ pub mod metrics;
|
|||||||
pub mod platform_expr_strategy;
|
pub mod platform_expr_strategy;
|
||||||
pub mod portfolio;
|
pub mod portfolio;
|
||||||
pub mod rules;
|
pub mod rules;
|
||||||
|
pub mod scheduler;
|
||||||
pub mod strategy;
|
pub mod strategy;
|
||||||
pub mod strategy_ai;
|
pub mod strategy_ai;
|
||||||
pub mod universe;
|
pub mod universe;
|
||||||
@@ -31,6 +32,7 @@ pub use metrics::{BacktestMetrics, compute_backtest_metrics};
|
|||||||
pub use platform_expr_strategy::{PlatformExprStrategy, PlatformExprStrategyConfig};
|
pub use platform_expr_strategy::{PlatformExprStrategy, PlatformExprStrategyConfig};
|
||||||
pub use portfolio::{CashReceivable, HoldingSummary, PortfolioState, Position};
|
pub use portfolio::{CashReceivable, HoldingSummary, PortfolioState, Position};
|
||||||
pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
|
pub use rules::{ChinaEquityRuleHooks, EquityRuleHooks, RuleCheck};
|
||||||
|
pub use scheduler::{ScheduleFrequency, ScheduleRule, ScheduleStage, Scheduler};
|
||||||
pub use strategy::{
|
pub use strategy::{
|
||||||
CnSmallCapRotationConfig, CnSmallCapRotationStrategy, JqMicroCapConfig, JqMicroCapStrategy,
|
CnSmallCapRotationConfig, CnSmallCapRotationStrategy, JqMicroCapConfig, JqMicroCapStrategy,
|
||||||
OrderIntent, Strategy, StrategyContext, StrategyDecision,
|
OrderIntent, Strategy, StrategyContext, StrategyDecision,
|
||||||
|
|||||||
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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,10 +11,21 @@ use crate::data::{DataSet, PriceField};
|
|||||||
use crate::engine::BacktestError;
|
use crate::engine::BacktestError;
|
||||||
use crate::events::OrderSide;
|
use crate::events::OrderSide;
|
||||||
use crate::portfolio::PortfolioState;
|
use crate::portfolio::PortfolioState;
|
||||||
|
use crate::scheduler::ScheduleRule;
|
||||||
use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSelector};
|
use crate::universe::{DynamicMarketCapBandSelector, SelectionContext, UniverseSelector};
|
||||||
|
|
||||||
pub trait Strategy {
|
pub trait Strategy {
|
||||||
fn name(&self) -> &str;
|
fn name(&self) -> &str;
|
||||||
|
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
fn on_scheduled(
|
||||||
|
&mut self,
|
||||||
|
_ctx: &StrategyContext<'_>,
|
||||||
|
_rule: &ScheduleRule,
|
||||||
|
) -> Result<StrategyDecision, BacktestError> {
|
||||||
|
Ok(StrategyDecision::default())
|
||||||
|
}
|
||||||
fn before_trading(&mut self, _ctx: &StrategyContext<'_>) -> Result<(), BacktestError> {
|
fn before_trading(&mut self, _ctx: &StrategyContext<'_>) -> Result<(), BacktestError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -51,6 +62,26 @@ pub struct StrategyDecision {
|
|||||||
pub diagnostics: Vec<String>,
|
pub diagnostics: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl StrategyDecision {
|
||||||
|
pub fn merge_from(&mut self, mut other: StrategyDecision) {
|
||||||
|
self.rebalance |= other.rebalance;
|
||||||
|
self.target_weights.append(&mut other.target_weights);
|
||||||
|
self.exit_symbols.append(&mut other.exit_symbols);
|
||||||
|
self.order_intents.append(&mut other.order_intents);
|
||||||
|
self.notes.append(&mut other.notes);
|
||||||
|
self.diagnostics.append(&mut other.diagnostics);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_empty(&self) -> bool {
|
||||||
|
!self.rebalance
|
||||||
|
&& self.target_weights.is_empty()
|
||||||
|
&& self.exit_symbols.is_empty()
|
||||||
|
&& self.order_intents.is_empty()
|
||||||
|
&& self.notes.is_empty()
|
||||||
|
&& self.diagnostics.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum OrderIntent {
|
pub enum OrderIntent {
|
||||||
TargetValue {
|
TargetValue {
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ use chrono::NaiveDate;
|
|||||||
use fidc_core::{
|
use fidc_core::{
|
||||||
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility,
|
||||||
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet,
|
||||||
Instrument, PriceField, Strategy, StrategyContext, StrategyDecision,
|
Instrument, PriceField, ScheduleRule, ScheduleStage, Strategy, StrategyContext,
|
||||||
|
StrategyDecision,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
|
||||||
@@ -115,6 +116,42 @@ impl Strategy for AuctionOrderStrategy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScheduledProbeStrategy {
|
||||||
|
log: Rc<RefCell<Vec<String>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Strategy for ScheduledProbeStrategy {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"scheduled-probe"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn schedule_rules(&self) -> Vec<ScheduleRule> {
|
||||||
|
vec![
|
||||||
|
ScheduleRule::daily("daily_auction", ScheduleStage::OpenAuction),
|
||||||
|
ScheduleRule::weekly_by_weekday("friday_on_day", 5, ScheduleStage::OnDay),
|
||||||
|
ScheduleRule::monthly("first_trading_day_on_day", 1, ScheduleStage::OnDay),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_scheduled(
|
||||||
|
&mut self,
|
||||||
|
ctx: &StrategyContext<'_>,
|
||||||
|
rule: &ScheduleRule,
|
||||||
|
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||||
|
self.log
|
||||||
|
.borrow_mut()
|
||||||
|
.push(format!("scheduled:{}:{}", rule.name, ctx.execution_date));
|
||||||
|
Ok(StrategyDecision::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_day(
|
||||||
|
&mut self,
|
||||||
|
_ctx: &StrategyContext<'_>,
|
||||||
|
) -> Result<StrategyDecision, fidc_core::BacktestError> {
|
||||||
|
Ok(StrategyDecision::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn engine_runs_strategy_hooks_in_daily_order() {
|
fn engine_runs_strategy_hooks_in_daily_order() {
|
||||||
let date1 = d(2025, 1, 2);
|
let date1 = d(2025, 1, 2);
|
||||||
@@ -381,3 +418,221 @@ fn engine_executes_open_auction_decisions_before_on_day() {
|
|||||||
assert_eq!(result.fills[0].reason, "auction_buy");
|
assert_eq!(result.fills[0].reason, "auction_buy");
|
||||||
assert_eq!(result.fills[0].quantity, 100);
|
assert_eq!(result.fills[0].quantity, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn engine_runs_scheduled_rules_for_daily_weekly_and_monthly_triggers() {
|
||||||
|
let date1 = d(2025, 1, 30);
|
||||||
|
let date2 = d(2025, 1, 31);
|
||||||
|
let date3 = d(2025, 2, 3);
|
||||||
|
let data = DataSet::from_components(
|
||||||
|
vec![Instrument {
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
name: "Anchor".to_string(),
|
||||||
|
board: "SZ".to_string(),
|
||||||
|
round_lot: 100,
|
||||||
|
listed_at: Some(d(2020, 1, 1)),
|
||||||
|
delisted_at: None,
|
||||||
|
status: "active".to_string(),
|
||||||
|
}],
|
||||||
|
vec![
|
||||||
|
DailyMarketSnapshot {
|
||||||
|
date: date1,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
timestamp: Some("2025-01-30 09:25:00".to_string()),
|
||||||
|
day_open: 10.0,
|
||||||
|
open: 10.0,
|
||||||
|
high: 10.1,
|
||||||
|
low: 9.9,
|
||||||
|
close: 10.0,
|
||||||
|
last_price: 10.0,
|
||||||
|
bid1: 10.0,
|
||||||
|
ask1: 10.0,
|
||||||
|
prev_close: 9.9,
|
||||||
|
volume: 100_000,
|
||||||
|
tick_volume: 100_000,
|
||||||
|
bid1_volume: 100_000,
|
||||||
|
ask1_volume: 100_000,
|
||||||
|
trading_phase: Some("open_auction".to_string()),
|
||||||
|
paused: false,
|
||||||
|
upper_limit: 11.0,
|
||||||
|
lower_limit: 9.0,
|
||||||
|
price_tick: 0.01,
|
||||||
|
},
|
||||||
|
DailyMarketSnapshot {
|
||||||
|
date: date2,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
timestamp: Some("2025-01-31 09:25:00".to_string()),
|
||||||
|
day_open: 10.1,
|
||||||
|
open: 10.1,
|
||||||
|
high: 10.2,
|
||||||
|
low: 10.0,
|
||||||
|
close: 10.1,
|
||||||
|
last_price: 10.1,
|
||||||
|
bid1: 10.1,
|
||||||
|
ask1: 10.1,
|
||||||
|
prev_close: 10.0,
|
||||||
|
volume: 110_000,
|
||||||
|
tick_volume: 110_000,
|
||||||
|
bid1_volume: 110_000,
|
||||||
|
ask1_volume: 110_000,
|
||||||
|
trading_phase: Some("open_auction".to_string()),
|
||||||
|
paused: false,
|
||||||
|
upper_limit: 11.1,
|
||||||
|
lower_limit: 9.1,
|
||||||
|
price_tick: 0.01,
|
||||||
|
},
|
||||||
|
DailyMarketSnapshot {
|
||||||
|
date: date3,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
timestamp: Some("2025-02-03 09:25:00".to_string()),
|
||||||
|
day_open: 10.2,
|
||||||
|
open: 10.2,
|
||||||
|
high: 10.3,
|
||||||
|
low: 10.1,
|
||||||
|
close: 10.2,
|
||||||
|
last_price: 10.2,
|
||||||
|
bid1: 10.2,
|
||||||
|
ask1: 10.2,
|
||||||
|
prev_close: 10.1,
|
||||||
|
volume: 120_000,
|
||||||
|
tick_volume: 120_000,
|
||||||
|
bid1_volume: 120_000,
|
||||||
|
ask1_volume: 120_000,
|
||||||
|
trading_phase: Some("open_auction".to_string()),
|
||||||
|
paused: false,
|
||||||
|
upper_limit: 11.2,
|
||||||
|
lower_limit: 9.2,
|
||||||
|
price_tick: 0.01,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
DailyFactorSnapshot {
|
||||||
|
date: date1,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
market_cap_bn: 20.0,
|
||||||
|
free_float_cap_bn: 18.0,
|
||||||
|
pe_ttm: 10.0,
|
||||||
|
turnover_ratio: Some(1.0),
|
||||||
|
effective_turnover_ratio: Some(1.0),
|
||||||
|
extra_factors: BTreeMap::new(),
|
||||||
|
},
|
||||||
|
DailyFactorSnapshot {
|
||||||
|
date: date2,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
market_cap_bn: 21.0,
|
||||||
|
free_float_cap_bn: 19.0,
|
||||||
|
pe_ttm: 10.0,
|
||||||
|
turnover_ratio: Some(1.0),
|
||||||
|
effective_turnover_ratio: Some(1.0),
|
||||||
|
extra_factors: BTreeMap::new(),
|
||||||
|
},
|
||||||
|
DailyFactorSnapshot {
|
||||||
|
date: date3,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
market_cap_bn: 22.0,
|
||||||
|
free_float_cap_bn: 20.0,
|
||||||
|
pe_ttm: 10.0,
|
||||||
|
turnover_ratio: Some(1.0),
|
||||||
|
effective_turnover_ratio: Some(1.0),
|
||||||
|
extra_factors: BTreeMap::new(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
CandidateEligibility {
|
||||||
|
date: date1,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
is_st: false,
|
||||||
|
is_new_listing: false,
|
||||||
|
is_paused: false,
|
||||||
|
allow_buy: true,
|
||||||
|
allow_sell: true,
|
||||||
|
is_kcb: false,
|
||||||
|
is_one_yuan: false,
|
||||||
|
},
|
||||||
|
CandidateEligibility {
|
||||||
|
date: date2,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
is_st: false,
|
||||||
|
is_new_listing: false,
|
||||||
|
is_paused: false,
|
||||||
|
allow_buy: true,
|
||||||
|
allow_sell: true,
|
||||||
|
is_kcb: false,
|
||||||
|
is_one_yuan: false,
|
||||||
|
},
|
||||||
|
CandidateEligibility {
|
||||||
|
date: date3,
|
||||||
|
symbol: "000001.SZ".to_string(),
|
||||||
|
is_st: false,
|
||||||
|
is_new_listing: false,
|
||||||
|
is_paused: false,
|
||||||
|
allow_buy: true,
|
||||||
|
allow_sell: true,
|
||||||
|
is_kcb: false,
|
||||||
|
is_one_yuan: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
vec![
|
||||||
|
BenchmarkSnapshot {
|
||||||
|
date: date1,
|
||||||
|
benchmark: "000300.SH".to_string(),
|
||||||
|
open: 100.0,
|
||||||
|
close: 100.0,
|
||||||
|
prev_close: 99.0,
|
||||||
|
volume: 1_000_000,
|
||||||
|
},
|
||||||
|
BenchmarkSnapshot {
|
||||||
|
date: date2,
|
||||||
|
benchmark: "000300.SH".to_string(),
|
||||||
|
open: 101.0,
|
||||||
|
close: 101.0,
|
||||||
|
prev_close: 100.0,
|
||||||
|
volume: 1_100_000,
|
||||||
|
},
|
||||||
|
BenchmarkSnapshot {
|
||||||
|
date: date3,
|
||||||
|
benchmark: "000300.SH".to_string(),
|
||||||
|
open: 102.0,
|
||||||
|
close: 102.0,
|
||||||
|
prev_close: 101.0,
|
||||||
|
volume: 1_200_000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.expect("dataset");
|
||||||
|
|
||||||
|
let log = Rc::new(RefCell::new(Vec::new()));
|
||||||
|
let strategy = ScheduledProbeStrategy { log: log.clone() };
|
||||||
|
let broker = BrokerSimulator::new_with_execution_price(
|
||||||
|
ChinaAShareCostModel::default(),
|
||||||
|
ChinaEquityRuleHooks::default(),
|
||||||
|
PriceField::DayOpen,
|
||||||
|
);
|
||||||
|
let mut engine = BacktestEngine::new(
|
||||||
|
data,
|
||||||
|
strategy,
|
||||||
|
broker,
|
||||||
|
BacktestConfig {
|
||||||
|
initial_cash: 100_000.0,
|
||||||
|
benchmark_code: "000300.SH".to_string(),
|
||||||
|
start_date: Some(date1),
|
||||||
|
end_date: Some(date3),
|
||||||
|
decision_lag_trading_days: 0,
|
||||||
|
execution_price_field: PriceField::DayOpen,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
engine.run().expect("backtest run");
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
log.borrow().as_slice(),
|
||||||
|
[
|
||||||
|
"scheduled:daily_auction:2025-01-30",
|
||||||
|
"scheduled:first_trading_day_on_day:2025-01-30",
|
||||||
|
"scheduled:daily_auction:2025-01-31",
|
||||||
|
"scheduled:friday_on_day:2025-01-31",
|
||||||
|
"scheduled:daily_auction:2025-02-03",
|
||||||
|
"scheduled:first_trading_day_on_day:2025-02-03",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user