fix: accept scoped blacklist runtime context
This commit is contained in:
@@ -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!({
|
||||||
|
|||||||
Reference in New Issue
Block a user