Add RQData factor helper APIs

This commit is contained in:
boris
2026-04-23 22:04:55 -07:00
parent f056aa3468
commit 882053e12b
6 changed files with 704 additions and 5 deletions

View File

@@ -1373,6 +1373,188 @@ impl DataSet {
.collect()
}
pub fn get_shares(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
share_type: &str,
) -> Vec<FactorValue> {
self.get_first_available_factor_series(
symbol,
start,
end,
&shares_factor_aliases(share_type),
&format!("shares_{}", normalize_field(share_type)),
)
}
pub fn get_turnover_rate(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.get_first_available_factor_series(
symbol,
start,
end,
&turnover_rate_factor_aliases(field),
&format!("turnover_rate_{}", normalize_field(field)),
)
}
pub fn get_price_change_rate(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
) -> Vec<FactorValue> {
if start > end {
return Vec::new();
}
let mut rows = self
.market_by_date
.range(start..=end)
.flat_map(|(_, snapshots)| snapshots.iter())
.filter(|snapshot| snapshot.symbol == symbol)
.filter_map(|snapshot| {
if snapshot.prev_close.is_finite() && snapshot.prev_close > 0.0 {
Some(FactorValue {
date: snapshot.date,
symbol: snapshot.symbol.clone(),
field: "price_change_rate".to_string(),
value: snapshot.close / snapshot.prev_close - 1.0,
})
} else {
None
}
})
.collect::<Vec<_>>();
if rows.is_empty() {
rows = self.get_first_available_factor_series(
symbol,
start,
end,
&[
"price_change_rate".to_string(),
"change_rate".to_string(),
"pct_change".to_string(),
],
"price_change_rate",
);
}
rows.sort_by_key(|row| row.date);
rows
}
pub fn get_stock_connect(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.get_first_available_factor_series(
symbol,
start,
end,
&stock_connect_factor_aliases(field),
&format!("stock_connect_{}", normalize_field(field)),
)
}
pub fn current_performance(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.get_first_available_factor_series(
symbol,
start,
end,
&prefixed_factor_aliases("current_performance", field),
field,
)
}
pub fn get_fundamentals(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.get_first_available_factor_series(
symbol,
start,
end,
&prefixed_factor_aliases("fundamental", field),
field,
)
}
pub fn get_financials(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.get_first_available_factor_series(
symbol,
start,
end,
&prefixed_factor_aliases("financial", field),
field,
)
}
pub fn get_pit_financials(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.get_first_available_factor_series(
symbol,
start,
end,
&prefixed_factor_aliases("pit_financial", field),
field,
)
}
pub fn get_industry(
&self,
symbol: &str,
date: NaiveDate,
source: &str,
level: usize,
) -> Option<FactorValue> {
let fields = industry_factor_aliases(source, level);
for (factor_date, snapshots) in self.factor_by_date.range(..=date).rev() {
let Some(snapshot) = snapshots.iter().find(|row| row.symbol == symbol) else {
continue;
};
for field in &fields {
if let Some(value) = factor_numeric_value(snapshot, field) {
return Some(FactorValue {
date: *factor_date,
symbol: snapshot.symbol.clone(),
field: field.clone(),
value,
});
}
}
}
None
}
pub fn get_dominant_future(&self, underlying_symbol: &str, date: NaiveDate) -> Option<String> {
let underlying = normalize_field(underlying_symbol);
let mut candidates = self
@@ -1614,6 +1796,39 @@ impl DataSet {
.and_then(|snapshot| factor_numeric_value(snapshot, field))
}
fn get_first_available_factor_series(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
fields: &[String],
output_field: &str,
) -> Vec<FactorValue> {
if start > end {
return Vec::new();
}
let output_field = normalize_field(output_field);
let mut rows = Vec::new();
for (_, snapshots) in self.factor_by_date.range(start..=end) {
let Some(snapshot) = snapshots.iter().find(|row| row.symbol == symbol) else {
continue;
};
for field in fields {
if let Some(value) = factor_numeric_value(snapshot, field) {
rows.push(FactorValue {
date: snapshot.date,
symbol: snapshot.symbol.clone(),
field: output_field.clone(),
value,
});
break;
}
}
}
rows.sort_by_key(|row| row.date);
rows
}
pub fn factor_moving_average(
&self,
date: NaiveDate,
@@ -1838,6 +2053,144 @@ fn read_factors(path: &Path) -> Result<Vec<DailyFactorSnapshot>, DataSetError> {
Ok(snapshots)
}
fn normalized_aliases(values: &[String]) -> Vec<String> {
let mut aliases = Vec::new();
for value in values {
let normalized = normalize_field(value);
if !aliases.contains(&normalized) {
aliases.push(normalized);
}
}
aliases
}
fn shares_factor_aliases(share_type: &str) -> Vec<String> {
let field = normalize_field(share_type);
let values = match field.as_str() {
"" | "all" | "total" => vec![
"total_shares",
"shares_total",
"total_share",
"total_share_capital",
"capitalization",
"shares",
],
"float" | "free_float" | "circulating" | "circulation" => vec![
"free_float_shares",
"float_shares",
"circulating_shares",
"circulation_shares",
"float_a_shares",
],
"a" | "a_share" | "a_shares" => vec!["a_shares", "shares_a", "a_share_capital"],
other => {
return normalized_aliases(&[
other.to_string(),
format!("shares_{other}"),
format!("{other}_shares"),
]);
}
};
normalized_aliases(
&values
.iter()
.map(|value| value.to_string())
.collect::<Vec<_>>(),
)
}
fn turnover_rate_factor_aliases(field: &str) -> Vec<String> {
let field = normalize_field(field);
let values = match field.as_str() {
"" | "all" | "rate" | "turnover" | "turnover_rate" | "turnover_ratio" => {
vec!["turnover_rate", "turnover_ratio"]
}
"effective" | "effective_turnover" | "effective_turnover_rate" => {
vec!["effective_turnover_rate", "effective_turnover_ratio"]
}
other => {
return normalized_aliases(&[
other.to_string(),
format!("turnover_rate_{other}"),
format!("{other}_turnover_rate"),
format!("turnover_ratio_{other}"),
format!("{other}_turnover_ratio"),
]);
}
};
normalized_aliases(
&values
.iter()
.map(|value| value.to_string())
.collect::<Vec<_>>(),
)
}
fn stock_connect_factor_aliases(field: &str) -> Vec<String> {
let field = normalize_field(field);
let values = match field.as_str() {
"" | "all" | "connect" | "stock_connect" => {
vec![
"stock_connect",
"stock_connect_all",
"connect_all",
"north_bound",
]
}
"north" | "north_bound" | "northbound" => vec![
"stock_connect_north_bound",
"stock_connect_northbound",
"connect_north_bound",
"north_bound",
"northbound",
],
"south" | "south_bound" | "southbound" => vec![
"stock_connect_south_bound",
"stock_connect_southbound",
"connect_south_bound",
"south_bound",
"southbound",
],
other => {
return normalized_aliases(&[
other.to_string(),
format!("stock_connect_{other}"),
format!("connect_{other}"),
]);
}
};
normalized_aliases(
&values
.iter()
.map(|value| value.to_string())
.collect::<Vec<_>>(),
)
}
fn prefixed_factor_aliases(prefix: &str, field: &str) -> Vec<String> {
let prefix = normalize_field(prefix);
let field = normalize_field(field);
let plural_prefix = format!("{prefix}s");
normalized_aliases(&[
format!("{prefix}_{field}"),
format!("{plural_prefix}_{field}"),
field.clone(),
])
}
fn industry_factor_aliases(source: &str, level: usize) -> Vec<String> {
let source = normalize_field(source);
normalized_aliases(&[
format!("industry_{source}_l{level}"),
format!("industry_{source}_{level}"),
format!("{source}_industry_l{level}"),
format!("{source}_industry_{level}"),
format!("industry_l{level}"),
format!("industry_{level}"),
"industry_code".to_string(),
])
}
fn factor_numeric_value(snapshot: &DailyFactorSnapshot, field: &str) -> Option<f64> {
match field {
"market_cap" | "market_cap_bn" => Some(snapshot.market_cap_bn),

View File

@@ -642,6 +642,23 @@ impl PlatformExprStrategy {
| "has_split"
| "securities_margin"
| "get_securities_margin_value"
| "shares"
| "get_shares_value"
| "turnover_rate"
| "get_turnover_rate_value"
| "price_change_rate"
| "get_price_change_rate_value"
| "stock_connect"
| "get_stock_connect_value"
| "current_performance"
| "fundamental"
| "get_fundamentals_value"
| "financial"
| "get_financials_value"
| "pit_financial"
| "get_pit_financials_value"
| "industry_code"
| "get_industry_code"
| "yield_curve"
| "get_yield_curve_value"
| "is_margin_stock"
@@ -2143,6 +2160,143 @@ impl PlatformExprStrategy {
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"shares" | "get_shares_value" => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let (field, lookback) =
Self::parse_field_lookback_helper_args(helper, &args, "total")?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.get_shares(&stock.symbol, start, day.date, &field)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"turnover_rate" | "get_turnover_rate_value" => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let (field, lookback) =
Self::parse_field_lookback_helper_args(helper, &args, "turnover_rate")?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.get_turnover_rate(&stock.symbol, start, day.date, &field)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"price_change_rate" | "get_price_change_rate_value" => {
if args.len() > 1 {
return Err(BacktestError::Execution(format!(
"{helper} expects optional lookback"
)));
}
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let lookback = Self::parse_optional_positive_usize(args.first(), 1)?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.get_price_change_rate(&stock.symbol, start, day.date)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"stock_connect" | "get_stock_connect_value" => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let (field, lookback) =
Self::parse_field_lookback_helper_args(helper, &args, "all")?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.get_stock_connect(&stock.symbol, start, day.date, &field)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"current_performance" => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let (field, lookback) =
Self::parse_required_field_lookback_helper_args(helper, &args)?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.current_performance(&stock.symbol, start, day.date, &field)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"fundamental" | "get_fundamentals_value" => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let (field, lookback) =
Self::parse_required_field_lookback_helper_args(helper, &args)?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.get_fundamentals(&stock.symbol, start, day.date, &field)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"financial" | "get_financials_value" => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let (field, lookback) =
Self::parse_required_field_lookback_helper_args(helper, &args)?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.get_financials(&stock.symbol, start, day.date, &field)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"pit_financial" | "get_pit_financials_value" => {
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let (field, lookback) =
Self::parse_required_field_lookback_helper_args(helper, &args)?;
let start = self.helper_start_date(ctx, day.date, lookback);
let value = ctx
.get_pit_financials(&stock.symbol, start, day.date, &field)
.last()
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"industry_code" | "get_industry_code" => {
if args.len() > 2 {
return Err(BacktestError::Execution(format!(
"{helper} expects optional source and optional level"
)));
}
let stock = stock.ok_or_else(|| {
BacktestError::Execution(format!("{helper} requires stock context"))
})?;
let source = args
.first()
.map(|arg| Self::parse_string_or_identifier(arg))
.transpose()?
.unwrap_or_else(|| "citics".to_string());
let level = Self::parse_optional_positive_usize(args.get(1), 1)?;
let value = ctx
.get_industry(&stock.symbol, &source, level)
.map(|row| row.value)
.unwrap_or(0.0);
Ok(Self::format_rhai_float(value))
}
"yield_curve" | "get_yield_curve_value" => {
if args.is_empty() || args.len() > 2 {
return Err(BacktestError::Execution(format!(
@@ -2264,6 +2418,46 @@ impl PlatformExprStrategy {
))
}
fn parse_field_lookback_helper_args(
helper: &str,
args: &[String],
default_field: &str,
) -> Result<(String, usize), BacktestError> {
if args.len() > 2 {
return Err(BacktestError::Execution(format!(
"{helper} expects optional field and optional lookback"
)));
}
if args.is_empty() {
return Ok((default_field.to_string(), 1));
}
if args.len() == 1 {
if let Ok(lookback) = Self::parse_positive_usize(&args[0]) {
return Ok((default_field.to_string(), lookback));
}
return Ok((Self::parse_string_or_identifier(&args[0])?, 1));
}
Ok((
Self::parse_string_or_identifier(&args[0])?,
Self::parse_positive_usize(&args[1])?,
))
}
fn parse_required_field_lookback_helper_args(
helper: &str,
args: &[String],
) -> Result<(String, usize), BacktestError> {
if args.is_empty() || args.len() > 2 {
return Err(BacktestError::Execution(format!(
"{helper} expects field and optional lookback"
)));
}
Ok((
Self::parse_string_or_identifier(&args[0])?,
Self::parse_optional_positive_usize(args.get(1), 1)?,
))
}
fn parse_optional_positive_usize(
raw: Option<&String>,
fallback: usize,
@@ -4517,6 +4711,13 @@ mod tests {
("custom_alpha".to_string(), 7.0),
("margin_all".to_string(), 1.0),
("yield_curve_1y".to_string(), 0.02),
("total_shares".to_string(), 123.0),
("stock_connect_north_bound".to_string(), 1.0),
("industry_citics_l1".to_string(), 10.0),
("fundamental_net_profit".to_string(), 99.0),
("financial_revenue".to_string(), 188.0),
("pit_financial_eps".to_string(), 0.88),
("current_performance_roe".to_string(), 12.0),
]),
}],
vec![CandidateEligibility {
@@ -4604,6 +4805,15 @@ mod tests {
" && factor_value(\"custom_alpha\") == 7.0",
" && securities_margin(\"margin_all\") == 1.0",
" && is_margin_stock(\"all\")",
" && shares(\"total\") == 123.0",
" && turnover_rate(\"effective\") == 18.0",
" && price_change_rate() > 0.015",
" && stock_connect(\"north_bound\") == 1.0",
" && industry_code(\"citics\", 1) == 10.0",
" && fundamental(\"net_profit\") == 99.0",
" && financial(\"revenue\") == 188.0",
" && pit_financial(\"eps\") > 0.87",
" && current_performance(\"roe\") == 12.0",
" && yield_curve(\"1y\") > 0.019",
" && dominant_future(\"IF\") == \"IF2501\"",
" && dominant_future_price(\"IF\", \"close\") == 4000.0",

View File

@@ -654,6 +654,90 @@ impl StrategyContext<'_> {
self.data.get_securities_margin(symbol, start, end, field)
}
pub fn get_shares(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
share_type: &str,
) -> Vec<FactorValue> {
self.data.get_shares(symbol, start, end, share_type)
}
pub fn get_turnover_rate(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.data.get_turnover_rate(symbol, start, end, field)
}
pub fn get_price_change_rate(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
) -> Vec<FactorValue> {
self.data.get_price_change_rate(symbol, start, end)
}
pub fn get_stock_connect(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.data.get_stock_connect(symbol, start, end, field)
}
pub fn current_performance(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.data.current_performance(symbol, start, end, field)
}
pub fn get_fundamentals(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.data.get_fundamentals(symbol, start, end, field)
}
pub fn get_financials(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.data.get_financials(symbol, start, end, field)
}
pub fn get_pit_financials(
&self,
symbol: &str,
start: NaiveDate,
end: NaiveDate,
field: &str,
) -> Vec<FactorValue> {
self.data.get_pit_financials(symbol, start, end, field)
}
pub fn get_industry(&self, symbol: &str, source: &str, level: usize) -> Option<FactorValue> {
self.data
.get_industry(symbol, self.execution_date, source, level)
}
pub fn get_dominant_future(&self, underlying_symbol: &str) -> Option<String> {
self.data
.get_dominant_future(underlying_symbol, self.execution_date)

View File

@@ -216,6 +216,12 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
ManualFunction { name: "get_yield_curve / yield_curve".to_string(), signature: "yield_curve(\"1y\", lookback=1)".to_string(), detail: "收益率曲线 API。平台表达式从 factors 中的 yield_curve_1y / yc_1y 等字段读取最近值Rust Context 可用 ctx.get_yield_curve(start, end, Some(\"1y\")) 读取序列。".to_string() },
ManualFunction { name: "get_margin_stocks / is_margin_stock".to_string(), signature: "is_margin_stock(\"all\" | \"stock\" | \"cash\")".to_string(), detail: "融资融券标的 API。平台表达式用 is_margin_stock(...) 判断当前股票是否在 margin_all/margin_stock/margin_cash 标记中Rust Context 可用 ctx.get_margin_stocks(type) 返回标的列表。".to_string() },
ManualFunction { name: "get_securities_margin / securities_margin".to_string(), signature: "securities_margin(\"field\", lookback=1)".to_string(), detail: "融资融券明细 API。平台表达式读取当前股票最近 N 个交易日指定融资融券字段最新值Rust Context 可用 ctx.get_securities_margin(symbol, start, end, field) 读取序列。".to_string() },
ManualFunction { name: "get_shares / shares".to_string(), signature: "shares(\"total\" | \"free_float\", lookback=1)".to_string(), detail: "股本 API。shares(\"total\") 会依次读取 total_shares/shares_total/total_share_capital 等字段shares(\"free_float\") 会读取 free_float_shares/float_shares/circulating_shares 等字段Rust Context 可用 ctx.get_shares(symbol, start, end, share_type)。".to_string() },
ManualFunction { name: "get_turnover_rate / turnover_rate".to_string(), signature: "turnover_rate(\"turnover\" | \"effective\", lookback=1)".to_string(), detail: "换手率 API。turnover_rate(\"turnover\") 读取 turnover_rate/turnover_ratioturnover_rate(\"effective\") 读取 effective_turnover_rate/effective_turnover_ratio也可传任意字段名映射数据库因子。".to_string() },
ManualFunction { name: "get_price_change_rate / price_change_rate".to_string(), signature: "price_change_rate(lookback=1)".to_string(), detail: "涨跌幅 API默认按日行情 close / prev_close - 1 计算,缺少行情时回退 factors 中的 price_change_rate/change_rate/pct_change。返回小数例如 0.1 表示上涨 10%。".to_string() },
ManualFunction { name: "get_stock_connect / stock_connect".to_string(), signature: "stock_connect(\"north_bound\" | \"south_bound\" | \"all\", lookback=1)".to_string(), detail: "陆股通/互联互通标记 API从 stock_connect_north_bound、north_bound、stock_connect_south_bound 等因子读取,返回数值标记。".to_string() },
ManualFunction { name: "current_performance / fundamental / financial / pit_financial".to_string(), signature: "fundamental(\"net_profit\", lookback=1)".to_string(), detail: "财务与基本面 API。它们都是对 factors 的通用映射fundamental(field) 会依次读取 fundamental_field / fundamentals_field / fieldfinancial(field) 读取 financial_field / financials_field / fieldpit_financial(field) 读取 pit_financial_field / pit_financials_field / fieldcurrent_performance(field) 读取 current_performance_field / current_performances_field / field。".to_string() },
ManualFunction { name: "get_industry / industry_code".to_string(), signature: "industry_code(\"citics\", 1)".to_string(), detail: "行业 API。当前 core 的 factors 仅承载数值字段,因此行业先支持数值 code按 industry_citics_l1、industry_citics_1、citics_industry_l1、industry_code 等字段读取最近可用值;字符串行业名称需要数据链路扩展字符串型因子后再暴露。".to_string() },
ManualFunction { name: "get_dominant_future / dominant_future / dominant_future_price".to_string(), signature: "dominant_future(\"IF\") / dominant_future_price(\"IF\", \"close\", lookback=1)".to_string(), detail: "主力合约 API。dominant_future 返回当前日期匹配前缀的主力期货合约代码dominant_future_price 读取该主力合约最近 N 个交易日指定字段的最新价格。Rust Context 可用 ctx.get_dominant_future(...) 和 ctx.get_dominant_future_price(...)。".to_string() },
ManualFunction { name: "order/order_status/order_avg_price/order_transaction_cost".to_string(), signature: "ctx.order(order_id)".to_string(), detail: "按订单 id 查询运行时订单对象,支持已结束订单和当前挂单。返回字段包括 status、filled_quantity、unfilled_quantity、avg_price、transaction_cost、symbol、side、reason可用便捷函数读取状态、成交均价和费用对齐 RQAlpha Order 的核心属性。".to_string() },
ManualFunction { name: "account/portfolio_view/accounts".to_string(), signature: "ctx.account()".to_string(), detail: "返回当前股票账户/组合运行时视图,字段包括 account_type、cash、available_cash、frozen_cash、market_value、total_value、unit_net_value、daily_pnl、daily_returns、total_returns、transaction_cost、trading_pnl、position_pnl 等DSL 中同名字段可直接使用。也可用 ctx.stock_account()、ctx.account_by_type(\"STOCK\")、ctx.accounts() 按账户类型读取;当前股票回测路径不会把 FUTURE 虚假映射成 STOCK。".to_string() },
@@ -239,7 +245,7 @@ pub fn built_in_strategy_manual() -> StrategyAiManual {
},
ManualFactorSource {
table: "fi_data_center.stock_indicator_factors_v1".to_string(),
detail: "股票指标因子原表,可映射进 factors[...]。".to_string(),
detail: "股票指标因子原表,可映射进 factors[...]。股本、换手率、财务、陆股通、行业 code 等 RQData 风格 API 均优先从这里或 bt_daily_features_v1 的 extra_factors 中读取。".to_string(),
fields: vec![],
},
ManualFactorSource {

View File

@@ -205,6 +205,10 @@ fn two_day_futures_data() -> DataSet {
("custom_alpha".to_string(), 7.0),
("margin_all".to_string(), 1.0),
("yield_curve_1y".to_string(), 0.02),
("total_shares".to_string(), 123.0),
("stock_connect_north_bound".to_string(), 1.0),
("industry_citics_l1".to_string(), 10.0),
("fundamental_net_profit".to_string(), 99.0),
]),
),
factor_row(
@@ -214,6 +218,10 @@ fn two_day_futures_data() -> DataSet {
("custom_alpha".to_string(), 8.0),
("margin_all".to_string(), 1.0),
("yield_curve_1y".to_string(), 0.021),
("total_shares".to_string(), 124.0),
("stock_connect_north_bound".to_string(), 1.0),
("industry_citics_l1".to_string(), 10.0),
("fundamental_net_profit".to_string(), 101.0),
]),
),
],
@@ -556,13 +564,44 @@ impl Strategy for AdvancedDataApiProbeStrategy {
let dominant = ctx.get_dominant_future("IF").unwrap_or_default();
let dominant_prices =
ctx.get_dominant_future_price("IF", ctx.execution_date, ctx.execution_date, "1d");
let shares = ctx.get_shares("000001.SZ", ctx.execution_date, ctx.execution_date, "total");
let turnover = ctx.get_turnover_rate(
"000001.SZ",
ctx.execution_date,
ctx.execution_date,
"turnover",
);
let price_change =
ctx.get_price_change_rate("000001.SZ", ctx.execution_date, ctx.execution_date);
let stock_connect = ctx.get_stock_connect(
"000001.SZ",
ctx.execution_date,
ctx.execution_date,
"north_bound",
);
let industry = ctx
.get_industry("000001.SZ", "citics", 1)
.map(|row| row.value)
.unwrap_or_default();
let fundamentals = ctx.get_fundamentals(
"000001.SZ",
ctx.execution_date,
ctx.execution_date,
"net_profit",
);
self.observed.borrow_mut().push(format!(
"factor={:.0};margin={};yield={:.3};dominant={};prices={}",
"factor={:.0};margin={};yield={:.3};dominant={};prices={};shares={:.0};turnover={:.1};change={:.3};connect={:.0};industry={:.0};profit={:.0}",
factors.first().map(|row| row.value).unwrap_or_default(),
margin_stocks.join(","),
yield_curve.first().map(|row| row.value).unwrap_or_default(),
dominant,
dominant_prices.len()
dominant_prices.len(),
shares.first().map(|row| row.value).unwrap_or_default(),
turnover.first().map(|row| row.value).unwrap_or_default(),
price_change.first().map(|row| row.value).unwrap_or_default(),
stock_connect.first().map(|row| row.value).unwrap_or_default(),
industry,
fundamentals.first().map(|row| row.value).unwrap_or_default()
));
Ok(StrategyDecision::default())
}
@@ -1857,7 +1896,9 @@ fn strategy_context_exposes_advanced_rqdata_helpers() {
assert_eq!(
observed.borrow().as_slice(),
&["factor=7;margin=000001.SZ;yield=0.020;dominant=IF2501;prices=1"]
&[
"factor=7;margin=000001.SZ;yield=0.020;dominant=IF2501;prices=1;shares=123;turnover=1.0;change=0.000;connect=1;industry=10;profit=99"
]
);
assert!(result.analyzer_report().positions.is_empty());
}

View File

@@ -51,7 +51,7 @@ Parity gaps found by this pass and current closure state:
| P1 | Futures transaction cost decider | RQAlpha supports by-money/by-volume futures commission with separate open, close, and close-today rates and a commission multiplier. | Closed. `FuturesTransactionCostModel` calculates by-money/by-volume open/close/close-today costs from trading parameters. | None. |
| P1 | Futures settlement price mode | RQAlpha can settle futures by `settlement` or `close`, including previous-settlement fields. | Closed. Engine supports configurable settlement price mode and resolves settlement/prev-settlement from factor fields with close/prev_close fallback. | Add dedicated settlement columns if the storage layer later separates them from factors. |
| P1 | Frontend risk validators for futures | RQAlpha applies cash/margin, position closable, price-limit, trading-status, and self-trade validators before order submission. | Closed for zero quantity, invalid limit price, active-contract, trading-phase, tick-aligned limit price, price-limit, self-trade crossing risk, paused/no executable price, margin, and close-position rejection diagnostics. These submission validators are controlled by `FuturesValidationConfig` so service-level callers can relax individual checks for compatibility tests or vendor-specific rules. | Add more exchange metadata columns only when source data exposes them. |
| P2 | RQData helper APIs | RQAlpha exposes `get_dividend`, `get_split`, `get_yield_curve`, `get_factor`, `get_margin_stocks`, `get_securities_margin`, `get_dominant_future`, and dominant futures price APIs. | Closed. These APIs are available through `DataSet` and `StrategyContext`; platform expressions also expose focused helpers such as `dividend_cash`, `factor_value`, `yield_curve`, `is_margin_stock`, `dominant_future`, and `dominant_future_price`. | Add more DSL aliases only when users need specific names. |
| P2 | RQData helper APIs | RQAlpha exposes `get_dividend`, `get_split`, `get_yield_curve`, `get_factor`, `get_margin_stocks`, `get_securities_margin`, `get_shares`, `get_turnover_rate`, `get_price_change_rate`, industry, stock-connect, fundamentals/financials/PIT-financials, `get_dominant_future`, and dominant futures price APIs. | Closed for the engine-native data model. These APIs are available through `DataSet` and `StrategyContext`; platform expressions expose focused helpers such as `dividend_cash`, `factor_value`, `yield_curve`, `is_margin_stock`, `shares`, `turnover_rate`, `price_change_rate`, `stock_connect`, `industry_code`, `fundamental`, `financial`, `pit_financial`, `current_performance`, `dominant_future`, and `dominant_future_price`. String-valued industry names remain a data-model extension because current factors are numeric. | Add string factor support only if source data exposes non-numeric categories. |
| P2 | Analyzer/report parity | RQAlpha analyser can export richer trades, positions, benchmark, monthly returns, risk, and summary artifacts. | Closed for normalized trades, positions, monthly returns, risk summary, equity curve, benchmark series, metrics, and JSON report bundle via `BacktestResult::analyzer_report(_json)`. | UI/service download endpoints can serialize this report directly. |
| P3 | Mod/config/plugin architecture | RQAlpha has pluggable mods, event bus extension points, and many config toggles. | Closed for a lightweight engine-native model: `BacktestProcessMod`, `BacktestProcessModLoader`, enabled-name installation, and event-bus lifecycle hooks. It intentionally avoids RQAlpha's Python global mod loader. | Add concrete production mods/toggles as requirements appear. |
@@ -161,6 +161,11 @@ Parity gaps found by this pass and current closure state:
- [x] `get_factor`
- [x] `get_margin_stocks`
- [x] `get_securities_margin`
- [x] `get_shares`
- [x] `get_turnover_rate`
- [x] `get_price_change_rate`
- [x] stock-connect, industry-code, fundamentals, financials, PIT-financials,
and current-performance factor wrappers
- [x] `get_dominant_future`
- [x] futures dominant price helpers
- [x] platform DSL helper aliases for advanced RQData-style APIs