缺行情调仓订单改为拒单

This commit is contained in:
boris
2026-06-29 16:22:50 +08:00
parent fbc6da1a8f
commit 1623994287
+144 -14
View File
@@ -1633,6 +1633,44 @@ where
});
}
fn reject_missing_market_snapshot_order(
&self,
report: &mut BrokerExecutionReport,
date: NaiveDate,
symbol: &str,
side: OrderSide,
requested_quantity: u32,
reason: &str,
) {
let order_id = self.reserve_order_id();
let order_reason = format!(
"{reason}: rejected because market snapshot is missing for {} price field {}",
symbol,
price_field_name(self.execution_price_field)
);
report.order_events.push(OrderEvent {
date,
order_id: Some(order_id),
symbol: symbol.to_string(),
side,
requested_quantity,
filled_quantity: 0,
status: OrderStatus::Rejected,
reason: order_reason.clone(),
});
Self::emit_order_process_event(
report,
date,
ProcessEventKind::OrderUnsolicitedUpdate,
order_id,
symbol,
side,
format!(
"status=Rejected requested_quantity={requested_quantity} filled_quantity=0 reason={order_reason}"
),
);
}
fn creation_reject_kind(emit_creation_events: bool) -> ProcessEventKind {
if emit_creation_events {
ProcessEventKind::OrderCreationReject
@@ -2948,13 +2986,6 @@ where
commission_state: &mut BTreeMap<u64, f64>,
report: &mut BrokerExecutionReport,
) -> Result<(), BacktestError> {
let snapshot = data
.market(date, symbol)
.ok_or_else(|| BacktestError::MissingPrice {
date,
symbol: symbol.to_string(),
field: price_field_name(self.execution_price_field),
})?;
let current_qty = portfolio
.position(symbol)
.map(|pos| pos.quantity)
@@ -2974,6 +3005,17 @@ where
});
return Ok(());
}
if data.market(date, symbol).is_none() {
self.reject_missing_market_snapshot_order(
report,
date,
symbol,
OrderSide::Sell,
current_qty,
reason,
);
return Ok(());
}
self.process_sell(
date,
portfolio,
@@ -2995,6 +3037,22 @@ where
return Ok(());
}
let Some(snapshot) = data.market(date, symbol) else {
self.reject_missing_market_snapshot_order(
report,
date,
symbol,
if current_qty > 0 {
OrderSide::Sell
} else {
OrderSide::Buy
},
0,
reason,
);
return Ok(());
};
let current_value = if self.aiquant_execution_rules {
let valuation_price = self.target_value_valuation_price(date, data, symbol, snapshot);
valuation_price * current_qty as f64
@@ -3486,13 +3544,29 @@ where
if value.abs() <= f64::EPSILON {
return Ok(());
}
let snapshot = data
.market(date, symbol)
.ok_or_else(|| BacktestError::MissingPrice {
let Some(snapshot) = data.market(date, symbol) else {
let requested_quantity = if value < 0.0 {
portfolio
.position(symbol)
.map(|position| position.quantity)
.unwrap_or(0)
} else {
0
};
self.reject_missing_market_snapshot_order(
report,
date,
symbol: symbol.to_string(),
field: price_field_name(self.execution_price_field),
})?;
symbol,
if value > 0.0 {
OrderSide::Buy
} else {
OrderSide::Sell
},
requested_quantity,
reason,
);
return Ok(());
};
if value > 0.0 {
let round_lot = self.round_lot(data, symbol);
let minimum_order_quantity = self.minimum_order_quantity(data, symbol);
@@ -5400,7 +5474,7 @@ mod tests {
BenchmarkSnapshot, CandidateEligibility, DailyMarketSnapshot, DataSet,
IntradayExecutionQuote, PriceField,
};
use crate::events::OrderSide;
use crate::events::{OrderSide, OrderStatus};
use crate::instrument::Instrument;
use crate::portfolio::PortfolioState;
use crate::rules::ChinaEquityRuleHooks;
@@ -5604,6 +5678,62 @@ mod tests {
);
}
#[test]
fn target_value_zero_rejects_sell_when_market_snapshot_missing() {
let trade_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
let missing_date = chrono::NaiveDate::from_ymd_opt(2025, 1, 3).expect("valid date");
let symbol = "000001.SZ";
let broker = BrokerSimulator::new_with_execution_price(
ChinaAShareCostModel::default(),
ChinaEquityRuleHooks,
PriceField::Open,
)
.with_volume_limit(false)
.with_liquidity_limit(false);
let data = DataSet::from_components_with_actions_and_quotes(
vec![limit_test_instrument()],
vec![limit_test_snapshot()],
Vec::new(),
vec![limit_test_candidate(true, true)],
vec![limit_test_benchmark()],
Vec::new(),
Vec::new(),
)
.expect("valid dataset");
let mut portfolio = PortfolioState::new(100_000.0);
portfolio.position_mut(symbol).buy(trade_date, 1_000, 10.0);
let mut report = BrokerExecutionReport::default();
broker
.process_target_value(
missing_date,
&mut portfolio,
&data,
symbol,
0.0,
"target_rebalance_exit",
&mut BTreeMap::new(),
&mut BTreeMap::new(),
&mut None,
&mut BTreeMap::new(),
&mut report,
)
.expect("missing market snapshot should reject order, not abort backtest");
assert_eq!(
portfolio.position(symbol).map(|pos| pos.quantity),
Some(1_000)
);
assert_eq!(report.fill_events.len(), 0);
assert_eq!(report.order_events.len(), 1);
let order = &report.order_events[0];
assert_eq!(order.side, OrderSide::Sell);
assert_eq!(order.status, OrderStatus::Rejected);
assert_eq!(order.requested_quantity, 1_000);
assert_eq!(order.filled_quantity, 0);
assert!(order.reason.contains("market snapshot is missing"));
}
#[test]
fn timed_target_value_zero_sells_full_position_at_requested_time_quote() {
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");