fix: validate price risk on every execution leg before and after slippage
This commit is contained in:
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user