Add RQData factor helper APIs
This commit is contained in:
@@ -1373,6 +1373,188 @@ impl DataSet {
|
|||||||
.collect()
|
.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> {
|
pub fn get_dominant_future(&self, underlying_symbol: &str, date: NaiveDate) -> Option<String> {
|
||||||
let underlying = normalize_field(underlying_symbol);
|
let underlying = normalize_field(underlying_symbol);
|
||||||
let mut candidates = self
|
let mut candidates = self
|
||||||
@@ -1614,6 +1796,39 @@ impl DataSet {
|
|||||||
.and_then(|snapshot| factor_numeric_value(snapshot, field))
|
.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(
|
pub fn factor_moving_average(
|
||||||
&self,
|
&self,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
@@ -1838,6 +2053,144 @@ fn read_factors(path: &Path) -> Result<Vec<DailyFactorSnapshot>, DataSetError> {
|
|||||||
Ok(snapshots)
|
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> {
|
fn factor_numeric_value(snapshot: &DailyFactorSnapshot, field: &str) -> Option<f64> {
|
||||||
match field {
|
match field {
|
||||||
"market_cap" | "market_cap_bn" => Some(snapshot.market_cap_bn),
|
"market_cap" | "market_cap_bn" => Some(snapshot.market_cap_bn),
|
||||||
|
|||||||
@@ -642,6 +642,23 @@ impl PlatformExprStrategy {
|
|||||||
| "has_split"
|
| "has_split"
|
||||||
| "securities_margin"
|
| "securities_margin"
|
||||||
| "get_securities_margin_value"
|
| "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"
|
| "yield_curve"
|
||||||
| "get_yield_curve_value"
|
| "get_yield_curve_value"
|
||||||
| "is_margin_stock"
|
| "is_margin_stock"
|
||||||
@@ -2143,6 +2160,143 @@ impl PlatformExprStrategy {
|
|||||||
.unwrap_or(0.0);
|
.unwrap_or(0.0);
|
||||||
Ok(Self::format_rhai_float(value))
|
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" => {
|
"yield_curve" | "get_yield_curve_value" => {
|
||||||
if args.is_empty() || args.len() > 2 {
|
if args.is_empty() || args.len() > 2 {
|
||||||
return Err(BacktestError::Execution(format!(
|
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(
|
fn parse_optional_positive_usize(
|
||||||
raw: Option<&String>,
|
raw: Option<&String>,
|
||||||
fallback: usize,
|
fallback: usize,
|
||||||
@@ -4517,6 +4711,13 @@ mod tests {
|
|||||||
("custom_alpha".to_string(), 7.0),
|
("custom_alpha".to_string(), 7.0),
|
||||||
("margin_all".to_string(), 1.0),
|
("margin_all".to_string(), 1.0),
|
||||||
("yield_curve_1y".to_string(), 0.02),
|
("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 {
|
vec![CandidateEligibility {
|
||||||
@@ -4604,6 +4805,15 @@ mod tests {
|
|||||||
" && factor_value(\"custom_alpha\") == 7.0",
|
" && factor_value(\"custom_alpha\") == 7.0",
|
||||||
" && securities_margin(\"margin_all\") == 1.0",
|
" && securities_margin(\"margin_all\") == 1.0",
|
||||||
" && is_margin_stock(\"all\")",
|
" && 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",
|
" && yield_curve(\"1y\") > 0.019",
|
||||||
" && dominant_future(\"IF\") == \"IF2501\"",
|
" && dominant_future(\"IF\") == \"IF2501\"",
|
||||||
" && dominant_future_price(\"IF\", \"close\") == 4000.0",
|
" && dominant_future_price(\"IF\", \"close\") == 4000.0",
|
||||||
|
|||||||
@@ -654,6 +654,90 @@ impl StrategyContext<'_> {
|
|||||||
self.data.get_securities_margin(symbol, start, end, field)
|
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> {
|
pub fn get_dominant_future(&self, underlying_symbol: &str) -> Option<String> {
|
||||||
self.data
|
self.data
|
||||||
.get_dominant_future(underlying_symbol, self.execution_date)
|
.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_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_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_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: "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: "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() },
|
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 {
|
ManualFactorSource {
|
||||||
table: "fi_data_center.stock_indicator_factors_v1".to_string(),
|
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![],
|
fields: vec![],
|
||||||
},
|
},
|
||||||
ManualFactorSource {
|
ManualFactorSource {
|
||||||
|
|||||||
@@ -205,6 +205,10 @@ fn two_day_futures_data() -> DataSet {
|
|||||||
("custom_alpha".to_string(), 7.0),
|
("custom_alpha".to_string(), 7.0),
|
||||||
("margin_all".to_string(), 1.0),
|
("margin_all".to_string(), 1.0),
|
||||||
("yield_curve_1y".to_string(), 0.02),
|
("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(
|
factor_row(
|
||||||
@@ -214,6 +218,10 @@ fn two_day_futures_data() -> DataSet {
|
|||||||
("custom_alpha".to_string(), 8.0),
|
("custom_alpha".to_string(), 8.0),
|
||||||
("margin_all".to_string(), 1.0),
|
("margin_all".to_string(), 1.0),
|
||||||
("yield_curve_1y".to_string(), 0.021),
|
("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 = ctx.get_dominant_future("IF").unwrap_or_default();
|
||||||
let dominant_prices =
|
let dominant_prices =
|
||||||
ctx.get_dominant_future_price("IF", ctx.execution_date, ctx.execution_date, "1d");
|
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!(
|
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(),
|
factors.first().map(|row| row.value).unwrap_or_default(),
|
||||||
margin_stocks.join(","),
|
margin_stocks.join(","),
|
||||||
yield_curve.first().map(|row| row.value).unwrap_or_default(),
|
yield_curve.first().map(|row| row.value).unwrap_or_default(),
|
||||||
dominant,
|
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())
|
Ok(StrategyDecision::default())
|
||||||
}
|
}
|
||||||
@@ -1857,7 +1896,9 @@ fn strategy_context_exposes_advanced_rqdata_helpers() {
|
|||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
observed.borrow().as_slice(),
|
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());
|
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 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 | 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. |
|
| 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. |
|
| 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. |
|
| 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_factor`
|
||||||
- [x] `get_margin_stocks`
|
- [x] `get_margin_stocks`
|
||||||
- [x] `get_securities_margin`
|
- [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] `get_dominant_future`
|
||||||
- [x] futures dominant price helpers
|
- [x] futures dominant price helpers
|
||||||
- [x] platform DSL helper aliases for advanced RQData-style APIs
|
- [x] platform DSL helper aliases for advanced RQData-style APIs
|
||||||
|
|||||||
Reference in New Issue
Block a user