From ba2470aefecdb5e0f0c825ef45788269bf22a6fd Mon Sep 17 00:00:00 2001 From: boris Date: Sun, 5 Jul 2026 08:47:52 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90=E5=9B=9E=E6=B5=8B=E4=BA=A4?= =?UTF-8?q?=E6=98=93=E6=97=A5=E6=9C=9F=E5=AE=A1=E8=AE=A1=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/fidc-core/src/broker.rs | 191 +++++++++++++++++++++++++++++++- crates/fidc-core/src/engine.rs | 102 ++++++++++++++--- crates/fidc-core/src/events.rs | 39 +++++++ crates/fidc-core/src/futures.rs | 12 ++ 4 files changed, 330 insertions(+), 14 deletions(-) diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index cd07a0b..b48f0bd 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -46,6 +46,8 @@ struct ExecutionFill { #[derive(Debug, Clone)] struct OpenOrder { order_id: u64, + decision_date: Option, + order_created_date: Option, symbol: String, side: OrderSide, requested_quantity: u32, @@ -176,6 +178,8 @@ pub struct BrokerSimulator { intraday_execution_start_time: Option, runtime_intraday_start_time: Cell>, runtime_intraday_end_time: Cell>, + runtime_decision_date: Cell>, + runtime_order_created_date: Cell>, next_order_id: Cell, open_orders: RefCell>, } @@ -201,6 +205,8 @@ impl BrokerSimulator { intraday_execution_start_time: None, runtime_intraday_start_time: Cell::new(None), runtime_intraday_end_time: Cell::new(None), + runtime_decision_date: Cell::new(None), + runtime_order_created_date: Cell::new(None), next_order_id: Cell::new(1), open_orders: RefCell::new(Vec::new()), } @@ -230,6 +236,8 @@ impl BrokerSimulator { intraday_execution_start_time: None, runtime_intraday_start_time: Cell::new(None), runtime_intraday_end_time: Cell::new(None), + runtime_decision_date: Cell::new(None), + runtime_order_created_date: Cell::new(None), next_order_id: Cell::new(1), open_orders: RefCell::new(Vec::new()), } @@ -640,6 +648,37 @@ where portfolio: &mut PortfolioState, data: &DataSet, decision: &StrategyDecision, + ) -> Result { + self.execute_with_event_dates(date, date, date, portfolio, data, decision) + } + + pub fn execute_with_event_dates( + &self, + date: NaiveDate, + decision_date: NaiveDate, + order_created_date: NaiveDate, + portfolio: &mut PortfolioState, + data: &DataSet, + decision: &StrategyDecision, + ) -> Result { + let previous_decision_date = self.runtime_decision_date.get(); + let previous_order_created_date = self.runtime_order_created_date.get(); + self.runtime_decision_date.set(Some(decision_date)); + self.runtime_order_created_date + .set(Some(order_created_date)); + let result = self.execute_with_runtime_dates(date, portfolio, data, decision); + self.runtime_decision_date.set(previous_decision_date); + self.runtime_order_created_date + .set(previous_order_created_date); + result + } + + fn execute_with_runtime_dates( + &self, + date: NaiveDate, + portfolio: &mut PortfolioState, + data: &DataSet, + decision: &StrategyDecision, ) -> Result { let mut report = BrokerExecutionReport::default(); let mut intraday_turnover = BTreeMap::::new(); @@ -769,12 +808,35 @@ where decision: &StrategyDecision, start_time: Option, end_time: Option, + ) -> Result { + self.execute_between_with_event_dates( + date, date, date, portfolio, data, decision, start_time, end_time, + ) + } + + pub fn execute_between_with_event_dates( + &self, + date: NaiveDate, + decision_date: NaiveDate, + order_created_date: NaiveDate, + portfolio: &mut PortfolioState, + data: &DataSet, + decision: &StrategyDecision, + start_time: Option, + end_time: Option, ) -> Result { let previous_start_time = self.runtime_intraday_start_time.get(); let previous_end_time = self.runtime_intraday_end_time.get(); self.runtime_intraday_start_time.set(start_time); self.runtime_intraday_end_time.set(end_time); - let result = self.execute(date, portfolio, data, decision); + let result = self.execute_with_event_dates( + date, + decision_date, + order_created_date, + portfolio, + data, + decision, + ); self.runtime_intraday_start_time.set(previous_start_time); self.runtime_intraday_end_time.set(previous_end_time); result @@ -1389,6 +1451,34 @@ where open_orders.push(open_order); } + fn current_decision_date(&self, date: NaiveDate) -> NaiveDate { + self.runtime_decision_date.get().unwrap_or(date) + } + + fn current_order_created_date(&self, date: NaiveDate) -> NaiveDate { + self.runtime_order_created_date.get().unwrap_or(date) + } + + fn annotate_report_range( + report: &mut BrokerExecutionReport, + order_event_start: usize, + fill_event_start: usize, + decision_date: NaiveDate, + order_created_date: NaiveDate, + execution_date: NaiveDate, + ) { + for event in &mut report.order_events[order_event_start..] { + event.decision_date.get_or_insert(decision_date); + event.order_created_date.get_or_insert(order_created_date); + event.execution_date.get_or_insert(execution_date); + } + for fill in &mut report.fill_events[fill_event_start..] { + fill.decision_date.get_or_insert(decision_date); + fill.order_created_date.get_or_insert(order_created_date); + fill.execution_date.get_or_insert(execution_date); + } + } + fn clear_open_order(&self, order_id: u64) { self.open_orders .borrow_mut() @@ -1461,6 +1551,8 @@ where std::mem::take(&mut *open_orders) }; for order in pending_orders { + let order_event_start = report.order_events.len(); + let fill_event_start = report.fill_events.len(); let signed_quantity = if order.side == OrderSide::Buy { order.remaining_quantity as i32 } else { @@ -1482,6 +1574,14 @@ where commission_state, report, )?; + Self::annotate_report_range( + report, + order_event_start, + fill_event_start, + order.decision_date.unwrap_or(date), + order.order_created_date.unwrap_or(date), + date, + ); } Ok(()) } @@ -1597,6 +1697,9 @@ where ); report.order_events.push(OrderEvent { date, + decision_date: order.decision_date, + order_created_date: order.order_created_date, + execution_date: Some(date), order_id: Some(order.order_id), symbol: order.symbol.clone(), side: order.side, @@ -1632,6 +1735,9 @@ where ); report.order_events.push(OrderEvent { date, + decision_date: order.decision_date, + order_created_date: order.order_created_date, + execution_date: Some(date), order_id: Some(order.order_id), symbol: order.symbol.clone(), side: order.side, @@ -1692,6 +1798,9 @@ where ); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side, @@ -1738,6 +1847,9 @@ where } report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side, @@ -2639,6 +2751,9 @@ where }; report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -2704,6 +2819,8 @@ where if allow_pending_limit { self.upsert_open_order(OpenOrder { order_id, + decision_date: Some(self.current_decision_date(date)), + order_created_date: Some(self.current_order_created_date(date)), symbol: symbol.to_string(), side: OrderSide::Sell, requested_quantity: requested_qty, @@ -2714,6 +2831,9 @@ where }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -2735,6 +2855,9 @@ where } report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -2765,6 +2888,8 @@ where .unwrap_or("no sellable quantity"); self.upsert_open_order(OpenOrder { order_id, + decision_date: Some(self.current_decision_date(date)), + order_created_date: Some(self.current_order_created_date(date)), symbol: symbol.to_string(), side: OrderSide::Sell, requested_quantity: requested_qty, @@ -2775,6 +2900,9 @@ where }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -2796,6 +2924,9 @@ where } report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -2891,6 +3022,8 @@ where if allow_pending_limit && Self::limit_order_can_remain_open(Some(detail)) { self.upsert_open_order(OpenOrder { order_id, + decision_date: Some(self.current_decision_date(date)), + order_created_date: Some(self.current_order_created_date(date)), symbol: symbol.to_string(), side: OrderSide::Sell, requested_quantity: requested_qty, @@ -2901,6 +3034,9 @@ where }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -2922,6 +3058,9 @@ where } report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -2973,6 +3112,9 @@ where report.fill_events.push(FillEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -3032,6 +3174,8 @@ where if keep_open { self.upsert_open_order(OpenOrder { order_id, + decision_date: Some(self.current_decision_date(date)), + order_created_date: Some(self.current_order_created_date(date)), symbol: symbol.to_string(), side: OrderSide::Sell, requested_quantity: requested_qty, @@ -3081,6 +3225,9 @@ where report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Sell, @@ -3126,6 +3273,9 @@ where if current_qty == 0 { report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: symbol.to_string(), side: OrderSide::Sell, @@ -3210,6 +3360,9 @@ where } else { report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: symbol.to_string(), side: if current_qty > 0 { @@ -3269,6 +3422,9 @@ where if current_qty == 0 { report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: symbol.to_string(), side: OrderSide::Sell, @@ -3323,6 +3479,9 @@ where } else { report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: symbol.to_string(), side: if current_qty > 0 { @@ -3419,6 +3578,9 @@ where } else { report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: symbol.to_string(), side: if current_qty > 0 { @@ -4279,6 +4441,9 @@ where }; report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Buy, @@ -4333,6 +4498,8 @@ where if allow_pending_limit { self.upsert_open_order(OpenOrder { order_id, + decision_date: Some(self.current_decision_date(date)), + order_created_date: Some(self.current_order_created_date(date)), symbol: symbol.to_string(), side: OrderSide::Buy, requested_quantity: requested_qty, @@ -4343,6 +4510,9 @@ where }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Buy, @@ -4364,6 +4534,9 @@ where } report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Buy, @@ -4523,6 +4696,8 @@ where if allow_pending_limit && Self::limit_order_can_remain_open(Some(detail)) { self.upsert_open_order(OpenOrder { order_id, + decision_date: Some(self.current_decision_date(date)), + order_created_date: Some(self.current_order_created_date(date)), symbol: symbol.to_string(), side: OrderSide::Buy, requested_quantity: requested_qty, @@ -4533,6 +4708,9 @@ where }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Buy, @@ -4554,6 +4732,9 @@ where } report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Buy, @@ -4609,6 +4790,9 @@ where report.fill_events.push(FillEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Buy, @@ -4666,6 +4850,8 @@ where if keep_open { self.upsert_open_order(OpenOrder { order_id, + decision_date: Some(self.current_decision_date(date)), + order_created_date: Some(self.current_order_created_date(date)), symbol: symbol.to_string(), side: OrderSide::Buy, requested_quantity: requested_qty, @@ -4715,6 +4901,9 @@ where report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: symbol.to_string(), side: OrderSide::Buy, diff --git a/crates/fidc-core/src/engine.rs b/crates/fidc-core/src/engine.rs index d98f306..b67b9fe 100644 --- a/crates/fidc-core/src/engine.rs +++ b/crates/fidc-core/src/engine.rs @@ -1192,6 +1192,9 @@ where }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: self .futures_open_orders @@ -1229,6 +1232,9 @@ where let mut report = FuturesExecutionReport::default(); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order_id), symbol: intent.symbol.clone(), side, @@ -1704,27 +1710,52 @@ where &mut portfolio, &mut corporate_action_notes, ); - self.extend_result(&mut result, pending_cash_flow_report); + self.extend_result( + &mut result, + pending_cash_flow_report, + execution_date, + execution_date, + ); let corporate_action_report = self.apply_corporate_actions( execution_date, &mut portfolio, &mut corporate_action_notes, )?; - self.extend_result(&mut result, corporate_action_report); + self.extend_result( + &mut result, + corporate_action_report, + execution_date, + execution_date, + ); let receivable_report = self.settle_cash_receivables( execution_date, &mut portfolio, &mut corporate_action_notes, )?; - self.extend_result(&mut result, receivable_report); + self.extend_result( + &mut result, + receivable_report, + execution_date, + execution_date, + ); let delisting_report = self.settle_delisted_positions( execution_date, &mut portfolio, &mut corporate_action_notes, )?; - self.extend_result(&mut result, delisting_report); + self.extend_result( + &mut result, + delisting_report, + execution_date, + execution_date, + ); let futures_open_order_report = self.process_futures_open_orders(execution_date); - self.extend_result(&mut result, futures_open_order_report); + self.extend_result( + &mut result, + futures_open_order_report, + execution_date, + execution_date, + ); let decision_slot = execution_idx .checked_sub(self.config.decision_lag_trading_days) @@ -1750,7 +1781,7 @@ where let day_fills = report.fill_events.clone(); let broker_diagnostics = report.diagnostics.clone(); let execution_risk_decisions = risk_decisions_from_order_events(&day_orders); - self.extend_result(&mut result, report); + self.extend_result(&mut result, report, execution_date, execution_date); result.risk_decisions.extend(execution_risk_decisions); let benchmark = @@ -2014,8 +2045,10 @@ where None, None, )?; - let mut report = self.broker.execute( + let mut report = self.broker.execute_with_event_dates( execution_date, + decision_date, + decision_date, &mut portfolio, &self.data, &auction_decision, @@ -2260,9 +2293,14 @@ where None, None, )?; - let mut intraday_report = - self.broker - .execute(execution_date, &mut portfolio, &self.data, &decision)?; + let mut intraday_report = self.broker.execute_with_event_dates( + execution_date, + decision_date, + decision_date, + &mut portfolio, + &self.data, + &decision, + )?; let post_intraday_open_orders = self.open_order_views(); publish_process_events( &mut self.strategy, @@ -2426,8 +2464,10 @@ where Some(minute_time), Some(minute_time), )?; - let mut minute_report = self.broker.execute_between( + let mut minute_report = self.broker.execute_between_with_event_dates( execution_date, + decision_date, + decision_date, &mut portfolio, &self.data, &minute_decision, @@ -2750,7 +2790,7 @@ where let day_fills = report.fill_events.clone(); let broker_diagnostics = report.diagnostics.clone(); let execution_risk_decisions = risk_decisions_from_order_events(&day_orders); - self.extend_result(&mut result, report); + self.extend_result(&mut result, report, decision_date, execution_date); result.risk_decisions.extend(decision.risk_decisions); result.risk_decisions.extend(execution_risk_decisions); @@ -2838,8 +2878,11 @@ where fn extend_result( &self, result: &mut BacktestResult, - report: BrokerExecutionReport, + mut report: BrokerExecutionReport, + decision_date: NaiveDate, + execution_date: NaiveDate, ) -> BrokerExecutionReport { + annotate_broker_report_dates(&mut report, decision_date, decision_date, execution_date); result.order_events.extend(report.order_events.clone()); result.fills.extend(report.fill_events.clone()); result @@ -3063,6 +3106,9 @@ where ); report.fill_events.push(FillEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: receivable.symbol.clone(), side: OrderSide::Buy, @@ -3366,6 +3412,9 @@ where notes.push(reason.clone()); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: symbol.clone(), side: OrderSide::Sell, @@ -3376,6 +3425,9 @@ where }); report.fill_events.push(FillEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: None, symbol: symbol.clone(), side: OrderSide::Sell, @@ -3827,6 +3879,24 @@ fn merge_futures_report(target: &mut BrokerExecutionReport, incoming: FuturesExe target.diagnostics.extend(incoming.diagnostics); } +fn annotate_broker_report_dates( + report: &mut BrokerExecutionReport, + decision_date: NaiveDate, + order_created_date: NaiveDate, + execution_date: NaiveDate, +) { + for event in &mut report.order_events { + event.decision_date.get_or_insert(decision_date); + event.order_created_date.get_or_insert(order_created_date); + event.execution_date.get_or_insert(execution_date); + } + for fill in &mut report.fill_events { + fill.decision_date.get_or_insert(decision_date); + fill.order_created_date.get_or_insert(order_created_date); + fill.execution_date.get_or_insert(execution_date); + } +} + fn risk_decisions_from_order_events(order_events: &[OrderEvent]) -> Vec { order_events .iter() @@ -4021,6 +4091,9 @@ fn futures_cancel_report( }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id: Some(order.order_id), symbol: order.intent.symbol.clone(), side, @@ -4779,6 +4852,9 @@ mod tests { assert_eq!(result.fills.len(), 1); assert_eq!(result.fills[0].date, d(2025, 1, 3)); + assert_eq!(result.fills[0].decision_date, Some(d(2025, 1, 2))); + assert_eq!(result.fills[0].order_created_date, Some(d(2025, 1, 2))); + assert_eq!(result.fills[0].execution_date, Some(d(2025, 1, 3))); assert_eq!(result.fills[0].price, 12.0); } diff --git a/crates/fidc-core/src/events.rs b/crates/fidc-core/src/events.rs index 0499b7c..554fc18 100644 --- a/crates/fidc-core/src/events.rs +++ b/crates/fidc-core/src/events.rs @@ -23,6 +23,33 @@ mod date_format { } } +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() + } +} + #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum OrderSide { Buy, @@ -63,6 +90,12 @@ impl OrderStatus { 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, @@ -77,6 +110,12 @@ pub struct OrderEvent { 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)] pub order_id: Option, pub symbol: String, diff --git a/crates/fidc-core/src/futures.rs b/crates/fidc-core/src/futures.rs index a076eca..be05c07 100644 --- a/crates/fidc-core/src/futures.rs +++ b/crates/fidc-core/src/futures.rs @@ -746,6 +746,9 @@ impl FuturesAccountState { ); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id, symbol: intent.symbol, side, @@ -823,6 +826,9 @@ impl FuturesAccountState { intent.price * intent.quantity as f64 * intent.spec.contract_multiplier; report.fill_events.push(FillEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id, symbol: intent.symbol.clone(), side, @@ -889,6 +895,9 @@ impl FuturesAccountState { }); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id, symbol: intent.symbol, side, @@ -915,6 +924,9 @@ impl FuturesAccountState { ); report.order_events.push(OrderEvent { date, + decision_date: None, + order_created_date: None, + execution_date: None, order_id, symbol: intent.symbol, side,