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,
|
execution_price: f64,
|
||||||
) -> Option<&'static str> {
|
) -> Option<&'static str> {
|
||||||
if !execution_price.is_finite() || execution_price <= 0.0 {
|
if !execution_price.is_finite() || execution_price <= 0.0 {
|
||||||
return None;
|
return Some("invalid execution price");
|
||||||
}
|
}
|
||||||
match side {
|
match side {
|
||||||
|
OrderSide::Buy
|
||||||
|
if self.risk_config.static_rules.reject_one_yuan_buy
|
||||||
|
&& execution_price <= 1.0 =>
|
||||||
|
{
|
||||||
|
Some("one_yuan")
|
||||||
|
}
|
||||||
OrderSide::Buy
|
OrderSide::Buy
|
||||||
if self.risk_config.static_rules.reject_upper_limit_buy
|
if self.risk_config.static_rules.reject_upper_limit_buy
|
||||||
&& snapshot.is_at_upper_limit_price(execution_price) =>
|
&& snapshot.is_at_upper_limit_price(execution_price) =>
|
||||||
@@ -7623,6 +7629,11 @@ where
|
|||||||
else {
|
else {
|
||||||
continue;
|
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 mark_price = self.quote_mark_price(quote, raw_quote_price);
|
||||||
let remaining_qty = requested_qty.saturating_sub(filled_qty);
|
let remaining_qty = requested_qty.saturating_sub(filled_qty);
|
||||||
if remaining_qty == 0 {
|
if remaining_qty == 0 {
|
||||||
@@ -8596,6 +8607,54 @@ mod tests {
|
|||||||
assert_eq!(fill.quantity, 1_200);
|
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]
|
#[test]
|
||||||
fn minute_last_uses_volume_delta_when_level1_depth_missing() {
|
fn minute_last_uses_volume_delta_when_level1_depth_missing() {
|
||||||
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
|||||||
Reference in New Issue
Block a user