记录分钟成交精确时间

This commit is contained in:
boris
2026-08-27 02:46:38 +08:00
parent 21cfa65af2
commit cdbd8a67de
5 changed files with 180 additions and 3 deletions
+21
View File
@@ -36,6 +36,9 @@ impl BrokerExecutionReport {
for event in &self.order_events { for event in &self.order_events {
event.validate().map_err(BacktestError::Execution)?; event.validate().map_err(BacktestError::Execution)?;
} }
for event in &self.fill_events {
event.validate().map_err(BacktestError::Execution)?;
}
Ok(()) Ok(())
} }
} }
@@ -45,6 +48,8 @@ struct ExecutionLeg {
price: f64, price: f64,
mark_price: f64, mark_price: f64,
quantity: u32, quantity: u32,
execution_start_timestamp: Option<NaiveDateTime>,
execution_timestamp: Option<NaiveDateTime>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -4084,6 +4089,8 @@ where
price: execution_price, price: execution_price,
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Sell), mark_price: self.snapshot_mark_price(snapshot, OrderSide::Sell),
quantity: fillable_qty, quantity: fillable_qty,
execution_start_timestamp: None,
execution_timestamp: None,
}], }],
None, None,
Vec::new(), Vec::new(),
@@ -4227,6 +4234,8 @@ where
decision_date: None, decision_date: None,
order_created_date: None, order_created_date: None,
execution_date: None, execution_date: None,
execution_start_timestamp: leg.execution_start_timestamp,
execution_timestamp: leg.execution_timestamp,
order_id: Some(order_id), order_id: Some(order_id),
symbol: symbol.to_string(), symbol: symbol.to_string(),
side: OrderSide::Sell, side: OrderSide::Sell,
@@ -5784,6 +5793,8 @@ where
price: execution_price, price: execution_price,
mark_price: self.snapshot_mark_price(snapshot, OrderSide::Buy), mark_price: self.snapshot_mark_price(snapshot, OrderSide::Buy),
quantity: filled_qty, quantity: filled_qty,
execution_start_timestamp: None,
execution_timestamp: None,
}], }],
None, None,
Vec::new(), Vec::new(),
@@ -5928,6 +5939,8 @@ where
decision_date: None, decision_date: None,
order_created_date: None, order_created_date: None,
execution_date: None, execution_date: None,
execution_start_timestamp: leg.execution_start_timestamp,
execution_timestamp: leg.execution_timestamp,
order_id: Some(order_id), order_id: Some(order_id),
symbol: symbol.to_string(), symbol: symbol.to_string(),
side: OrderSide::Buy, side: OrderSide::Buy,
@@ -6812,6 +6825,7 @@ where
let mut filled_qty = 0_u32; let mut filled_qty = 0_u32;
let mut gross_amount = 0.0_f64; let mut gross_amount = 0.0_f64;
let mut mark_amount = 0.0_f64; let mut mark_amount = 0.0_f64;
let mut first_timestamp = None;
let mut last_timestamp = None; let mut last_timestamp = None;
let mut legs = Vec::new(); let mut legs = Vec::new();
let mut budget_block_reason = None; let mut budget_block_reason = None;
@@ -7000,11 +7014,14 @@ where
gross_amount += quote_price * take_qty as f64; gross_amount += quote_price * take_qty as f64;
mark_amount += mark_price * take_qty as f64; mark_amount += mark_price * take_qty as f64;
filled_qty += take_qty; filled_qty += take_qty;
first_timestamp.get_or_insert(quote.timestamp);
last_timestamp = Some(quote.timestamp); last_timestamp = Some(quote.timestamp);
legs.push(ExecutionLeg { legs.push(ExecutionLeg {
price: quote_price, price: quote_price,
mark_price, mark_price,
quantity: take_qty, quantity: take_qty,
execution_start_timestamp: Some(quote.timestamp),
execution_timestamp: Some(quote.timestamp),
}); });
if consume_depth { if consume_depth {
let state = depth_state let state = depth_state
@@ -7064,6 +7081,8 @@ where
price: gross_amount / filled_qty as f64, price: gross_amount / filled_qty as f64,
mark_price: mark_amount / filled_qty as f64, mark_price: mark_amount / filled_qty as f64,
quantity: filled_qty, quantity: filled_qty,
execution_start_timestamp: first_timestamp,
execution_timestamp: last_timestamp,
}] }]
} else { } else {
legs legs
@@ -9966,6 +9985,8 @@ mod tests {
assert_eq!(fill.quantity, 200); assert_eq!(fill.quantity, 200);
assert_eq!(fill.legs.len(), 1); assert_eq!(fill.legs.len(), 1);
assert_eq!(fill.legs[0].price, 10.8); 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!( assert_eq!(
fill.next_cursor, fill.next_cursor,
quote_timestamp + chrono::Duration::seconds(1) quote_timestamp + chrono::Duration::seconds(1)
+2
View File
@@ -3494,6 +3494,8 @@ where
decision_date: None, decision_date: None,
order_created_date: None, order_created_date: None,
execution_date: None, execution_date: None,
execution_start_timestamp: None,
execution_timestamp: None,
order_id: None, order_id: None,
symbol: receivable.symbol.clone(), symbol: receivable.symbol.clone(),
side: OrderSide::Buy, side: OrderSide::Buy,
+139 -3
View File
@@ -1,4 +1,4 @@
use chrono::NaiveDate; use chrono::{NaiveDate, NaiveDateTime};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
mod date_format { 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)] #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum OrderSide { pub enum OrderSide {
Buy, Buy,
@@ -162,6 +191,18 @@ pub struct FillEvent {
pub order_created_date: Option<NaiveDate>, pub order_created_date: Option<NaiveDate>,
#[serde(default, with = "optional_date_format")] #[serde(default, with = "optional_date_format")]
pub execution_date: Option<NaiveDate>, 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)] #[serde(default)]
pub order_id: Option<u64>, pub order_id: Option<u64>,
pub symbol: String, pub symbol: String,
@@ -176,6 +217,42 @@ pub struct FillEvent {
pub reason: String, 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PositionEvent { pub struct PositionEvent {
#[serde(with = "date_format")] #[serde(with = "date_format")]
@@ -299,9 +376,9 @@ pub struct ProcessEvent {
#[cfg(test)] #[cfg(test)]
mod tests { 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 { fn order_event(status: OrderStatus, filled_quantity: u32) -> OrderEvent {
OrderEvent { OrderEvent {
@@ -342,4 +419,63 @@ mod tests {
assert!(order_event(OrderStatus::Rejected, 1).validate().is_err()); assert!(order_event(OrderStatus::Rejected, 1).validate().is_err());
assert!(order_event(OrderStatus::Expired, 100).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"
);
}
} }
+2
View File
@@ -1052,6 +1052,8 @@ impl FuturesAccountState {
decision_date: None, decision_date: None,
order_created_date: None, order_created_date: None,
execution_date: None, execution_date: None,
execution_start_timestamp: None,
execution_timestamp: None,
order_id, order_id,
symbol: intent.symbol.clone(), symbol: intent.symbol.clone(),
side, side,
@@ -2537,6 +2537,14 @@ fn broker_splits_intraday_quote_fills_and_tracks_commission_by_order() {
assert_eq!(report.fill_events[1].quantity, 100); assert_eq!(report.fill_events[1].quantity, 100);
assert!((report.fill_events[0].price - 10.01).abs() < 1e-9); assert!((report.fill_events[0].price - 10.01).abs() < 1e-9);
assert!((report.fill_events[1].price - 10.03).abs() < 1e-9); assert!((report.fill_events[1].price - 10.03).abs() < 1e-9);
assert_eq!(
report.fill_events[0].execution_timestamp,
date.and_hms_opt(10, 18, 3)
);
assert_eq!(
report.fill_events[1].execution_timestamp,
date.and_hms_opt(10, 18, 6)
);
assert!((report.fill_events[0].commission - 5.0).abs() < 1e-9); assert!((report.fill_events[0].commission - 5.0).abs() < 1e-9);
assert_eq!(report.fill_events[1].commission, 0.0); assert_eq!(report.fill_events[1].commission, 0.0);
assert_eq!(report.account_events.len(), 2); assert_eq!(report.account_events.len(), 2);
@@ -2699,6 +2707,14 @@ fn broker_aggregates_intraday_quote_fills_into_vwap_leg() {
assert_eq!(report.fill_events.len(), 1); assert_eq!(report.fill_events.len(), 1);
assert_eq!(report.fill_events[0].quantity, 200); assert_eq!(report.fill_events[0].quantity, 200);
assert!((report.fill_events[0].price - 10.02).abs() < 1e-9); assert!((report.fill_events[0].price - 10.02).abs() < 1e-9);
assert_eq!(
report.fill_events[0].execution_start_timestamp,
date.and_hms_opt(10, 18, 3)
);
assert_eq!(
report.fill_events[0].execution_timestamp,
date.and_hms_opt(10, 18, 6)
);
assert!((report.fill_events[0].commission - 5.0).abs() < 1e-9); assert!((report.fill_events[0].commission - 5.0).abs() < 1e-9);
assert_eq!(report.account_events.len(), 1); assert_eq!(report.account_events.len(), 1);
assert_eq!( assert_eq!(