use chrono::{NaiveDate, NaiveDateTime}; use serde::{Deserialize, Serialize}; mod date_format { use chrono::NaiveDate; use serde::{self, Deserialize, Deserializer, Serializer}; const FORMAT: &str = "%Y-%m-%d"; pub fn serialize(date: &NaiveDate, serializer: S) -> Result where S: Serializer, { serializer.serialize_str(&date.format(FORMAT).to_string()) } pub fn deserialize<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, { let text = String::deserialize(deserializer)?; NaiveDate::parse_from_str(&text, FORMAT).map_err(serde::de::Error::custom) } } mod optional_date_format { use chrono::NaiveDate; use serde::{self, Deserialize, Deserializer, Serializer}; const FORMAT: &str = "%Y-%m-%d"; pub fn serialize(date: &Option, serializer: S) -> Result where S: Serializer, { match date { Some(date) => serializer.serialize_some(&date.format(FORMAT).to_string()), None => serializer.serialize_none(), } } pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, { let value = Option::::deserialize(deserializer)?; value .map(|text| NaiveDate::parse_from_str(&text, FORMAT).map_err(serde::de::Error::custom)) .transpose() } } 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(datetime: &Option, serializer: S) -> Result 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, D::Error> where D: Deserializer<'de>, { let value = Option::::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, Sell, } impl OrderSide { pub fn as_str(&self) -> &'static str { match self { Self::Buy => "buy", Self::Sell => "sell", } } } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum OrderStatus { Pending, Filled, PartiallyFilled, Canceled, Rejected, Expired, } impl OrderStatus { pub fn as_str(&self) -> &'static str { match self { Self::Pending => "pending", Self::Filled => "filled", Self::PartiallyFilled => "partially_filled", Self::Canceled => "canceled", Self::Rejected => "rejected", Self::Expired => "expired", } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct OrderEvent { #[serde(with = "date_format")] pub date: NaiveDate, #[serde(default, with = "optional_date_format")] pub decision_date: Option, #[serde(default, with = "optional_date_format")] pub order_created_date: Option, #[serde(default, with = "optional_date_format")] pub execution_date: Option, #[serde(default)] pub order_id: Option, pub symbol: String, pub side: OrderSide, pub requested_quantity: u32, pub filled_quantity: u32, pub status: OrderStatus, 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, OrderStatus::Expired => self.filled_quantity < self.requested_quantity, }; 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")] pub date: NaiveDate, #[serde(default, with = "optional_date_format")] pub decision_date: Option, #[serde(default, with = "optional_date_format")] pub order_created_date: Option, #[serde(default, with = "optional_date_format")] pub execution_date: Option, #[serde( default, with = "optional_datetime_format", skip_serializing_if = "Option::is_none" )] pub execution_start_timestamp: Option, #[serde( default, with = "optional_datetime_format", skip_serializing_if = "Option::is_none" )] pub execution_timestamp: Option, #[serde(default)] pub order_id: Option, pub symbol: String, pub side: OrderSide, pub quantity: u32, pub price: f64, pub gross_amount: f64, pub commission: f64, pub stamp_tax: f64, pub transfer_fee: f64, pub net_cash_flow: f64, 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")] pub date: NaiveDate, pub symbol: String, pub delta_quantity: i32, pub quantity_after: u32, pub average_cost: f64, pub realized_pnl_delta: f64, pub reason: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AccountEvent { #[serde(with = "date_format")] pub date: NaiveDate, pub cash_before: f64, pub cash_after: f64, pub total_equity: f64, pub note: String, } #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] pub enum ProcessEventKind { PreBeforeTrading, BeforeTrading, PostBeforeTrading, PreOpenAuction, OpenAuction, PostOpenAuction, PreBar, Bar, PostBar, PreMinute, Minute, PostMinute, PreScheduled, PostScheduled, PreOnDay, OnDay, PostOnDay, PreAfterTrading, AfterTrading, PostAfterTrading, PreSettlement, Settlement, PostSettlement, OrderPendingNew, OrderCreationPass, OrderCreationReject, OrderPendingCancel, OrderCancellationPass, OrderCancellationReject, OrderPendingUpdate, OrderUpdatePass, OrderUpdateReject, OrderUnsolicitedUpdate, Trade, UniverseUpdated, UniverseSubscribed, UniverseUnsubscribed, AccountDepositWithdraw, AccountFinanceRepay, AccountManagementFee, } impl ProcessEventKind { pub fn as_str(&self) -> &'static str { match self { Self::PreBeforeTrading => "pre_before_trading", Self::BeforeTrading => "before_trading", Self::PostBeforeTrading => "post_before_trading", Self::PreOpenAuction => "pre_open_auction", Self::OpenAuction => "open_auction", Self::PostOpenAuction => "post_open_auction", Self::PreBar => "pre_bar", Self::Bar => "bar", Self::PostBar => "post_bar", Self::PreMinute => "pre_minute", Self::Minute => "minute", Self::PostMinute => "post_minute", Self::PreScheduled => "pre_scheduled", Self::PostScheduled => "post_scheduled", Self::PreOnDay => "pre_on_day", Self::OnDay => "on_day", Self::PostOnDay => "post_on_day", Self::PreAfterTrading => "pre_after_trading", Self::AfterTrading => "after_trading", Self::PostAfterTrading => "post_after_trading", Self::PreSettlement => "pre_settlement", Self::Settlement => "settlement", Self::PostSettlement => "post_settlement", Self::OrderPendingNew => "order_pending_new", Self::OrderCreationPass => "order_creation_pass", Self::OrderCreationReject => "order_creation_reject", Self::OrderPendingCancel => "order_pending_cancel", Self::OrderCancellationPass => "order_cancellation_pass", Self::OrderCancellationReject => "order_cancellation_reject", Self::OrderPendingUpdate => "order_pending_update", Self::OrderUpdatePass => "order_update_pass", Self::OrderUpdateReject => "order_update_reject", Self::OrderUnsolicitedUpdate => "order_unsolicited_update", Self::Trade => "trade", Self::UniverseUpdated => "universe_updated", Self::UniverseSubscribed => "universe_subscribed", Self::UniverseUnsubscribed => "universe_unsubscribed", Self::AccountDepositWithdraw => "account_deposit_withdraw", Self::AccountFinanceRepay => "account_finance_repay", Self::AccountManagementFee => "account_management_fee", } } /// Returns whether the event is part of the durable business lifecycle /// audit. Phase boundary events are useful during interactive debugging, /// but retaining every minute phase marker for a long run is unnecessary. pub fn is_business_lifecycle(&self) -> bool { matches!( *self, Self::PreScheduled | Self::PostScheduled | Self::PreOnDay | Self::OnDay | Self::PostOnDay | Self::OrderPendingNew | Self::OrderCreationPass | Self::OrderCreationReject | Self::OrderPendingCancel | Self::OrderCancellationPass | Self::OrderCancellationReject | Self::OrderPendingUpdate | Self::OrderUpdatePass | Self::OrderUpdateReject | Self::OrderUnsolicitedUpdate | Self::Trade | Self::UniverseUpdated | Self::UniverseSubscribed | Self::UniverseUnsubscribed | Self::AccountDepositWithdraw | Self::AccountFinanceRepay | Self::AccountManagementFee | Self::Settlement ) } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ProcessEvent { #[serde(with = "date_format")] pub date: NaiveDate, pub kind: ProcessEventKind, #[serde(default)] pub order_id: Option, #[serde(default)] pub symbol: Option, #[serde(default)] pub side: Option, pub detail: String, } #[cfg(test)] mod tests { use chrono::{NaiveDate, NaiveDateTime}; use super::{FillEvent, OrderEvent, OrderSide, OrderStatus, ProcessEventKind}; 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::Expired, 40).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()); assert!(order_event(OrderStatus::Expired, 100).validate().is_err()); } fn fill_event(start: Option, end: Option) -> 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" ); } #[test] fn process_event_business_lifecycle_filter_keeps_audit_events_only() { assert!(ProcessEventKind::OrderUpdateReject.is_business_lifecycle()); assert!(ProcessEventKind::Settlement.is_business_lifecycle()); assert!(!ProcessEventKind::PreMinute.is_business_lifecycle()); assert!(!ProcessEventKind::PostBar.is_business_lifecycle()); } }