From bb8f40f33c660b84177ac906887d95df68676441 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 23 Apr 2026 19:37:50 -0700 Subject: [PATCH] Add suspended and ST data helpers --- crates/fidc-core/src/data.rs | 42 +++++++++++++++++++++++ crates/fidc-core/src/strategy.rs | 10 ++++++ crates/fidc-core/src/strategy_ai.rs | 1 + crates/fidc-core/tests/engine_hooks.rs | 46 +++++++++++++++++--------- docs/rqalpha-gap-roadmap.md | 16 ++++++--- 5 files changed, 95 insertions(+), 20 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index cc5f758..b650437 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -947,6 +947,18 @@ impl DataSet { self.calendar.next_trading_date(date, n) } + pub fn is_suspended_flags(&self, date: NaiveDate, symbol: &str, count: usize) -> Vec { + self.historical_daily_flags(date, symbol, count, |candidate, market| { + candidate.is_some_and(|row| row.is_paused) || market.is_some_and(|row| row.paused) + }) + } + + pub fn is_st_stock_flags(&self, date: NaiveDate, symbol: &str, count: usize) -> Vec { + self.historical_daily_flags(date, symbol, count, |candidate, _| { + candidate.is_some_and(|row| row.is_st) + }) + } + pub fn price(&self, date: NaiveDate, symbol: &str, field: PriceField) -> Option { let snapshot = self.market(date, symbol)?; Some(snapshot.price(field)) @@ -1047,6 +1059,36 @@ impl DataSet { .collect() } + fn historical_daily_flags( + &self, + date: NaiveDate, + symbol: &str, + count: usize, + evaluator: F, + ) -> Vec + where + F: Fn(Option<&CandidateEligibility>, Option<&DailyMarketSnapshot>) -> bool, + { + if count == 0 { + return Vec::new(); + } + let days = self + .calendar + .iter() + .filter(|day| *day <= date) + .collect::>(); + let start = days.len().saturating_sub(count); + days[start..] + .iter() + .map(|day| { + evaluator( + self.candidate_index.get(&(*day, symbol.to_string())), + self.market_index.get(&(*day, symbol.to_string())), + ) + }) + .collect() + } + pub fn market_decision_close(&self, date: NaiveDate, symbol: &str) -> Option { self.market_series_by_symbol .get(symbol) diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 7aa7815..81ad9a2 100644 --- a/crates/fidc-core/src/strategy.rs +++ b/crates/fidc-core/src/strategy.rs @@ -285,6 +285,16 @@ impl StrategyContext<'_> { self.data.next_trading_date(date, n) } + pub fn is_suspended(&self, symbol: &str, count: usize) -> Vec { + self.data + .is_suspended_flags(self.execution_date, symbol, count) + } + + pub fn is_st_stock(&self, symbol: &str, count: usize) -> Vec { + self.data + .is_st_stock_flags(self.execution_date, symbol, count) + } + pub fn has_subscriptions(&self) -> bool { !self.subscriptions.is_empty() } diff --git a/crates/fidc-core/src/strategy_ai.rs b/crates/fidc-core/src/strategy_ai.rs index deca831..2836948 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -199,6 +199,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { ManualFunction { name: "current_snapshot".to_string(), signature: "ctx.current_snapshot(symbol)".to_string(), detail: "读取当前交易日指定证券的日级快照,可用于获得当日 open/close/last/upper_limit/lower_limit 等字段。".to_string() }, ManualFunction { name: "instrument/instruments/all_instruments".to_string(), signature: "ctx.instrument(symbol)".to_string(), detail: "读取证券元数据,包括名称、板块、上市日期、退市日期、最小下单量、整手、最小价位等;all_instruments 按证券代码稳定排序返回全量证券。".to_string() }, ManualFunction { name: "get_trading_dates/get_previous_trading_date/get_next_trading_date".to_string(), signature: "ctx.get_previous_trading_date(date, n)".to_string(), detail: "交易日历 API。get_trading_dates 返回闭区间交易日;previous/next 返回相对某日向前或向后的第 n 个交易日,当前日自身不计入。".to_string() }, + ManualFunction { name: "is_suspended/is_st_stock".to_string(), signature: "ctx.is_suspended(symbol, count)".to_string(), detail: "读取指定证券截至当前交易日最近 count 个交易日的停牌或 ST 标记,返回 bool 序列,顺序从旧到新;对应 RQAlpha 的 is_suspended/is_st_stock 数据源能力。".to_string() }, ManualFunction { name: "rolling_mean".to_string(), signature: "rolling_mean(\"field\", lookback)".to_string(), detail: "任意字段滚动均值,支持 volume/amount/turnover_ratio、signal_open/signal_close、benchmark_open/benchmark_close 等。任意成交量窗口推荐用它,比如 rolling_mean(\"volume\", 15)。".to_string() }, ManualFunction { name: "sma".to_string(), signature: "sma(\"field\", lookback)".to_string(), detail: "rolling_mean 的别名。任意价格均线窗口推荐用它,比如 sma(\"close\", 15)。".to_string() }, ManualFunction { name: "round/floor/ceil/abs/min/max/clamp".to_string(), signature: "round(x)".to_string(), detail: "常用数值函数。".to_string() }, diff --git a/crates/fidc-core/tests/engine_hooks.rs b/crates/fidc-core/tests/engine_hooks.rs index 69c0803..567d5bb 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -20,6 +20,14 @@ fn dt(year: i32, month: u32, day: u32, hour: u32, minute: u32, second: u32) -> N .expect("valid datetime") } +fn bool_flags(values: Vec) -> String { + values + .into_iter() + .map(|value| if value { "1" } else { "0" }) + .collect::>() + .join(",") +} + struct HookProbeStrategy { log: Rc>>, } @@ -420,8 +428,10 @@ impl Strategy for DataApiProbeStrategy { let trading_date_count = ctx .get_trading_dates(d(2025, 1, 2), ctx.execution_date) .len(); + let suspended = bool_flags(ctx.is_suspended("000001.SZ", 3)); + let st_flags = bool_flags(ctx.is_st_stock("000001.SZ", 3)); self.snapshots.borrow_mut().push(format!( - "daily={daily_close};previous={previous_close};tick={tick_last};previous_tick={previous_tick_last};current={current_close};instrument={instrument_name};all={};range={trading_date_count};prev={prev_date};next={next_date}", + "daily={daily_close};previous={previous_close};tick={tick_last};previous_tick={previous_tick_last};current={current_close};instrument={instrument_name};all={};range={trading_date_count};prev={prev_date};next={next_date};suspended={suspended};st={st_flags}", ctx.all_instruments().len() )); } @@ -935,20 +945,24 @@ fn strategy_context_exposes_rqalpha_style_data_helpers() { extra_factors: BTreeMap::new(), }) .collect::>(); - let candidates = [date1, date2, date3] - .into_iter() - .map(|date| 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, - }) - .collect::>(); + let candidates = [ + (date1, false, false), + (date2, true, true), + (date3, false, false), + ] + .into_iter() + .map(|(date, is_paused, is_st)| CandidateEligibility { + date, + symbol: "000001.SZ".to_string(), + is_st, + is_new_listing: false, + is_paused, + allow_buy: true, + allow_sell: true, + is_kcb: false, + is_one_yuan: false, + }) + .collect::>(); let benchmarks = [ (date1, 100.0, 99.0), (date2, 101.0, 100.0), @@ -1045,7 +1059,7 @@ fn strategy_context_exposes_rqalpha_style_data_helpers() { assert_eq!( snapshots.borrow().as_slice(), [ - "daily=10.10,10.20;previous=10.00,10.10;tick=10.15,10.25;previous_tick=10.15;current=10.20;instrument=Anchor;all=1;range=3;prev=2025-01-03;next=2025-01-06" + "daily=10.10,10.20;previous=10.00,10.10;tick=10.15,10.25;previous_tick=10.15;current=10.20;instrument=Anchor;all=1;range=3;prev=2025-01-03;next=2025-01-06;suspended=0,1,0;st=0,1,0" ] ); } diff --git a/docs/rqalpha-gap-roadmap.md b/docs/rqalpha-gap-roadmap.md index d6a01ab..d32e2f5 100644 --- a/docs/rqalpha-gap-roadmap.md +++ b/docs/rqalpha-gap-roadmap.md @@ -56,6 +56,13 @@ current alignment pass. - [x] phase-aware minute/tick history cursor semantics matching the active bar or tick callback +### Phase 7: Remaining stock data-source API parity + +- [x] `is_suspended` +- [x] `is_st_stock` +- [ ] `get_price` style date-range tabular API +- [ ] `instruments_history` + ## Execution Order 1. Close the explicit order API gap with target-shares / `order_to` parity. @@ -64,10 +71,11 @@ current alignment pass. 4. Add dynamic universe APIs. 5. Add algo-order styles. 6. Finish position accounting parity. -7. Continue parity audit for remaining account, order, and data-source APIs. +7. Continue stock data-source API parity. +8. Continue parity audit for remaining account and order object APIs. ## Current Step -Active implementation target: continue parity audit for remaining account, -order, and data-source APIs after the stock strategy API, scheduler, universe, -algo-order, position accounting, and core strategy data helpers are covered. +Active implementation target: continue stock data-source API parity after +covering suspended/ST historical flags; next larger gap is a `get_price` style +date-range tabular API and instruments history.