重构分钟线事件流与订阅加载

This commit is contained in:
boris
2026-08-25 01:41:50 +08:00
parent 4cf0224d2d
commit a147c495af
6 changed files with 316 additions and 47 deletions
+68 -9
View File
@@ -1,18 +1,19 @@
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use chrono::{NaiveDate, NaiveDateTime};
use fidc_core::{
BacktestConfig, BacktestEngine, BacktestProcessMod, BacktestProcessModLoader,
BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel,
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, FuturesAccountState,
FuturesCommissionType, FuturesContractSpec, FuturesDirection, FuturesOrderIntent,
FuturesTradingParameter, FuturesValidationConfig, Instrument, IntradayExecutionQuote,
IntradayOrderBookDepthLevel, MatchingType, OpenOrderView, OrderIntent, OrderSide, OrderStatus,
PlatformExprStrategy, PlatformExprStrategyConfig, PortfolioState, PriceField, ProcessEvent,
ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage, ScheduleTimeRule, Strategy,
StrategyContext, StrategyDecision,
ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, ExecutionQuoteRequest,
FuturesAccountState, FuturesCommissionType, FuturesContractSpec, FuturesDirection,
FuturesOrderIntent, FuturesTradingParameter, FuturesValidationConfig, Instrument,
IntradayExecutionQuote, IntradayOrderBookDepthLevel, MatchingType, OpenOrderView, OrderIntent,
OrderSide, OrderStatus, PlatformExprStrategy, PlatformExprStrategyConfig, PortfolioState,
PriceField, ProcessEvent, ProcessEventBus, ProcessEventKind, ScheduleRule, ScheduleStage,
ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision,
};
fn d(year: i32, month: u32, day: u32) -> NaiveDate {
@@ -634,6 +635,8 @@ struct UniverseDirectiveStrategy {
struct MinuteProbeStrategy {
seen_ticks: Rc<RefCell<Vec<String>>>,
scheduled_count: Rc<RefCell<usize>>,
subscribe_symbols: BTreeSet<String>,
ordered: bool,
}
@@ -809,6 +812,22 @@ impl Strategy for MinuteProbeStrategy {
"minute-probe"
}
fn schedule_rules(&self) -> Vec<ScheduleRule> {
vec![
ScheduleRule::daily("minute_barrier", ScheduleStage::Minute)
.with_time_rule(ScheduleTimeRule::physical_time(10, 18)),
]
}
fn on_scheduled(
&mut self,
_ctx: &StrategyContext<'_>,
_rule: &ScheduleRule,
) -> Result<StrategyDecision, fidc_core::BacktestError> {
*self.scheduled_count.borrow_mut() += 1;
Ok(StrategyDecision::default())
}
fn on_day(
&mut self,
_ctx: &StrategyContext<'_>,
@@ -818,7 +837,7 @@ impl Strategy for MinuteProbeStrategy {
target_weights: BTreeMap::new(),
exit_symbols: BTreeSet::new(),
order_intents: vec![OrderIntent::Subscribe {
symbols: BTreeSet::from(["000001.SZ".to_string()]),
symbols: self.subscribe_symbols.clone(),
reason: "subscribe_minute_probe".to_string(),
}],
notes: Vec::new(),
@@ -2011,6 +2030,19 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
amount_delta: 10_200.0,
trading_phase: Some("continuous".to_string()),
},
IntradayExecutionQuote {
date,
symbol: "000002.SZ".to_string(),
timestamp: dt(2025, 1, 2, 10, 18, 0),
last_price: 20.4,
bid1: 20.3,
ask1: 20.4,
bid1_volume: 1_000,
ask1_volume: 1_000,
volume_delta: 1_000,
amount_delta: 20_400.0,
trading_phase: Some("continuous".to_string()),
},
IntradayExecutionQuote {
date,
symbol: "000001.SZ".to_string(),
@@ -2029,8 +2061,11 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
.expect("dataset");
let seen_ticks = Rc::new(RefCell::new(Vec::new()));
let scheduled_count = Rc::new(RefCell::new(0usize));
let strategy = MinuteProbeStrategy {
seen_ticks: seen_ticks.clone(),
scheduled_count: scheduled_count.clone(),
subscribe_symbols: BTreeSet::from(["000001.SZ".to_string(), "000002.SZ".to_string()]),
ordered: false,
};
let broker = BrokerSimulator::new_with_execution_price(
@@ -2038,6 +2073,8 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
ChinaEquityRuleHooks::default(),
PriceField::Last,
);
let loader_requests = Arc::new(Mutex::new(Vec::<ExecutionQuoteRequest>::new()));
let loader_requests_for_callback = Arc::clone(&loader_requests);
let mut engine = BacktestEngine::new(
data,
strategy,
@@ -2050,7 +2087,11 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
decision_lag_trading_days: 0,
execution_price_field: PriceField::Last,
},
);
)
.with_execution_quote_loader(move |request| {
loader_requests_for_callback.lock().unwrap().push(request);
Ok(Vec::new())
});
let result = engine.run().expect("backtest run");
@@ -2058,9 +2099,19 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
seen_ticks.borrow().as_slice(),
[
"000001.SZ:10:18:00:true:visible=10.20:previous=",
"000002.SZ:10:18:00:true:visible=20.40:previous=",
"000001.SZ:10:19:00:true:visible=10.20,10.30:previous=10.20"
]
);
assert_eq!(*scheduled_count.borrow(), 1);
let loader_requests = loader_requests.lock().unwrap();
assert_eq!(loader_requests.len(), 1);
assert_eq!(loader_requests[0].start_time, None);
assert_eq!(loader_requests[0].end_time, None);
assert_eq!(
loader_requests[0].symbols,
BTreeSet::from(["000001.SZ".to_string(), "000002.SZ".to_string()])
);
assert_eq!(result.fills.len(), 1);
assert_eq!(result.fills[0].reason, "minute_buy");
assert_eq!(result.fills[0].quantity, 100);
@@ -2082,6 +2133,14 @@ fn engine_runs_minute_hooks_and_executes_minute_orders() {
.iter()
.any(|event| event.kind == ProcessEventKind::PostMinute)
);
assert_eq!(
result
.process_events
.iter()
.filter(|event| event.kind == ProcessEventKind::PreMinute)
.count(),
2
);
}
#[test]