Add RQData factor helper APIs
This commit is contained in:
@@ -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),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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_ratio;turnover_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 / field,financial(field) 读取 financial_field / financials_field / field,pit_financial(field) 读取 pit_financial_field / pit_financials_field / field,current_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 {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user