拒绝非法回测成交量比例配置
This commit is contained in:
@@ -668,18 +668,17 @@ fn valid_non_negative(value: Option<f64>) -> Option<f64> {
|
||||
value.filter(|item| item.is_finite() && *item >= 0.0)
|
||||
}
|
||||
|
||||
fn normalize_percent_ratio(value: Option<f64>) -> Option<f64> {
|
||||
value
|
||||
.filter(|item| item.is_finite() && *item > 0.0)
|
||||
.and_then(|item| {
|
||||
if item <= 1.0 {
|
||||
Some(item)
|
||||
} else if item <= 100.0 {
|
||||
Some(item / 100.0)
|
||||
} else {
|
||||
None
|
||||
fn normalize_percent_ratio(value: f64, field_name: &str) -> Result<f64, String> {
|
||||
if !value.is_finite() || value <= 0.0 {
|
||||
return Err(format!("{field_name} must be a positive finite number"));
|
||||
}
|
||||
if value <= 1.0 {
|
||||
Ok(value)
|
||||
} else if value <= 100.0 {
|
||||
Ok(value / 100.0)
|
||||
} else {
|
||||
Err(format!("{field_name} must be <= 100"))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_policy_date(value: Option<&str>) -> Option<NaiveDate> {
|
||||
@@ -734,27 +733,29 @@ fn apply_flat_risk_overrides(
|
||||
volume_limit: Option<bool>,
|
||||
liquidity_limit: Option<bool>,
|
||||
volume_percent: Option<f64>,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
if let Some(enabled) = volume_limit {
|
||||
cfg.risk_config.trading_constraints.volume_limit_enabled = enabled;
|
||||
}
|
||||
if let Some(enabled) = liquidity_limit {
|
||||
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.quote_quantity_limit = cfg.risk_config.trading_constraints.volume_limit_enabled
|
||||
|| cfg.risk_config.trading_constraints.liquidity_limit_enabled
|
||||
|| cfg.matching_type != MatchingType::MinuteLast;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_risk_policy_overrides(
|
||||
cfg: &mut PlatformExprStrategyConfig,
|
||||
policy: Option<&StrategyRiskPolicySpec>,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
let Some(policy) = policy else {
|
||||
return;
|
||||
return Ok(());
|
||||
};
|
||||
let static_rules = &mut cfg.risk_config.static_rules;
|
||||
if let Some(value) = policy.reject_st_selection {
|
||||
@@ -834,7 +835,7 @@ fn apply_risk_policy_overrides(
|
||||
policy.volume_limit_enabled,
|
||||
policy.liquidity_limit_enabled,
|
||||
policy.volume_percent,
|
||||
);
|
||||
)?;
|
||||
apply_cost_overrides(
|
||||
cfg,
|
||||
policy.commission_rate,
|
||||
@@ -843,6 +844,7 @@ fn apply_risk_policy_overrides(
|
||||
policy.stamp_tax_rate_after_change,
|
||||
policy.stamp_tax_change_date.as_deref(),
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn normalize_model_name(value: &str) -> String {
|
||||
@@ -1026,7 +1028,7 @@ fn infer_expression_windows(
|
||||
stock_short_explicit: bool,
|
||||
stock_mid_explicit: bool,
|
||||
stock_long_explicit: bool,
|
||||
) {
|
||||
) -> Result<(), String> {
|
||||
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"));
|
||||
@@ -1057,6 +1059,7 @@ fn infer_expression_windows(
|
||||
if !stock_long_explicit && let Some(long) = stock_days.last().copied() {
|
||||
cfg.stock_long_ma_days = long;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn platform_expr_config_from_spec(
|
||||
@@ -1188,8 +1191,8 @@ pub fn platform_expr_config_from_spec(
|
||||
engine.volume_limit,
|
||||
engine.liquidity_limit,
|
||||
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(
|
||||
&mut cfg,
|
||||
engine.matching_type.as_deref(),
|
||||
@@ -1531,7 +1534,7 @@ pub fn platform_expr_config_from_spec(
|
||||
stock_short_explicit,
|
||||
stock_mid_explicit,
|
||||
stock_long_explicit,
|
||||
);
|
||||
)?;
|
||||
|
||||
if !cfg.signal_symbol.trim().is_empty() {
|
||||
cfg.signal_symbol = normalize_symbol(&cfg.signal_symbol, None);
|
||||
@@ -1597,8 +1600,8 @@ pub fn platform_expr_config_from_spec(
|
||||
execution.volume_limit,
|
||||
execution.liquidity_limit,
|
||||
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(
|
||||
&mut cfg,
|
||||
execution.matching_type.as_deref(),
|
||||
@@ -2258,6 +2261,37 @@ mod tests {
|
||||
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]
|
||||
fn parses_execution_slippage_overrides_into_platform_config() {
|
||||
let spec = serde_json::json!({
|
||||
|
||||
Reference in New Issue
Block a user