fix: accept scoped blacklist runtime context

This commit is contained in:
boris
2026-08-31 09:40:49 +08:00
parent d0ca09d4d8
commit cf1b60c996
+62 -18
View File
@@ -336,6 +336,15 @@ pub struct StrategyRiskPolicySpec {
alias = "blacklist" alias = "blacklist"
)] )]
pub blacklisted_symbols: Vec<String>, pub blacklisted_symbols: Vec<String>,
/// Account- and strategy-scoped blacklist facts are injected by the
/// trading platform before runtime execution. They are typed here so the
/// shared contract validator accepts the context without silently
/// discarding malformed values; the trading risk layer applies the
/// account/strategy match with the actual execution identity.
#[serde(default, alias = "accountBlacklistedInstruments")]
pub account_blacklisted_instruments: BTreeMap<String, BTreeSet<String>>,
#[serde(default, alias = "strategyBlacklistedInstruments")]
pub strategy_blacklisted_instruments: BTreeMap<String, BTreeSet<String>>,
#[serde( #[serde(
default, default,
alias = "volume_limit_enabled", alias = "volume_limit_enabled",
@@ -675,8 +684,14 @@ const STRATEGY_ALIAS_GROUPS: &[(&str, &[&str])] = &[
("matchingType", &["matching_type"]), ("matchingType", &["matching_type"]),
("slippageModel", &["slippage_model"]), ("slippageModel", &["slippage_model"]),
("slippageValue", &["slippage_value"]), ("slippageValue", &["slippage_value"]),
("slippageImpactCoefficient", &["slippage_impact_coefficient"]), (
("slippageVolatilityCoefficient", &["slippage_volatility_coefficient"]), "slippageImpactCoefficient",
&["slippage_impact_coefficient"],
),
(
"slippageVolatilityCoefficient",
&["slippage_volatility_coefficient"],
),
( (
"slippageMaxValue", "slippageMaxValue",
&["slippage_max_value", "slippage_max_rate"], &["slippage_max_value", "slippage_max_rate"],
@@ -688,7 +703,10 @@ const STRATEGY_ALIAS_GROUPS: &[(&str, &[&str])] = &[
), ),
("transferFeeRate", &["transfer_fee_rate", "transferFeeRate"]), ("transferFeeRate", &["transfer_fee_rate", "transferFeeRate"]),
("stampTaxRate", &["stamp_tax_rate"]), ("stampTaxRate", &["stamp_tax_rate"]),
("stampTaxRateBeforeChange", &["stamp_tax_rate_before_change"]), (
"stampTaxRateBeforeChange",
&["stamp_tax_rate_before_change"],
),
("stampTaxRateAfterChange", &["stamp_tax_rate_after_change"]), ("stampTaxRateAfterChange", &["stamp_tax_rate_after_change"]),
("stampTaxChangeDate", &["stamp_tax_change_date"]), ("stampTaxChangeDate", &["stamp_tax_change_date"]),
("volumeLimit", &["volume_limit"]), ("volumeLimit", &["volume_limit"]),
@@ -1080,6 +1098,10 @@ pub fn validate_strategy_risk_policy_fields(value: &Value) -> Result<(), String>
"blacklistedInstruments", "blacklistedInstruments",
"blacklisted_instruments", "blacklisted_instruments",
"blacklist", "blacklist",
"accountBlacklistedInstruments",
"account_blacklisted_instruments",
"strategyBlacklistedInstruments",
"strategy_blacklisted_instruments",
// Legacy execution aliases are accepted by StrategyExecutionSpec and // Legacy execution aliases are accepted by StrategyExecutionSpec and
// normalized into the same shared switches. // normalized into the same shared switches.
"volumeLimit", "volumeLimit",
@@ -1295,18 +1317,12 @@ fn apply_risk_policy_overrides(
let Some(policy) = policy else { let Some(policy) = policy else {
return Ok(()); return Ok(());
}; };
let max_order_quantity = valid_positive_limit( let max_order_quantity =
policy.max_order_quantity, valid_positive_limit(policy.max_order_quantity, "riskPolicy.maxOrderQuantity")?;
"riskPolicy.maxOrderQuantity", let max_order_notional =
)?; valid_positive_limit(policy.max_order_notional, "riskPolicy.maxOrderNotional")?;
let max_order_notional = valid_positive_limit( let max_symbol_position =
policy.max_order_notional, valid_positive_limit(policy.max_symbol_position, "riskPolicy.maxSymbolPosition")?;
"riskPolicy.maxOrderNotional",
)?;
let max_symbol_position = valid_positive_limit(
policy.max_symbol_position,
"riskPolicy.maxSymbolPosition",
)?;
if let Some(value) = max_order_quantity { if let Some(value) = max_order_quantity {
cfg.risk_config.trading_constraints.max_order_quantity = value; cfg.risk_config.trading_constraints.max_order_quantity = value;
} }
@@ -3433,9 +3449,18 @@ mod tests {
); );
assert!(cfg.risk_config.trading_constraints.volume_limit_enabled); assert!(cfg.risk_config.trading_constraints.volume_limit_enabled);
assert!(cfg.risk_config.trading_constraints.liquidity_limit_enabled); assert!(cfg.risk_config.trading_constraints.liquidity_limit_enabled);
assert_eq!(cfg.risk_config.trading_constraints.max_order_quantity, 8000.0); assert_eq!(
assert_eq!(cfg.risk_config.trading_constraints.max_order_notional, 2_000_000.0); cfg.risk_config.trading_constraints.max_order_quantity,
assert_eq!(cfg.risk_config.trading_constraints.max_symbol_position, 12_000.0); 8000.0
);
assert_eq!(
cfg.risk_config.trading_constraints.max_order_notional,
2_000_000.0
);
assert_eq!(
cfg.risk_config.trading_constraints.max_symbol_position,
12_000.0
);
assert!((cfg.risk_config.trading_constraints.volume_percent - 0.25).abs() < 1e-12); assert!((cfg.risk_config.trading_constraints.volume_percent - 0.25).abs() < 1e-12);
assert_eq!( assert_eq!(
cfg.risk_config cfg.risk_config
@@ -3647,6 +3672,25 @@ mod tests {
assert_eq!(cfg.risk_config.trading_constraints.minimum_commission, 5.0); assert_eq!(cfg.risk_config.trading_constraints.minimum_commission, 5.0);
} }
#[test]
fn accepts_scoped_blacklist_context_in_runtime_risk_policy() {
let spec = serde_json::json!({
"execution": {
"riskPolicy": {
"account_blacklisted_instruments": {
"2075773": ["000001.SZ"]
},
"strategyBlacklistedInstruments": {
"live-gt-2075773-20260829": ["600000.SH"]
}
}
}
});
platform_expr_config_from_value("105", "932000.CSI", &spec)
.expect("scoped blacklist context is part of the shared runtime contract");
}
#[test] #[test]
fn rejects_conflicting_risk_policy_alias_values() { fn rejects_conflicting_risk_policy_alias_values() {
let bool_conflict = serde_json::json!({ let bool_conflict = serde_json::json!({