缺行情调仓订单改为拒单
This commit is contained in:
+144
-14
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user