1use std::{
19 future::Future,
20 sync::{Arc, Mutex},
21 time::{Duration, Instant},
22};
23
24use anyhow::Context;
25use async_trait::async_trait;
26use chrono::{DateTime, Utc};
27use nautilus_common::{
28 cache::InstrumentLookupError,
29 clients::ExecutionClient,
30 live::{get_runtime, runner::get_exec_event_sender},
31 messages::execution::{
32 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
33 GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
34 ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
35 },
36};
37use nautilus_core::{
38 AtomicMap, MUTEX_POISONED, UnixNanos,
39 time::{AtomicTime, get_atomic_clock_realtime},
40};
41use nautilus_live::{ExecutionClientCore, ExecutionEventEmitter};
42use nautilus_model::{
43 accounts::AccountAny,
44 enums::{AccountType, OmsType, OrderSide, OrderStatus, OrderType},
45 identifiers::{
46 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, Venue, VenueOrderId,
47 },
48 instruments::{Instrument, InstrumentAny},
49 orders::{Order, OrderAny},
50 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
51 types::{AccountBalance, MarginBalance, Quantity},
52};
53use rust_decimal::Decimal;
54use tokio::task::JoinHandle;
55use tokio_util::sync::CancellationToken;
56
57use super::{CancelCommandFailure, classify_cancel_http_failure};
58use crate::{
59 common::{
60 consts::KRAKEN_VENUE,
61 credential::KrakenCredential,
62 enums::{KrakenApiResult, KrakenSendStatus},
63 parse::truncate_cl_ord_id,
64 },
65 config::KrakenExecClientConfig,
66 http::{
67 KrakenFuturesHttpClient,
68 futures::{
69 client::KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND, models::FuturesBatchCancelStatus,
70 query::KrakenFuturesBatchCancelItem,
71 },
72 },
73 websocket::{
74 dispatch::{self, OrderIdentity, WsDispatchState},
75 futures::{client::KrakenFuturesWebSocketClient, messages::KrakenFuturesWsMessage},
76 },
77};
78
79const FUTURES_BATCH_CANCEL_LIMIT: usize = 50;
80
81#[allow(dead_code)]
86#[derive(Debug)]
87pub struct KrakenFuturesExecutionClient {
88 core: ExecutionClientCore,
89 clock: &'static AtomicTime,
90 config: KrakenExecClientConfig,
91 emitter: ExecutionEventEmitter,
92 http: KrakenFuturesHttpClient,
93 ws: KrakenFuturesWebSocketClient,
94 cancellation_token: CancellationToken,
95 ws_stream_handle: Option<JoinHandle<()>>,
96 pending_tasks: Mutex<Vec<JoinHandle<()>>>,
97 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
98 truncated_id_map: Arc<AtomicMap<String, ClientOrderId>>,
99 order_instrument_map: Arc<AtomicMap<String, InstrumentId>>,
100 venue_client_map: Arc<AtomicMap<String, ClientOrderId>>,
101 venue_order_qty: Arc<AtomicMap<String, Quantity>>,
102 ws_dispatch_state: Arc<WsDispatchState>,
103}
104
105impl KrakenFuturesExecutionClient {
106 pub fn new(core: ExecutionClientCore, config: KrakenExecClientConfig) -> anyhow::Result<Self> {
108 let clock = get_atomic_clock_realtime();
109 let emitter = ExecutionEventEmitter::new(
110 clock,
111 core.trader_id,
112 core.account_id,
113 AccountType::Margin,
114 None,
115 );
116
117 let cancellation_token = CancellationToken::new();
118
119 let http = KrakenFuturesHttpClient::with_credentials(
120 config.api_key.clone(),
121 config.api_secret.clone(),
122 config.environment,
123 config.base_url.clone(),
124 config.timeout_secs,
125 None,
126 None,
127 None,
128 config.proxy_url.clone(),
129 config
130 .max_requests_per_second
131 .unwrap_or(KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND),
132 )?;
133
134 let credential = KrakenCredential::new(config.api_key.clone(), config.api_secret.clone());
135 let ws = KrakenFuturesWebSocketClient::with_credentials(
136 config.ws_url(),
137 config.heartbeat_interval_secs,
138 Some(credential),
139 config.transport_backend,
140 config.proxy_url.clone(),
141 );
142
143 Ok(Self {
144 core,
145 clock,
146 config,
147 emitter,
148 http,
149 ws,
150 cancellation_token,
151 ws_stream_handle: None,
152 pending_tasks: Mutex::new(Vec::new()),
153 instruments: Arc::new(AtomicMap::new()),
154 truncated_id_map: Arc::new(AtomicMap::new()),
155 order_instrument_map: Arc::new(AtomicMap::new()),
156 venue_client_map: Arc::new(AtomicMap::new()),
157 venue_order_qty: Arc::new(AtomicMap::new()),
158 ws_dispatch_state: Arc::new(WsDispatchState::new()),
159 })
160 }
161
162 fn register_order_identity(&self, order: &OrderAny) {
163 self.ws_dispatch_state.register_identity(
164 order.client_order_id(),
165 OrderIdentity {
166 strategy_id: order.strategy_id(),
167 instrument_id: order.instrument_id(),
168 order_side: order.order_side(),
169 order_type: order.order_type(),
170 quantity: order.quantity(),
171 },
172 );
173 }
174
175 #[must_use]
177 pub fn clock(&self) -> &'static AtomicTime {
178 self.clock
179 }
180
181 #[must_use]
183 pub fn emitter(&self) -> &ExecutionEventEmitter {
184 &self.emitter
185 }
186
187 fn spawn_task<F>(&self, description: &'static str, fut: F)
188 where
189 F: Future<Output = anyhow::Result<()>> + Send + 'static,
190 {
191 let runtime = get_runtime();
192 let handle = runtime.spawn(async move {
193 if let Err(e) = fut.await {
194 log::warn!("{description} failed: {e:?}");
195 }
196 });
197
198 let mut tasks = self.pending_tasks.lock().expect(MUTEX_POISONED);
199 tasks.retain(|handle| !handle.is_finished());
200 tasks.push(handle);
201 }
202
203 fn submit_single_order(&self, order: &OrderAny, task_name: &'static str) {
204 if order.is_closed() {
205 log::warn!(
206 "Cannot submit closed order: client_order_id={}",
207 order.client_order_id()
208 );
209 return;
210 }
211
212 let account_id = self.core.account_id;
213 let client_order_id = order.client_order_id();
214 let strategy_id = order.strategy_id();
215 let instrument_id = order.instrument_id();
216 let order_side = order.order_side();
217 let order_type = order.order_type();
218 let quantity = order.quantity();
219 let time_in_force = order.time_in_force();
220 let price = order.price();
221 let trigger_price = order.trigger_price();
222 let trigger_type = order.trigger_type();
223 let is_reduce_only = order.is_reduce_only();
224 let is_post_only = order.is_post_only();
225
226 log::debug!("OrderSubmitted: client_order_id={client_order_id}");
227 self.register_order_identity(order);
228 self.emitter.emit_order_submitted(order);
229
230 let kraken_cl_ord_id = truncate_cl_ord_id(&client_order_id);
231
232 if kraken_cl_ord_id != client_order_id.as_str() {
233 self.truncated_id_map
234 .insert(kraken_cl_ord_id, client_order_id);
235 }
236
237 let http = self.http.clone();
238 let emitter = self.emitter.clone();
239 let clock = self.clock;
240 let dispatch_state = self.ws_dispatch_state.clone();
241
242 self.spawn_task(task_name, async move {
243 let result = http
244 .submit_order(
245 account_id,
246 instrument_id,
247 client_order_id,
248 order_side,
249 order_type,
250 quantity,
251 time_in_force,
252 price,
253 trigger_price,
254 trigger_type,
255 is_reduce_only,
256 is_post_only,
257 )
258 .await;
259
260 match result {
261 Ok(_report) => Ok(()),
262 Err(e) => {
263 let ts_event = clock.get_time_ns();
264 let error_msg = format!("{task_name} error: {e}");
265 let due_post_only = error_msg.contains("POST_ONLY_REJECTED");
266 dispatch_state.cleanup_terminal(&client_order_id);
269 emitter.emit_order_rejected_event(
270 strategy_id,
271 instrument_id,
272 client_order_id,
273 &error_msg,
274 ts_event,
275 due_post_only,
276 );
277 Ok(())
278 }
279 }
280 });
281 }
282
283 fn cancel_single_order(&self, cmd: &CancelOrder) {
284 let account_id = self.core.account_id;
285 let client_order_id = cmd.client_order_id;
286 let venue_order_id = cmd.venue_order_id;
287 let strategy_id = cmd.strategy_id;
288 let instrument_id = cmd.instrument_id;
289
290 log::debug!(
291 "Canceling order: venue_order_id={venue_order_id:?}, client_order_id={client_order_id}"
292 );
293
294 let http = self.http.clone();
295 let emitter = self.emitter.clone();
296 let clock = self.clock;
297
298 self.spawn_task("cancel_order", async move {
299 if let Err(failure) = cancel_order_for_futures(
300 &http,
301 account_id,
302 instrument_id,
303 Some(client_order_id),
304 venue_order_id,
305 )
306 .await
307 {
308 handle_cancel_failure(
309 &emitter,
310 clock,
311 strategy_id,
312 instrument_id,
313 client_order_id,
314 venue_order_id,
315 failure,
316 );
317 }
318 Ok(())
319 });
320 }
321
322 fn spawn_message_handler(&mut self) -> anyhow::Result<()> {
323 let mut rx = self
324 .ws
325 .take_output_rx()
326 .context("Failed to take futures WebSocket output receiver")?;
327 let emitter = self.emitter.clone();
328 let instruments = self.instruments.clone();
329 let truncated_id_map = self.truncated_id_map.clone();
330 let order_instrument_map = self.order_instrument_map.clone();
331 let venue_client_map = self.venue_client_map.clone();
332 let venue_order_qty = self.venue_order_qty.clone();
333 let dispatch_state = self.ws_dispatch_state.clone();
334 let account_id = self.core.account_id;
335 let clock = self.clock;
336 let cancellation_token = self.cancellation_token.clone();
337
338 let handle = get_runtime().spawn(async move {
339 loop {
340 tokio::select! {
341 () = cancellation_token.cancelled() => {
342 log::debug!("Futures execution message handler cancelled");
343 break;
344 }
345 msg = rx.recv() => {
346 match msg {
347 Some(ws_msg) => {
348 Self::handle_ws_message(
349 ws_msg,
350 &emitter,
351 &dispatch_state,
352 &instruments,
353 &truncated_id_map,
354 &order_instrument_map,
355 &venue_client_map,
356 &venue_order_qty,
357 account_id,
358 clock,
359 );
360 }
361 None => {
362 log::debug!("Futures execution WebSocket stream ended");
363 break;
364 }
365 }
366 }
367 }
368 }
369 });
370
371 self.ws_stream_handle = Some(handle);
372 Ok(())
373 }
374
375 #[expect(clippy::too_many_arguments)]
376 fn handle_ws_message(
377 msg: KrakenFuturesWsMessage,
378 emitter: &ExecutionEventEmitter,
379 dispatch_state: &Arc<WsDispatchState>,
380 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
381 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
382 order_instrument_map: &Arc<AtomicMap<String, InstrumentId>>,
383 venue_client_map: &Arc<AtomicMap<String, ClientOrderId>>,
384 venue_order_qty: &Arc<AtomicMap<String, Quantity>>,
385 account_id: AccountId,
386 clock: &'static AtomicTime,
387 ) {
388 let ts_init = clock.get_time_ns();
389
390 match msg {
391 KrakenFuturesWsMessage::OpenOrdersDelta(delta) => {
392 dispatch::futures::open_orders_delta(
393 &delta,
394 dispatch_state,
395 emitter,
396 instruments,
397 truncated_id_map,
398 order_instrument_map,
399 venue_client_map,
400 venue_order_qty,
401 account_id,
402 ts_init,
403 );
404 }
405 KrakenFuturesWsMessage::OpenOrdersCancel(cancel) => {
406 dispatch::futures::open_orders_cancel(
407 &cancel,
408 dispatch_state,
409 emitter,
410 truncated_id_map,
411 order_instrument_map,
412 venue_client_map,
413 venue_order_qty,
414 account_id,
415 ts_init,
416 );
417 }
418 KrakenFuturesWsMessage::FillsDelta(fills_delta) => {
419 dispatch::futures::fills_delta(
420 &fills_delta,
421 dispatch_state,
422 emitter,
423 instruments,
424 truncated_id_map,
425 venue_client_map,
426 account_id,
427 ts_init,
428 );
429 }
430 KrakenFuturesWsMessage::Challenge(challenge) => {
431 log::debug!("Received challenge: length={}", challenge.len());
432 }
433 KrakenFuturesWsMessage::Reconnected => {
434 log::info!("Futures execution WebSocket reconnected");
435 }
436 KrakenFuturesWsMessage::Ticker(_)
437 | KrakenFuturesWsMessage::Trade(_)
438 | KrakenFuturesWsMessage::BookSnapshot(_)
439 | KrakenFuturesWsMessage::BookDelta(_) => {}
440 }
441 }
442
443 async fn await_account_registered(&self, timeout_secs: f64) -> anyhow::Result<()> {
444 let account_id = self.core.account_id;
445
446 if self.core.cache().account(&account_id).is_some() {
447 log::info!("Account {account_id} registered");
448 return Ok(());
449 }
450
451 let start = Instant::now();
452 let timeout = Duration::from_secs_f64(timeout_secs);
453 let interval = Duration::from_millis(10);
454
455 loop {
456 tokio::time::sleep(interval).await;
457
458 if self.core.cache().account(&account_id).is_some() {
459 log::info!("Account {account_id} registered");
460 return Ok(());
461 }
462
463 if start.elapsed() >= timeout {
464 anyhow::bail!(
465 "Timeout waiting for account {account_id} to be registered after {timeout_secs}s"
466 );
467 }
468 }
469 }
470
471 fn modify_single_order(&self, cmd: &ModifyOrder) {
472 let client_order_id = cmd.client_order_id;
473 let venue_order_id = cmd.venue_order_id;
474 let strategy_id = cmd.strategy_id;
475 let instrument_id = cmd.instrument_id;
476 let quantity = cmd.quantity;
477 let price = cmd.price;
478
479 log::debug!(
480 "Modifying order: venue_order_id={venue_order_id:?}, client_order_id={client_order_id}"
481 );
482
483 let http = self.http.clone();
484 let emitter = self.emitter.clone();
485 let clock = self.clock;
486
487 self.spawn_task("modify_order", async move {
488 if let Err(e) = http
489 .modify_order(
490 instrument_id,
491 Some(client_order_id),
492 venue_order_id,
493 quantity,
494 price,
495 None,
496 )
497 .await
498 {
499 let ts_event = clock.get_time_ns();
500 emitter.emit_order_modify_rejected_event(
501 strategy_id,
502 instrument_id,
503 client_order_id,
504 venue_order_id,
505 &format!("modify-order error: {e}"),
506 ts_event,
507 );
508 anyhow::bail!("Modify order failed: {e}");
509 }
510 Ok(())
511 });
512 }
513}
514
515#[async_trait(?Send)]
516impl ExecutionClient for KrakenFuturesExecutionClient {
517 fn is_connected(&self) -> bool {
518 self.core.is_connected()
519 }
520
521 fn client_id(&self) -> ClientId {
522 self.core.client_id
523 }
524
525 fn account_id(&self) -> AccountId {
526 self.core.account_id
527 }
528
529 fn venue(&self) -> Venue {
530 *KRAKEN_VENUE
531 }
532
533 fn oms_type(&self) -> OmsType {
534 self.core.oms_type
535 }
536
537 fn get_account(&self) -> Option<AccountAny> {
538 self.core.cache().account_owned(&self.core.account_id)
539 }
540
541 fn generate_account_state(
542 &self,
543 balances: Vec<AccountBalance>,
544 margins: Vec<MarginBalance>,
545 reported: bool,
546 ts_event: UnixNanos,
547 ) -> anyhow::Result<()> {
548 self.emitter
549 .emit_account_state(balances, margins, reported, ts_event);
550 Ok(())
551 }
552
553 fn start(&mut self) -> anyhow::Result<()> {
554 if self.core.is_started() {
555 return Ok(());
556 }
557
558 self.emitter.set_sender(get_exec_event_sender());
559 self.core.set_started();
560
561 log::info!(
562 "Started: client_id={}, account_id={}, product_type=Futures, environment={:?}",
563 self.core.client_id,
564 self.core.account_id,
565 self.config.environment
566 );
567 Ok(())
568 }
569
570 fn stop(&mut self) -> anyhow::Result<()> {
571 if self.core.is_stopped() {
572 return Ok(());
573 }
574
575 self.cancellation_token.cancel();
576 self.core.set_stopped();
577 self.core.set_disconnected();
578 log::info!("Stopped: client_id={}", self.core.client_id);
579 Ok(())
580 }
581
582 async fn connect(&mut self) -> anyhow::Result<()> {
583 if self.core.is_connected() {
584 return Ok(());
585 }
586
587 if !self.core.instruments_initialized() {
588 let instruments = self
589 .http
590 .request_instruments()
591 .await
592 .context("Failed to load Kraken futures instruments")?;
593 log::debug!("Loaded {} Futures instruments", instruments.len());
594 self.http.cache_instruments(&instruments);
595 self.core.set_instruments_initialized();
596 }
597
598 self.instruments.rcu(|m| {
599 for instrument in self.http.instruments_cache.load().values() {
600 m.insert(instrument.id(), instrument.clone());
601 }
602 });
603
604 self.ws
605 .connect()
606 .await
607 .context("Failed to connect futures WebSocket")?;
608 self.ws
609 .wait_until_active(10.0)
610 .await
611 .context("Futures WebSocket failed to become active")?;
612
613 self.ws
614 .authenticate()
615 .await
616 .context("Failed to authenticate futures WebSocket")?;
617
618 let account_state = self
620 .http
621 .request_account_state(self.core.account_id)
622 .await
623 .context("Failed to request Kraken futures account state")?;
624
625 if !account_state.balances.is_empty() {
626 log::debug!(
627 "Received account state with {} balance(s)",
628 account_state.balances.len()
629 );
630 }
631 self.emitter.send_account_state(account_state);
632 self.await_account_registered(30.0).await?;
633
634 self.spawn_message_handler()?;
635
636 self.ws
637 .subscribe_executions()
638 .await
639 .context("Failed to subscribe to executions")?;
640
641 log::debug!("Futures WebSocket authenticated and subscribed to executions");
642
643 self.core.set_connected();
644 log::info!("Connected: client_id={}", self.core.client_id);
645 Ok(())
646 }
647
648 async fn disconnect(&mut self) -> anyhow::Result<()> {
649 if self.core.is_disconnected() {
650 return Ok(());
651 }
652
653 self.cancellation_token.cancel();
654
655 if let Some(handle) = self.ws_stream_handle.take() {
656 handle.abort();
657 }
658
659 let _ = self.ws.close().await;
660
661 self.cancellation_token = CancellationToken::new();
662 self.core.set_disconnected();
663 log::info!("Disconnected: client_id={}", self.core.client_id);
664 Ok(())
665 }
666
667 async fn generate_order_status_report(
668 &self,
669 cmd: &GenerateOrderStatusReport,
670 ) -> anyhow::Result<Option<OrderStatusReport>> {
671 log::debug!(
672 "Generating order status report: venue_order_id={:?}, client_order_id={:?}",
673 cmd.venue_order_id,
674 cmd.client_order_id
675 );
676
677 let account_id = self.core.account_id;
678 let reports = self
679 .http
680 .request_order_status_reports(account_id, None, None, None, false)
681 .await?;
682
683 let matched = reports.into_iter().find(|r| {
686 cmd.venue_order_id
687 .is_some_and(|id| r.venue_order_id.as_str() == id.as_str())
688 || cmd.client_order_id.is_some_and(|id| {
689 r.client_order_id
690 .as_ref()
691 .is_some_and(|r_id| r_id.as_str() == truncate_cl_ord_id(&id))
692 })
693 });
694
695 if matched.is_some() {
696 return Ok(matched);
697 }
698
699 let Some(order) = self.get_cached_order_for_status_command(cmd) else {
700 return Ok(None);
701 };
702
703 let now = Utc::now();
704 let start = now - Duration::from_secs(5 * 60);
705 let fills = self
706 .http
707 .request_fill_reports(
708 account_id,
709 Some(order.instrument_id()),
710 Some(start),
711 Some(now),
712 )
713 .await?;
714
715 Ok(synthesize_filled_order_status_report(cmd, &order, &fills))
716 }
717
718 async fn generate_order_status_reports(
719 &self,
720 cmd: &GenerateOrderStatusReports,
721 ) -> anyhow::Result<Vec<OrderStatusReport>> {
722 log::debug!(
723 "Generating order status reports: instrument_id={:?}, open_only={}",
724 cmd.instrument_id,
725 cmd.open_only
726 );
727
728 let account_id = self.core.account_id;
729 let start = cmd.start.map(DateTime::<Utc>::from);
730 let end = cmd.end.map(DateTime::<Utc>::from);
731 self.http
732 .request_order_status_reports(account_id, cmd.instrument_id, start, end, cmd.open_only)
733 .await
734 }
735
736 async fn generate_fill_reports(
737 &self,
738 cmd: GenerateFillReports,
739 ) -> anyhow::Result<Vec<FillReport>> {
740 log::debug!(
741 "Generating fill reports: instrument_id={:?}",
742 cmd.instrument_id
743 );
744
745 let account_id = self.core.account_id;
746 let start = cmd.start.map(DateTime::<Utc>::from);
747 let end = cmd.end.map(DateTime::<Utc>::from);
748 let mut reports = self
749 .http
750 .request_fill_reports(account_id, cmd.instrument_id, start, end)
751 .await?;
752
753 if let Some(venue_order_id) = cmd.venue_order_id {
754 reports.retain(|report| report.venue_order_id == venue_order_id);
755 }
756
757 Ok(reports)
758 }
759
760 async fn generate_position_status_reports(
761 &self,
762 cmd: &GeneratePositionStatusReports,
763 ) -> anyhow::Result<Vec<PositionStatusReport>> {
764 log::debug!(
765 "Generating position status reports: instrument_id={:?}",
766 cmd.instrument_id
767 );
768
769 let account_id = self.core.account_id;
770 self.http
771 .request_position_status_reports(account_id, cmd.instrument_id)
772 .await
773 }
774
775 async fn generate_mass_status(
776 &self,
777 lookback_mins: Option<u64>,
778 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
779 log::debug!("Generating mass status: lookback_mins={lookback_mins:?}");
780
781 let start = lookback_mins.map(|mins| Utc::now() - Duration::from_secs(mins * 60));
782
783 let account_id = self.core.account_id;
784 let order_reports = self
785 .http
786 .request_order_status_reports(account_id, None, start, None, true)
787 .await?;
788 let fill_reports = self
789 .http
790 .request_fill_reports(account_id, None, start, None)
791 .await?;
792 let position_reports = self
793 .http
794 .request_position_status_reports(account_id, None)
795 .await?;
796
797 let mut mass_status = ExecutionMassStatus::new(
798 self.core.client_id,
799 self.core.account_id,
800 *KRAKEN_VENUE,
801 self.clock.get_time_ns(),
802 None,
803 );
804 mass_status.add_order_reports(order_reports);
805 mass_status.add_fill_reports(fill_reports);
806 mass_status.add_position_reports(position_reports);
807
808 Ok(Some(mass_status))
809 }
810
811 fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
812 log::debug!("Querying account: {cmd:?}");
813
814 let account_id = self.core.account_id;
815 let http = self.http.clone();
816 let emitter = self.emitter.clone();
817
818 self.spawn_task("query_account", async move {
819 let account_state = http.request_account_state(account_id).await?;
820 emitter.emit_account_state(
821 account_state.balances.clone(),
822 account_state.margins.clone(),
823 account_state.is_reported,
824 account_state.ts_event,
825 );
826 Ok(())
827 });
828
829 Ok(())
830 }
831
832 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
833 log::debug!("Querying order: {cmd:?}");
834
835 let venue_order_id = cmd
836 .venue_order_id
837 .context("venue_order_id required for query_order")?;
838 let account_id = self.core.account_id;
839 let http = self.http.clone();
840 let emitter = self.emitter.clone();
841
842 self.spawn_task("query_order", async move {
843 let reports = http
844 .request_order_status_reports(account_id, None, None, None, true)
845 .await
846 .context("Failed to query order")?;
847
848 if let Some(report) = reports
849 .into_iter()
850 .find(|r| r.venue_order_id == venue_order_id)
851 {
852 emitter.send_order_status_report(report);
853 }
854 Ok(())
855 });
856
857 Ok(())
858 }
859
860 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
861 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
862 self.submit_single_order(&order, "submit_order");
863 Ok(())
864 }
865
866 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
867 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
868
869 log::debug!(
870 "Submitting order list: order_list_id={}, count={}",
871 cmd.order_list.id,
872 orders.len()
873 );
874
875 let mut order_tuples = Vec::with_capacity(orders.len());
876 let mut order_meta = Vec::with_capacity(orders.len());
877
878 for order in &orders {
879 if order.is_closed() {
880 log::warn!(
881 "Cannot submit closed order: client_order_id={}",
882 order.client_order_id()
883 );
884 continue;
885 }
886
887 if order.order_type() == OrderType::Market {
890 self.submit_single_order(order, "submit_order_list");
891 continue;
892 }
893
894 let client_order_id = order.client_order_id();
895 let kraken_cl_ord_id = truncate_cl_ord_id(&client_order_id);
896
897 if kraken_cl_ord_id != client_order_id.as_str() {
898 self.truncated_id_map
899 .insert(kraken_cl_ord_id, client_order_id);
900 }
901
902 self.register_order_identity(order);
903 self.emitter.emit_order_submitted(order);
904
905 order_tuples.push((
906 order.instrument_id(),
907 client_order_id,
908 order.order_side(),
909 order.order_type(),
910 order.quantity(),
911 order.time_in_force(),
912 order.price(),
913 order.trigger_price(),
914 order.trigger_type(),
915 order.is_reduce_only(),
916 order.is_post_only(),
917 ));
918
919 order_meta.push((order.strategy_id(), order.instrument_id(), client_order_id));
920 }
921
922 if order_tuples.is_empty() {
923 return Ok(());
924 }
925
926 let http = self.http.clone();
927 let emitter = self.emitter.clone();
928 let clock = self.clock;
929 let dispatch_state = self.ws_dispatch_state.clone();
930
931 self.spawn_task("submit_order_list", async move {
932 match http.submit_orders_batch(order_tuples).await {
933 Ok(statuses) => {
934 for (i, status) in statuses.iter().enumerate() {
935 if status.status != "placed"
936 && status.status != "filled"
937 && let Some((strategy_id, instrument_id, client_order_id)) =
938 order_meta.get(i)
939 {
940 let ts_event = clock.get_time_ns();
941 let error_msg = format!(
942 "submit_order_list batch item rejected: {}",
943 status.status,
944 );
945 dispatch_state.cleanup_terminal(client_order_id);
946 emitter.emit_order_rejected_event(
947 *strategy_id,
948 *instrument_id,
949 *client_order_id,
950 &error_msg,
951 ts_event,
952 status.status == "postWouldExecute",
953 );
954 }
955 }
956 Ok(())
957 }
958 Err(e) => {
959 let ts_event = clock.get_time_ns();
960
961 for (strategy_id, instrument_id, client_order_id) in &order_meta {
962 let error_msg = format!("submit_order_list batch error: {e}");
963 dispatch_state.cleanup_terminal(client_order_id);
964 emitter.emit_order_rejected_event(
965 *strategy_id,
966 *instrument_id,
967 *client_order_id,
968 &error_msg,
969 ts_event,
970 false,
971 );
972 }
973 Ok(())
974 }
975 }
976 });
977
978 Ok(())
979 }
980
981 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
982 self.modify_single_order(&cmd);
983 Ok(())
984 }
985
986 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
987 self.cancel_single_order(&cmd);
988 Ok(())
989 }
990
991 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
992 let instrument_id = cmd.instrument_id;
993
994 if cmd.order_side == OrderSide::NoOrderSide {
995 log::debug!("Canceling all orders: instrument_id={instrument_id} (bulk)");
996
997 let http = self.http.clone();
998 let symbol = instrument_id.symbol.to_string();
999
1000 self.spawn_task("cancel_all_orders", async move {
1001 match http.inner.cancel_all_orders(Some(symbol)).await {
1002 Ok(response) => {
1003 if response.result != KrakenApiResult::Success
1004 && response.cancel_status.cancelled_orders.is_empty()
1005 {
1006 log::warn!(
1007 "Cancel-all failed without per-order results, awaiting reconciliation: status={}",
1008 response.cancel_status.status
1009 );
1010 }
1011 }
1012 Err(e) => match classify_cancel_http_failure(e) {
1013 CancelCommandFailure::LocalValidation(reason) => {
1014 log::warn!("Cancel-all failed local validation: {reason}");
1015 }
1016 CancelCommandFailure::Ambiguous(reason)
1017 | CancelCommandFailure::VenueReject(reason) => {
1018 log::warn!(
1019 "Cancel-all ambiguous failure, awaiting reconciliation: {reason}"
1020 );
1021 }
1022 },
1023 }
1024 Ok(())
1025 });
1026
1027 return Ok(());
1028 }
1029
1030 log::debug!(
1031 "Canceling all orders: instrument_id={instrument_id}, side={:?}",
1032 cmd.order_side
1033 );
1034
1035 let orders_to_cancel: Vec<_> = {
1036 let cache = self.core.cache();
1037 let open_orders = cache.orders_open(None, Some(&instrument_id), None, None, None);
1038
1039 open_orders
1040 .into_iter()
1041 .filter(|order| order.order_side() == cmd.order_side)
1042 .filter_map(|order| {
1043 Some((
1044 order.venue_order_id()?,
1045 order.client_order_id(),
1046 order.instrument_id(),
1047 order.strategy_id(),
1048 ))
1049 })
1050 .collect()
1051 };
1052
1053 let account_id = self.core.account_id;
1054
1055 for (venue_order_id, client_order_id, order_instrument_id, strategy_id) in orders_to_cancel
1056 {
1057 let http = self.http.clone();
1058 let emitter = self.emitter.clone();
1059 let clock = self.clock;
1060
1061 self.spawn_task("cancel_order_by_side", async move {
1062 if let Err(failure) = cancel_order_for_futures(
1063 &http,
1064 account_id,
1065 order_instrument_id,
1066 Some(client_order_id),
1067 Some(venue_order_id),
1068 )
1069 .await
1070 {
1071 handle_cancel_failure(
1072 &emitter,
1073 clock,
1074 strategy_id,
1075 order_instrument_id,
1076 client_order_id,
1077 Some(venue_order_id),
1078 failure,
1079 );
1080 }
1081 Ok(())
1082 });
1083 }
1084
1085 Ok(())
1086 }
1087
1088 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1089 log::debug!(
1090 "Batch canceling orders: instrument_id={}, count={}",
1091 cmd.instrument_id,
1092 cmd.cancels.len()
1093 );
1094
1095 let http = self.http.clone();
1096 let emitter = self.emitter.clone();
1097 let clock = self.clock;
1098 let cancels = cmd.cancels;
1099
1100 self.spawn_task("batch_cancel_orders", async move {
1101 batch_cancel_orders_for_futures(&http, &emitter, clock, &cancels).await;
1102 Ok(())
1103 });
1104
1105 Ok(())
1106 }
1107}
1108
1109#[derive(Debug, Clone)]
1110struct CancelRequestContext {
1111 strategy_id: StrategyId,
1112 instrument_id: InstrumentId,
1113 client_order_id: ClientOrderId,
1114 truncated_client_order_id: String,
1115 venue_order_id: Option<VenueOrderId>,
1116}
1117
1118async fn cancel_order_for_futures(
1119 http: &KrakenFuturesHttpClient,
1120 _account_id: AccountId,
1121 instrument_id: InstrumentId,
1122 client_order_id: Option<ClientOrderId>,
1123 venue_order_id: Option<VenueOrderId>,
1124) -> Result<(), CancelCommandFailure> {
1125 http.get_cached_instrument(&instrument_id.symbol.inner())
1126 .ok_or_else(|| {
1127 CancelCommandFailure::local(InstrumentLookupError::not_found(instrument_id).to_string())
1128 })?;
1129
1130 let order_id = venue_order_id.as_ref().map(ToString::to_string);
1131 let cli_ord_id = client_order_id.as_ref().map(truncate_cl_ord_id);
1132
1133 if order_id.is_none() && cli_ord_id.is_none() {
1134 return Err(CancelCommandFailure::local(
1135 "Either client_order_id or venue_order_id must be provided",
1136 ));
1137 }
1138
1139 let response = http
1140 .inner
1141 .cancel_order(order_id, cli_ord_id)
1142 .await
1143 .map_err(classify_cancel_http_failure)?;
1144
1145 if response.result != KrakenApiResult::Success
1146 || response.cancel_status.status != KrakenSendStatus::Cancelled
1147 {
1148 return Err(CancelCommandFailure::venue_reject(format!(
1149 "cancel-order rejected: status={}",
1150 response.cancel_status.status
1151 )));
1152 }
1153
1154 Ok(())
1155}
1156
1157async fn batch_cancel_orders_for_futures(
1158 http: &KrakenFuturesHttpClient,
1159 emitter: &ExecutionEventEmitter,
1160 clock: &'static AtomicTime,
1161 cancels: &[CancelOrder],
1162) {
1163 let mut contexts = Vec::new();
1164 let mut items = Vec::new();
1165
1166 for cancel in cancels {
1167 match batch_cancel_item_for_futures(http, cancel) {
1168 Ok((context, item)) => {
1169 contexts.push(context);
1170 items.push(item);
1171 }
1172 Err(CancelCommandFailure::LocalValidation(reason)) => {
1173 log::warn!(
1174 "Batch cancel command failed local validation for {}: {reason}",
1175 cancel.client_order_id
1176 );
1177 }
1178 Err(
1179 CancelCommandFailure::Ambiguous(reason) | CancelCommandFailure::VenueReject(reason),
1180 ) => {
1181 log::warn!(
1182 "Batch cancel command ambiguous failure for {}, awaiting reconciliation: {reason}",
1183 cancel.client_order_id
1184 );
1185 }
1186 }
1187 }
1188
1189 for (item_chunk, context_chunk) in items
1190 .chunks(FUTURES_BATCH_CANCEL_LIMIT)
1191 .zip(contexts.chunks(FUTURES_BATCH_CANCEL_LIMIT))
1192 {
1193 let response = match http
1194 .inner
1195 .cancel_order_items_batch(item_chunk.to_vec())
1196 .await
1197 {
1198 Ok(response) => response,
1199 Err(e) => {
1200 match classify_cancel_http_failure(e) {
1201 CancelCommandFailure::LocalValidation(reason) => {
1202 log::warn!("Batch cancel failed local validation: {reason}");
1203 }
1204 CancelCommandFailure::Ambiguous(reason)
1205 | CancelCommandFailure::VenueReject(reason) => {
1206 log::warn!(
1207 "Batch cancel failed without per-order results, awaiting reconciliation: {reason}"
1208 );
1209 }
1210 }
1211 continue;
1212 }
1213 };
1214
1215 if response.batch_status.is_empty() {
1216 if response.result != KrakenApiResult::Success {
1217 let reason = response.error.as_deref().unwrap_or("Unknown error");
1218 log::warn!(
1219 "Batch cancel failed without per-order results, awaiting reconciliation: {reason}"
1220 );
1221 }
1222 continue;
1223 }
1224
1225 if response.batch_status.len() != context_chunk.len() {
1226 log::warn!(
1227 "Batch cancel returned {} per-order result(s) for {} request(s); unmatched results await reconciliation",
1228 response.batch_status.len(),
1229 context_chunk.len()
1230 );
1231 }
1232
1233 for (index, status) in response.batch_status.iter().enumerate() {
1234 let Some(cancel_status) = batch_cancel_status(status) else {
1235 log::warn!("Batch cancel result without status at index {index}");
1236 continue;
1237 };
1238
1239 if cancel_status == KrakenSendStatus::Cancelled {
1240 continue;
1241 }
1242
1243 let Some(context) = batch_cancel_context(status, context_chunk, index) else {
1244 log::warn!(
1245 "Batch cancel rejected item without matching request context at index {index}: status={cancel_status}"
1246 );
1247 continue;
1248 };
1249
1250 emitter.emit_order_cancel_rejected_event(
1251 context.strategy_id,
1252 context.instrument_id,
1253 context.client_order_id,
1254 context.venue_order_id,
1255 &format!("batch-cancel rejected: status={cancel_status}"),
1256 clock.get_time_ns(),
1257 );
1258 }
1259 }
1260}
1261
1262fn batch_cancel_item_for_futures(
1263 http: &KrakenFuturesHttpClient,
1264 cancel: &CancelOrder,
1265) -> Result<(CancelRequestContext, KrakenFuturesBatchCancelItem), CancelCommandFailure> {
1266 http.get_cached_instrument(&cancel.instrument_id.symbol.inner())
1267 .ok_or_else(|| {
1268 CancelCommandFailure::local(
1269 InstrumentLookupError::not_found(cancel.instrument_id).to_string(),
1270 )
1271 })?;
1272
1273 let truncated_client_order_id = truncate_cl_ord_id(&cancel.client_order_id);
1274 let item = if let Some(venue_order_id) = cancel.venue_order_id {
1275 KrakenFuturesBatchCancelItem::from_order_id(venue_order_id.to_string())
1276 } else {
1277 KrakenFuturesBatchCancelItem::from_client_order_id(truncated_client_order_id.clone())
1278 };
1279
1280 Ok((
1281 CancelRequestContext {
1282 strategy_id: cancel.strategy_id,
1283 instrument_id: cancel.instrument_id,
1284 client_order_id: cancel.client_order_id,
1285 truncated_client_order_id,
1286 venue_order_id: cancel.venue_order_id,
1287 },
1288 item,
1289 ))
1290}
1291
1292fn batch_cancel_status(status: &FuturesBatchCancelStatus) -> Option<KrakenSendStatus> {
1293 status
1294 .cancel_status
1295 .as_ref()
1296 .map(|cancel_status| cancel_status.status)
1297 .or(status.status)
1298}
1299
1300fn batch_cancel_context<'a>(
1301 status: &FuturesBatchCancelStatus,
1302 contexts: &'a [CancelRequestContext],
1303 index: usize,
1304) -> Option<&'a CancelRequestContext> {
1305 if let Some(order_id) = status.order_id.as_deref()
1306 && let Some(context) = contexts.iter().find(|context| {
1307 context
1308 .venue_order_id
1309 .is_some_and(|venue_order_id| venue_order_id.as_str() == order_id)
1310 })
1311 {
1312 return Some(context);
1313 }
1314
1315 if let Some(cli_ord_id) = status.cli_ord_id.as_deref()
1316 && let Some(context) = contexts
1317 .iter()
1318 .find(|context| context.truncated_client_order_id == cli_ord_id)
1319 {
1320 return Some(context);
1321 }
1322
1323 if index < contexts.len() && status.order_id.is_none() && status.cli_ord_id.is_none() {
1324 return contexts.get(index);
1325 }
1326
1327 None
1328}
1329
1330fn handle_cancel_failure(
1331 emitter: &ExecutionEventEmitter,
1332 clock: &'static AtomicTime,
1333 strategy_id: StrategyId,
1334 instrument_id: InstrumentId,
1335 client_order_id: ClientOrderId,
1336 venue_order_id: Option<VenueOrderId>,
1337 failure: CancelCommandFailure,
1338) {
1339 match failure {
1340 CancelCommandFailure::VenueReject(reason) => {
1341 emitter.emit_order_cancel_rejected_event(
1342 strategy_id,
1343 instrument_id,
1344 client_order_id,
1345 venue_order_id,
1346 &reason,
1347 clock.get_time_ns(),
1348 );
1349 }
1350 CancelCommandFailure::LocalValidation(reason) => {
1351 log::warn!("Cancel command failed local validation for {client_order_id}: {reason}");
1352 }
1353 CancelCommandFailure::Ambiguous(reason) => {
1354 log::warn!(
1355 "Ambiguous cancel failure for {client_order_id}, awaiting reconciliation: {reason}"
1356 );
1357 }
1358 }
1359}
1360
1361impl KrakenFuturesExecutionClient {
1362 fn get_cached_order_for_status_command(
1363 &self,
1364 cmd: &GenerateOrderStatusReport,
1365 ) -> Option<OrderAny> {
1366 let cache = self.core.cache();
1367
1368 if let Some(client_order_id) = cmd.client_order_id {
1369 return cache.order(&client_order_id).map(|o| o.clone());
1370 }
1371
1372 let venue_order_id = cmd.venue_order_id?;
1373 let client_order_id = *cache.client_order_id(&venue_order_id)?;
1374 cache.order(&client_order_id).map(|o| o.clone())
1375 }
1376}
1377
1378fn synthesize_filled_order_status_report(
1379 cmd: &GenerateOrderStatusReport,
1380 order: &OrderAny,
1381 fills: &[FillReport],
1382) -> Option<OrderStatusReport> {
1383 let venue_order_id = cmd.venue_order_id.or(order.venue_order_id());
1384 let truncated_client_order_id = truncate_cl_ord_id(&order.client_order_id());
1385
1386 let mut matched: Vec<&FillReport> = if let Some(venue_order_id) = venue_order_id {
1387 fills
1388 .iter()
1389 .filter(|fill| fill.venue_order_id == venue_order_id)
1390 .collect()
1391 } else {
1392 Vec::new()
1393 };
1394
1395 if matched.is_empty() {
1396 matched = fills
1397 .iter()
1398 .filter(|fill| {
1399 fill.client_order_id == Some(order.client_order_id())
1400 || fill
1401 .client_order_id
1402 .as_ref()
1403 .is_some_and(|fill_client_order_id| {
1404 fill_client_order_id.as_str() == truncated_client_order_id
1405 })
1406 })
1407 .collect();
1408 }
1409
1410 if matched.is_empty() {
1411 return None;
1412 }
1413
1414 matched.sort_by_key(|fill| fill.ts_event);
1415 let first_fill = *matched.first()?;
1416 let last_fill = *matched.last()?;
1417
1418 let total_filled = matched
1419 .iter()
1420 .fold(Decimal::ZERO, |acc, fill| acc + fill.last_qty.as_decimal());
1421 if total_filled < order.quantity().as_decimal() {
1422 return None;
1423 }
1424
1425 let total_notional = matched.iter().fold(Decimal::ZERO, |acc, fill| {
1426 acc + fill.last_qty.as_decimal() * fill.last_px.as_decimal()
1427 });
1428 let avg_px = if total_filled.is_zero() {
1429 None
1430 } else {
1431 Some(total_notional / total_filled)
1432 };
1433 let venue_order_id = venue_order_id.unwrap_or(first_fill.venue_order_id);
1434
1435 let mut report = OrderStatusReport::new(
1436 first_fill.account_id,
1437 order.instrument_id(),
1438 Some(order.client_order_id()),
1439 venue_order_id,
1440 order.order_side(),
1441 order.order_type(),
1442 order.time_in_force(),
1443 OrderStatus::Filled,
1444 order.quantity(),
1445 order.quantity(),
1446 first_fill.ts_event,
1447 last_fill.ts_event,
1448 last_fill.ts_init,
1449 None,
1450 );
1451 report.order_list_id = order.order_list_id();
1452 report.venue_position_id = matched.iter().rev().find_map(|fill| fill.venue_position_id);
1453 report.linked_order_ids = order
1454 .linked_order_ids()
1455 .map(|linked_order_ids| linked_order_ids.to_vec());
1456 report.parent_order_id = order.parent_order_id();
1457 report.expire_time = order.expire_time();
1458 report.price = order.price();
1459 report.trigger_price = order.trigger_price();
1460 report.trigger_type = order.trigger_type();
1461 report.avg_px = avg_px;
1462 report.display_qty = order.display_qty();
1463 report.post_only = order.is_post_only();
1464 report.reduce_only = order.is_reduce_only();
1465 Some(report)
1466}
1467
1468#[cfg(test)]
1469mod tests {
1470 use nautilus_core::{UUID4, UnixNanos};
1471 use nautilus_model::{
1472 enums::{LiquiditySide, OrderSide, OrderType, TimeInForce},
1473 identifiers::{
1474 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
1475 },
1476 orders::OrderTestBuilder,
1477 reports::FillReport,
1478 types::{Currency, Money, Price, Quantity},
1479 };
1480 use rstest::rstest;
1481
1482 use super::*;
1483
1484 const TEST_INSTRUMENT_ID: &str = "PF_XBTUSD.KRAKEN";
1485
1486 #[tokio::test]
1487 async fn test_cancel_order_for_futures_missing_cached_instrument_returns_canonical_error() {
1488 let http = KrakenFuturesHttpClient::default();
1489 let instrument_id = InstrumentId::from(TEST_INSTRUMENT_ID);
1490 let client_order_id = ClientOrderId::from("C-001");
1491 let venue_order_id = VenueOrderId::from("V-001");
1492
1493 let result = cancel_order_for_futures(
1494 &http,
1495 AccountId::from("KRAKEN-001"),
1496 instrument_id,
1497 Some(client_order_id),
1498 Some(venue_order_id),
1499 )
1500 .await;
1501
1502 match result {
1503 Err(CancelCommandFailure::LocalValidation(reason)) => {
1504 assert_eq!(
1505 reason,
1506 InstrumentLookupError::not_found(instrument_id).to_string()
1507 );
1508 }
1509 _ => panic!("Expected local validation failure"),
1510 }
1511 }
1512
1513 #[rstest]
1514 fn test_batch_cancel_item_for_futures_missing_cached_instrument_returns_canonical_error() {
1515 let http = KrakenFuturesHttpClient::default();
1516 let instrument_id = InstrumentId::from(TEST_INSTRUMENT_ID);
1517 let cancel = CancelOrder::new(
1518 TraderId::from("TESTER-001"),
1519 None,
1520 StrategyId::from("S-001"),
1521 instrument_id,
1522 ClientOrderId::from("C-001"),
1523 Some(VenueOrderId::from("V-001")),
1524 UUID4::new(),
1525 UnixNanos::default(),
1526 None,
1527 None,
1528 );
1529
1530 let result = batch_cancel_item_for_futures(&http, &cancel);
1531
1532 match result {
1533 Err(CancelCommandFailure::LocalValidation(reason)) => {
1534 assert_eq!(
1535 reason,
1536 InstrumentLookupError::not_found(instrument_id).to_string()
1537 );
1538 }
1539 _ => panic!("Expected local validation failure"),
1540 }
1541 }
1542
1543 fn make_fill(
1544 venue_order_id: &str,
1545 client_order_id: Option<&str>,
1546 quantity: &str,
1547 price: &str,
1548 ts_event: u64,
1549 ) -> FillReport {
1550 FillReport::new(
1551 AccountId::from("KRAKEN-001"),
1552 InstrumentId::from(TEST_INSTRUMENT_ID),
1553 VenueOrderId::from(venue_order_id),
1554 TradeId::from(format!("T-{ts_event}").as_str()),
1555 OrderSide::Buy,
1556 Quantity::from(quantity),
1557 Price::from(price),
1558 Money::new(0.0, Currency::USD()),
1559 LiquiditySide::Taker,
1560 client_order_id.map(ClientOrderId::from),
1561 None,
1562 UnixNanos::from(ts_event),
1563 UnixNanos::from(ts_event),
1564 None,
1565 )
1566 }
1567
1568 fn make_cmd(
1569 client_order_id: Option<&str>,
1570 venue_order_id: Option<&str>,
1571 ) -> GenerateOrderStatusReport {
1572 GenerateOrderStatusReport::new(
1573 UUID4::new(),
1574 UnixNanos::default(),
1575 Some(InstrumentId::from(TEST_INSTRUMENT_ID)),
1576 client_order_id.map(ClientOrderId::from),
1577 venue_order_id.map(VenueOrderId::from),
1578 None,
1579 None,
1580 )
1581 }
1582
1583 fn make_order(client_order_id: &str) -> OrderAny {
1584 OrderTestBuilder::new(OrderType::Market)
1585 .instrument_id(InstrumentId::from(TEST_INSTRUMENT_ID))
1586 .client_order_id(ClientOrderId::from(client_order_id))
1587 .side(OrderSide::Buy)
1588 .quantity(Quantity::from("100"))
1589 .time_in_force(TimeInForce::Ioc)
1590 .build()
1591 }
1592
1593 #[rstest]
1594 fn test_synthesize_filled_order_status_report_matches_full_fill_by_venue_order_id() {
1595 let order = make_order("O-123456");
1596 let cmd = make_cmd(Some("O-123456"), Some("KRAKEN-789"));
1597 let fills = vec![
1598 make_fill("KRAKEN-789", Some("O-123456"), "40", "50000.0", 1),
1599 make_fill("KRAKEN-789", Some("O-123456"), "60", "50010.0", 2),
1600 make_fill("KRAKEN-OTHER", Some("O-123456"), "999", "1.0", 3),
1601 ];
1602
1603 let report = synthesize_filled_order_status_report(&cmd, &order, &fills)
1604 .expect("expected a filled report");
1605
1606 assert_eq!(report.venue_order_id, VenueOrderId::from("KRAKEN-789"));
1607 assert_eq!(
1608 report.client_order_id,
1609 Some(ClientOrderId::from("O-123456"))
1610 );
1611 assert_eq!(report.order_status, OrderStatus::Filled);
1612 assert_eq!(report.order_type, OrderType::Market);
1613 assert_eq!(report.time_in_force, TimeInForce::Ioc);
1614 assert_eq!(report.quantity, Quantity::from("100"));
1615 assert_eq!(report.filled_qty, Quantity::from("100"));
1616 assert_eq!(
1617 report.avg_px,
1618 Some(Decimal::from_str_exact("50006.0").unwrap())
1619 );
1620 }
1621
1622 #[rstest]
1623 fn test_synthesize_filled_order_status_report_requires_full_fill_size() {
1624 let order = make_order("O-123457");
1625 let cmd = make_cmd(Some("O-123457"), Some("KRAKEN-790"));
1626 let fills = vec![make_fill(
1627 "KRAKEN-790",
1628 Some("O-123457"),
1629 "40",
1630 "50000.0",
1631 1,
1632 )];
1633
1634 assert!(synthesize_filled_order_status_report(&cmd, &order, &fills).is_none());
1635 }
1636
1637 #[rstest]
1638 fn test_synthesize_filled_order_status_report_matches_truncated_client_order_id() {
1639 let long_client_order_id = "O202602270023210040011";
1640 let order = make_order(long_client_order_id);
1641 let cmd = make_cmd(Some(long_client_order_id), None);
1642 let fills = vec![make_fill(
1643 "KRAKEN-791",
1644 Some(truncate_cl_ord_id(&ClientOrderId::from(long_client_order_id)).as_str()),
1645 "100",
1646 "50000.0",
1647 1,
1648 )];
1649
1650 let report = synthesize_filled_order_status_report(&cmd, &order, &fills)
1651 .expect("expected a filled report");
1652
1653 assert_eq!(
1654 report.client_order_id,
1655 Some(ClientOrderId::from(long_client_order_id))
1656 );
1657 assert_eq!(report.venue_order_id, VenueOrderId::from("KRAKEN-791"));
1658 assert_eq!(report.order_status, OrderStatus::Filled);
1659 }
1660}