缺行情调仓订单改为拒单
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 {
|
fn creation_reject_kind(emit_creation_events: bool) -> ProcessEventKind {
|
||||||
if emit_creation_events {
|
if emit_creation_events {
|
||||||
ProcessEventKind::OrderCreationReject
|
ProcessEventKind::OrderCreationReject
|
||||||
@@ -2948,13 +2986,6 @@ where
|
|||||||
commission_state: &mut BTreeMap<u64, f64>,
|
commission_state: &mut BTreeMap<u64, f64>,
|
||||||
report: &mut BrokerExecutionReport,
|
report: &mut BrokerExecutionReport,
|
||||||
) -> Result<(), BacktestError> {
|
) -> 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
|
let current_qty = portfolio
|
||||||
.position(symbol)
|
.position(symbol)
|
||||||
.map(|pos| pos.quantity)
|
.map(|pos| pos.quantity)
|
||||||
@@ -2974,6 +3005,17 @@ where
|
|||||||
});
|
});
|
||||||
return Ok(());
|
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(
|
self.process_sell(
|
||||||
date,
|
date,
|
||||||
portfolio,
|
portfolio,
|
||||||
@@ -2995,6 +3037,22 @@ where
|
|||||||
return Ok(());
|
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 current_value = if self.aiquant_execution_rules {
|
||||||
let valuation_price = self.target_value_valuation_price(date, data, symbol, snapshot);
|
let valuation_price = self.target_value_valuation_price(date, data, symbol, snapshot);
|
||||||
valuation_price * current_qty as f64
|
valuation_price * current_qty as f64
|
||||||
@@ -3486,13 +3544,29 @@ where
|
|||||||
if value.abs() <= f64::EPSILON {
|
if value.abs() <= f64::EPSILON {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let snapshot = data
|
let Some(snapshot) = data.market(date, symbol) else {
|
||||||
.market(date, symbol)
|
let requested_quantity = if value < 0.0 {
|
||||||
.ok_or_else(|| BacktestError::MissingPrice {
|
portfolio
|
||||||
|
.position(symbol)
|
||||||
|
.map(|position| position.quantity)
|
||||||
|
.unwrap_or(0)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
self.reject_missing_market_snapshot_order(
|
||||||
|
report,
|
||||||
date,
|
date,
|
||||||
symbol: symbol.to_string(),
|
symbol,
|
||||||
field: price_field_name(self.execution_price_field),
|
if value > 0.0 {
|
||||||
})?;
|
OrderSide::Buy
|
||||||
|
} else {
|
||||||
|
OrderSide::Sell
|
||||||
|
},
|
||||||
|
requested_quantity,
|
||||||
|
reason,
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
if value > 0.0 {
|
if value > 0.0 {
|
||||||
let round_lot = self.round_lot(data, symbol);
|
let round_lot = self.round_lot(data, symbol);
|
||||||
let minimum_order_quantity = self.minimum_order_quantity(data, symbol);
|
let minimum_order_quantity = self.minimum_order_quantity(data, symbol);
|
||||||
@@ -5400,7 +5474,7 @@ mod tests {
|
|||||||
BenchmarkSnapshot, CandidateEligibility, DailyMarketSnapshot, DataSet,
|
BenchmarkSnapshot, CandidateEligibility, DailyMarketSnapshot, DataSet,
|
||||||
IntradayExecutionQuote, PriceField,
|
IntradayExecutionQuote, PriceField,
|
||||||
};
|
};
|
||||||
use crate::events::OrderSide;
|
use crate::events::{OrderSide, OrderStatus};
|
||||||
use crate::instrument::Instrument;
|
use crate::instrument::Instrument;
|
||||||
use crate::portfolio::PortfolioState;
|
use crate::portfolio::PortfolioState;
|
||||||
use crate::rules::ChinaEquityRuleHooks;
|
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]
|
#[test]
|
||||||
fn timed_target_value_zero_sells_full_position_at_requested_time_quote() {
|
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");
|
let date = chrono::NaiveDate::from_ymd_opt(2025, 1, 2).expect("valid date");
|
||||||
|
|||||||
Reference in New Issue
Block a user