修正执行日退市缺行情拒单原因
This commit is contained in:
+141
-12
@@ -12,7 +12,7 @@ use crate::events::{
|
|||||||
};
|
};
|
||||||
use crate::instrument::Instrument;
|
use crate::instrument::Instrument;
|
||||||
use crate::portfolio::PortfolioState;
|
use crate::portfolio::PortfolioState;
|
||||||
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig};
|
use crate::risk_control::{ChinaAShareRiskControl, FidcRiskControlConfig, RiskCheckScope};
|
||||||
use crate::rules::{EquityRuleHooks, RuleCheck};
|
use crate::rules::{EquityRuleHooks, RuleCheck};
|
||||||
use crate::strategy::{
|
use crate::strategy::{
|
||||||
AlgoOrderStyle, OpenOrderView, OrderIntent, StrategyDecision, TargetPortfolioOrderPricing,
|
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(
|
fn reject_unavailable_order(
|
||||||
report: &mut BrokerExecutionReport,
|
report: &mut BrokerExecutionReport,
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
@@ -2870,6 +2927,15 @@ where
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
let Some(snapshot) = data.market(date, symbol) else {
|
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(
|
Self::reject_unavailable_order(
|
||||||
report,
|
report,
|
||||||
date,
|
date,
|
||||||
@@ -2878,10 +2944,7 @@ where
|
|||||||
OrderSide::Sell,
|
OrderSide::Sell,
|
||||||
requested_qty,
|
requested_qty,
|
||||||
reason,
|
reason,
|
||||||
format!(
|
unavailable_reason,
|
||||||
"market snapshot is missing for price field {}",
|
|
||||||
price_field_name(self.execution_price_field)
|
|
||||||
),
|
|
||||||
emit_creation_events,
|
emit_creation_events,
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
@@ -3471,9 +3534,10 @@ where
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if data.market(date, symbol).is_none() {
|
if data.market(date, symbol).is_none() {
|
||||||
self.reject_missing_market_snapshot_order(
|
self.reject_missing_market_or_execution_risk_order(
|
||||||
report,
|
report,
|
||||||
date,
|
date,
|
||||||
|
data,
|
||||||
symbol,
|
symbol,
|
||||||
OrderSide::Sell,
|
OrderSide::Sell,
|
||||||
current_qty,
|
current_qty,
|
||||||
@@ -3503,9 +3567,10 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let Some(snapshot) = data.market(date, symbol) else {
|
let Some(snapshot) = data.market(date, symbol) else {
|
||||||
self.reject_missing_market_snapshot_order(
|
self.reject_missing_market_or_execution_risk_order(
|
||||||
report,
|
report,
|
||||||
date,
|
date,
|
||||||
|
data,
|
||||||
symbol,
|
symbol,
|
||||||
if current_qty > 0 {
|
if current_qty > 0 {
|
||||||
OrderSide::Sell
|
OrderSide::Sell
|
||||||
@@ -4030,9 +4095,10 @@ where
|
|||||||
} else {
|
} else {
|
||||||
0
|
0
|
||||||
};
|
};
|
||||||
self.reject_missing_market_snapshot_order(
|
self.reject_missing_market_or_execution_risk_order(
|
||||||
report,
|
report,
|
||||||
date,
|
date,
|
||||||
|
data,
|
||||||
symbol,
|
symbol,
|
||||||
if value > 0.0 {
|
if value > 0.0 {
|
||||||
OrderSide::Buy
|
OrderSide::Buy
|
||||||
@@ -4561,6 +4627,15 @@ where
|
|||||||
report: &mut BrokerExecutionReport,
|
report: &mut BrokerExecutionReport,
|
||||||
) -> Result<(), BacktestError> {
|
) -> Result<(), BacktestError> {
|
||||||
let Some(snapshot) = data.market(date, symbol) else {
|
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(
|
Self::reject_unavailable_order(
|
||||||
report,
|
report,
|
||||||
date,
|
date,
|
||||||
@@ -4569,10 +4644,7 @@ where
|
|||||||
OrderSide::Buy,
|
OrderSide::Buy,
|
||||||
requested_qty,
|
requested_qty,
|
||||||
reason,
|
reason,
|
||||||
format!(
|
unavailable_reason,
|
||||||
"market snapshot is missing for price field {}",
|
|
||||||
price_field_name(self.execution_price_field)
|
|
||||||
),
|
|
||||||
emit_creation_events,
|
emit_creation_events,
|
||||||
);
|
);
|
||||||
return Ok(());
|
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]
|
#[test]
|
||||||
fn next_open_buy_limit_risk_uses_open_not_close() {
|
fn next_open_buy_limit_risk_uses_open_not_close() {
|
||||||
let execution_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).expect("valid date");
|
let execution_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).expect("valid date");
|
||||||
|
|||||||
@@ -223,6 +223,21 @@ impl ChinaAShareRiskControl {
|
|||||||
Self::instrument_rejection_reason(instrument, date)
|
Self::instrument_rejection_reason(instrument, date)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn active_status_rejection_reason_with_config(
|
||||||
|
date: NaiveDate,
|
||||||
|
candidate: Option<&CandidateEligibility>,
|
||||||
|
instrument: Option<&Instrument>,
|
||||||
|
config: &FidcRiskControlConfig,
|
||||||
|
scope: RiskCheckScope,
|
||||||
|
) -> Option<&'static str> {
|
||||||
|
if let Some(reason) =
|
||||||
|
Self::instrument_rejection_reason_with_config(instrument, date, config, scope)
|
||||||
|
{
|
||||||
|
return Some(reason);
|
||||||
|
}
|
||||||
|
candidate.and_then(|candidate| candidate_active_status_rejection(candidate, config, scope))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selection_rejection_reason(
|
pub fn selection_rejection_reason(
|
||||||
date: NaiveDate,
|
date: NaiveDate,
|
||||||
candidate: &CandidateEligibility,
|
candidate: &CandidateEligibility,
|
||||||
|
|||||||
Reference in New Issue
Block a user