修正回测指标和成交时间口径
This commit is contained in:
@@ -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")
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user