修正执行日退市缺行情拒单原因
This commit is contained in:
+141
-12
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user