fix: validate price risk on every execution leg before and after slippage

This commit is contained in:
boris
2026-09-11 10:21:46 +08:00
parent 36833b7a6a
commit 19feec9ec9
+60 -1
View File
@@ -7318,9 +7318,15 @@ where
execution_price: f64,
) -> Option<&'static str> {
if !execution_price.is_finite() || execution_price <= 0.0 {
return None;
return Some("invalid execution price");
}
match side {
OrderSide::Buy
if self.risk_config.static_rules.reject_one_yuan_buy
&& execution_price <= 1.0 =>
{
Some("one_yuan")
}
OrderSide::Buy
if self.risk_config.static_rules.reject_upper_limit_buy
&& snapshot.is_at_upper_limit_price(execution_price) =>
@@ -7623,6 +7629,11 @@ where
else {
continue;
};
if let Some(reason) = self.execution_limit_rejection_reason(snapshot, side, raw_quote_price) {
execution_block_reason.get_or_insert(reason);
execution_block_timestamp = Some(quote.timestamp);
continue;
}
let mark_price = self.quote_mark_price(quote, raw_quote_price);
let remaining_qty = requested_qty.saturating_sub(filled_qty);
if remaining_qty == 0 {
@@ -8596,6 +8607,54 @@ mod tests {
assert_eq!(fill.quantity, 1_200);
}
#[test]
fn each_execution_leg_rechecks_one_yuan_including_slippage_and_limit_price() {
let mut snapshot = limit_test_snapshot();
snapshot.open = 1.2;
snapshot.last_price = 1.2;
snapshot.upper_limit = 2.0;
snapshot.lower_limit = 0.5;
let date = snapshot.date;
let start = date.and_hms_opt(10, 0, 0).unwrap();
let end = date.and_hms_opt(10, 2, 0).unwrap();
let mut cheap = limit_test_quote(0.9, 0.9, 0.9);
cheap.timestamp = date.and_hms_opt(10, 1, 0).unwrap();
let mut later = limit_test_quote(1.2, 1.2, 1.2);
later.timestamp = end;
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(false).with_liquidity_limit(false);
let fill = broker.select_execution_fill(
&snapshot, &[cheap.clone(), later], OrderSide::Buy, MatchingType::Vwap,
Some(start), Some(end), 100, 100, 100, 100, false, None, None, None,
).unwrap();
assert_eq!(fill.quantity, 100);
assert_eq!(fill.legs.len(), 1);
assert_eq!(fill.legs[0].execution_timestamp, Some(end));
assert_eq!(fill.legs[0].price, 1.2);
let slipped = broker.with_slippage_model(SlippageModel::PriceRatio(0.2));
let blocked = slipped.select_execution_fill(
&snapshot, &[cheap], OrderSide::Buy, MatchingType::Vwap,
Some(start), Some(end), 100, 100, 100, 100, false, None, None, None,
).unwrap();
assert_eq!(blocked.quantity, 0);
assert_eq!(blocked.unfilled_reason, Some("one_yuan"));
assert_eq!(slipped.execution_price_with_limit_slippage_or_rejection(&snapshot, OrderSide::Buy, 1.0, None), Err("one_yuan"));
let limit_broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_slippage_model(SlippageModel::LimitPrice);
assert_eq!(limit_broker.execution_price_with_limit_slippage_or_rejection(
&snapshot, OrderSide::Buy, 1.2, Some(0.9)), Err("one_yuan"));
let mut risk = FidcRiskControlConfig::default();
risk.static_rules.reject_one_yuan_buy = false;
let allowed = limit_broker.with_risk_config(risk);
assert_eq!(allowed.execution_price_with_limit_slippage_or_rejection(
&snapshot, OrderSide::Buy, 1.2, Some(0.9)), Ok(0.9));
assert_eq!(allowed.execution_limit_rejection_reason(&snapshot, OrderSide::Buy, f64::NAN), Some("invalid execution price"));
assert_eq!(allowed.execution_limit_rejection_reason(&snapshot, OrderSide::Sell, 0.9), None);
}
#[test]
fn minute_last_uses_volume_delta_when_level1_depth_missing() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");