收紧风控策略别名冲突校验
This commit is contained in:
@@ -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!({
|
||||
|
||||
Reference in New Issue
Block a user