修正执行日退市缺行情拒单原因

This commit is contained in:
boris
2026-07-08 11:07:30 +08:00
parent 2fcacb4313
commit 7951ba67e3
2 changed files with 156 additions and 12 deletions
+141 -12
View File
@@ -12,7 +12,7 @@ use crate::events::{
};
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig};
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, RiskCheckScope};
use crate::rules::{EquityRuleHooks, RuleCheck};
use crate::strategy::{
AlgoOrderStyle, OpenOrderView, OrderIntent, StrategyDecision, TargetPortfolioOrderPricing,
@@ -1878,6 +1878,63 @@ where
);
}
fn reject_missing_market_or_execution_risk_order(
&self,
report: &mut BrokerExecutionReport,
date: NaiveDate,
data: &DataSet,
symbol: &str,
side: OrderSide,
requested_quantity: u32,
reason: &str,
) {
if let Some(unavailable_reason) =
self.missing_market_execution_risk_rejection_reason(date, data, symbol, side)
{
let order_id = self.reserve_order_id();
Self::reject_unavailable_order(
report,
date,
order_id,
symbol,
side,
requested_quantity,
reason,
unavailable_reason,
false,
);
return;
}
self.reject_missing_market_snapshot_order(
report,
date,
symbol,
side,
requested_quantity,
reason,
);
}
fn missing_market_execution_risk_rejection_reason(
&self,
date: NaiveDate,
data: &DataSet,
symbol: &str,
side: OrderSide,
) -> Option<&'static str> {
let scope = match side {
OrderSide::Buy => RiskCheckScope::Buy,
OrderSide::Sell => RiskCheckScope::Sell,
};
ChinaAShareRiskControl::active_status_rejection_reason_with_config(
date,
data.candidate(date, symbol),
data.instrument(symbol),
&self.risk_config,
scope,
)
}
fn reject_unavailable_order(
report: &mut BrokerExecutionReport,
date: NaiveDate,
@@ -2870,6 +2927,15 @@ where
return Ok(());
};
let Some(snapshot) = data.market(date, symbol) else {
let unavailable_reason = self
.missing_market_execution_risk_rejection_reason(date, data, symbol, OrderSide::Sell)
.map(str::to_string)
.unwrap_or_else(|| {
format!(
"market snapshot is missing for price field {}",
price_field_name(self.execution_price_field)
)
});
Self::reject_unavailable_order(
report,
date,
@@ -2878,10 +2944,7 @@ where
OrderSide::Sell,
requested_qty,
reason,
format!(
"market snapshot is missing for price field {}",
price_field_name(self.execution_price_field)
),
unavailable_reason,
emit_creation_events,
);
return Ok(());
@@ -3471,9 +3534,10 @@ where
return Ok(());
}
if data.market(date, symbol).is_none() {
self.reject_missing_market_snapshot_order(
self.reject_missing_market_or_execution_risk_order(
report,
date,
data,
symbol,
OrderSide::Sell,
current_qty,
@@ -3503,9 +3567,10 @@ where
}
let Some(snapshot) = data.market(date, symbol) else {
self.reject_missing_market_snapshot_order(
self.reject_missing_market_or_execution_risk_order(
report,
date,
data,
symbol,
if current_qty > 0 {
OrderSide::Sell
@@ -4030,9 +4095,10 @@ where
} else {
0
};
self.reject_missing_market_snapshot_order(
self.reject_missing_market_or_execution_risk_order(
report,
date,
data,
symbol,
if value > 0.0 {
OrderSide::Buy
@@ -4561,6 +4627,15 @@ where
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let Some(snapshot) = data.market(date, symbol) else {
let unavailable_reason = self
.missing_market_execution_risk_rejection_reason(date, data, symbol, OrderSide::Buy)
.map(str::to_string)
.unwrap_or_else(|| {
format!(
"market snapshot is missing for price field {}",
price_field_name(self.execution_price_field)
)
});
Self::reject_unavailable_order(
report,
date,
@@ -4569,10 +4644,7 @@ where
OrderSide::Buy,
requested_qty,
reason,
format!(
"market snapshot is missing for price field {}",
price_field_name(self.execution_price_field)
),
unavailable_reason,
emit_creation_events,
);
return Ok(());
@@ -6521,6 +6593,63 @@ mod tests {
);
}
#[test]
fn next_open_buy_risk_rejects_execution_date_delisted_even_without_market_snapshot() {
let signal_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let execution_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).expect("valid date");
let broker = BrokerSimulator::new(ChinaAShareCostModel::default(), ChinaEquityRuleHooks)
.with_volume_limit(false)
.with_liquidity_limit(false);
let mut instrument = limit_test_instrument();
instrument.delisted_at = Some(execution_date);
let data = DataSet::from_components_with_actions_and_quotes(
vec![instrument],
vec![dated_limit_test_snapshot(signal_date)],
Vec::new(),
vec![
dated_limit_test_candidate(signal_date, false, false, true, true),
dated_limit_test_candidate(execution_date, false, false, true, true),
],
vec![
dated_limit_test_benchmark(signal_date),
dated_limit_test_benchmark(execution_date),
],
Vec::new(),
Vec::new(),
)
.expect("valid dataset");
let mut portfolio = PortfolioState::new(20_000.0);
let report = broker
.execute(
execution_date,
&mut portfolio,
&data,
&next_open_buy_decision(),
)
.expect("execute next open buy");
assert!(report.fill_events.is_empty());
assert!(portfolio.position("000001.SZ").is_none());
let rejected_order = report
.order_events
.iter()
.find(|event| event.side == OrderSide::Buy)
.expect("buy order event");
assert_eq!(rejected_order.date, execution_date);
assert_eq!(rejected_order.status, OrderStatus::Rejected);
assert!(
rejected_order.reason.contains("inactive_or_delisted"),
"{}",
rejected_order.reason
);
assert!(
!rejected_order.reason.contains("market snapshot is missing"),
"{}",
rejected_order.reason
);
}
#[test]
fn next_open_buy_limit_risk_uses_open_not_close() {
let execution_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).expect("valid date");