修正回测指标和成交时间口径

This commit is contained in:
boris
2026-06-14 01:08:29 +08:00
parent 4c3653e009
commit ea76670bc0
4 changed files with 313 additions and 94 deletions
+2
View File
@@ -4936,6 +4936,7 @@ fn zero_fill_status_for_reason(reason: &str) -> OrderStatus {
"tick no volume" "tick no volume"
| "tick volume limit" | "tick volume limit"
| "intraday quote liquidity exhausted" | "intraday quote liquidity exhausted"
| "no execution quotes at or before start"
| "no execution quotes after start" | "no execution quotes after start"
| "upper_limit" | "upper_limit"
| "lower_limit" | "lower_limit"
@@ -4950,6 +4951,7 @@ fn final_partial_fill_status(partial_reason: Option<&str>) -> OrderStatus {
Some(reason) Some(reason)
if reason.contains("market liquidity or volume limit") if reason.contains("market liquidity or volume limit")
|| reason.contains("intraday quote liquidity exhausted") || reason.contains("intraday quote liquidity exhausted")
|| reason.contains("no execution quotes at or before start")
|| reason.contains("no execution quotes after start") || reason.contains("no execution quotes after start")
|| reason.contains("upper_limit") || reason.contains("upper_limit")
|| reason.contains("lower_limit") || reason.contains("lower_limit")
+77 -88
View File
@@ -1769,92 +1769,78 @@ impl PlatformExprStrategy {
let factor = ctx.data.require_factor(factor_date, symbol)?; let factor = ctx.data.require_factor(factor_date, symbol)?;
let candidate = ctx.data.require_candidate(date, symbol)?; let candidate = ctx.data.require_candidate(date, symbol)?;
let instrument = ctx.data.instrument(symbol); let instrument = ctx.data.instrument(symbol);
let stock_ma_short = precomputed_stock_rolling_mean( let stock_ma_short = ctx
&factor.extra_factors, .data
"close", .market_decision_close_moving_average(date, symbol, self.config.stock_short_ma_days)
self.config.stock_short_ma_days,
)
.or_else(|| {
ctx.data.market_decision_close_moving_average(
date,
symbol,
self.config.stock_short_ma_days,
)
})
.unwrap_or(f64::NAN);
let stock_ma_mid = precomputed_stock_rolling_mean(
&factor.extra_factors,
"close",
self.config.stock_mid_ma_days,
)
.or_else(|| {
ctx.data.market_decision_close_moving_average(
date,
symbol,
self.config.stock_mid_ma_days,
)
})
.unwrap_or(f64::NAN);
let stock_ma_long = precomputed_stock_rolling_mean(
&factor.extra_factors,
"close",
self.config.stock_long_ma_days,
)
.or_else(|| {
ctx.data.market_decision_close_moving_average(
date,
symbol,
self.config.stock_long_ma_days,
)
})
.unwrap_or(f64::NAN);
let stock_ma5 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5)
.or_else(|| { .or_else(|| {
ctx.data precomputed_stock_rolling_mean(
.market_decision_close_moving_average(date, symbol, 5) &factor.extra_factors,
"close",
self.config.stock_short_ma_days,
)
}) })
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let stock_ma10 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10) let stock_ma_mid = ctx
.data
.market_decision_close_moving_average(date, symbol, self.config.stock_mid_ma_days)
.or_else(|| { .or_else(|| {
ctx.data precomputed_stock_rolling_mean(
.market_decision_close_moving_average(date, symbol, 10) &factor.extra_factors,
"close",
self.config.stock_mid_ma_days,
)
}) })
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let stock_ma20 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20) let stock_ma_long = ctx
.data
.market_decision_close_moving_average(date, symbol, self.config.stock_long_ma_days)
.or_else(|| { .or_else(|| {
ctx.data precomputed_stock_rolling_mean(
.market_decision_close_moving_average(date, symbol, 20) &factor.extra_factors,
"close",
self.config.stock_long_ma_days,
)
}) })
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let stock_ma30 = precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30) let stock_ma5 = ctx
.or_else(|| { .data
ctx.data .market_decision_close_moving_average(date, symbol, 5)
.market_decision_close_moving_average(date, symbol, 30) .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 5))
})
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let stock_volume_ma5 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5) let stock_ma10 = ctx
.or_else(|| { .data
ctx.data .market_decision_close_moving_average(date, symbol, 10)
.market_decision_volume_moving_average(date, symbol, 5) .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 10))
})
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let stock_volume_ma10 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10) let stock_ma20 = ctx
.or_else(|| { .data
ctx.data .market_decision_close_moving_average(date, symbol, 20)
.market_decision_volume_moving_average(date, symbol, 10) .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 20))
})
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let stock_volume_ma20 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20) let stock_ma30 = ctx
.or_else(|| { .data
ctx.data .market_decision_close_moving_average(date, symbol, 30)
.market_decision_volume_moving_average(date, symbol, 20) .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "close", 30))
})
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let stock_volume_ma60 = precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 60) let stock_volume_ma5 = ctx
.or_else(|| { .data
ctx.data .market_decision_volume_moving_average(date, symbol, 5)
.market_decision_volume_moving_average(date, symbol, 60) .or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 5))
}) .unwrap_or(f64::NAN);
let stock_volume_ma10 = ctx
.data
.market_decision_volume_moving_average(date, symbol, 10)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 10))
.unwrap_or(f64::NAN);
let stock_volume_ma20 = ctx
.data
.market_decision_volume_moving_average(date, symbol, 20)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 20))
.unwrap_or(f64::NAN);
let stock_volume_ma60 = ctx
.data
.market_decision_volume_moving_average(date, symbol, 60)
.or_else(|| precomputed_stock_rolling_mean(&factor.extra_factors, "volume", 60))
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
let touched_upper_limit = !market.paused let touched_upper_limit = !market.paused
&& (market.is_at_upper_limit_price(market.close) && (market.is_at_upper_limit_price(market.close)
@@ -3557,16 +3543,16 @@ impl PlatformExprStrategy {
"rolling_mean(\"{other}\", {lookback}) requires stock context" "rolling_mean(\"{other}\", {lookback}) requires stock context"
)) ))
})?; })?;
precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback).or_else( ctx.data
|| { .market_decision_numeric_moving_average(
ctx.data.market_decision_numeric_moving_average( day.date,
day.date, &stock.symbol,
&stock.symbol, other,
other, lookback,
lookback, )
) .or_else(|| {
}, precomputed_stock_rolling_mean(&stock.extra_factors, other, lookback)
) })
} }
}; };
value.ok_or_else(|| { value.ok_or_else(|| {
@@ -5095,11 +5081,10 @@ impl PlatformExprStrategy {
let max_volume_ratio = self let max_volume_ratio = self
.prelude_numeric_constant("max_volume_ratio") .prelude_numeric_constant("max_volume_ratio")
.unwrap_or(1.0); .unwrap_or(1.0);
let volume_ma100 = precomputed_stock_rolling_mean(&stock.extra_factors, "volume", 100) let volume_ma100 = ctx
.or_else(|| { .data
ctx.data .market_decision_volume_moving_average(day.date, &stock.symbol, 100)
.market_decision_volume_moving_average(day.date, &stock.symbol, 100) .or_else(|| precomputed_stock_rolling_mean(&stock.extra_factors, "volume", 100))
})
.unwrap_or(f64::NAN); .unwrap_or(f64::NAN);
Some( Some(
@@ -7762,6 +7747,10 @@ mod tests {
.stock_state_with_factor_date(&ctx, current, current, symbol) .stock_state_with_factor_date(&ctx, current, current, symbol)
.expect("stock state"); .expect("stock state");
assert!((stock.stock_ma5 - 9.9).abs() < 1e-9);
assert!((stock.stock_ma10 - 9.9).abs() < 1e-9);
assert!((stock.stock_ma30 - 9.9).abs() < 1e-9);
assert!( assert!(
!strategy !strategy
.stock_passes_expr(&ctx, &day, &stock) .stock_passes_expr(&ctx, &day, &stock)
+231 -3
View File
@@ -384,6 +384,115 @@ fn apply_cost_overrides(
} }
} }
fn parse_usize_after(text: &str, start: usize) -> Option<(usize, usize)> {
let bytes = text.as_bytes();
let mut end = start;
while end < bytes.len() && bytes[end].is_ascii_digit() {
end += 1;
}
if end == start {
return None;
}
text[start..end]
.parse::<usize>()
.ok()
.filter(|value| *value > 0)
.map(|value| (value, end))
}
fn prefixed_ma_lookbacks(expr: &str, prefix: &str) -> Vec<usize> {
let lower = expr.to_ascii_lowercase();
let mut values = Vec::new();
let mut cursor = 0;
while let Some(offset) = lower[cursor..].find(prefix) {
let start = cursor + offset + prefix.len();
if let Some((value, end)) = parse_usize_after(&lower, start) {
values.push(value);
cursor = end;
} else {
cursor = start;
}
}
values
}
fn compact_ascii_whitespace(value: &str) -> String {
value
.chars()
.filter(|ch| !ch.is_ascii_whitespace())
.collect::<String>()
.to_ascii_lowercase()
}
fn rolling_mean_lookbacks(expr: &str, field: &str) -> Vec<usize> {
let compact = compact_ascii_whitespace(expr);
let patterns = [
format!("rolling_mean(\"{field}\","),
format!("rolling_mean('{field}',"),
];
let mut values = Vec::new();
for pattern in patterns {
let mut cursor = 0;
while let Some(offset) = compact[cursor..].find(&pattern) {
let start = cursor + offset + pattern.len();
if let Some((value, end)) = parse_usize_after(&compact, start) {
values.push(value);
cursor = end;
} else {
cursor = start;
}
}
}
values
}
fn sorted_unique_positive(mut values: Vec<usize>) -> Vec<usize> {
values.retain(|value| *value > 0);
values.sort_unstable();
values.dedup();
values
}
fn infer_expression_windows(
cfg: &mut PlatformExprStrategyConfig,
benchmark_short_explicit: bool,
benchmark_long_explicit: bool,
stock_short_explicit: bool,
stock_mid_explicit: bool,
stock_long_explicit: bool,
) {
let mut benchmark_days = Vec::new();
for expr in [&cfg.exposure_expr, &cfg.buy_scale_expr] {
benchmark_days.extend(prefixed_ma_lookbacks(expr, "benchmark_ma"));
benchmark_days.extend(rolling_mean_lookbacks(expr, "benchmark_close"));
}
let benchmark_days = sorted_unique_positive(benchmark_days);
if !benchmark_short_explicit && let Some(short) = benchmark_days.first().copied() {
cfg.benchmark_short_ma_days = short;
}
if !benchmark_long_explicit && let Some(long) = benchmark_days.last().copied() {
cfg.benchmark_long_ma_days = long;
}
let mut stock_days = Vec::new();
for expr in [&cfg.stock_filter_expr, &cfg.buy_scale_expr] {
stock_days.extend(prefixed_ma_lookbacks(expr, "stock_ma"));
stock_days.extend(rolling_mean_lookbacks(expr, "close"));
}
let stock_days = sorted_unique_positive(stock_days);
if !stock_short_explicit && let Some(short) = stock_days.first().copied() {
cfg.stock_short_ma_days = short;
}
if !stock_mid_explicit {
if let Some(mid) = stock_days.get(1).copied() {
cfg.stock_mid_ma_days = mid;
}
}
if !stock_long_explicit && let Some(long) = stock_days.last().copied() {
cfg.stock_long_ma_days = long;
}
}
pub fn platform_expr_config_from_spec( pub fn platform_expr_config_from_spec(
strategy_id: &str, strategy_id: &str,
signal_symbol: &str, signal_symbol: &str,
@@ -398,6 +507,11 @@ pub fn platform_expr_config_from_spec(
let Some(spec) = strategy_spec else { let Some(spec) = strategy_spec else {
return cfg; return cfg;
}; };
let mut benchmark_short_explicit = false;
let mut benchmark_long_explicit = false;
let mut stock_short_explicit = false;
let mut stock_mid_explicit = false;
let mut stock_long_explicit = false;
if let Some(spec_strategy_id) = spec if let Some(spec_strategy_id) = spec
.strategy_id .strategy_id
@@ -437,20 +551,25 @@ pub fn platform_expr_config_from_spec(
if let Some(stock_ma_filter) = engine.stock_ma_filter.as_ref() { if let Some(stock_ma_filter) = engine.stock_ma_filter.as_ref() {
if let Some(days) = stock_ma_filter.short_days.filter(|value| *value > 0) { if let Some(days) = stock_ma_filter.short_days.filter(|value| *value > 0) {
cfg.stock_short_ma_days = days; cfg.stock_short_ma_days = days;
stock_short_explicit = true;
} }
if let Some(days) = stock_ma_filter.mid_days.filter(|value| *value > 0) { if let Some(days) = stock_ma_filter.mid_days.filter(|value| *value > 0) {
cfg.stock_mid_ma_days = days; cfg.stock_mid_ma_days = days;
stock_mid_explicit = true;
} }
if let Some(days) = stock_ma_filter.long_days.filter(|value| *value > 0) { if let Some(days) = stock_ma_filter.long_days.filter(|value| *value > 0) {
cfg.stock_long_ma_days = days; cfg.stock_long_ma_days = days;
stock_long_explicit = true;
} }
} }
if let Some(index_throttle) = engine.index_throttle.as_ref() { if let Some(index_throttle) = engine.index_throttle.as_ref() {
if let Some(days) = index_throttle.short_days.filter(|value| *value > 0) { if let Some(days) = index_throttle.short_days.filter(|value| *value > 0) {
cfg.benchmark_short_ma_days = days; cfg.benchmark_short_ma_days = days;
benchmark_short_explicit = true;
} }
if let Some(days) = index_throttle.long_days.filter(|value| *value > 0) { if let Some(days) = index_throttle.long_days.filter(|value| *value > 0) {
cfg.benchmark_long_ma_days = days; cfg.benchmark_long_ma_days = days;
benchmark_long_explicit = true;
} }
} }
if !engine.skip_windows.is_empty() { if !engine.skip_windows.is_empty() {
@@ -774,20 +893,42 @@ pub fn platform_expr_config_from_spec(
cfg.selection_limit_expr = cfg.max_positions.to_string(); cfg.selection_limit_expr = cfg.max_positions.to_string();
} }
infer_expression_windows(
&mut cfg,
benchmark_short_explicit,
benchmark_long_explicit,
stock_short_explicit,
stock_mid_explicit,
stock_long_explicit,
);
if !cfg.signal_symbol.trim().is_empty() { if !cfg.signal_symbol.trim().is_empty() {
cfg.signal_symbol = normalize_symbol(&cfg.signal_symbol, None); cfg.signal_symbol = normalize_symbol(&cfg.signal_symbol, None);
} }
if !cfg.benchmark_symbol.trim().is_empty() { if !cfg.benchmark_symbol.trim().is_empty() {
cfg.benchmark_symbol = normalize_symbol(&cfg.benchmark_symbol, None); cfg.benchmark_symbol = normalize_symbol(&cfg.benchmark_symbol, None);
} }
if spec let aiquant_compat = spec
.execution .execution
.as_ref() .as_ref()
.and_then(|execution| execution.compatibility_profile.as_deref()) .and_then(|execution| execution.compatibility_profile.as_deref())
.map(|value| value.trim().to_ascii_lowercase()) .map(|value| value.trim().to_ascii_lowercase())
.is_some_and(|value| value == "aiquant_rqalpha" || value == "aiquant") .is_some_and(|value| value == "aiquant_rqalpha" || value == "aiquant");
{ if aiquant_compat {
cfg.aiquant_transaction_cost = true; cfg.aiquant_transaction_cost = true;
let trading = spec
.runtime_expressions
.as_ref()
.and_then(|runtime_expr| runtime_expr.trading.as_ref());
if trading.and_then(|item| item.daily_top_up).is_none() {
cfg.daily_top_up_enabled = true;
}
if trading
.and_then(|item| item.retry_empty_rebalance)
.is_none()
{
cfg.retry_empty_rebalance = true;
}
} }
if let Some(execution) = spec.execution.as_ref() { if let Some(execution) = spec.execution.as_ref() {
apply_cost_overrides( apply_cost_overrides(
@@ -1250,6 +1391,93 @@ mod tests {
assert_eq!(cfg.stamp_tax_rate_after_change, Some(0.0005)); assert_eq!(cfg.stamp_tax_rate_after_change, Some(0.0005));
} }
#[test]
fn aiquant_profile_defaults_to_daily_top_up_and_empty_retry() {
let spec = serde_json::json!({
"execution": {
"compatibilityProfile": "aiquant_rqalpha"
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert!(cfg.aiquant_transaction_cost);
assert!(cfg.daily_top_up_enabled);
assert!(cfg.retry_empty_rebalance);
let explicit_off = serde_json::json!({
"execution": {
"compatibilityProfile": "aiquant_rqalpha"
},
"runtimeExpressions": {
"trading": {
"dailyTopUp": false,
"retryEmptyRebalance": false
}
}
});
let cfg = platform_expr_config_from_value("", "", &explicit_off).expect("config");
assert!(!cfg.daily_top_up_enabled);
assert!(!cfg.retry_empty_rebalance);
}
#[test]
fn runtime_expressions_infer_ma_windows_from_literal_strategy_logic() {
let spec = serde_json::json!({
"execution": {
"compatibilityProfile": "aiquant_rqalpha"
},
"runtimeExpressions": {
"selection": {
"stockFilterExpr": "rolling_mean(\"close\", 5) > rolling_mean(\"close\", 10) && rolling_mean(\"close\", 10) > rolling_mean(\"close\", 30)"
},
"risk": {
"exposureExpr": "benchmark_ma5 > benchmark_ma20 ? 1.0 : weak_market_trade_rate"
}
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert_eq!(cfg.benchmark_short_ma_days, 5);
assert_eq!(cfg.benchmark_long_ma_days, 20);
assert_eq!(cfg.stock_short_ma_days, 5);
assert_eq!(cfg.stock_mid_ma_days, 10);
assert_eq!(cfg.stock_long_ma_days, 30);
let explicit = serde_json::json!({
"engineConfig": {
"stockMaFilter": {
"shortDays": 4,
"midDays": 9,
"longDays": 21
},
"indexThrottle": {
"shortDays": 3,
"longDays": 13
}
},
"runtimeExpressions": {
"selection": {
"stockFilterExpr": "rolling_mean(\"close\", 5) > rolling_mean(\"close\", 10) && rolling_mean(\"close\", 10) > rolling_mean(\"close\", 30)"
},
"risk": {
"exposureExpr": "benchmark_ma5 > benchmark_ma20 ? 1.0 : 0.5"
}
}
});
let cfg = platform_expr_config_from_value("", "", &explicit).expect("config");
assert_eq!(cfg.benchmark_short_ma_days, 3);
assert_eq!(cfg.benchmark_long_ma_days, 13);
assert_eq!(cfg.stock_short_ma_days, 4);
assert_eq!(cfg.stock_mid_ma_days, 9);
assert_eq!(cfg.stock_long_ma_days, 21);
}
#[test] #[test]
fn parses_daily_schedule_time_for_aiquant_execution_quotes() { fn parses_daily_schedule_time_for_aiquant_execution_quotes() {
let spec = serde_json::json!({ let spec = serde_json::json!({
@@ -1658,7 +1658,7 @@ fn broker_applies_tick_size_slippage_on_intraday_last_fills() {
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote {
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
last_price: 10.0, last_price: 10.0,
bid1: 9.99, bid1: 9.99,
ask1: 10.01, ask1: 10.01,
@@ -1804,7 +1804,7 @@ fn broker_rejects_intraday_last_order_without_execution_quotes() {
assert!( assert!(
report.order_events[0] report.order_events[0]
.reason .reason
.contains("no execution quotes after start") .contains("no execution quotes at or before start")
); );
assert!(portfolio.position("000002.SZ").is_none()); assert!(portfolio.position("000002.SZ").is_none());
} }
@@ -1993,7 +1993,7 @@ fn broker_cancels_market_order_remainder_when_intraday_quote_liquidity_exhausted
vec![IntradayExecutionQuote { vec![IntradayExecutionQuote {
date, date,
symbol: "000002.SZ".to_string(), symbol: "000002.SZ".to_string(),
timestamp: date.and_hms_opt(10, 18, 3).unwrap(), timestamp: date.and_hms_opt(10, 18, 0).unwrap(),
last_price: 10.02, last_price: 10.02,
bid1: 10.01, bid1: 10.01,
ask1: 10.03, ask1: 10.03,