修正回测部分成交终态合同
This commit is contained in:
@@ -29,6 +29,15 @@ pub struct BrokerExecutionReport {
|
||||
pub diagnostics: Vec<String>,
|
||||
}
|
||||
|
||||
impl BrokerExecutionReport {
|
||||
fn validate(&self) -> Result<(), BacktestError> {
|
||||
for event in &self.order_events {
|
||||
event.validate().map_err(BacktestError::Execution)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct ExecutionLeg {
|
||||
price: f64,
|
||||
@@ -1038,6 +1047,7 @@ where
|
||||
self.runtime_target_position_limit
|
||||
.set(previous_target_position_limit);
|
||||
portfolio.prune_flat_positions();
|
||||
report.validate()?;
|
||||
return Ok(report);
|
||||
}
|
||||
|
||||
@@ -1153,6 +1163,7 @@ where
|
||||
}
|
||||
|
||||
portfolio.prune_flat_positions();
|
||||
report.validate()?;
|
||||
Ok(report)
|
||||
}
|
||||
|
||||
@@ -3850,7 +3861,7 @@ where
|
||||
let status = if keep_open {
|
||||
OrderStatus::PartiallyFilled
|
||||
} else if filled_qty < requested_qty {
|
||||
final_partial_fill_status(partial_fill_reason.as_deref())
|
||||
OrderStatus::Canceled
|
||||
} else {
|
||||
OrderStatus::Filled
|
||||
};
|
||||
@@ -3862,14 +3873,6 @@ where
|
||||
"order_partial_fill symbol={symbol} side=sell requested={requested_qty} filled={filled_qty} reason={detail}; remaining open"
|
||||
));
|
||||
format!("{reason}: partial fill due to {detail}; remaining quantity pending")
|
||||
} else if status == OrderStatus::PartiallyFilled {
|
||||
let detail = partial_fill_reason
|
||||
.as_deref()
|
||||
.unwrap_or("remaining quantity could not be filled");
|
||||
report.diagnostics.push(format!(
|
||||
"order_partial_fill symbol={symbol} side=sell requested={requested_qty} filled={filled_qty} reason={detail}"
|
||||
));
|
||||
format!("{reason}: partial fill due to {detail}")
|
||||
} else if status == OrderStatus::Canceled && filled_qty < requested_qty {
|
||||
let detail = partial_fill_reason
|
||||
.as_deref()
|
||||
@@ -5506,7 +5509,7 @@ where
|
||||
let status = if keep_open {
|
||||
OrderStatus::PartiallyFilled
|
||||
} else if filled_qty < requested_qty {
|
||||
final_partial_fill_status(partial_fill_reason.as_deref())
|
||||
OrderStatus::Canceled
|
||||
} else {
|
||||
OrderStatus::Filled
|
||||
};
|
||||
@@ -5518,14 +5521,6 @@ where
|
||||
"order_partial_fill symbol={symbol} side=buy requested={requested_qty} filled={filled_qty} reason={detail}; remaining open"
|
||||
));
|
||||
format!("{reason}: partial fill due to {detail}; remaining quantity pending")
|
||||
} else if status == OrderStatus::PartiallyFilled {
|
||||
let detail = partial_fill_reason
|
||||
.as_deref()
|
||||
.unwrap_or("remaining quantity could not be filled");
|
||||
report.diagnostics.push(format!(
|
||||
"order_partial_fill symbol={symbol} side=buy requested={requested_qty} filled={filled_qty} reason={detail}"
|
||||
));
|
||||
format!("{reason}: partial fill due to {detail}")
|
||||
} else if status == OrderStatus::Canceled && filled_qty < requested_qty {
|
||||
let detail = partial_fill_reason
|
||||
.as_deref()
|
||||
@@ -6589,24 +6584,6 @@ fn zero_fill_status_for_reason(reason: &str) -> OrderStatus {
|
||||
}
|
||||
}
|
||||
|
||||
fn final_partial_fill_status(partial_reason: Option<&str>) -> OrderStatus {
|
||||
match partial_reason {
|
||||
Some(reason)
|
||||
if reason.contains("market liquidity or volume limit")
|
||||
|| reason.contains("intraday quote liquidity exhausted")
|
||||
|| reason.contains("no execution quotes at or before start")
|
||||
|| reason.contains("no execution quotes after start")
|
||||
|| reason.contains("upper_limit")
|
||||
|| reason.contains("lower_limit")
|
||||
|| reason.contains("open at or above upper limit")
|
||||
|| reason.contains("open at or below lower limit") =>
|
||||
{
|
||||
OrderStatus::Canceled
|
||||
}
|
||||
_ => OrderStatus::PartiallyFilled,
|
||||
}
|
||||
}
|
||||
|
||||
fn price_field_name(field: PriceField) -> &'static str {
|
||||
match field {
|
||||
PriceField::DayOpen => "day_open",
|
||||
|
||||
@@ -106,6 +106,49 @@ pub struct OrderEvent {
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
impl OrderEvent {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.symbol.trim().is_empty() || self.requested_quantity == 0 {
|
||||
return Err(format!(
|
||||
"invalid order identity/quantity order_id={:?} symbol={} requested={}",
|
||||
self.order_id, self.symbol, self.requested_quantity
|
||||
));
|
||||
}
|
||||
if self.filled_quantity > self.requested_quantity {
|
||||
return Err(format!(
|
||||
"order overfill order_id={:?} requested={} filled={}",
|
||||
self.order_id, self.requested_quantity, self.filled_quantity
|
||||
));
|
||||
}
|
||||
let quantity_valid = match self.status {
|
||||
OrderStatus::Pending => self.filled_quantity < self.requested_quantity,
|
||||
OrderStatus::Filled => self.filled_quantity == self.requested_quantity,
|
||||
OrderStatus::PartiallyFilled => {
|
||||
self.filled_quantity > 0 && self.filled_quantity < self.requested_quantity
|
||||
}
|
||||
OrderStatus::Canceled => self.filled_quantity < self.requested_quantity,
|
||||
OrderStatus::Rejected => self.filled_quantity == 0,
|
||||
};
|
||||
if !quantity_valid {
|
||||
return Err(format!(
|
||||
"order status/quantity mismatch order_id={:?} status={} requested={} filled={}",
|
||||
self.order_id,
|
||||
self.status.as_str(),
|
||||
self.requested_quantity,
|
||||
self.filled_quantity
|
||||
));
|
||||
}
|
||||
if self.reason.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"order reason is empty order_id={:?} status={}",
|
||||
self.order_id,
|
||||
self.status.as_str()
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FillEvent {
|
||||
#[serde(with = "date_format")]
|
||||
@@ -250,3 +293,44 @@ pub struct ProcessEvent {
|
||||
pub side: Option<OrderSide>,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::NaiveDate;
|
||||
|
||||
use super::{OrderEvent, OrderSide, OrderStatus};
|
||||
|
||||
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
|
||||
OrderEvent {
|
||||
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
order_id: Some(1),
|
||||
symbol: "600000.SH".to_string(),
|
||||
side: OrderSide::Buy,
|
||||
requested_quantity: 100,
|
||||
filled_quantity,
|
||||
status,
|
||||
reason: "test".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_event_status_quantity_contract_is_explicit() {
|
||||
assert!(order_event(OrderStatus::Pending, 0).validate().is_ok());
|
||||
assert!(
|
||||
order_event(OrderStatus::PartiallyFilled, 40)
|
||||
.validate()
|
||||
.is_ok()
|
||||
);
|
||||
assert!(order_event(OrderStatus::Filled, 100).validate().is_ok());
|
||||
assert!(order_event(OrderStatus::Canceled, 40).validate().is_ok());
|
||||
assert!(order_event(OrderStatus::Rejected, 0).validate().is_ok());
|
||||
|
||||
assert!(order_event(OrderStatus::PartiallyFilled, 0).validate().is_err());
|
||||
assert!(order_event(OrderStatus::Filled, 99).validate().is_err());
|
||||
assert!(order_event(OrderStatus::Canceled, 100).validate().is_err());
|
||||
assert!(order_event(OrderStatus::Rejected, 1).validate().is_err());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5150,8 +5150,13 @@ fn broker_reserves_sellable_quantity_for_open_limit_sells() {
|
||||
|
||||
assert_eq!(report.order_events.len(), 2);
|
||||
assert_eq!(report.order_events[0].status, OrderStatus::Pending);
|
||||
assert_eq!(report.order_events[1].status, OrderStatus::PartiallyFilled);
|
||||
assert_eq!(report.order_events[1].status, OrderStatus::Canceled);
|
||||
assert_eq!(report.order_events[1].filled_quantity, 100);
|
||||
assert!(report.order_events[1].reason.contains("remaining quantity canceled"));
|
||||
let open_orders = broker.open_order_views();
|
||||
assert_eq!(open_orders.len(), 1);
|
||||
assert_eq!(open_orders[0].reason, "reserve_sell");
|
||||
assert_eq!(open_orders[0].remaining_quantity, 200);
|
||||
assert_eq!(
|
||||
portfolio.position("000002.SZ").expect("position").quantity,
|
||||
200
|
||||
|
||||
Reference in New Issue
Block a user