From 1760fc6cd1d27fdd17ff64a3078a78820a12739e Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 23 Apr 2026 19:17:04 -0700 Subject: [PATCH] Add bar and tick strategy lifecycle --- crates/fidc-core/src/broker.rs | 29 ++++ crates/fidc-core/src/data.rs | 15 ++ crates/fidc-core/src/engine.rs | 222 ++++++++++++++++++++++++ crates/fidc-core/src/events.rs | 12 ++ crates/fidc-core/src/scheduler.rs | 4 + crates/fidc-core/src/strategy.rs | 16 +- crates/fidc-core/src/strategy_ai.rs | 4 + crates/fidc-core/tests/engine_hooks.rs | 226 ++++++++++++++++++++++++- docs/rqalpha-gap-roadmap.md | 7 +- 9 files changed, 525 insertions(+), 10 deletions(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 055a261..ab963ea 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -111,6 +111,8 @@ pub struct BrokerSimulator { inactive_limit: bool, liquidity_limit: bool, intraday_execution_start_time: Option, + runtime_intraday_start_time: Cell>, + runtime_intraday_end_time: Cell>, next_order_id: Cell, open_orders: RefCell>, } @@ -129,6 +131,8 @@ impl BrokerSimulator { inactive_limit: true, liquidity_limit: true, intraday_execution_start_time: None, + runtime_intraday_start_time: Cell::new(None), + runtime_intraday_end_time: Cell::new(None), next_order_id: Cell::new(1), open_orders: RefCell::new(Vec::new()), } @@ -151,6 +155,8 @@ impl BrokerSimulator { inactive_limit: true, liquidity_limit: true, intraday_execution_start_time: None, + runtime_intraday_start_time: Cell::new(None), + runtime_intraday_end_time: Cell::new(None), next_order_id: Cell::new(1), open_orders: RefCell::new(Vec::new()), } @@ -521,6 +527,25 @@ where Ok(report) } + pub fn execute_between( + &self, + date: NaiveDate, + portfolio: &mut PortfolioState, + data: &DataSet, + decision: &StrategyDecision, + start_time: Option, + end_time: Option, + ) -> Result { + let previous_start_time = self.runtime_intraday_start_time.get(); + let previous_end_time = self.runtime_intraday_end_time.get(); + self.runtime_intraday_start_time.set(start_time); + self.runtime_intraday_end_time.set(end_time); + let result = self.execute(date, portfolio, data, decision); + self.runtime_intraday_start_time.set(previous_start_time); + self.runtime_intraday_end_time.set(previous_end_time); + result + } + fn process_order_intent( &self, date: NaiveDate, @@ -4128,12 +4153,16 @@ where return None; } + let runtime_start_time = self.runtime_intraday_start_time.get(); + let runtime_end_time = self.runtime_intraday_end_time.get(); let start_cursor = algo_request .and_then(|request| request.start_time) + .or(runtime_start_time) .or(self.intraday_execution_start_time) .map(|start_time| date.and_time(start_time)); let end_cursor = algo_request .and_then(|request| request.end_time) + .or(runtime_end_time) .map(|end_time| date.and_time(end_time)); let quotes = data.execution_quotes_on(date, symbol); diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index b323b64..54a113f 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -810,6 +810,21 @@ impl DataSet { .unwrap_or(&[]) } + pub fn execution_quotes_on_date(&self, date: NaiveDate) -> Vec { + let mut quotes = self + .execution_quotes_index + .iter() + .filter(|((quote_date, _), _)| *quote_date == date) + .flat_map(|(_, rows)| rows.iter().cloned()) + .collect::>(); + quotes.sort_by(|left, right| { + left.timestamp + .cmp(&right.timestamp) + .then_with(|| left.symbol.cmp(&right.symbol)) + }); + quotes + } + pub fn benchmark_series(&self) -> Vec { self.benchmark_by_date.values().cloned().collect() } diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index 49709fc..8326d73 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -640,6 +640,68 @@ where ProcessEventKind::OnDay, "on_day", )?; + let bar_open_orders = self.broker.open_order_views(); + publish_phase_event( + &mut self.strategy, + &mut self.process_event_bus, + execution_date, + decision_date, + decision_index, + &self.data, + &portfolio, + &bar_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + execution_date, + ProcessEventKind::PreBar, + "bar:pre", + )?; + decision.merge_from(collect_scheduled_decisions( + &mut self.strategy, + &scheduler, + execution_date, + ScheduleStage::Bar, + &schedule_rules, + decision_date, + decision_index, + &self.data, + &portfolio, + &bar_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + &mut self.process_event_bus, + default_stage_time(ScheduleStage::Bar), + )?); + decision.merge_from(self.strategy.on_bar(&StrategyContext { + execution_date, + decision_date, + decision_index, + data: &self.data, + portfolio: &portfolio, + open_orders: &bar_open_orders, + dynamic_universe: self.dynamic_universe.as_ref(), + subscriptions: &self.subscriptions, + process_events: &process_events, + active_process_event: None, + })?); + publish_phase_event( + &mut self.strategy, + &mut self.process_event_bus, + execution_date, + decision_date, + decision_index, + &self.data, + &portfolio, + &bar_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + execution_date, + ProcessEventKind::Bar, + "bar", + )?; self.apply_strategy_directives( execution_date, decision_date, @@ -691,6 +753,151 @@ where ProcessEventKind::PostOnDay, "on_day:post", )?; + publish_phase_event( + &mut self.strategy, + &mut self.process_event_bus, + execution_date, + decision_date, + decision_index, + &self.data, + &portfolio, + &post_intraday_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + execution_date, + ProcessEventKind::PostBar, + "bar:post", + )?; + + if should_run_tick_events(&schedule_rules, &self.subscriptions) { + let filter_by_subscription = !self.subscriptions.is_empty(); + let tick_quotes = self + .data + .execution_quotes_on_date(execution_date) + .into_iter() + .filter(|quote| { + !filter_by_subscription || self.subscriptions.contains("e.symbol) + }) + .collect::>(); + for quote in tick_quotes { + let tick_time = quote.timestamp.time(); + let tick_open_orders = self.broker.open_order_views(); + publish_phase_event( + &mut self.strategy, + &mut self.process_event_bus, + execution_date, + decision_date, + decision_index, + &self.data, + &portfolio, + &tick_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + execution_date, + ProcessEventKind::PreTick, + format!("tick:{}:{}:pre", quote.symbol, quote.timestamp), + )?; + let mut tick_decision = collect_scheduled_decisions( + &mut self.strategy, + &scheduler, + execution_date, + ScheduleStage::Tick, + &schedule_rules, + decision_date, + decision_index, + &self.data, + &portfolio, + &tick_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + &mut self.process_event_bus, + Some(tick_time), + )?; + tick_decision.merge_from(self.strategy.on_tick( + &StrategyContext { + execution_date, + decision_date, + decision_index, + data: &self.data, + portfolio: &portfolio, + open_orders: &tick_open_orders, + dynamic_universe: self.dynamic_universe.as_ref(), + subscriptions: &self.subscriptions, + process_events: &process_events, + active_process_event: None, + }, + "e, + )?); + publish_phase_event( + &mut self.strategy, + &mut self.process_event_bus, + execution_date, + decision_date, + decision_index, + &self.data, + &portfolio, + &tick_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + execution_date, + ProcessEventKind::Tick, + format!("tick:{}:{}", quote.symbol, quote.timestamp), + )?; + self.apply_strategy_directives( + execution_date, + decision_date, + decision_index, + &portfolio, + &tick_open_orders, + &mut process_events, + &mut tick_decision, + )?; + let mut tick_report = self.broker.execute_between( + execution_date, + &mut portfolio, + &self.data, + &tick_decision, + Some(tick_time), + Some(tick_time), + )?; + let post_tick_open_orders = self.broker.open_order_views(); + publish_process_events( + &mut self.strategy, + &mut self.process_event_bus, + execution_date, + decision_date, + decision_index, + &self.data, + &portfolio, + &post_tick_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + &mut tick_report.process_events, + )?; + merge_broker_report(&mut report, tick_report); + publish_phase_event( + &mut self.strategy, + &mut self.process_event_bus, + execution_date, + decision_date, + decision_index, + &self.data, + &portfolio, + &post_tick_open_orders, + self.dynamic_universe.as_ref(), + &self.subscriptions, + &mut process_events, + execution_date, + ProcessEventKind::PostTick, + format!("tick:{}:{}:post", quote.symbol, quote.timestamp), + )?; + } + } portfolio.update_prices(execution_date, &self.data, PriceField::Close)?; @@ -1557,12 +1764,27 @@ fn stage_label(stage: ScheduleStage) -> &'static str { match stage { ScheduleStage::BeforeTrading => "before_trading", ScheduleStage::OpenAuction => "open_auction", + ScheduleStage::Bar => "bar", + ScheduleStage::Tick => "tick", ScheduleStage::OnDay => "on_day", ScheduleStage::AfterTrading => "after_trading", ScheduleStage::Settlement => "settlement", } } +fn should_run_tick_events(rules: &[ScheduleRule], subscriptions: &BTreeSet) -> bool { + !subscriptions.is_empty() || rules.iter().any(|rule| rule.stage == ScheduleStage::Tick) +} + +fn merge_broker_report(target: &mut BrokerExecutionReport, incoming: BrokerExecutionReport) { + target.order_events.extend(incoming.order_events); + target.fill_events.extend(incoming.fill_events); + target.position_events.extend(incoming.position_events); + target.account_events.extend(incoming.account_events); + target.process_events.extend(incoming.process_events); + target.diagnostics.extend(incoming.diagnostics); +} + mod date_format { use chrono::NaiveDate; use serde::Serializer; diff --git a/crates/fidc-core/src/events.rs b/crates/fidc-core/src/events.rs index f964c31..19c096d 100644 --- a/crates/fidc-core/src/events.rs +++ b/crates/fidc-core/src/events.rs @@ -108,6 +108,12 @@ pub enum ProcessEventKind { PreOpenAuction, OpenAuction, PostOpenAuction, + PreBar, + Bar, + PostBar, + PreTick, + Tick, + PostTick, PreScheduled, PostScheduled, PreOnDay, @@ -141,6 +147,12 @@ impl ProcessEventKind { Self::PreOpenAuction => "pre_open_auction", Self::OpenAuction => "open_auction", Self::PostOpenAuction => "post_open_auction", + Self::PreBar => "pre_bar", + Self::Bar => "bar", + Self::PostBar => "post_bar", + Self::PreTick => "pre_tick", + Self::Tick => "tick", + Self::PostTick => "post_tick", Self::PreScheduled => "pre_scheduled", Self::PostScheduled => "post_scheduled", Self::PreOnDay => "pre_on_day", diff --git a/crates/fidc-core/src/scheduler.rs b/crates/fidc-core/src/scheduler.rs index 869cfcb..9077cbf 100644 --- a/crates/fidc-core/src/scheduler.rs +++ b/crates/fidc-core/src/scheduler.rs @@ -6,6 +6,8 @@ use crate::calendar::TradingCalendar; pub enum ScheduleStage { BeforeTrading, OpenAuction, + Bar, + Tick, OnDay, AfterTrading, Settlement, @@ -222,6 +224,8 @@ pub fn default_stage_time(stage: ScheduleStage) -> Option { 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::Bar => Some(NaiveTime::from_hms_opt(10, 18, 0).expect("valid time")), + ScheduleStage::Tick => None, 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")), diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index fbfc61f..b059ea8 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -7,7 +7,7 @@ use std::sync::OnceLock; use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime}; use crate::cost::ChinaAShareCostModel; -use crate::data::{DataSet, PriceField}; +use crate::data::{DataSet, IntradayExecutionQuote, PriceField}; use crate::engine::BacktestError; use crate::events::{OrderSide, ProcessEvent}; use crate::portfolio::PortfolioState; @@ -42,7 +42,19 @@ pub trait Strategy { ) -> Result { Ok(StrategyDecision::default()) } - fn on_day(&mut self, ctx: &StrategyContext<'_>) -> Result; + fn on_bar(&mut self, _ctx: &StrategyContext<'_>) -> Result { + Ok(StrategyDecision::default()) + } + fn on_tick( + &mut self, + _ctx: &StrategyContext<'_>, + _quote: &IntradayExecutionQuote, + ) -> Result { + Ok(StrategyDecision::default()) + } + fn on_day(&mut self, _ctx: &StrategyContext<'_>) -> Result { + Ok(StrategyDecision::default()) + } fn after_trading(&mut self, _ctx: &StrategyContext<'_>) -> Result<(), BacktestError> { Ok(()) } diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index 85a1146..f650edb 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -102,6 +102,10 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { title: "rebalance.weekly / rebalance.monthly".to_string(), detail: "支持按交易周或交易月调仓,例如 rebalance.weekly(weekday=5).at([\"10:18\"])、rebalance.weekly(tradingday=-1).at([\"10:18\"])、rebalance.monthly(tradingday=1).at([\"10:18\"])。`.at([...])` 的最后一个时刻会编进分钟级 schedule/time_rule;当前平台把 on_day 近似到 10:18,把 open_auction 近似到 09:31。".to_string(), }, + ManualSection { + title: "bar / tick 生命周期".to_string(), + detail: "回测内核支持 rqalpha 风格的 bar/tick 生命周期:日内会发布 pre_bar/bar/post_bar 过程事件;存在 tick 订阅或 tick 调度规则时,会按 execution_quotes 的时间顺序发布 pre_tick/tick/post_tick,并把 tick 阶段下单限制在当前 tick 时间窗内撮合。平台 DSL 中可通过 subscribe([...])、trading.subscription_guard(true) 和 process_event 字段配合显式订单模拟 tick 订阅策略。".to_string(), + }, ManualSection { title: "selection.market_cap_band / selection.limit / ordering.rank_by / ordering.rank_expr".to_string(), detail: "控制候选范围、数量和排序。支持表达式驱动的动态市值带和排序表达式。".to_string(), diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index 5d2d8e1..5a415ad 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -2,18 +2,24 @@ use std::cell::RefCell; use std::collections::{BTreeMap, BTreeSet}; use std::rc::Rc; -use chrono::NaiveDate; +use chrono::{NaiveDate, NaiveDateTime}; use fidc_core::{ BacktestConfig, BacktestEngine, BenchmarkSnapshot, BrokerSimulator, CandidateEligibility, ChinaAShareCostModel, ChinaEquityRuleHooks, DailyFactorSnapshot, DailyMarketSnapshot, DataSet, - Instrument, OrderIntent, PriceField, ProcessEventKind, ScheduleRule, ScheduleStage, - ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision, + Instrument, IntradayExecutionQuote, OrderIntent, PriceField, ProcessEventKind, ScheduleRule, + ScheduleStage, ScheduleTimeRule, Strategy, StrategyContext, StrategyDecision, }; fn d(year: i32, month: u32, day: u32) -> NaiveDate { NaiveDate::from_ymd_opt(year, month, day).expect("valid date") } +fn dt(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> NaiveDateTime { + d(year, month, day) + .and_hms_opt(hour, minute, second) + .expect("valid datetime") +} + struct HookProbeStrategy { log: Rc>>, } @@ -133,6 +139,11 @@ struct UniverseDirectiveStrategy { snapshots: Rc>>, } +struct TickProbeStrategy { + seen_ticks: Rc>>, + ordered: bool, +} + impl Strategy for ScheduledProbeStrategy { fn name(&self) -> &str { "scheduled-probe" @@ -287,6 +298,58 @@ impl Strategy for UniverseDirectiveStrategy { } } +impl Strategy for TickProbeStrategy { + fn name(&self) -> &str { + "tick-probe" + } + + fn on_day( + &mut self, + _ctx: &StrategyContext<'_>, + ) -> Result { + Ok(StrategyDecision { + rebalance: false, + target_weights: BTreeMap::new(), + exit_symbols: BTreeSet::new(), + order_intents: vec![OrderIntent::Subscribe { + symbols: BTreeSet::from(["000001.SZ".to_string()]), + reason: "subscribe_tick_probe".to_string(), + }], + notes: Vec::new(), + diagnostics: Vec::new(), + }) + } + + fn on_tick( + &mut self, + ctx: &StrategyContext<'_>, + quote: &IntradayExecutionQuote, + ) -> Result { + self.seen_ticks.borrow_mut().push(format!( + "{}:{}:{}", + quote.symbol, + quote.timestamp.time(), + ctx.is_subscribed("e.symbol) + )); + if self.ordered { + return Ok(StrategyDecision::default()); + } + self.ordered = true; + Ok(StrategyDecision { + rebalance: false, + target_weights: BTreeMap::new(), + exit_symbols: BTreeSet::new(), + order_intents: vec![OrderIntent::Shares { + symbol: quote.symbol.clone(), + quantity: 100, + reason: "tick_buy".to_string(), + }], + notes: Vec::new(), + diagnostics: Vec::new(), + }) + } +} + #[test] fn engine_runs_strategy_hooks_in_daily_order() { let date1 = d(2025, 1, 2); @@ -454,9 +517,9 @@ fn engine_runs_strategy_hooks_in_daily_order() { "settlement:2025-01-03", ] ); - assert_eq!(result.process_events.len(), 30); + assert_eq!(result.process_events.len(), 36); assert_eq!( - result.process_events[..15] + result.process_events[..18] .iter() .map(|event| &event.kind) .collect::>(), @@ -469,7 +532,10 @@ fn engine_runs_strategy_hooks_in_daily_order() { &ProcessEventKind::PostOpenAuction, &ProcessEventKind::PreOnDay, &ProcessEventKind::OnDay, + &ProcessEventKind::PreBar, + &ProcessEventKind::Bar, &ProcessEventKind::PostOnDay, + &ProcessEventKind::PostBar, &ProcessEventKind::PreAfterTrading, &ProcessEventKind::AfterTrading, &ProcessEventKind::PostAfterTrading, @@ -578,6 +644,156 @@ fn engine_executes_open_auction_decisions_before_on_day() { assert_eq!(result.fills[0].quantity, 100); } +#[test] +fn engine_runs_subscribed_tick_hooks_and_executes_tick_orders() { + let date = d(2025, 1, 2); + let data = DataSet::from_components_with_actions_and_quotes( + 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, + symbol: "000001.SZ".to_string(), + timestamp: Some("2025-01-02 10:18:00".to_string()), + day_open: 10.0, + open: 10.0, + high: 10.4, + low: 9.9, + close: 10.3, + last_price: 10.2, + bid1: 10.1, + ask1: 10.2, + prev_close: 10.0, + volume: 100_000, + tick_volume: 100_000, + bid1_volume: 100_000, + ask1_volume: 100_000, + trading_phase: Some("continuous".to_string()), + paused: false, + upper_limit: 11.0, + lower_limit: 9.0, + price_tick: 0.01, + }], + vec![DailyFactorSnapshot { + date, + 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(), + }], + vec![CandidateEligibility { + date, + 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, + benchmark: "000300.SH".to_string(), + open: 100.0, + close: 100.0, + prev_close: 99.0, + volume: 1_000_000, + }], + Vec::new(), + vec![ + IntradayExecutionQuote { + date, + symbol: "000001.SZ".to_string(), + timestamp: dt(2025, 1, 2, 10, 18, 0), + last_price: 10.2, + bid1: 10.1, + ask1: 10.2, + bid1_volume: 1_000, + ask1_volume: 1_000, + volume_delta: 1_000, + amount_delta: 10_200.0, + trading_phase: Some("continuous".to_string()), + }, + IntradayExecutionQuote { + date, + symbol: "000001.SZ".to_string(), + timestamp: dt(2025, 1, 2, 10, 19, 0), + last_price: 10.3, + bid1: 10.2, + ask1: 10.3, + bid1_volume: 1_000, + ask1_volume: 1_000, + volume_delta: 1_000, + amount_delta: 10_300.0, + trading_phase: Some("continuous".to_string()), + }, + ], + ) + .expect("dataset"); + + let seen_ticks = Rc::new(RefCell::new(Vec::new())); + let strategy = TickProbeStrategy { + seen_ticks: seen_ticks.clone(), + ordered: false, + }; + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::Last, + ); + let mut engine = BacktestEngine::new( + data, + strategy, + broker, + BacktestConfig { + initial_cash: 10_000.0, + benchmark_code: "000300.SH".to_string(), + start_date: Some(date), + end_date: Some(date), + decision_lag_trading_days: 0, + execution_price_field: PriceField::Last, + }, + ); + + let result = engine.run().expect("backtest run"); + + assert_eq!( + seen_ticks.borrow().as_slice(), + ["000001.SZ:10:18:00:true", "000001.SZ:10:19:00:true"] + ); + assert_eq!(result.fills.len(), 1); + assert_eq!(result.fills[0].reason, "tick_buy"); + assert_eq!(result.fills[0].quantity, 100); + assert!( + result + .process_events + .iter() + .any(|event| event.kind == ProcessEventKind::PreTick) + ); + assert!( + result + .process_events + .iter() + .any(|event| event.kind == ProcessEventKind::Tick) + ); + assert!( + result + .process_events + .iter() + .any(|event| event.kind == ProcessEventKind::PostTick) + ); +} + #[test] fn engine_rejects_pending_limit_orders_at_market_close() { let date1 = d(2025, 1, 2); diff --git a/docs/rqalpha-gap-roadmap.md b/docs/rqalpha-gap-roadmap.md index fd0eff1..2562950 100644 --- a/docs/rqalpha-gap-roadmap.md +++ b/docs/rqalpha-gap-roadmap.md @@ -22,7 +22,7 @@ current alignment pass. - [x] minute-level `time_rule` semantics like `market_open`, `market_close`, `physical_time` -- [ ] finer `1m` / `tick` strategy execution entrypoints beyond `open_auction` +- [x] finer `1m` / `tick` strategy execution entrypoints beyond `open_auction` and `on_day` - [x] scheduled actions evaluated against explicit intraday times @@ -57,5 +57,6 @@ current alignment pass. ## Current Step -Active implementation target: Phase 2 follow-up, finer `1m`/`tick` -strategy execution entrypoints beyond the current explicit intraday schedules. +Active implementation target: Phase 5 follow-up plus strategy data API parity: +expose richer position lifecycle fields and RQAlpha-style data helpers such as +`history_bars`, `current_snapshot`, instruments, and trading-date access.