diff --git a/crates/fidc-core/src/broker.rs b/crates/fidc-core/src/broker.rs index 506aad8..70f523a 100644 --- a/crates/fidc-core/src/broker.rs +++ b/crates/fidc-core/src/broker.rs @@ -950,6 +950,15 @@ where }; if let Some(order) = canceled { self.emit_user_canceled_open_order(date, order, reason, report); + } else { + report.process_events.push(ProcessEvent { + date, + kind: ProcessEventKind::OrderCancellationReject, + order_id: Some(order_id), + symbol: None, + side: None, + detail: format!("reason={reason} status=not_found"), + }); } } @@ -974,6 +983,16 @@ where *open_orders = retained; canceled }; + if canceled.is_empty() { + report.process_events.push(ProcessEvent { + date, + kind: ProcessEventKind::OrderCancellationReject, + order_id: None, + symbol: Some(symbol.to_string()), + side: None, + detail: format!("reason={reason} status=no_open_orders_for_symbol"), + }); + } for order in canceled { self.emit_user_canceled_open_order(date, order, reason, report); } @@ -989,6 +1008,16 @@ where let mut open_orders = self.open_orders.borrow_mut(); std::mem::take(&mut *open_orders) }; + if canceled.is_empty() { + report.process_events.push(ProcessEvent { + date, + kind: ProcessEventKind::OrderCancellationReject, + order_id: None, + symbol: None, + side: None, + detail: format!("reason={reason} status=no_open_orders"), + }); + } for order in canceled { self.emit_user_canceled_open_order(date, order, reason, report); } @@ -1090,6 +1119,14 @@ where }); } + fn creation_reject_kind(emit_creation_events: bool) -> ProcessEventKind { + if emit_creation_events { + ProcessEventKind::OrderCreationReject + } else { + ProcessEventKind::OrderUnsolicitedUpdate + } + } + fn target_quantities( &self, date: NaiveDate, @@ -1578,7 +1615,7 @@ where Self::emit_order_process_event( report, date, - ProcessEventKind::OrderUnsolicitedUpdate, + Self::creation_reject_kind(emit_creation_events), order_id, symbol, OrderSide::Sell, @@ -1674,7 +1711,7 @@ where Self::emit_order_process_event( report, date, - ProcessEventKind::OrderUnsolicitedUpdate, + Self::creation_reject_kind(emit_creation_events), order_id, symbol, OrderSide::Sell, @@ -1735,7 +1772,7 @@ where Self::emit_order_process_event( report, date, - ProcessEventKind::OrderUnsolicitedUpdate, + Self::creation_reject_kind(emit_creation_events), order_id, symbol, OrderSide::Sell, @@ -1843,7 +1880,7 @@ where Self::emit_order_process_event( report, date, - ProcessEventKind::OrderUnsolicitedUpdate, + Self::creation_reject_kind(emit_creation_events), order_id, symbol, OrderSide::Sell, @@ -2653,7 +2690,7 @@ where Self::emit_order_process_event( report, date, - ProcessEventKind::OrderUnsolicitedUpdate, + Self::creation_reject_kind(emit_creation_events), order_id, symbol, OrderSide::Buy, @@ -2738,7 +2775,7 @@ where Self::emit_order_process_event( report, date, - ProcessEventKind::OrderUnsolicitedUpdate, + Self::creation_reject_kind(emit_creation_events), order_id, symbol, OrderSide::Buy, @@ -2871,7 +2908,7 @@ where Self::emit_order_process_event( report, date, - ProcessEventKind::OrderUnsolicitedUpdate, + Self::creation_reject_kind(emit_creation_events), order_id, symbol, OrderSide::Buy, diff --git a/crates/fidc-core/src/events.rs b/crates/fidc-core/src/events.rs index a8c9e4d..3d9c3ef 100644 --- a/crates/fidc-core/src/events.rs +++ b/crates/fidc-core/src/events.rs @@ -110,8 +110,10 @@ pub enum ProcessEventKind { PostSettlement, OrderPendingNew, OrderCreationPass, + OrderCreationReject, OrderPendingCancel, OrderCancellationPass, + OrderCancellationReject, OrderUnsolicitedUpdate, Trade, } diff --git a/crates/fidc-core/tests/explicit_order_flow.rs b/crates/fidc-core/tests/explicit_order_flow.rs index eea96f4..bc07663 100644 --- a/crates/fidc-core/tests/explicit_order_flow.rs +++ b/crates/fidc-core/tests/explicit_order_flow.rs @@ -648,7 +648,7 @@ fn broker_cancels_buy_when_open_hits_upper_limit() { .contains("open at or above upper limit") ); assert!(report.process_events.iter().any(|event| { - event.kind == ProcessEventKind::OrderUnsolicitedUpdate + event.kind == ProcessEventKind::OrderCreationReject && event.symbol.as_deref() == Some("000002.SZ") && event.side == Some(fidc_core::OrderSide::Buy) })); @@ -2974,6 +2974,43 @@ fn broker_cancels_open_order_by_order_id() { assert!(portfolio.position("000002.SZ").is_none()); } +#[test] +fn broker_emits_cancellation_reject_for_unknown_order() { + let day1 = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap(); + let data = two_day_limit_order_data(10.0, 10.1); + let broker = BrokerSimulator::new_with_execution_price( + ChinaAShareCostModel::default(), + ChinaEquityRuleHooks::default(), + PriceField::Open, + ); + let mut portfolio = PortfolioState::new(1_000_000.0); + + let report = broker + .execute( + day1, + &mut portfolio, + &data, + &StrategyDecision { + rebalance: false, + target_weights: BTreeMap::new(), + exit_symbols: BTreeSet::new(), + order_intents: vec![OrderIntent::CancelOrder { + order_id: 999_999, + reason: "user_cancel".to_string(), + }], + notes: Vec::new(), + diagnostics: Vec::new(), + }, + ) + .expect("cancel reject execution"); + + assert!(report.order_events.is_empty()); + assert!(report.fill_events.is_empty()); + assert!(report.process_events.iter().any(|event| { + event.kind == ProcessEventKind::OrderCancellationReject && event.order_id == Some(999_999) + })); +} + #[test] fn broker_reserves_sellable_quantity_for_open_limit_sells() { let date = NaiveDate::from_ymd_opt(2024, 1, 10).unwrap();