记录分钟成交精确时间

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
+139 -3
View File
@@ -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"
);
}
}