收紧风控策略别名冲突校验

This commit is contained in:
boris
2026-07-04 20:35:06 +08:00
parent e8ecc037c9
commit cb02041b3b
+118 -4
View File
@@ -364,6 +364,55 @@ fn parse_policy_bool(canonical: &str, value: Value) -> Result<bool, String> {
}
}
fn policy_value_semantically_equals(canonical: &str, left: &Value, right: &Value) -> bool {
if left == right {
return true;
}
if let (Some(left_number), Some(right_number)) =
(policy_value_as_number(left), policy_value_as_number(right))
{
if canonical == "volumePercent" {
return (normalize_percent_ratio_for_alias_compare(left_number)
- normalize_percent_ratio_for_alias_compare(right_number))
.abs()
< 1e-12;
}
return (left_number - right_number).abs() < 1e-12;
}
if let (Some(left_text), Some(right_text)) = (left.as_str(), right.as_str()) {
return left_text.trim() == right_text.trim();
}
false
}
fn normalize_percent_ratio_for_alias_compare(value: f64) -> f64 {
if value > 1.0 { value / 100.0 } else { value }
}
fn policy_value_as_number(value: &Value) -> Option<f64> {
match value {
Value::Number(number) => number.as_f64(),
Value::String(text) => text.trim().parse::<f64>().ok(),
_ => None,
}
}
fn reject_conflicting_policy_values(canonical: &str, values: &[Value]) -> Result<(), String> {
let Some(first) = values.first() else {
return Ok(());
};
if values
.iter()
.skip(1)
.any(|value| !policy_value_semantically_equals(canonical, first, value))
{
return Err(format!(
"riskPolicy.{canonical} has conflicting alias values"
));
}
Ok(())
}
fn push_blacklist_symbols_from_str(
raw: &str,
symbols: &mut Vec<Value>,
@@ -384,17 +433,25 @@ fn normalize_risk_policy_object_aliases(
for (canonical, aliases) in RISK_POLICY_BOOL_ALIAS_GROUPS {
let values = remove_policy_alias_values(object, canonical, aliases);
if !values.is_empty() {
let mut merged = false;
let mut parsed_values = Vec::with_capacity(values.len());
for value in values {
merged |= parse_policy_bool(canonical, value)?;
parsed_values.push(parse_policy_bool(canonical, value)?);
}
object.insert((*canonical).to_string(), Value::Bool(merged));
let first = parsed_values[0];
if parsed_values.iter().any(|value| *value != first) {
return Err(format!(
"riskPolicy.{canonical} has conflicting alias values"
));
}
object.insert((*canonical).to_string(), Value::Bool(first));
}
}
for (canonical, aliases) in RISK_POLICY_VALUE_ALIAS_GROUPS {
let values = remove_policy_alias_values(object, canonical, aliases);
if let Some(value) = values.into_iter().next() {
if !values.is_empty() {
reject_conflicting_policy_values(canonical, &values)?;
let value = values.into_iter().next().expect("non-empty values");
object.insert((*canonical).to_string(), value);
}
}
@@ -2423,6 +2480,63 @@ mod tests {
);
}
#[test]
fn accepts_equivalent_risk_policy_alias_values() {
let spec = serde_json::json!({
"engineConfig": {
"riskPolicy": {
"rejectStBuy": false,
"reject_st_buy": 0,
"volumePercent": 0.25,
"volume_percent": 25,
"minimumCommission": 5,
"minimum_commission": "5"
}
}
});
let cfg = platform_expr_config_from_value("", "", &spec).expect("config");
assert!(!cfg.risk_config.static_rules.reject_st_buy);
assert!((cfg.risk_config.trading_constraints.volume_percent - 0.25).abs() < 1e-12);
assert_eq!(cfg.risk_config.trading_constraints.minimum_commission, 5.0);
}
#[test]
fn rejects_conflicting_risk_policy_alias_values() {
let bool_conflict = serde_json::json!({
"engineConfig": {
"riskPolicy": {
"rejectStBuy": false,
"reject_st_buy": true
}
}
});
let bool_error = platform_expr_config_from_value("", "", &bool_conflict)
.expect_err("conflicting bool aliases must fail");
assert!(
bool_error
.to_string()
.contains("riskPolicy.rejectStBuy has conflicting alias values")
);
let value_conflict = serde_json::json!({
"engineConfig": {
"riskPolicy": {
"minimumCommission": 5,
"minimum_commission": 6
}
}
});
let value_error = platform_expr_config_from_value("", "", &value_conflict)
.expect_err("conflicting value aliases must fail");
assert!(
value_error
.to_string()
.contains("riskPolicy.minimumCommission has conflicting alias values")
);
}
#[test]
fn parses_execution_slippage_overrides_into_platform_config() {
let spec = serde_json::json!({