From ca49b6dbb3149be1d3d080f2238c4a6bf59b5004 Mon Sep 17 00:00:00 2001 From: boris Date: Thu, 23 Apr 2026 19:41:01 -0700 Subject: [PATCH] Add get price data helper --- crates/fidc-core/src/data.rs | 99 ++++++++++++++++++++++++++ crates/fidc-core/src/lib.rs | 2 +- crates/fidc-core/src/strategy.rs | 12 +++- crates/fidc-core/src/strategy_ai.rs | 1 + crates/fidc-core/tests/engine_hooks.rs | 10 ++- docs/rqalpha-gap-roadmap.md | 6 +- 6 files changed, 123 insertions(+), 7 deletions(-) diff --git a/crates/fidc-core/src/data.rs b/crates/fidc-core/src/data.rs index b650437..46fe15e 100644 --- a/crates/fidc-core/src/data.rs +++ b/crates/fidc-core/src/data.rs @@ -325,6 +325,26 @@ pub struct DailySnapshotBundle { pub corporate_actions: Vec, } +#[derive(Debug, Clone, Serialize)] +pub struct PriceBar { + #[serde(with = "date_format")] + pub date: NaiveDate, + pub timestamp: Option, + pub symbol: String, + pub frequency: String, + pub open: f64, + pub high: f64, + pub low: f64, + pub close: f64, + pub last_price: f64, + pub volume: u64, + pub amount: f64, + pub bid1: f64, + pub ask1: f64, + pub bid1_volume: u64, + pub ask1_volume: u64, +} + #[derive(Debug, Clone)] pub struct EligibleUniverseSnapshot { pub symbol: String, @@ -959,6 +979,45 @@ impl DataSet { }) } + pub fn get_price( + &self, + symbol: &str, + start: NaiveDate, + end: NaiveDate, + frequency: &str, + ) -> Vec { + if start > end { + return Vec::new(); + } + match normalize_history_frequency(frequency).as_deref() { + Some("1d") => self + .market_by_date + .range(start..=end) + .flat_map(|(_, rows)| rows.iter()) + .filter(|row| row.symbol == symbol) + .map(daily_market_price_bar) + .collect(), + Some("1m") | Some("tick") => { + let mut bars = self + .execution_quotes_index + .iter() + .filter(|((date, quote_symbol), _)| { + quote_symbol == symbol && *date >= start && *date <= end + }) + .flat_map(|(_, rows)| rows.iter()) + .map(intraday_quote_price_bar) + .collect::>(); + bars.sort_by(|left, right| { + left.date + .cmp(&right.date) + .then_with(|| left.timestamp.cmp(&right.timestamp)) + }); + bars + } + _ => Vec::new(), + } + } + pub fn price(&self, date: NaiveDate, symbol: &str, field: PriceField) -> Option { let snapshot = self.market(date, symbol)?; Some(snapshot.price(field)) @@ -1416,6 +1475,46 @@ fn intraday_quote_visible( } } +fn daily_market_price_bar(snapshot: &DailyMarketSnapshot) -> PriceBar { + PriceBar { + date: snapshot.date, + timestamp: snapshot.timestamp.clone(), + symbol: snapshot.symbol.clone(), + frequency: "1d".to_string(), + open: snapshot.open, + high: snapshot.high, + low: snapshot.low, + close: snapshot.close, + last_price: snapshot.last_price, + volume: snapshot.volume, + amount: 0.0, + bid1: snapshot.bid1, + ask1: snapshot.ask1, + bid1_volume: snapshot.bid1_volume, + ask1_volume: snapshot.ask1_volume, + } +} + +fn intraday_quote_price_bar(snapshot: &IntradayExecutionQuote) -> PriceBar { + PriceBar { + date: snapshot.date, + timestamp: Some(snapshot.timestamp.format("%Y-%m-%d %H:%M:%S").to_string()), + symbol: snapshot.symbol.clone(), + frequency: "tick".to_string(), + open: snapshot.last_price, + high: snapshot.last_price, + low: snapshot.last_price, + close: snapshot.last_price, + last_price: snapshot.last_price, + volume: snapshot.volume_delta, + amount: snapshot.amount_delta, + bid1: snapshot.bid1, + ask1: snapshot.ask1, + bid1_volume: snapshot.bid1_volume, + ask1_volume: snapshot.ask1_volume, + } +} + fn normalize_field(field: &str) -> String { field .trim() diff --git a/crates/fidc-core/src/lib.rs b/crates/fidc-core/src/lib.rs index 47b40dd..b97583f 100644 --- a/crates/fidc-core/src/lib.rs +++ b/crates/fidc-core/src/lib.rs @@ -21,7 +21,7 @@ pub use cost::{ChinaAShareCostModel, CostModel, TradingCost}; pub use data::{ BenchmarkSnapshot, CandidateEligibility, CorporateAction, DailyFactorSnapshot, DailyMarketSnapshot, DailySnapshotBundle, DataSet, DataSetError, EligibleUniverseSnapshot, - IntradayExecutionQuote, PriceField, + IntradayExecutionQuote, PriceBar, PriceField, }; pub use engine::{ BacktestConfig, BacktestDayProgress, BacktestEngine, BacktestError, BacktestResult, diff --git a/crates/fidc-core/src/strategy.rs b/crates/fidc-core/src/strategy.rs index 81ad9a2..4f8a020 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::{DailyMarketSnapshot, DataSet, IntradayExecutionQuote, PriceField}; +use crate::data::{DailyMarketSnapshot, DataSet, IntradayExecutionQuote, PriceBar, PriceField}; use crate::engine::BacktestError; use crate::events::{OrderSide, ProcessEvent}; use crate::instrument::Instrument; @@ -295,6 +295,16 @@ impl StrategyContext<'_> { .is_st_stock_flags(self.execution_date, symbol, count) } + pub fn get_price( + &self, + symbol: &str, + start: NaiveDate, + end: NaiveDate, + frequency: &str, + ) -> Vec { + self.data.get_price(symbol, start, end, frequency) + } + 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 2836948..ff440c6 100644 --- a/crates/fidc-core/src/strategy_ai.rs +++ b/crates/fidc-core/src/strategy_ai.rs @@ -200,6 +200,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual { 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: "get_price".to_string(), signature: "ctx.get_price(symbol, start_date, end_date, \"1d\" | \"1m\" | \"tick\")".to_string(), detail: "按日期区间读取统一 PriceBar 序列。日线返回 open/high/low/close/last/volume/盘口字段;分钟或 tick 返回按 timestamp 排序的 last/bid1/ask1/volume_delta/amount_delta 映射,便于服务层转成表格或前端明细。".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 567d5bb..e91d1e7 100644 --- a/crates/fidc-core/tests/engine_hooks.rs +++ b/crates/fidc-core/tests/engine_hooks.rs @@ -430,8 +430,14 @@ impl Strategy for DataApiProbeStrategy { .len(); let suspended = bool_flags(ctx.is_suspended("000001.SZ", 3)); let st_flags = bool_flags(ctx.is_st_stock("000001.SZ", 3)); + let daily_price_count = ctx + .get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "1d") + .len(); + let tick_price_count = ctx + .get_price("000001.SZ", d(2025, 1, 3), ctx.execution_date, "tick") + .len(); 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};suspended={suspended};st={st_flags}", + "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};price_daily={daily_price_count};price_tick={tick_price_count}", ctx.all_instruments().len() )); } @@ -1059,7 +1065,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;suspended=0,1,0;st=0,1,0" + "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;price_daily=2;price_tick=3" ] ); } diff --git a/docs/rqalpha-gap-roadmap.md b/docs/rqalpha-gap-roadmap.md index d32e2f5..0c5b77c 100644 --- a/docs/rqalpha-gap-roadmap.md +++ b/docs/rqalpha-gap-roadmap.md @@ -60,7 +60,7 @@ current alignment pass. - [x] `is_suspended` - [x] `is_st_stock` -- [ ] `get_price` style date-range tabular API +- [x] `get_price` style date-range tabular API - [ ] `instruments_history` ## Execution Order @@ -77,5 +77,5 @@ current alignment pass. ## Current Step 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. +covering suspended/ST historical flags and `get_price` style date-range +queries; next larger gap is instruments history.