From cf1b60c996b98da557f3557faf05cfa681533159 Mon Sep 17 00:00:00 2001 From: boris Date: Mon, 31 Aug 2026 09:40:49 +0800 Subject: [PATCH] fix: accept scoped blacklist runtime context --- .../fidc-core/src/platform_strategy_spec.rs | 80 ++++++++++++++----- 1 file changed, 62 insertions(+), 18 deletions(-) diff --git a/crates/fidc-core/src/platform_strategy_spec.rs b/crates/fidc-core/src/platform_strategy_spec.rs index ba89260..3917c5c 100644 --- a/crates/fidc-core/src/platform_strategy_spec.rs +++ b/crates/fidc-core/src/platform_strategy_spec.rs @@ -336,6 +336,15 @@ pub struct StrategyRiskPolicySpec { alias = "blacklist" )] pub blacklisted_symbols: Vec, + /// 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>, + #[serde(default, alias = "strategyBlacklistedInstruments")] + pub strategy_blacklisted_instruments: BTreeMap>, #[serde( default, alias = "volume_limit_enabled", @@ -675,8 +684,14 @@ const STRATEGY_ALIAS_GROUPS: &[(&str, &[&str])] = &[ ("matchingType", &["matching_type"]), ("slippageModel", &["slippage_model"]), ("slippageValue", &["slippage_value"]), - ("slippageImpactCoefficient", &["slippage_impact_coefficient"]), - ("slippageVolatilityCoefficient", &["slippage_volatility_coefficient"]), + ( + "slippageImpactCoefficient", + &["slippage_impact_coefficient"], + ), + ( + "slippageVolatilityCoefficient", + &["slippage_volatility_coefficient"], + ), ( "slippageMaxValue", &["slippage_max_value", "slippage_max_rate"], @@ -688,7 +703,10 @@ const STRATEGY_ALIAS_GROUPS: &[(&str, &[&str])] = &[ ), ("transferFeeRate", &["transfer_fee_rate", "transferFeeRate"]), ("stampTaxRate", &["stamp_tax_rate"]), - ("stampTaxRateBeforeChange", &["stamp_tax_rate_before_change"]), + ( + "stampTaxRateBeforeChange", + &["stamp_tax_rate_before_change"], + ), ("stampTaxRateAfterChange", &["stamp_tax_rate_after_change"]), ("stampTaxChangeDate", &["stamp_tax_change_date"]), ("volumeLimit", &["volume_limit"]), @@ -1080,6 +1098,10 @@ pub fn validate_strategy_risk_policy_fields(value: &Value) -> Result<(), String> "blacklistedInstruments", "blacklisted_instruments", "blacklist", + "accountBlacklistedInstruments", + "account_blacklisted_instruments", + "strategyBlacklistedInstruments", + "strategy_blacklisted_instruments", // Legacy execution aliases are accepted by StrategyExecutionSpec and // normalized into the same shared switches. "volumeLimit", @@ -1295,18 +1317,12 @@ fn apply_risk_policy_overrides( let Some(policy) = policy else { return Ok(()); }; - let max_order_quantity = valid_positive_limit( - policy.max_order_quantity, - "riskPolicy.maxOrderQuantity", - )?; - let max_order_notional = valid_positive_limit( - policy.max_order_notional, - "riskPolicy.maxOrderNotional", - )?; - let max_symbol_position = valid_positive_limit( - policy.max_symbol_position, - "riskPolicy.maxSymbolPosition", - )?; + let max_order_quantity = + valid_positive_limit(policy.max_order_quantity, "riskPolicy.maxOrderQuantity")?; + let max_order_notional = + valid_positive_limit(policy.max_order_notional, "riskPolicy.maxOrderNotional")?; + let max_symbol_position = + valid_positive_limit(policy.max_symbol_position, "riskPolicy.maxSymbolPosition")?; if let Some(value) = max_order_quantity { 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.liquidity_limit_enabled); - assert_eq!(cfg.risk_config.trading_constraints.max_order_quantity, 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_eq!( + cfg.risk_config.trading_constraints.max_order_quantity, + 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_eq!( cfg.risk_config @@ -3647,6 +3672,25 @@ mod tests { 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] fn rejects_conflicting_risk_policy_alias_values() { let bool_conflict = serde_json::json!({