记录分钟成交精确时间
This commit is contained in:
@@ -36,6 +36,9 @@ impl BrokerExecutionReport {
|
||||
for event in &self.order_events {
|
||||
event.validate().map_err(BacktestError::Execution)?;
|
||||
}
|
||||
for event in &self.fill_events {
|
||||
event.validate().map_err(BacktestError::Execution)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -45,6 +48,8 @@ struct ExecutionLeg {
|
||||
price: f64,
|
||||
mark_price: f64,
|
||||
quantity: u32,
|
||||
execution_start_timestamp: Option<NaiveDateTime>,
|
||||
execution_timestamp: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -4084,6 +4089,8 @@ where
|
||||
price: execution_price,
|
||||
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Sell),
|
||||
quantity: fillable_qty,
|
||||
execution_start_timestamp: None,
|
||||
execution_timestamp: None,
|
||||
}],
|
||||
None,
|
||||
Vec::new(),
|
||||
@@ -4227,6 +4234,8 @@ where
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
execution_start_timestamp: leg.execution_start_timestamp,
|
||||
execution_timestamp: leg.execution_timestamp,
|
||||
order_id: Some(order_id),
|
||||
symbol: symbol.to_string(),
|
||||
side: OrderSide::Sell,
|
||||
@@ -5784,6 +5793,8 @@ where
|
||||
price: execution_price,
|
||||
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Buy),
|
||||
quantity: filled_qty,
|
||||
execution_start_timestamp: None,
|
||||
execution_timestamp: None,
|
||||
}],
|
||||
None,
|
||||
Vec::new(),
|
||||
@@ -5928,6 +5939,8 @@ where
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
execution_start_timestamp: leg.execution_start_timestamp,
|
||||
execution_timestamp: leg.execution_timestamp,
|
||||
order_id: Some(order_id),
|
||||
symbol: symbol.to_string(),
|
||||
side: OrderSide::Buy,
|
||||
@@ -6812,6 +6825,7 @@ where
|
||||
let mut filled_qty = 0_u32;
|
||||
let mut gross_amount = 0.0_f64;
|
||||
let mut mark_amount = 0.0_f64;
|
||||
let mut first_timestamp = None;
|
||||
let mut last_timestamp = None;
|
||||
let mut legs = Vec::new();
|
||||
let mut budget_block_reason = None;
|
||||
@@ -7000,11 +7014,14 @@ where
|
||||
gross_amount += quote_price * take_qty as f64;
|
||||
mark_amount += mark_price * take_qty as f64;
|
||||
filled_qty += take_qty;
|
||||
first_timestamp.get_or_insert(quote.timestamp);
|
||||
last_timestamp = Some(quote.timestamp);
|
||||
legs.push(ExecutionLeg {
|
||||
price: quote_price,
|
||||
mark_price,
|
||||
quantity: take_qty,
|
||||
execution_start_timestamp: Some(quote.timestamp),
|
||||
execution_timestamp: Some(quote.timestamp),
|
||||
});
|
||||
if consume_depth {
|
||||
let state = depth_state
|
||||
@@ -7064,6 +7081,8 @@ where
|
||||
price: gross_amount / filled_qty as f64,
|
||||
mark_price: mark_amount / filled_qty as f64,
|
||||
quantity: filled_qty,
|
||||
execution_start_timestamp: first_timestamp,
|
||||
execution_timestamp: last_timestamp,
|
||||
}]
|
||||
} else {
|
||||
legs
|
||||
@@ -9966,6 +9985,8 @@ mod tests {
|
||||
assert_eq!(fill.quantity, 200);
|
||||
assert_eq!(fill.legs.len(), 1);
|
||||
assert_eq!(fill.legs[0].price, 10.8);
|
||||
assert_eq!(fill.legs[0].execution_timestamp, Some(quote_timestamp));
|
||||
assert!(fill.legs[0].execution_timestamp.unwrap() <= decision_time);
|
||||
assert_eq!(
|
||||
fill.next_cursor,
|
||||
quote_timestamp + chrono::Duration::seconds(1)
|
||||
|
||||
@@ -3494,6 +3494,8 @@ where
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
execution_start_timestamp: None,
|
||||
execution_timestamp: None,
|
||||
order_id: None,
|
||||
symbol: receivable.symbol.clone(),
|
||||
side: OrderSide::Buy,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
mod date_format {
|
||||
@@ -50,6 +50,35 @@ mod optional_date_format {
|
||||
}
|
||||
}
|
||||
|
||||
mod optional_datetime_format {
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{self, Deserialize, Deserializer, Serializer};
|
||||
|
||||
const FORMAT: &str = "%Y-%m-%d %H:%M:%S%.f";
|
||||
|
||||
pub fn serialize<S>(datetime: &Option<NaiveDateTime>, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
{
|
||||
match datetime {
|
||||
Some(datetime) => serializer.serialize_some(&datetime.format(FORMAT).to_string()),
|
||||
None => serializer.serialize_none(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<NaiveDateTime>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let value = Option::<String>::deserialize(deserializer)?;
|
||||
value
|
||||
.map(|text| {
|
||||
NaiveDateTime::parse_from_str(&text, FORMAT).map_err(serde::de::Error::custom)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum OrderSide {
|
||||
Buy,
|
||||
@@ -162,6 +191,18 @@ pub struct FillEvent {
|
||||
pub order_created_date: Option<NaiveDate>,
|
||||
#[serde(default, with = "optional_date_format")]
|
||||
pub execution_date: Option<NaiveDate>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "optional_datetime_format",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub execution_start_timestamp: Option<NaiveDateTime>,
|
||||
#[serde(
|
||||
default,
|
||||
with = "optional_datetime_format",
|
||||
skip_serializing_if = "Option::is_none"
|
||||
)]
|
||||
pub execution_timestamp: Option<NaiveDateTime>,
|
||||
#[serde(default)]
|
||||
pub order_id: Option<u64>,
|
||||
pub symbol: String,
|
||||
@@ -176,6 +217,42 @@ pub struct FillEvent {
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
impl FillEvent {
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
if self.symbol.trim().is_empty()
|
||||
|| self.quantity == 0
|
||||
|| !self.price.is_finite()
|
||||
|| self.price <= 0.0
|
||||
{
|
||||
return Err(format!(
|
||||
"invalid fill identity/quantity/price order_id={:?} symbol={} quantity={} price={}",
|
||||
self.order_id, self.symbol, self.quantity, self.price
|
||||
));
|
||||
}
|
||||
if let (Some(start), Some(end)) = (self.execution_start_timestamp, self.execution_timestamp)
|
||||
{
|
||||
if start > end {
|
||||
return Err(format!(
|
||||
"fill execution timestamp order is invalid order_id={:?} start={} end={}",
|
||||
self.order_id, start, end
|
||||
));
|
||||
}
|
||||
if start.date() != self.date || end.date() != self.date {
|
||||
return Err(format!(
|
||||
"fill execution timestamp date mismatch order_id={:?} fill_date={} start={} end={}",
|
||||
self.order_id, self.date, start, end
|
||||
));
|
||||
}
|
||||
} else if self.execution_start_timestamp.is_some() || self.execution_timestamp.is_some() {
|
||||
return Err(format!(
|
||||
"fill execution timestamp range is incomplete order_id={:?}",
|
||||
self.order_id
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PositionEvent {
|
||||
#[serde(with = "date_format")]
|
||||
@@ -299,9 +376,9 @@ pub struct ProcessEvent {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{NaiveDate, NaiveDateTime};
|
||||
|
||||
use super::{OrderEvent, OrderSide, OrderStatus};
|
||||
use super::{FillEvent, OrderEvent, OrderSide, OrderStatus};
|
||||
|
||||
fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
|
||||
OrderEvent {
|
||||
@@ -342,4 +419,63 @@ mod tests {
|
||||
assert!(order_event(OrderStatus::Rejected, 1).validate().is_err());
|
||||
assert!(order_event(OrderStatus::Expired, 100).validate().is_err());
|
||||
}
|
||||
|
||||
fn fill_event(start: Option<NaiveDateTime>, end: Option<NaiveDateTime>) -> FillEvent {
|
||||
FillEvent {
|
||||
date: NaiveDate::from_ymd_opt(2025, 1, 2).unwrap(),
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
execution_start_timestamp: start,
|
||||
execution_timestamp: end,
|
||||
order_id: Some(1),
|
||||
symbol: "600000.SH".to_string(),
|
||||
side: OrderSide::Buy,
|
||||
quantity: 100,
|
||||
price: 10.0,
|
||||
gross_amount: 1_000.0,
|
||||
commission: 5.0,
|
||||
stamp_tax: 0.0,
|
||||
transfer_fee: 0.0,
|
||||
net_cash_flow: -1_005.0,
|
||||
reason: "test".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_execution_timestamp_range_is_explicit_and_backward_compatible() {
|
||||
let start = NaiveDate::from_ymd_opt(2025, 1, 2)
|
||||
.unwrap()
|
||||
.and_hms_opt(10, 18, 0)
|
||||
.unwrap();
|
||||
let end = start + chrono::Duration::seconds(3);
|
||||
assert!(fill_event(Some(start), Some(end)).validate().is_ok());
|
||||
assert!(fill_event(Some(end), Some(start)).validate().is_err());
|
||||
assert!(fill_event(Some(start), None).validate().is_err());
|
||||
|
||||
let next_day = start + chrono::Duration::days(1);
|
||||
assert!(
|
||||
fill_event(Some(next_day), Some(next_day))
|
||||
.validate()
|
||||
.is_err()
|
||||
);
|
||||
|
||||
let legacy = fill_event(None, None);
|
||||
let legacy_json = serde_json::to_value(&legacy).unwrap();
|
||||
assert!(legacy_json.get("execution_start_timestamp").is_none());
|
||||
assert!(legacy_json.get("execution_timestamp").is_none());
|
||||
let decoded: FillEvent = serde_json::from_value(legacy_json).unwrap();
|
||||
assert_eq!(decoded.execution_start_timestamp, None);
|
||||
assert_eq!(decoded.execution_timestamp, None);
|
||||
|
||||
let timestamped_json = serde_json::to_value(fill_event(Some(start), Some(end))).unwrap();
|
||||
assert_eq!(
|
||||
timestamped_json["execution_start_timestamp"],
|
||||
"2025-01-02 10:18:00"
|
||||
);
|
||||
assert_eq!(
|
||||
timestamped_json["execution_timestamp"],
|
||||
"2025-01-02 10:18:03"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1052,6 +1052,8 @@ impl FuturesAccountState {
|
||||
decision_date: None,
|
||||
order_created_date: None,
|
||||
execution_date: None,
|
||||
execution_start_timestamp: None,
|
||||
execution_timestamp: None,
|
||||
order_id,
|
||||
symbol: intent.symbol.clone(),
|
||||
side,
|
||||
|
||||
Reference in New Issue
Block a user