Add get price data helper
This commit is contained in:
@@ -325,6 +325,26 @@ pub struct DailySnapshotBundle {
|
||||
pub corporate_actions: Vec<CorporateAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct PriceBar {
|
||||
#[serde(with = "date_format")]
|
||||
pub date: NaiveDate,
|
||||
pub timestamp: Option<String>,
|
||||
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<PriceBar> {
|
||||
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::<Vec<_>>();
|
||||
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<f64> {
|
||||
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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<PriceBar> {
|
||||
self.data.get_price(symbol, start, end, frequency)
|
||||
}
|
||||
|
||||
pub fn has_subscriptions(&self) -> bool {
|
||||
!self.subscriptions.is_empty()
|
||||
}
|
||||
|
||||
@@ -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() },
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user