拒绝非法回测成交量比例配置

This commit is contained in:
boris
2026-07-03 04:30:40 +08:00
parent 564a2fb9b2
commit 32b6da5aca
+56 -22
View File
@@ -668,18 +668,17 @@ fn valid_non_negative(value: Option<f64>) -> Option<f64> {
value.filter(|item| item.is_finite() && *item >= 0.0) value.filter(|item| item.is_finite() && *item >= 0.0)
} }
fn normalize_percent_ratio(value: Option<f64>) -> Option<f64> { fn normalize_percent_ratio(value: f64, field_name: &str) -> Result<f64, String> {
value if !value.is_finite() || value <= 0.0 {
.filter(|item| item.is_finite() && *item > 0.0) return Err(format!("{field_name} must be a positive finite number"));
.and_then(|item| { }
if item <= 1.0 { if value <= 1.0 {
Some(item) Ok(value)
} else if item <= 100.0 { } else if value <= 100.0 {
Some(item / 100.0) Ok(value / 100.0)
} else { } else {
None Err(format!("{field_name} must be <= 100"))
} }
})
} }
fn parse_policy_date(value: Option<&str>) -> Option<NaiveDate> { fn parse_policy_date(value: Option<&str>) -> Option<NaiveDate> {
@@ -734,27 +733,29 @@ fn apply_flat_risk_overrides(
volume_limit: Option<bool>, volume_limit: Option<bool>,
liquidity_limit: Option<bool>, liquidity_limit: Option<bool>,
volume_percent: Option<f64>, volume_percent: Option<f64>,
) { ) -> Result<(), String> {
if let Some(enabled) = volume_limit { if let Some(enabled) = volume_limit {
cfg.risk_config.trading_constraints.volume_limit_enabled = enabled; cfg.risk_config.trading_constraints.volume_limit_enabled = enabled;
} }
if let Some(enabled) = liquidity_limit { if let Some(enabled) = liquidity_limit {
cfg.risk_config.trading_constraints.liquidity_limit_enabled = enabled; cfg.risk_config.trading_constraints.liquidity_limit_enabled = enabled;
} }
if let Some(value) = normalize_percent_ratio(volume_percent) { if let Some(raw_value) = volume_percent {
let value = normalize_percent_ratio(raw_value, "volume_percent")?;
cfg.risk_config.trading_constraints.volume_percent = value; cfg.risk_config.trading_constraints.volume_percent = value;
} }
cfg.quote_quantity_limit = cfg.risk_config.trading_constraints.volume_limit_enabled cfg.quote_quantity_limit = cfg.risk_config.trading_constraints.volume_limit_enabled
|| cfg.risk_config.trading_constraints.liquidity_limit_enabled || cfg.risk_config.trading_constraints.liquidity_limit_enabled
|| cfg.matching_type != MatchingType::MinuteLast; || cfg.matching_type != MatchingType::MinuteLast;
Ok(())
} }
fn apply_risk_policy_overrides( fn apply_risk_policy_overrides(
cfg: &mut PlatformExprStrategyConfig, cfg: &mut PlatformExprStrategyConfig,
policy: Option<&StrategyRiskPolicySpec>, policy: Option<&StrategyRiskPolicySpec>,
) { ) -> Result<(), String> {
let Some(policy) = policy else { let Some(policy) = policy else {
return; return Ok(());
}; };
let static_rules = &mut cfg.risk_config.static_rules; let static_rules = &mut cfg.risk_config.static_rules;
if let Some(value) = policy.reject_st_selection { if let Some(value) = policy.reject_st_selection {
@@ -834,7 +835,7 @@ fn apply_risk_policy_overrides(
policy.volume_limit_enabled, policy.volume_limit_enabled,
policy.liquidity_limit_enabled, policy.liquidity_limit_enabled,
policy.volume_percent, policy.volume_percent,
); )?;
apply_cost_overrides( apply_cost_overrides(
cfg, cfg,
policy.commission_rate, policy.commission_rate,
@@ -843,6 +844,7 @@ fn apply_risk_policy_overrides(
policy.stamp_tax_rate_after_change, policy.stamp_tax_rate_after_change,
policy.stamp_tax_change_date.as_deref(), policy.stamp_tax_change_date.as_deref(),
); );
Ok(())
} }
fn normalize_model_name(value: &str) -> String { fn normalize_model_name(value: &str) -> String {
@@ -1026,7 +1028,7 @@ fn infer_expression_windows(
stock_short_explicit: bool, stock_short_explicit: bool,
stock_mid_explicit: bool, stock_mid_explicit: bool,
stock_long_explicit: bool, stock_long_explicit: bool,
) { ) -> Result<(), String> {
let mut benchmark_days = Vec::new(); let mut benchmark_days = Vec::new();
for expr in [&cfg.exposure_expr, &cfg.buy_scale_expr] { for expr in [&cfg.exposure_expr, &cfg.buy_scale_expr] {
benchmark_days.extend(prefixed_ma_lookbacks(expr, "benchmark_ma")); benchmark_days.extend(prefixed_ma_lookbacks(expr, "benchmark_ma"));
@@ -1057,6 +1059,7 @@ fn infer_expression_windows(
if !stock_long_explicit && let Some(long) = stock_days.last().copied() { if !stock_long_explicit && let Some(long) = stock_days.last().copied() {
cfg.stock_long_ma_days = long; cfg.stock_long_ma_days = long;
} }
Ok(())
} }
pub fn platform_expr_config_from_spec( pub fn platform_expr_config_from_spec(
@@ -1188,8 +1191,8 @@ pub fn platform_expr_config_from_spec(
engine.volume_limit, engine.volume_limit,
engine.liquidity_limit, engine.liquidity_limit,
engine.volume_percent, engine.volume_percent,
); )?;
apply_risk_policy_overrides(&mut cfg, engine.risk_policy.as_ref()); apply_risk_policy_overrides(&mut cfg, engine.risk_policy.as_ref())?;
apply_execution_behavior_overrides( apply_execution_behavior_overrides(
&mut cfg, &mut cfg,
engine.matching_type.as_deref(), engine.matching_type.as_deref(),
@@ -1531,7 +1534,7 @@ pub fn platform_expr_config_from_spec(
stock_short_explicit, stock_short_explicit,
stock_mid_explicit, stock_mid_explicit,
stock_long_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);
@@ -1597,8 +1600,8 @@ pub fn platform_expr_config_from_spec(
execution.volume_limit, execution.volume_limit,
execution.liquidity_limit, execution.liquidity_limit,
execution.volume_percent, execution.volume_percent,
); )?;
apply_risk_policy_overrides(&mut cfg, execution.risk_policy.as_ref()); apply_risk_policy_overrides(&mut cfg, execution.risk_policy.as_ref())?;
apply_execution_behavior_overrides( apply_execution_behavior_overrides(
&mut cfg, &mut cfg,
execution.matching_type.as_deref(), execution.matching_type.as_deref(),
@@ -2258,6 +2261,37 @@ mod tests {
assert_eq!(cfg.matching_type, MatchingType::MinuteLast); assert_eq!(cfg.matching_type, MatchingType::MinuteLast);
} }
#[test]
fn rejects_invalid_explicit_volume_percent_in_platform_config() {
let engine_value = serde_json::json!({
"engine_config": {
"volume_percent": 101
}
});
let engine_error = platform_expr_config_from_value("", "", &engine_value)
.expect_err("volume_percent above 100 must fail");
assert!(
engine_error
.to_string()
.contains("volume_percent must be <= 100")
);
let policy_value = serde_json::json!({
"execution": {
"riskPolicy": {
"volumePercent": 0
}
}
});
let policy_error = platform_expr_config_from_value("", "", &policy_value)
.expect_err("zero riskPolicy volumePercent must fail");
assert!(
policy_error
.to_string()
.contains("volume_percent must be a positive finite number")
);
}
#[test] #[test]
fn parses_execution_slippage_overrides_into_platform_config() { fn parses_execution_slippage_overrides_into_platform_config() {
let spec = serde_json::json!({ let spec = serde_json::json!({