1use std::{
19 collections::{HashMap, HashSet},
20 future::Future,
21 sync::Arc,
22 time::{Duration, Instant},
23};
24
25use anyhow::Context;
26use async_trait::async_trait;
27use indexmap::IndexMap;
28use jiff::Timestamp;
29use nautilus_common::{
30 cache::{Cache, InstrumentLookupError},
31 clients::ExecutionClient,
32 live::runner::get_exec_event_sender,
33 messages::execution::{
34 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
35 GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
36 ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
37 },
38};
39use nautilus_core::{
40 AtomicMap, DurationNanos, Params, UUID4, UnixNanos,
41 time::{AtomicTime, get_atomic_clock_realtime},
42};
43use nautilus_live::{
44 ExecutionClientCore, ExecutionEventEmitter, SocketControl, execution::failure::CommandFailure,
45 task::TaskGroup,
46};
47use nautilus_model::{
48 accounts::AccountAny,
49 enums::{AccountType, OmsType, OrderSide, OrderStatus, OrderType},
50 identifiers::{
51 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, TradeId, Venue, VenueOrderId,
52 },
53 instruments::{Instrument, InstrumentAny},
54 orders::{Order, OrderAny},
55 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
56 types::{AccountBalance, MarginBalance, Quantity},
57};
58use rust_decimal::Decimal;
59use tokio_util::sync::CancellationToken;
60
61use super::{
62 absent_unless_unread, cached_order_for_status_command, command_failure_from_cancel_error,
63 command_failure_from_futures_batch_error, command_failure_from_futures_batch_item,
64 command_failure_from_modify_error, command_failure_from_submit_error, earliest_venue_time,
65 status_command_label,
66};
67use crate::{
68 common::{
69 consts::KRAKEN_VENUE,
70 credential::KrakenCredential,
71 enums::{KrakenApiResult, KrakenProductType, KrakenSendStatus, product_type_from_symbol},
72 parse::truncate_cl_ord_id,
73 },
74 config::KrakenExecutionClientConfig,
75 http::{
76 KrakenFuturesHttpClient, KrakenHttpError,
77 futures::{
78 client::{KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND, supersedes},
79 models::FuturesBatchCancelStatus,
80 query::KrakenFuturesBatchCancelItem,
81 },
82 },
83 websocket::{
84 dispatch::{self, OrderIdentity, WsDispatchState},
85 futures::{client::KrakenFuturesWebSocketClient, messages::KrakenFuturesWsMessage},
86 },
87};
88
89const FUTURES_BATCH_CANCEL_LIMIT: usize = 50;
90
91const FUTURES_ORDERS_STATUS_LIMIT: usize = 50;
93
94const ORDERS_STATUS_CLOCK_SKEW: DurationNanos = DurationNanos::from_secs(5);
98
99#[allow(dead_code)]
104#[derive(Debug)]
105pub struct KrakenFuturesExecutionClient {
106 core: ExecutionClientCore,
107 clock: &'static AtomicTime,
108 config: KrakenExecutionClientConfig,
109 emitter: ExecutionEventEmitter,
110 http: KrakenFuturesHttpClient,
111 ws: KrakenFuturesWebSocketClient,
112 cancellation_token: CancellationToken,
113 session_tasks: TaskGroup,
114 pending_tasks: TaskGroup,
115 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
116 truncated_id_map: Arc<AtomicMap<String, ClientOrderId>>,
117 order_instrument_map: Arc<AtomicMap<String, InstrumentId>>,
118 venue_client_map: Arc<AtomicMap<String, ClientOrderId>>,
119 venue_order_qty: Arc<AtomicMap<String, Quantity>>,
120 ws_dispatch_state: Arc<WsDispatchState>,
121}
122
123impl KrakenFuturesExecutionClient {
124 pub fn new(
126 core: ExecutionClientCore,
127 config: KrakenExecutionClientConfig,
128 ) -> anyhow::Result<Self> {
129 let clock = get_atomic_clock_realtime();
130 let emitter = ExecutionEventEmitter::new(
131 clock,
132 core.trader_id,
133 core.account_id,
134 AccountType::Margin,
135 None,
136 );
137
138 let session_tasks = TaskGroup::new();
139 let cancellation_token = session_tasks.cancellation_token();
140 let pending_tasks = TaskGroup::new();
141 let api_key = config.api_key.expose_secret().to_owned();
142 let api_secret = config.api_secret.expose_secret().to_owned();
143 let proxy_url = config
144 .proxy_url
145 .as_ref()
146 .map(|value| value.expose_secret().to_owned());
147
148 let http = KrakenFuturesHttpClient::with_credentials(
149 api_key.clone(),
150 api_secret.clone(),
151 config.environment,
152 config.base_url.clone(),
153 config.timeout_secs,
154 Some(config.max_retries),
155 None,
156 None,
157 proxy_url.clone(),
158 config
159 .max_requests_per_second
160 .unwrap_or(KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND),
161 )?;
162
163 let credential = KrakenCredential::new(api_key, api_secret);
164 let ws = KrakenFuturesWebSocketClient::with_credentials(
165 config.ws_url(),
166 config.heartbeat_interval_secs,
167 Some(credential),
168 config.auth_timeout_secs,
169 config.transport_backend,
170 proxy_url,
171 )
172 .with_socket_control(SocketControl::new(
173 core.client_id,
174 Some(*KRAKEN_VENUE),
175 "kraken-futures-user-streams",
176 ));
177
178 Ok(Self {
179 core,
180 clock,
181 config,
182 emitter,
183 http,
184 ws,
185 cancellation_token,
186 session_tasks,
187 pending_tasks,
188 instruments: Arc::new(AtomicMap::new()),
189 truncated_id_map: Arc::new(AtomicMap::new()),
190 order_instrument_map: Arc::new(AtomicMap::new()),
191 venue_client_map: Arc::new(AtomicMap::new()),
192 venue_order_qty: Arc::new(AtomicMap::new()),
193 ws_dispatch_state: Arc::new(WsDispatchState::new()),
194 })
195 }
196
197 fn register_order_identity(&self, order: &OrderAny) {
198 self.ws_dispatch_state.register_identity(
199 order.client_order_id(),
200 OrderIdentity {
201 strategy_id: order.strategy_id(),
202 instrument_id: order.instrument_id(),
203 order_side: order.order_side(),
204 order_type: order.order_type(),
205 quantity: order.quantity(),
206 },
207 );
208 }
209
210 #[must_use]
212 pub fn clock(&self) -> &'static AtomicTime {
213 self.clock
214 }
215
216 #[must_use]
218 pub fn emitter(&self) -> &ExecutionEventEmitter {
219 &self.emitter
220 }
221
222 fn spawn_task<F>(&self, description: &'static str, fut: F)
223 where
224 F: Future<Output = anyhow::Result<()>> + Send + 'static,
225 {
226 let future = async move {
227 if let Err(e) = fut.await {
228 log::warn!("{description} failed: {e:?}");
229 }
230 };
231
232 if let Err(e) = self.pending_tasks.spawn(future) {
233 log::warn!("Skipping Kraken Futures {description} after shutdown began: {e}");
234 }
235 }
236
237 async fn finish_tasks(&self) -> anyhow::Result<()> {
238 let (session_result, pending_result) = tokio::join!(
239 self.session_tasks
240 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2)),
241 self.pending_tasks
242 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2)),
243 );
244 session_result.context("failed to finish Kraken Futures execution session tasks")?;
245 pending_result.context("failed to finish Kraken Futures execution command tasks")?;
246 Ok(())
247 }
248
249 async fn prepare_task_groups(&mut self) -> anyhow::Result<()> {
250 if !self.session_tasks.is_open() || !self.pending_tasks.is_open() {
251 self.session_tasks.begin_shutdown();
252 self.pending_tasks.begin_shutdown();
253 self.finish_tasks().await?;
254 self.session_tasks
255 .start_generation()
256 .context("failed to start Kraken Futures execution session task generation")?;
257 self.pending_tasks
258 .start_generation()
259 .context("failed to start Kraken Futures execution command task generation")?;
260 self.cancellation_token = self.session_tasks.cancellation_token();
261 }
262 Ok(())
263 }
264
265 async fn teardown_partial_connect(&mut self) -> anyhow::Result<()> {
266 self.http.cancel_all_requests();
267 self.pending_tasks.begin_shutdown();
268 let ws_result = self.ws.close().await;
269 self.session_tasks.begin_shutdown();
270 let tasks_result = self.finish_tasks().await;
271 self.core.set_disconnected();
272 tasks_result?;
273 Ok(ws_result?)
274 }
275
276 fn submit_single_order(&self, order: &OrderAny, task_name: &'static str) {
277 if order.is_closed() {
278 log::warn!(
279 "Cannot submit closed order: client_order_id={}",
280 order.client_order_id()
281 );
282 return;
283 }
284
285 let account_id = self.core.account_id;
286 let client_order_id = order.client_order_id();
287 let strategy_id = order.strategy_id();
288 let instrument_id = order.instrument_id();
289 let order_side = order.order_side();
290 let order_type = order.order_type();
291 let quantity = order.quantity();
292 let time_in_force = order.time_in_force();
293 let price = order.price();
294 let trigger_price = order.trigger_price();
295 let trigger_type = order.trigger_type();
296 let is_reduce_only = order.is_reduce_only();
297 let is_post_only = order.is_post_only();
298
299 log::debug!("OrderSubmitted: client_order_id={client_order_id}");
300 self.register_order_identity(order);
301 self.emitter.emit_order_submitted(order);
302
303 let kraken_cl_ord_id = truncate_cl_ord_id(&client_order_id);
304
305 if kraken_cl_ord_id != client_order_id.as_str() {
306 self.truncated_id_map
307 .insert(kraken_cl_ord_id, client_order_id);
308 }
309
310 let http = self.http.clone();
311 let emitter = self.emitter.clone();
312 let clock = self.clock;
313 let dispatch_state = self.ws_dispatch_state.clone();
314
315 self.spawn_task(task_name, async move {
316 let result = http
317 .submit_order(
318 account_id,
319 instrument_id,
320 client_order_id,
321 order_side,
322 order_type,
323 quantity,
324 time_in_force,
325 price,
326 trigger_price,
327 trigger_type,
328 is_reduce_only,
329 is_post_only,
330 )
331 .await;
332
333 match result {
334 Ok(_) => {}
335 Err(e) => match command_failure_from_submit_error(&e) {
336 CommandFailure::Ambiguous(reason) => {
337 log::warn!(
338 "{task_name} outcome is ambiguous for client_order_id={client_order_id}: {reason}"
339 );
340 }
341 CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason) => {
342 let ts_event = clock.get_time_ns();
343 let error_msg = format!("{task_name} error: {reason}");
344 let due_post_only = error_msg.contains("POST_ONLY_REJECTED");
345 dispatch_state.cleanup_terminal(&client_order_id);
346 emitter.emit_order_rejected_event(
347 strategy_id,
348 instrument_id,
349 client_order_id,
350 &error_msg,
351 ts_event,
352 due_post_only,
353 );
354 }
355 },
356 }
357 Ok(())
358 });
359 }
360
361 fn cancel_single_order(&self, cmd: &CancelOrder) {
362 let account_id = self.core.account_id;
363 let client_order_id = cmd.client_order_id;
364 let venue_order_id = cmd.venue_order_id;
365 let strategy_id = cmd.strategy_id;
366 let instrument_id = cmd.instrument_id;
367
368 log::debug!(
369 "Canceling order: venue_order_id={venue_order_id:?}, client_order_id={client_order_id}"
370 );
371
372 let http = self.http.clone();
373 let emitter = self.emitter.clone();
374 let clock = self.clock;
375
376 self.spawn_task("cancel_order", async move {
377 if let Err(failure) = cancel_order_for_futures(
378 &http,
379 account_id,
380 instrument_id,
381 Some(client_order_id),
382 venue_order_id,
383 )
384 .await
385 {
386 handle_cancel_failure(
387 &emitter,
388 clock,
389 strategy_id,
390 instrument_id,
391 client_order_id,
392 venue_order_id,
393 failure,
394 );
395 }
396 Ok(())
397 });
398 }
399
400 fn spawn_message_handler(&mut self) -> anyhow::Result<()> {
401 let mut rx = self
402 .ws
403 .take_output_rx()
404 .context("Failed to take futures WebSocket output receiver")?;
405 let emitter = self.emitter.clone();
406 let instruments = self.instruments.clone();
407 let truncated_id_map = self.truncated_id_map.clone();
408 let order_instrument_map = self.order_instrument_map.clone();
409 let venue_client_map = self.venue_client_map.clone();
410 let venue_order_qty = self.venue_order_qty.clone();
411 let dispatch_state = self.ws_dispatch_state.clone();
412 let account_id = self.core.account_id;
413 let clock = self.clock;
414 let cancellation_token = self.cancellation_token.clone();
415
416 let future = async move {
417 loop {
418 tokio::select! {
419 () = cancellation_token.cancelled() => {
420 log::debug!("Futures execution message handler cancelled");
421 break;
422 }
423 msg = rx.recv() => {
424 match msg {
425 Some(ws_msg) => {
426 Self::handle_ws_message(
427 ws_msg,
428 &emitter,
429 &dispatch_state,
430 &instruments,
431 &truncated_id_map,
432 &order_instrument_map,
433 &venue_client_map,
434 &venue_order_qty,
435 account_id,
436 clock,
437 );
438 }
439 None => {
440 log::debug!("Futures execution WebSocket stream ended");
441 break;
442 }
443 }
444 }
445 }
446 }
447 };
448
449 self.session_tasks
450 .spawn(future)
451 .context("failed to register Kraken Futures execution stream task")
452 }
453
454 #[expect(clippy::too_many_arguments)]
455 fn handle_ws_message(
456 msg: KrakenFuturesWsMessage,
457 emitter: &ExecutionEventEmitter,
458 dispatch_state: &Arc<WsDispatchState>,
459 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
460 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
461 order_instrument_map: &Arc<AtomicMap<String, InstrumentId>>,
462 venue_client_map: &Arc<AtomicMap<String, ClientOrderId>>,
463 venue_order_qty: &Arc<AtomicMap<String, Quantity>>,
464 account_id: AccountId,
465 clock: &'static AtomicTime,
466 ) {
467 let ts_init = clock.get_time_ns();
468
469 match msg {
470 KrakenFuturesWsMessage::OpenOrdersDelta(delta) => {
471 dispatch::futures::open_orders_delta(
472 &delta,
473 dispatch_state,
474 emitter,
475 instruments,
476 truncated_id_map,
477 order_instrument_map,
478 venue_client_map,
479 venue_order_qty,
480 account_id,
481 ts_init,
482 );
483 }
484 KrakenFuturesWsMessage::OpenOrdersCancel(cancel) => {
485 dispatch::futures::open_orders_cancel(
486 &cancel,
487 dispatch_state,
488 emitter,
489 truncated_id_map,
490 order_instrument_map,
491 venue_client_map,
492 venue_order_qty,
493 account_id,
494 ts_init,
495 );
496 }
497 KrakenFuturesWsMessage::FillsDelta(fills_delta) => {
498 dispatch::futures::fills_delta(
499 &fills_delta,
500 dispatch_state,
501 emitter,
502 instruments,
503 truncated_id_map,
504 venue_client_map,
505 account_id,
506 ts_init,
507 );
508 }
509 KrakenFuturesWsMessage::Challenge(challenge) => {
510 log::debug!("Received challenge: length={}", challenge.len());
511 }
512 KrakenFuturesWsMessage::Reconnected => {
513 log::info!("Futures execution WebSocket reconnected");
514 }
515 KrakenFuturesWsMessage::Ticker(_)
516 | KrakenFuturesWsMessage::Trade(_)
517 | KrakenFuturesWsMessage::BookSnapshot(_)
518 | KrakenFuturesWsMessage::BookDelta(_) => {}
519 }
520 }
521
522 async fn await_account_registered(&self, timeout_secs: f64) -> anyhow::Result<()> {
523 let account_id = self.core.account_id;
524
525 if self.core.cache().account(&account_id).is_some() {
526 log::info!("Account {account_id} registered");
527 return Ok(());
528 }
529
530 let start = Instant::now();
531 let timeout = Duration::from_secs_f64(timeout_secs);
532 let interval = Duration::from_millis(10);
533
534 loop {
535 tokio::time::sleep(interval).await;
536
537 if self.core.cache().account(&account_id).is_some() {
538 log::info!("Account {account_id} registered");
539 return Ok(());
540 }
541
542 if start.elapsed() >= timeout {
543 anyhow::bail!(
544 "Timeout waiting for account {account_id} to be registered after {timeout_secs}s"
545 );
546 }
547 }
548 }
549
550 fn modify_single_order(&self, cmd: &ModifyOrder) {
551 let client_order_id = cmd.client_order_id;
552 let venue_order_id = cmd.venue_order_id;
553 let strategy_id = cmd.strategy_id;
554 let instrument_id = cmd.instrument_id;
555 let quantity = cmd.quantity;
556 let price = cmd.price;
557
558 log::debug!(
559 "Modifying order: venue_order_id={venue_order_id:?}, client_order_id={client_order_id}"
560 );
561
562 let http = self.http.clone();
563 let emitter = self.emitter.clone();
564 let clock = self.clock;
565
566 self.spawn_task("modify_order", async move {
567 match http
568 .modify_order(
569 instrument_id,
570 Some(client_order_id),
571 venue_order_id,
572 quantity,
573 price,
574 None,
575 )
576 .await
577 {
578 Ok(_) => {}
579 Err(e) => match command_failure_from_modify_error(&e) {
580 CommandFailure::Ambiguous(reason) => {
581 log::warn!(
582 "modify_order outcome is ambiguous for client_order_id={client_order_id}: {reason}"
583 );
584 }
585 CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason) => {
586 let ts_event = clock.get_time_ns();
587 emitter.emit_order_modify_rejected_event(
588 strategy_id,
589 instrument_id,
590 client_order_id,
591 venue_order_id,
592 &format!("modify-order error: {reason}"),
593 ts_event,
594 );
595 }
596 },
597 }
598 Ok(())
599 });
600 }
601}
602
603#[async_trait(?Send)]
604impl ExecutionClient for KrakenFuturesExecutionClient {
605 fn is_connected(&self) -> bool {
606 self.core.is_connected()
607 }
608
609 fn client_id(&self) -> ClientId {
610 self.core.client_id
611 }
612
613 fn account_id(&self) -> AccountId {
614 self.core.account_id
615 }
616
617 fn venue(&self) -> Venue {
618 *KRAKEN_VENUE
619 }
620
621 fn oms_type(&self) -> OmsType {
622 self.core.oms_type
623 }
624
625 fn get_account(&self) -> Option<AccountAny> {
626 self.core.cache().account_owned(&self.core.account_id)
627 }
628
629 fn generate_account_state(
630 &self,
631 balances: Vec<AccountBalance>,
632 margins: Vec<MarginBalance>,
633 reported: bool,
634 ts_event: UnixNanos,
635 info: Option<Params>,
636 ) -> anyhow::Result<()> {
637 self.emitter
638 .emit_account_state(balances, margins, reported, ts_event, info);
639 Ok(())
640 }
641
642 fn start(&mut self) -> anyhow::Result<()> {
643 if self.core.is_started() {
644 return Ok(());
645 }
646
647 self.emitter.set_sender(get_exec_event_sender());
648 self.core.set_started();
649
650 log::info!(
651 "Started: client_id={}, account_id={}, product_type=Futures, environment={:?}",
652 self.core.client_id,
653 self.core.account_id,
654 self.config.environment
655 );
656 Ok(())
657 }
658
659 fn stop(&mut self) -> anyhow::Result<()> {
660 if self.core.is_stopped() {
661 return Ok(());
662 }
663
664 self.http.cancel_all_requests();
665 self.session_tasks.begin_shutdown();
666 self.pending_tasks.begin_shutdown();
667 self.ws.begin_shutdown();
668 self.core.set_stopped();
669 self.core.set_disconnected();
670 log::info!("Stopped: client_id={}", self.core.client_id);
671 Ok(())
672 }
673
674 async fn connect(&mut self) -> anyhow::Result<()> {
675 if self.core.is_connected() && self.session_tasks.is_open() && self.pending_tasks.is_open()
676 {
677 return Ok(());
678 }
679
680 self.http.reset_cancellation_token();
681 self.prepare_task_groups().await?;
682
683 if !self.core.instruments_initialized() {
684 let instruments = self
685 .http
686 .request_instruments()
687 .await
688 .context("Failed to load Kraken futures instruments")?;
689 log::debug!("Loaded {} Futures instruments", instruments.len());
690 self.http.cache_instruments(&instruments);
691 self.core.set_instruments_initialized();
692 }
693
694 self.instruments.rcu(|m| {
695 for instrument in self.http.instruments_cache.load().values() {
696 m.insert(instrument.id(), instrument.clone());
697 }
698 });
699
700 let session_result = async {
701 self.ws
702 .connect()
703 .await
704 .context("Failed to connect futures WebSocket")?;
705 self.ws
706 .wait_until_active(10.0)
707 .await
708 .context("Futures WebSocket failed to become active")?;
709
710 self.ws
711 .authenticate()
712 .await
713 .context("Failed to authenticate futures WebSocket")?;
714
715 let account_state = self
716 .http
717 .request_account_state(self.core.account_id)
718 .await
719 .context("Failed to request Kraken futures account state")?;
720
721 if !account_state.balances.is_empty() {
722 log::debug!(
723 "Received account state with {} balance(s)",
724 account_state.balances.len()
725 );
726 }
727 self.emitter.send_account_state(account_state);
728 self.await_account_registered(30.0).await?;
729
730 self.spawn_message_handler()?;
731
732 self.ws
733 .subscribe_executions()
734 .await
735 .context("Failed to subscribe to executions")?;
736
737 log::debug!("Futures WebSocket authenticated and subscribed to executions");
738
739 Ok::<(), anyhow::Error>(())
740 }
741 .await;
742
743 if let Err(e) = session_result {
744 if let Err(teardown_error) = self.teardown_partial_connect().await {
745 return Err(e.context(format!(
746 "Kraken Futures execution startup teardown failed: {teardown_error}"
747 )));
748 }
749 return Err(e);
750 }
751
752 self.core.set_connected();
753 log::info!("Connected: client_id={}", self.core.client_id);
754 Ok(())
755 }
756
757 async fn disconnect(&mut self) -> anyhow::Result<()> {
758 self.teardown_partial_connect().await?;
759 log::info!("Disconnected: client_id={}", self.core.client_id);
760 Ok(())
761 }
762
763 async fn generate_order_status_report(
764 &self,
765 cmd: &GenerateOrderStatusReport,
766 ) -> anyhow::Result<Option<OrderStatusReport>> {
767 log::debug!(
768 "Generating order status report: venue_order_id={:?}, client_order_id={:?}",
769 cmd.venue_order_id,
770 cmd.client_order_id
771 );
772
773 let account_id = self.core.account_id;
774 let (reports, coverage) = self
777 .http
778 .request_order_status_reports_checked(account_id, cmd.instrument_id, None, None, false)
779 .await?;
780
781 let matched = reports.into_iter().find(|r| {
784 cmd.venue_order_id
785 .is_some_and(|id| r.venue_order_id.as_str() == id.as_str())
786 || cmd.client_order_id.is_some_and(|id| {
787 r.client_order_id
788 .as_ref()
789 .is_some_and(|r_id| r_id.as_str() == truncate_cl_ord_id(&id))
790 })
791 });
792
793 if let Some(mut report) = matched {
794 if is_unpriced_terminal_report(&report) {
798 let cached = cached_order_for_status_command(&self.core.cache(), cmd);
799 let fills = self
800 .http
801 .request_fill_reports(account_id, Some(report.instrument_id), None, None)
802 .await;
803 let page = fills.as_deref().unwrap_or_default();
804
805 if let Err(covered) = price_from_fills(&mut report, cached.as_ref(), page) {
806 fills?;
807 anyhow::bail!(
808 "Order {} executed per the order history with fills covering {covered} \
809 of {}; deferring until the fills feed prices it",
810 report.venue_order_id,
811 report.filled_qty,
812 );
813 }
814 }
815
816 return Ok(Some(report));
817 }
818
819 let Some(order) = cached_order_for_status_command(&self.core.cache(), cmd) else {
822 return absent_unless_unread(
823 &coverage,
824 cmd.venue_order_id,
825 None,
826 &status_command_label(cmd, cmd.venue_order_id),
827 );
828 };
829
830 let order_ids: Vec<String> = cmd
832 .venue_order_id
833 .or(order.venue_order_id())
834 .map(|id| id.to_string())
835 .into_iter()
836 .collect();
837 let cli_ord_ids: Vec<String> = cmd
838 .client_order_id
839 .map(|id| truncate_cl_ord_id(&id))
840 .into_iter()
841 .collect();
842
843 let recent_reports = self
844 .http
845 .request_orders_status_reports(account_id, &order_ids, &cli_ord_ids)
846 .await?;
847
848 let matched_recent = recent_reports
849 .iter()
850 .find(|report| {
851 cmd.venue_order_id
852 .is_some_and(|id| report.venue_order_id == id)
853 || cmd.client_order_id.is_some_and(|id| {
854 report
855 .client_order_id
856 .as_ref()
857 .is_some_and(|report_id| report_id.as_str() == truncate_cl_ord_id(&id))
858 })
859 })
860 .cloned();
861
862 if matched_recent
864 .as_ref()
865 .is_some_and(|report| report.order_status != OrderStatus::Filled)
866 {
867 return Ok(matched_recent);
868 }
869
870 let now = Timestamp::now();
871 let start = now - Duration::from_secs(5 * 60);
872 let fills = self
873 .http
874 .request_fill_reports(
875 account_id,
876 Some(order.instrument_id()),
877 Some(start),
878 Some(now),
879 )
880 .await?;
881
882 match (
883 synthesize_filled_order_status_report(cmd, &order, &fills),
884 matched_recent,
885 ) {
886 (Some(report), _) => Ok(Some(report)),
887 (None, Some(_)) => anyhow::bail!(
889 "Order {} fully executed in the orders-status window without visible \
890 fills; deferring until the fills feed prices it",
891 order.client_order_id(),
892 ),
893 (None, None) => absent_unless_unread(
894 &coverage,
895 cmd.venue_order_id.or(order.venue_order_id()),
896 Some(earliest_venue_time(&order)),
897 order.client_order_id().as_str(),
898 ),
899 }
900 }
901
902 async fn generate_order_status_reports(
903 &self,
904 cmd: &GenerateOrderStatusReports,
905 ) -> anyhow::Result<Vec<OrderStatusReport>> {
906 log::debug!(
907 "Generating order status reports: instrument_id={:?}, open_only={}",
908 cmd.instrument_id,
909 cmd.open_only
910 );
911
912 let account_id = self.core.account_id;
913 let start = cmd.start.map(Timestamp::from);
914 let end = cmd.end.map(Timestamp::from);
915 let mut reports = self
916 .http
917 .request_order_status_reports(account_id, cmd.instrument_id, start, end, cmd.open_only)
918 .await?;
919
920 if reports.iter().any(is_unpriced_terminal_report) {
925 let fills = match self
928 .http
929 .request_fill_reports(account_id, cmd.instrument_id, None, None)
930 .await
931 {
932 Ok(fills) => fills,
933 Err(e) => {
934 log::warn!("Failed to read fills to price executed history orders: {e}");
935 Vec::new()
936 }
937 };
938 let cache = self.core.cache();
939 reports.retain_mut(|report| {
940 if !is_unpriced_terminal_report(report) {
941 return true;
942 }
943
944 let cached = cached_order_for_report(&cache, report);
945
946 match price_from_fills(report, cached.as_ref(), &fills) {
947 Ok(()) => true,
948 Err(covered) => {
949 log::warn!(
950 "Withholding executed order {} from the bulk response: fills cover \
951 {covered} of {}",
952 report.venue_order_id,
953 report.filled_qty,
954 );
955 false
956 }
957 }
958 });
959 }
960
961 if cmd.open_only {
962 let (extension, _) = self
963 .reports_for_open_orders_absent_from_venue(account_id, cmd.instrument_id, &reports)
964 .await?;
965
966 for report in extension {
970 if report.order_status == OrderStatus::Filled {
971 log::warn!(
972 "Withholding fully executed order {} from the bulk response: the \
973 orders-status window carries no fill price",
974 report.venue_order_id,
975 );
976 continue;
977 }
978
979 reports.push(report);
980 }
981 }
982
983 Ok(reports)
984 }
985
986 async fn generate_fill_reports(
987 &self,
988 cmd: GenerateFillReports,
989 ) -> anyhow::Result<Vec<FillReport>> {
990 log::debug!(
991 "Generating fill reports: instrument_id={:?}",
992 cmd.instrument_id
993 );
994
995 let account_id = self.core.account_id;
996 let start = cmd.start.map(Timestamp::from);
997 let end = cmd.end.map(Timestamp::from);
998 let mut reports = self
999 .http
1000 .request_fill_reports(account_id, cmd.instrument_id, start, end)
1001 .await?;
1002
1003 if let Some(venue_order_id) = cmd.venue_order_id {
1004 reports.retain(|report| report.venue_order_id == venue_order_id);
1005 }
1006
1007 Ok(reports)
1008 }
1009
1010 async fn generate_position_status_reports(
1011 &self,
1012 cmd: &GeneratePositionStatusReports,
1013 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1014 log::debug!(
1015 "Generating position status reports: instrument_id={:?}",
1016 cmd.instrument_id
1017 );
1018
1019 let account_id = self.core.account_id;
1020 self.http
1021 .request_position_status_reports(account_id, cmd.instrument_id)
1022 .await
1023 }
1024
1025 async fn generate_mass_status(
1026 &self,
1027 lookback_mins: Option<u64>,
1028 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1029 log::debug!("Generating mass status: lookback_mins={lookback_mins:?}");
1030
1031 let ts_init = self.clock.get_time_ns();
1032 let lookback_start = lookback_mins.map(|mins| {
1035 let nanos = mins.saturating_mul(60).saturating_mul(1_000_000_000);
1036 ts_init.saturating_sub(DurationNanos::new(nanos))
1037 });
1038 let start = lookback_start.map(Timestamp::from);
1039 let account_id = self.core.account_id;
1040
1041 let (mut order_reports, orders_complete) = match self
1047 .http
1048 .request_order_status_reports_checked(account_id, None, start, None, false)
1049 .await
1050 {
1051 Ok((reports, coverage)) => (reports, coverage.is_complete()),
1052 Err(e) if is_refused_history_read(&e) => {
1053 log::warn!(
1054 "Order history read refused for mass status, reading open orders only and \
1055 marking the set incomplete: {e}"
1056 );
1057 let (reports, _) = self
1058 .http
1059 .request_order_status_reports_checked(account_id, None, start, None, true)
1060 .await?;
1061 (reports, false)
1062 }
1063 Err(e) => return Err(e),
1064 };
1065 let from_venue_read: HashSet<VenueOrderId> = order_reports
1068 .iter()
1069 .map(|report| report.venue_order_id)
1070 .collect();
1071
1072 let (extension, mut extension_withheld) = self
1073 .reports_for_open_orders_absent_from_venue(account_id, None, &order_reports)
1074 .await?;
1075
1076 for report in extension {
1080 if report.order_status == OrderStatus::Filled {
1081 log::warn!(
1082 "Withholding fully executed order {} from mass status: the orders-status \
1083 window carries no fill price",
1084 report.venue_order_id,
1085 );
1086 extension_withheld = true;
1087 continue;
1088 }
1089
1090 order_reports.push(report);
1091 }
1092
1093 let orders_complete = orders_complete && !extension_withheld;
1094
1095 let (mut fill_reports, fills_complete) = self
1096 .http
1097 .request_fill_reports_checked(account_id, None, start, None)
1098 .await?;
1099
1100 let mut withheld = false;
1112 let mut withheld_fills: HashSet<VenueOrderId> = HashSet::new();
1113 {
1114 let cache = self.core.cache();
1115 order_reports.retain_mut(|report| {
1116 if !from_venue_read.contains(&report.venue_order_id)
1117 || !is_unpriced_terminal_report(report)
1118 {
1119 return true;
1120 }
1121
1122 let cached = cached_order_for_report(&cache, report);
1123
1124 match price_from_fills(report, cached.as_ref(), &fill_reports) {
1125 Ok(()) => true,
1126 Err(covered) => {
1127 log::warn!(
1128 "Withholding executed order {} from mass status: fills cover \
1129 {covered} of {}; {}",
1130 report.venue_order_id,
1131 report.filled_qty,
1132 if cached.is_some() {
1133 "its page fills stay for the cached order"
1134 } else {
1135 "its page fills are withheld with it"
1136 },
1137 );
1138 withheld = true;
1139
1140 if cached.is_none() {
1141 withheld_fills.insert(report.venue_order_id);
1142 }
1143 false
1144 }
1145 }
1146 });
1147 }
1148 fill_reports.retain(|fill| !withheld_fills.contains(&fill.venue_order_id));
1149 let orders_complete = orders_complete && !withheld;
1150
1151 let position_reports = self
1152 .http
1153 .request_position_status_reports(account_id, None)
1154 .await?;
1155
1156 let orders_complete = if lookback_start.is_none() {
1171 let held: HashSet<InstrumentId> = position_reports
1172 .iter()
1173 .map(|report| report.instrument_id)
1174 .collect();
1175 let netted: HashSet<VenueOrderId> = {
1176 let cache = self.core.cache();
1177 order_reports
1178 .iter()
1179 .filter(|report| {
1180 from_venue_read.contains(&report.venue_order_id)
1181 && !held.contains(&report.instrument_id)
1182 && cached_order_for_report(&cache, report).is_none()
1183 })
1184 .map(|report| report.venue_order_id)
1185 .collect()
1186 };
1187 let candidates: HashMap<VenueOrderId, InstrumentId> = order_reports
1188 .iter()
1189 .filter(|report| {
1190 netted.contains(&report.venue_order_id)
1191 && matches!(
1192 report.order_status,
1193 OrderStatus::Filled
1194 | OrderStatus::Canceled
1195 | OrderStatus::Expired
1196 | OrderStatus::Voided
1197 )
1198 })
1199 .map(|report| (report.venue_order_id, report.instrument_id))
1200 .collect();
1201
1202 let mut net: HashMap<InstrumentId, Decimal> = HashMap::new();
1203
1204 for fill in fill_reports
1205 .iter()
1206 .filter(|fill| netted.contains(&fill.venue_order_id))
1207 {
1208 let qty = fill.last_qty.as_decimal();
1209 let signed = match fill.order_side {
1210 OrderSide::Buy => qty,
1211 OrderSide::Sell => -qty,
1212 };
1213 *net.entry(fill.instrument_id).or_default() += signed;
1214 }
1215 net.retain(|instrument_id, net| {
1216 !net.is_zero() && candidates.values().any(|id| id == instrument_id)
1217 });
1218
1219 if net.is_empty() {
1220 orders_complete
1221 } else {
1222 for (instrument_id, net) in &net {
1223 log::warn!(
1224 "Withholding terminal orders on {instrument_id} from mass status: the venue \
1225 reports it flat but the fills of orders the cache does not hold net to \
1226 {net}, so reconciliation would open a position; marking the set incomplete",
1227 );
1228 }
1229 let withheld_flat: HashSet<VenueOrderId> = candidates
1230 .iter()
1231 .filter(|(_, instrument_id)| net.contains_key(instrument_id))
1232 .map(|(venue_order_id, _)| *venue_order_id)
1233 .collect();
1234 order_reports.retain(|report| !withheld_flat.contains(&report.venue_order_id));
1235 fill_reports.retain(|fill| !withheld_flat.contains(&fill.venue_order_id));
1236 false
1237 }
1238 } else {
1239 orders_complete
1240 };
1241
1242 let mut mass_status = ExecutionMassStatus::new(
1243 self.core.client_id,
1244 self.core.account_id,
1245 *KRAKEN_VENUE,
1246 ts_init,
1247 None,
1248 );
1249 mass_status.add_order_reports(order_reports);
1250 mass_status.add_fill_reports(fill_reports);
1251 mass_status.add_position_reports(position_reports);
1252 mass_status.set_report_window(lookback_start, orders_complete && fills_complete);
1254
1255 Ok(Some(mass_status))
1256 }
1257
1258 fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
1259 log::debug!("Querying account: {cmd}");
1260
1261 let account_id = self.core.account_id;
1262 let http = self.http.clone();
1263 let emitter = self.emitter.clone();
1264
1265 self.spawn_task("query_account", async move {
1266 let account_state = http.request_account_state(account_id).await?;
1267 emitter.emit_account_state(
1268 account_state.balances.clone(),
1269 account_state.margins.clone(),
1270 account_state.is_reported,
1271 account_state.ts_event,
1272 account_state.info,
1273 );
1274 Ok(())
1275 });
1276
1277 Ok(())
1278 }
1279
1280 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
1281 log::debug!("Querying order: {cmd}");
1282
1283 let venue_order_id = cmd
1284 .venue_order_id
1285 .context("venue_order_id required for query_order")?;
1286 let account_id = self.core.account_id;
1287 let instrument_id = cmd.instrument_id;
1288 let http = self.http.clone();
1289 let emitter = self.emitter.clone();
1290
1291 self.spawn_task("query_order", async move {
1292 let reports = http
1295 .request_order_status_reports(account_id, Some(instrument_id), None, None, true)
1296 .await
1297 .context("Failed to query order")?;
1298
1299 match reports
1300 .into_iter()
1301 .find(|r| r.venue_order_id == venue_order_id)
1302 {
1303 Some(report) => emitter.send_order_status_report(report),
1304 None => log::warn!(
1305 "No open order {venue_order_id} found for {instrument_id}; no report emitted"
1306 ),
1307 }
1308 Ok(())
1309 });
1310
1311 Ok(())
1312 }
1313
1314 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
1315 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
1316 self.submit_single_order(&order, "submit_order");
1317 Ok(())
1318 }
1319
1320 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
1321 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
1322
1323 log::debug!(
1324 "Submitting order list: order_list_id={}, count={}",
1325 cmd.order_list.id,
1326 orders.len()
1327 );
1328
1329 let mut order_tuples = Vec::with_capacity(orders.len());
1330 let mut order_meta = Vec::with_capacity(orders.len());
1331
1332 for order in &orders {
1333 if order.is_closed() {
1334 log::warn!(
1335 "Cannot submit closed order: client_order_id={}",
1336 order.client_order_id()
1337 );
1338 continue;
1339 }
1340
1341 if order.order_type() == OrderType::Market {
1344 self.submit_single_order(order, "submit_order_list");
1345 continue;
1346 }
1347
1348 let client_order_id = order.client_order_id();
1349 let kraken_cl_ord_id = truncate_cl_ord_id(&client_order_id);
1350
1351 if kraken_cl_ord_id != client_order_id.as_str() {
1352 self.truncated_id_map
1353 .insert(kraken_cl_ord_id, client_order_id);
1354 }
1355
1356 self.register_order_identity(order);
1357 self.emitter.emit_order_submitted(order);
1358
1359 order_tuples.push((
1360 order.instrument_id(),
1361 client_order_id,
1362 order.order_side(),
1363 order.order_type(),
1364 order.quantity(),
1365 order.time_in_force(),
1366 order.price(),
1367 order.trigger_price(),
1368 order.trigger_type(),
1369 order.is_reduce_only(),
1370 order.is_post_only(),
1371 ));
1372
1373 order_meta.push((order.strategy_id(), order.instrument_id(), client_order_id));
1374 }
1375
1376 if order_tuples.is_empty() {
1377 return Ok(());
1378 }
1379
1380 let http = self.http.clone();
1381 let emitter = self.emitter.clone();
1382 let clock = self.clock;
1383 let dispatch_state = self.ws_dispatch_state.clone();
1384
1385 self.spawn_task("submit_order_list", async move {
1386 let results = http.send_order_batches(order_tuples).await;
1387 for (result, (strategy_id, instrument_id, client_order_id)) in
1388 results.into_iter().zip(&order_meta)
1389 {
1390 let outcome = match result {
1391 Ok(item) => command_failure_from_futures_batch_item(item),
1392 Err(e) => Err(command_failure_from_futures_batch_error(&e)),
1393 };
1394
1395 match outcome {
1396 Ok(()) => {}
1397 Err(CommandFailure::Ambiguous(reason)) => {
1398 log::warn!(
1399 "submit_order_list outcome is ambiguous for client_order_id={client_order_id}: {reason}"
1400 );
1401 }
1402 Err(
1403 CommandFailure::NotSent(reason)
1404 | CommandFailure::VenueRejected(reason),
1405 ) => {
1406 let ts_event = clock.get_time_ns();
1407 let error_msg =
1408 format!("submit_order_list batch item rejected: {reason}");
1409 dispatch_state.cleanup_terminal(client_order_id);
1410 emitter.emit_order_rejected_event(
1411 *strategy_id,
1412 *instrument_id,
1413 *client_order_id,
1414 &error_msg,
1415 ts_event,
1416 reason == "postWouldExecute",
1417 );
1418 }
1419 }
1420 }
1421 Ok(())
1422 });
1423
1424 Ok(())
1425 }
1426
1427 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
1428 self.modify_single_order(&cmd);
1429 Ok(())
1430 }
1431
1432 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
1433 self.cancel_single_order(&cmd);
1434 Ok(())
1435 }
1436
1437 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
1438 let instrument_id = cmd.instrument_id;
1439
1440 if cmd.order_side.is_none() {
1441 log::debug!("Canceling all orders: instrument_id={instrument_id} (bulk)");
1442
1443 let http = self.http.clone();
1444 let symbol = instrument_id.symbol.to_string();
1445
1446 self.spawn_task("cancel_all_orders", async move {
1447 match http.inner.cancel_all_orders(Some(symbol)).await {
1448 Ok(response) => {
1449 if response.result != KrakenApiResult::Success
1450 && response.cancel_status.cancelled_orders.is_empty()
1451 {
1452 log::warn!(
1453 "Cancel-all failed without per-order results, awaiting reconciliation: status={}",
1454 response.cancel_status.status
1455 );
1456 }
1457 }
1458 Err(e) => match command_failure_from_cancel_error(e) {
1459 CommandFailure::NotSent(reason) => {
1460 log::warn!("Cancel-all failed local validation: {reason}");
1461 }
1462 CommandFailure::Ambiguous(reason)
1463 | CommandFailure::VenueRejected(reason) => {
1464 log::warn!(
1465 "Cancel-all ambiguous failure, awaiting reconciliation: {reason}"
1466 );
1467 }
1468 },
1469 }
1470 Ok(())
1471 });
1472
1473 return Ok(());
1474 }
1475
1476 log::debug!(
1477 "Canceling all orders: instrument_id={instrument_id}, side={:?}",
1478 cmd.order_side
1479 );
1480
1481 let cancels: Vec<CancelOrder> = {
1484 let cache = self.core.cache();
1485 let ts_init = self.clock.get_time_ns();
1486 let correlation_id = cmd.correlation_id.or(Some(cmd.command_id));
1487
1488 let mut seen = HashSet::new();
1491
1492 cache
1493 .orders_open(None, Some(&instrument_id), None, None, None)
1494 .into_iter()
1495 .chain(cache.orders_inflight(None, Some(&instrument_id), None, None, None))
1496 .filter(|order| Some(order.order_side()) == cmd.order_side)
1497 .filter(|order| seen.insert(order.client_order_id()))
1498 .map(|order| {
1499 CancelOrder::new(
1500 cmd.trader_id,
1501 cmd.client_id,
1502 order.strategy_id(),
1504 order.instrument_id(),
1505 order.client_order_id(),
1506 order.venue_order_id(),
1507 UUID4::new(),
1508 ts_init,
1509 cmd.params.clone(),
1510 correlation_id,
1511 )
1512 })
1513 .collect()
1514 };
1515
1516 if cancels.is_empty() {
1517 log::debug!("No open orders to cancel for {instrument_id}");
1518 return Ok(());
1519 }
1520
1521 let http = self.http.clone();
1522 let emitter = self.emitter.clone();
1523 let clock = self.clock;
1524
1525 self.spawn_task("cancel_all_orders_by_side", async move {
1526 batch_cancel_orders_for_futures(&http, &emitter, clock, &cancels).await;
1527 Ok(())
1528 });
1529
1530 Ok(())
1531 }
1532
1533 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1534 log::debug!(
1535 "Batch canceling orders: instrument_id={}, count={}",
1536 cmd.instrument_id,
1537 cmd.cancels.len()
1538 );
1539
1540 let http = self.http.clone();
1541 let emitter = self.emitter.clone();
1542 let clock = self.clock;
1543 let cancels = cmd.cancels;
1544
1545 self.spawn_task("batch_cancel_orders", async move {
1546 batch_cancel_orders_for_futures(&http, &emitter, clock, &cancels).await;
1547 Ok(())
1548 });
1549
1550 Ok(())
1551 }
1552}
1553
1554#[derive(Debug, Clone)]
1555struct CancelRequestContext {
1556 strategy_id: StrategyId,
1557 instrument_id: InstrumentId,
1558 client_order_id: ClientOrderId,
1559 truncated_client_order_id: String,
1560 venue_order_id: Option<VenueOrderId>,
1561}
1562
1563async fn cancel_order_for_futures(
1564 http: &KrakenFuturesHttpClient,
1565 _account_id: AccountId,
1566 instrument_id: InstrumentId,
1567 client_order_id: Option<ClientOrderId>,
1568 venue_order_id: Option<VenueOrderId>,
1569) -> Result<(), CommandFailure> {
1570 http.get_cached_instrument(&instrument_id.symbol.inner())
1571 .ok_or_else(|| {
1572 CommandFailure::not_sent(InstrumentLookupError::not_found(instrument_id).to_string())
1573 })?;
1574
1575 let order_id = venue_order_id.as_ref().map(ToString::to_string);
1576 let cli_ord_id = client_order_id.as_ref().map(truncate_cl_ord_id);
1577
1578 if order_id.is_none() && cli_ord_id.is_none() {
1579 return Err(CommandFailure::not_sent(
1580 "Either client_order_id or venue_order_id must be provided",
1581 ));
1582 }
1583
1584 let response = http
1585 .inner
1586 .cancel_order(order_id, cli_ord_id)
1587 .await
1588 .map_err(command_failure_from_cancel_error)?;
1589
1590 if response.result != KrakenApiResult::Success
1591 || response.cancel_status.status != KrakenSendStatus::Cancelled
1592 {
1593 return Err(CommandFailure::venue_rejected(format!(
1594 "cancel-order rejected: status={}",
1595 response.cancel_status.status
1596 )));
1597 }
1598
1599 Ok(())
1600}
1601
1602async fn batch_cancel_orders_for_futures(
1603 http: &KrakenFuturesHttpClient,
1604 emitter: &ExecutionEventEmitter,
1605 clock: &'static AtomicTime,
1606 cancels: &[CancelOrder],
1607) {
1608 let mut contexts = Vec::new();
1609 let mut items = Vec::new();
1610
1611 for cancel in cancels {
1612 match batch_cancel_item_for_futures(http, cancel) {
1613 Ok((context, item)) => {
1614 contexts.push(context);
1615 items.push(item);
1616 }
1617 Err(CommandFailure::NotSent(reason)) => {
1618 log::warn!(
1619 "Batch cancel command failed local validation for {}: {reason}",
1620 cancel.client_order_id
1621 );
1622 }
1623 Err(CommandFailure::Ambiguous(reason) | CommandFailure::VenueRejected(reason)) => {
1624 log::warn!(
1625 "Batch cancel command ambiguous failure for {}, awaiting reconciliation: {reason}",
1626 cancel.client_order_id
1627 );
1628 }
1629 }
1630 }
1631
1632 for (item_chunk, context_chunk) in items
1633 .chunks(FUTURES_BATCH_CANCEL_LIMIT)
1634 .zip(contexts.chunks(FUTURES_BATCH_CANCEL_LIMIT))
1635 {
1636 let response = match http
1637 .inner
1638 .cancel_order_items_batch(item_chunk.to_vec())
1639 .await
1640 {
1641 Ok(response) => response,
1642 Err(e) => {
1643 match command_failure_from_cancel_error(e) {
1644 CommandFailure::NotSent(reason) => {
1645 log::warn!("Batch cancel failed local validation: {reason}");
1646 }
1647 CommandFailure::Ambiguous(reason) | CommandFailure::VenueRejected(reason) => {
1648 log::warn!(
1649 "Batch cancel failed without per-order results, awaiting reconciliation: {reason}"
1650 );
1651 }
1652 }
1653 continue;
1654 }
1655 };
1656
1657 if response.batch_status.is_empty() {
1658 if response.result != KrakenApiResult::Success {
1659 let reason = response.error.as_deref().unwrap_or("Unknown error");
1660 log::warn!(
1661 "Batch cancel failed without per-order results, awaiting reconciliation: {reason}"
1662 );
1663 }
1664 continue;
1665 }
1666
1667 if response.batch_status.len() != context_chunk.len() {
1668 log::warn!(
1669 "Batch cancel returned {} per-order result(s) for {} request(s); unmatched results await reconciliation",
1670 response.batch_status.len(),
1671 context_chunk.len()
1672 );
1673 }
1674
1675 for (index, status) in response.batch_status.iter().enumerate() {
1676 let Some(cancel_status) = batch_cancel_status(status) else {
1677 log::warn!("Batch cancel result without status at index {index}");
1678 continue;
1679 };
1680
1681 if cancel_status == KrakenSendStatus::Cancelled {
1682 continue;
1683 }
1684
1685 let Some(context) = batch_cancel_context(status, context_chunk, index) else {
1686 log::warn!(
1687 "Batch cancel rejected item without matching request context at index {index}: status={cancel_status}"
1688 );
1689 continue;
1690 };
1691
1692 emitter.emit_order_cancel_rejected_event(
1693 context.strategy_id,
1694 context.instrument_id,
1695 context.client_order_id,
1696 context.venue_order_id,
1697 &format!("batch-cancel rejected: status={cancel_status}"),
1698 clock.get_time_ns(),
1699 );
1700 }
1701 }
1702}
1703
1704fn batch_cancel_item_for_futures(
1705 http: &KrakenFuturesHttpClient,
1706 cancel: &CancelOrder,
1707) -> Result<(CancelRequestContext, KrakenFuturesBatchCancelItem), CommandFailure> {
1708 http.get_cached_instrument(&cancel.instrument_id.symbol.inner())
1709 .ok_or_else(|| {
1710 CommandFailure::not_sent(
1711 InstrumentLookupError::not_found(cancel.instrument_id).to_string(),
1712 )
1713 })?;
1714
1715 let truncated_client_order_id = truncate_cl_ord_id(&cancel.client_order_id);
1716 let item = if let Some(venue_order_id) = cancel.venue_order_id {
1717 KrakenFuturesBatchCancelItem::from_order_id(venue_order_id.to_string())
1718 } else {
1719 KrakenFuturesBatchCancelItem::from_client_order_id(truncated_client_order_id.clone())
1720 };
1721
1722 Ok((
1723 CancelRequestContext {
1724 strategy_id: cancel.strategy_id,
1725 instrument_id: cancel.instrument_id,
1726 client_order_id: cancel.client_order_id,
1727 truncated_client_order_id,
1728 venue_order_id: cancel.venue_order_id,
1729 },
1730 item,
1731 ))
1732}
1733
1734fn batch_cancel_status(status: &FuturesBatchCancelStatus) -> Option<KrakenSendStatus> {
1735 status
1736 .cancel_status
1737 .as_ref()
1738 .map(|cancel_status| cancel_status.status)
1739 .or(status.status)
1740}
1741
1742fn batch_cancel_context<'a>(
1743 status: &FuturesBatchCancelStatus,
1744 contexts: &'a [CancelRequestContext],
1745 index: usize,
1746) -> Option<&'a CancelRequestContext> {
1747 if let Some(order_id) = status.order_id.as_deref()
1748 && let Some(context) = contexts.iter().find(|context| {
1749 context
1750 .venue_order_id
1751 .is_some_and(|venue_order_id| venue_order_id.as_str() == order_id)
1752 })
1753 {
1754 return Some(context);
1755 }
1756
1757 if let Some(cli_ord_id) = status.cli_ord_id.as_deref()
1758 && let Some(context) = contexts
1759 .iter()
1760 .find(|context| context.truncated_client_order_id == cli_ord_id)
1761 {
1762 return Some(context);
1763 }
1764
1765 if index < contexts.len() && status.order_id.is_none() && status.cli_ord_id.is_none() {
1766 return contexts.get(index);
1767 }
1768
1769 None
1770}
1771
1772fn handle_cancel_failure(
1773 emitter: &ExecutionEventEmitter,
1774 clock: &'static AtomicTime,
1775 strategy_id: StrategyId,
1776 instrument_id: InstrumentId,
1777 client_order_id: ClientOrderId,
1778 venue_order_id: Option<VenueOrderId>,
1779 failure: CommandFailure,
1780) {
1781 match failure {
1782 CommandFailure::VenueRejected(reason) => {
1783 emitter.emit_order_cancel_rejected_event(
1784 strategy_id,
1785 instrument_id,
1786 client_order_id,
1787 venue_order_id,
1788 &reason,
1789 clock.get_time_ns(),
1790 );
1791 }
1792 CommandFailure::NotSent(reason) => {
1793 log::warn!("Cancel command failed local validation for {client_order_id}: {reason}");
1794 }
1795 CommandFailure::Ambiguous(reason) => {
1796 log::warn!(
1797 "Ambiguous cancel failure for {client_order_id}, awaiting reconciliation: {reason}"
1798 );
1799 }
1800 }
1801}
1802
1803impl KrakenFuturesExecutionClient {
1804 async fn reports_for_open_orders_absent_from_venue(
1820 &self,
1821 account_id: AccountId,
1822 instrument_id: Option<InstrumentId>,
1823 reported: &[OrderStatusReport],
1824 ) -> anyhow::Result<(Vec<OrderStatusReport>, bool)> {
1825 let mut order_ids = Vec::new();
1826 let mut queried_by_cli_ord_id = Vec::new();
1827
1828 {
1829 let cache = self.core.cache();
1830 for order in cache.orders_open(
1831 Some(&*KRAKEN_VENUE),
1832 instrument_id.as_ref(),
1833 None,
1834 None,
1835 None,
1836 ) {
1837 if product_type_from_symbol(order.instrument_id().symbol.inner().as_str())
1839 != KrakenProductType::Futures
1840 {
1841 continue;
1842 }
1843
1844 match order.venue_order_id() {
1845 Some(venue_order_id) => {
1846 let already_reported = reported
1847 .iter()
1848 .any(|report| report.venue_order_id == venue_order_id);
1849 if !already_reported {
1850 order_ids.push(venue_order_id.to_string());
1851 }
1852 }
1853 None => {
1854 queried_by_cli_ord_id.push(HeldOrder {
1855 client_order_id: order.client_order_id(),
1856 instrument_id: order.instrument_id(),
1857 side: order.order_side(),
1858 ts_init: order.ts_init(),
1859 });
1860 }
1861 }
1862 }
1863 }
1864
1865 if order_ids.is_empty() && queried_by_cli_ord_id.is_empty() {
1866 return Ok((Vec::new(), false));
1867 }
1868
1869 let cli_ord_ids: Vec<String> = queried_by_cli_ord_id
1870 .iter()
1871 .map(|order| truncate_cl_ord_id(&order.client_order_id))
1872 .collect();
1873
1874 log::debug!(
1875 "Resolving {} venue order ID(s) and {} client order ID(s) from the orders-status window",
1876 order_ids.len(),
1877 cli_ord_ids.len(),
1878 );
1879
1880 let mut reports = Vec::new();
1881 for chunk in order_ids.chunks(FUTURES_ORDERS_STATUS_LIMIT) {
1882 reports.extend(
1883 self.http
1884 .request_orders_status_reports(account_id, chunk, &[])
1885 .await?,
1886 );
1887 }
1888
1889 for chunk in cli_ord_ids.chunks(FUTURES_ORDERS_STATUS_LIMIT) {
1890 reports.extend(
1891 self.http
1892 .request_orders_status_reports(account_id, &[], chunk)
1893 .await?,
1894 );
1895 }
1896
1897 Ok(attribute_orders_status_reports(
1898 &self.core.cache(),
1899 &queried_by_cli_ord_id,
1900 latest_report_per_order(reports),
1901 ))
1902 }
1903}
1904
1905fn cached_order_for_report(cache: &Cache, report: &OrderStatusReport) -> Option<OrderAny> {
1907 let client_order_id = cache.client_order_id(&report.venue_order_id)?;
1908 cache.order(client_order_id).map(|order| order.clone())
1909}
1910
1911#[derive(Clone, Copy, Debug)]
1913struct HeldOrder {
1914 client_order_id: ClientOrderId,
1915 instrument_id: InstrumentId,
1916 side: OrderSide,
1917 ts_init: UnixNanos,
1918}
1919
1920fn latest_report_per_order(reports: Vec<OrderStatusReport>) -> Vec<OrderStatusReport> {
1927 let mut latest: IndexMap<VenueOrderId, OrderStatusReport> =
1928 IndexMap::with_capacity(reports.len());
1929
1930 for report in reports {
1931 match latest.get(&report.venue_order_id) {
1932 Some(existing) if !supersedes(&report, existing) => {}
1933 _ => {
1934 latest.insert(report.venue_order_id, report);
1935 }
1936 }
1937 }
1938
1939 latest.into_values().collect()
1940}
1941
1942fn attribute_orders_status_reports(
1956 cache: &Cache,
1957 queried_by_cli_ord_id: &[HeldOrder],
1958 reports: Vec<OrderStatusReport>,
1959) -> (Vec<OrderStatusReport>, bool) {
1960 let mut held: HashMap<String, Option<HeldOrder>> = HashMap::new();
1962
1963 for order in queried_by_cli_ord_id {
1964 held.entry(truncate_cl_ord_id(&order.client_order_id))
1965 .and_modify(|named| *named = None)
1966 .or_insert(Some(*order));
1967 }
1968
1969 let indexed =
1970 |report: &OrderStatusReport| cache.client_order_id(&report.venue_order_id).copied();
1971
1972 let mut unindexed: HashMap<ClientOrderId, HashSet<VenueOrderId>> = HashMap::new();
1973
1974 for report in &reports {
1975 if indexed(report).is_none()
1976 && let Some(echoed) = report.client_order_id
1977 {
1978 unindexed
1979 .entry(echoed)
1980 .or_default()
1981 .insert(report.venue_order_id);
1982 }
1983 }
1984
1985 let by_shortened_id = |report: &OrderStatusReport| -> Result<ClientOrderId, &'static str> {
1986 let echoed = report
1987 .client_order_id
1988 .ok_or("it carries no client order ID")?;
1989 if unindexed
1990 .get(&echoed)
1991 .is_none_or(|venue_order_ids| venue_order_ids.len() != 1)
1992 {
1993 return Err("several reports carry its client order ID");
1994 }
1995 let order = held
1996 .get(echoed.as_str())
1997 .ok_or("no order queried by client order ID holds its client order ID")?
1998 .ok_or("several queried orders hold its client order ID")?;
1999
2000 if report.instrument_id != order.instrument_id || report.order_side != Some(order.side) {
2001 return Err("its instrument or side is not the queried order's");
2002 }
2003
2004 if report.ts_accepted.saturating_add(ORDERS_STATUS_CLOCK_SKEW) < order.ts_init {
2005 return Err(
2006 "the venue placed it before the queried order's creation, beyond the clock skew \
2007 allowance; check the node clock if the order is recent",
2008 );
2009 }
2010
2011 Ok(order.client_order_id)
2012 };
2013
2014 let mut attributed = Vec::with_capacity(reports.len());
2015 let mut withheld = false;
2016
2017 for mut report in reports {
2018 let named = match indexed(&report) {
2019 Some(client_order_id) => Ok(client_order_id),
2020 None => by_shortened_id(&report),
2021 };
2022
2023 match named {
2024 Ok(client_order_id) => {
2025 report.client_order_id = Some(client_order_id);
2026 attributed.push(report);
2027 }
2028 Err(reason) => {
2029 log::warn!(
2030 "Withholding orders-status report {} with client order ID {:?}: {reason}",
2031 report.venue_order_id,
2032 report.client_order_id,
2033 );
2034 withheld = true;
2035 }
2036 }
2037 }
2038
2039 (attributed, withheld)
2040}
2041
2042fn is_refused_history_read(error: &anyhow::Error) -> bool {
2049 error
2050 .chain()
2051 .filter_map(|cause| cause.downcast_ref::<KrakenHttpError>())
2052 .any(|e| match e {
2053 KrakenHttpError::ApiError(_) => true,
2054 KrakenHttpError::NetworkError(message) => message.starts_with("HTTP error 429"),
2055 _ => false,
2056 })
2057}
2058
2059fn is_unpriced_terminal_report(report: &OrderStatusReport) -> bool {
2064 matches!(
2065 report.order_status,
2066 OrderStatus::Filled | OrderStatus::Canceled | OrderStatus::Expired | OrderStatus::Voided
2067 ) && !report.filled_qty.is_zero()
2068 && report.avg_px.is_none()
2069}
2070
2071fn price_from_fills(
2085 report: &mut OrderStatusReport,
2086 cached: Option<&OrderAny>,
2087 fills: &[FillReport],
2088) -> Result<(), Decimal> {
2089 let target = report.filled_qty.as_decimal();
2090 let order_fills: Vec<&FillReport> = fills
2091 .iter()
2092 .filter(|fill| fill.venue_order_id == report.venue_order_id)
2093 .collect();
2094 let total = |fills: &mut dyn Iterator<Item = &&FillReport>| {
2095 fills.fold((Decimal::ZERO, Decimal::ZERO), |(qty, notional), fill| {
2096 let fill_qty = fill.last_qty.as_decimal();
2097 (
2098 qty + fill_qty,
2099 notional + fill_qty * fill.last_px.as_decimal(),
2100 )
2101 })
2102 };
2103
2104 let (page_qty, page_notional) = total(&mut order_fills.iter());
2105
2106 if !target.is_zero() && page_qty == target {
2107 report.avg_px = Some(page_notional / target);
2108 return Ok(());
2109 }
2110
2111 let Some(order) = cached else {
2112 return Err(page_qty);
2113 };
2114 let recorded: HashSet<TradeId> = order.trade_ids().into_iter().copied().collect();
2115 let (new_qty, new_notional) = total(
2116 &mut order_fills
2117 .iter()
2118 .filter(|fill| !recorded.contains(&fill.trade_id)),
2119 );
2120 let cached_qty = order.filled_qty().as_decimal();
2121 let covered = cached_qty + new_qty;
2122
2123 if !target.is_zero() && covered == target {
2124 let cached_notional = order.avg_px().unwrap_or(Decimal::ZERO) * cached_qty;
2125 report.avg_px = Some((cached_notional + new_notional) / target);
2126 return Ok(());
2127 }
2128
2129 Err(covered.max(page_qty))
2130}
2131
2132fn synthesize_filled_order_status_report(
2133 cmd: &GenerateOrderStatusReport,
2134 order: &OrderAny,
2135 fills: &[FillReport],
2136) -> Option<OrderStatusReport> {
2137 let venue_order_id = cmd.venue_order_id.or(order.venue_order_id());
2138 let truncated_client_order_id = truncate_cl_ord_id(&order.client_order_id());
2139
2140 let mut matched: Vec<&FillReport> = if let Some(venue_order_id) = venue_order_id {
2141 fills
2142 .iter()
2143 .filter(|fill| fill.venue_order_id == venue_order_id)
2144 .collect()
2145 } else {
2146 Vec::new()
2147 };
2148
2149 if matched.is_empty() {
2150 matched = fills
2151 .iter()
2152 .filter(|fill| {
2153 fill.client_order_id == Some(order.client_order_id())
2154 || fill
2155 .client_order_id
2156 .as_ref()
2157 .is_some_and(|fill_client_order_id| {
2158 fill_client_order_id.as_str() == truncated_client_order_id
2159 })
2160 })
2161 .collect();
2162 }
2163
2164 if matched.is_empty() {
2165 return None;
2166 }
2167
2168 matched.sort_by_key(|fill| fill.ts_event);
2169 let first_fill = *matched.first()?;
2170 let last_fill = *matched.last()?;
2171
2172 let total_filled = matched
2173 .iter()
2174 .fold(Decimal::ZERO, |acc, fill| acc + fill.last_qty.as_decimal());
2175 if total_filled < order.quantity().as_decimal() {
2176 return None;
2177 }
2178
2179 let total_notional = matched.iter().fold(Decimal::ZERO, |acc, fill| {
2180 acc + fill.last_qty.as_decimal() * fill.last_px.as_decimal()
2181 });
2182 let avg_px = if total_filled.is_zero() {
2183 None
2184 } else {
2185 Some(total_notional / total_filled)
2186 };
2187 let venue_order_id = venue_order_id.unwrap_or(first_fill.venue_order_id);
2188
2189 let mut report = OrderStatusReport::new(
2190 first_fill.account_id,
2191 order.instrument_id(),
2192 Some(order.client_order_id()),
2193 venue_order_id,
2194 order.order_side().into(),
2195 order.order_type(),
2196 order.time_in_force(),
2197 OrderStatus::Filled,
2198 order.quantity(),
2199 order.quantity(),
2200 first_fill.ts_event,
2201 last_fill.ts_event,
2202 last_fill.ts_init,
2203 None,
2204 );
2205 report.order_list_id = order.order_list_id();
2206 report.venue_position_id = matched.iter().rev().find_map(|fill| fill.venue_position_id);
2207 report.linked_order_ids = order
2208 .linked_order_ids()
2209 .map(|linked_order_ids| linked_order_ids.to_vec());
2210 report.parent_order_id = order.parent_order_id();
2211 report.expire_time = order.expire_time();
2212 report.price = order.price();
2213 report.trigger_price = order.trigger_price();
2214 report.trigger_type = order.trigger_type();
2215 report.avg_px = avg_px;
2216 report.display_qty = order.display_qty();
2217 report.post_only = order.is_post_only();
2218 report.reduce_only = order.is_reduce_only();
2219 Some(report)
2220}
2221
2222#[cfg(test)]
2223mod tests {
2224 use nautilus_core::{UUID4, UnixNanos};
2225 use nautilus_model::{
2226 enums::{LiquiditySide, OrderSide, OrderType, TimeInForce},
2227 identifiers::{
2228 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
2229 },
2230 orders::OrderTestBuilder,
2231 reports::FillReport,
2232 types::{Currency, Money, Price, Quantity},
2233 };
2234 use rstest::rstest;
2235
2236 use super::*;
2237
2238 const TEST_INSTRUMENT_ID: &str = "PF_XBTUSD.KRAKEN";
2239
2240 #[tokio::test]
2241 async fn test_cancel_order_for_futures_missing_cached_instrument_returns_canonical_error() {
2242 let http = KrakenFuturesHttpClient::default();
2243 let instrument_id = InstrumentId::from(TEST_INSTRUMENT_ID);
2244 let client_order_id = ClientOrderId::from("C-001");
2245 let venue_order_id = VenueOrderId::from("V-001");
2246
2247 let result = cancel_order_for_futures(
2248 &http,
2249 AccountId::from("KRAKEN-001"),
2250 instrument_id,
2251 Some(client_order_id),
2252 Some(venue_order_id),
2253 )
2254 .await;
2255
2256 match result {
2257 Err(CommandFailure::NotSent(reason)) => {
2258 assert_eq!(
2259 reason,
2260 InstrumentLookupError::not_found(instrument_id).to_string()
2261 );
2262 }
2263 _ => panic!("Expected local validation failure"),
2264 }
2265 }
2266
2267 #[rstest]
2268 fn test_batch_cancel_item_for_futures_missing_cached_instrument_returns_canonical_error() {
2269 let http = KrakenFuturesHttpClient::default();
2270 let instrument_id = InstrumentId::from(TEST_INSTRUMENT_ID);
2271 let cancel = CancelOrder::new(
2272 TraderId::from("TESTER-001"),
2273 None,
2274 StrategyId::from("S-001"),
2275 instrument_id,
2276 ClientOrderId::from("C-001"),
2277 Some(VenueOrderId::from("V-001")),
2278 UUID4::new(),
2279 UnixNanos::default(),
2280 None,
2281 None,
2282 );
2283
2284 let result = batch_cancel_item_for_futures(&http, &cancel);
2285
2286 match result {
2287 Err(CommandFailure::NotSent(reason)) => {
2288 assert_eq!(
2289 reason,
2290 InstrumentLookupError::not_found(instrument_id).to_string()
2291 );
2292 }
2293 _ => panic!("Expected local validation failure"),
2294 }
2295 }
2296
2297 fn make_fill(
2298 venue_order_id: &str,
2299 client_order_id: Option<&str>,
2300 quantity: &str,
2301 price: &str,
2302 ts_event: u64,
2303 ) -> FillReport {
2304 FillReport::new(
2305 AccountId::from("KRAKEN-001"),
2306 InstrumentId::from(TEST_INSTRUMENT_ID),
2307 VenueOrderId::from(venue_order_id),
2308 TradeId::from(format!("T-{ts_event}").as_str()),
2309 OrderSide::Buy,
2310 Quantity::from(quantity),
2311 Price::from(price),
2312 Money::from_decimal(Decimal::ZERO, Currency::USD()).unwrap(),
2313 LiquiditySide::Taker,
2314 client_order_id.map(ClientOrderId::from),
2315 None,
2316 UnixNanos::from(ts_event),
2317 UnixNanos::from(ts_event),
2318 None,
2319 )
2320 }
2321
2322 fn make_cmd(
2323 client_order_id: Option<&str>,
2324 venue_order_id: Option<&str>,
2325 ) -> GenerateOrderStatusReport {
2326 GenerateOrderStatusReport::new(
2327 UUID4::new(),
2328 UnixNanos::default(),
2329 Some(InstrumentId::from(TEST_INSTRUMENT_ID)),
2330 client_order_id.map(ClientOrderId::from),
2331 venue_order_id.map(VenueOrderId::from),
2332 None,
2333 None,
2334 )
2335 }
2336
2337 fn make_order(client_order_id: &str) -> OrderAny {
2338 OrderTestBuilder::new(OrderType::Market)
2339 .instrument_id(InstrumentId::from(TEST_INSTRUMENT_ID))
2340 .client_order_id(ClientOrderId::from(client_order_id))
2341 .side(OrderSide::Buy)
2342 .quantity(Quantity::from("100"))
2343 .time_in_force(TimeInForce::Ioc)
2344 .build()
2345 }
2346
2347 #[rstest]
2348 fn test_synthesize_filled_order_status_report_matches_full_fill_by_venue_order_id() {
2349 let order = make_order("O-123456");
2350 let cmd = make_cmd(Some("O-123456"), Some("KRAKEN-789"));
2351 let fills = vec![
2352 make_fill("KRAKEN-789", Some("O-123456"), "40", "50000.0", 1),
2353 make_fill("KRAKEN-789", Some("O-123456"), "60", "50010.0", 2),
2354 make_fill("KRAKEN-OTHER", Some("O-123456"), "999", "1.0", 3),
2355 ];
2356
2357 let report = synthesize_filled_order_status_report(&cmd, &order, &fills)
2358 .expect("expected a filled report");
2359
2360 assert_eq!(report.venue_order_id, VenueOrderId::from("KRAKEN-789"));
2361 assert_eq!(
2362 report.client_order_id,
2363 Some(ClientOrderId::from("O-123456"))
2364 );
2365 assert_eq!(report.order_status, OrderStatus::Filled);
2366 assert_eq!(report.order_type, OrderType::Market);
2367 assert_eq!(report.time_in_force, TimeInForce::Ioc);
2368 assert_eq!(report.quantity, Quantity::from("100"));
2369 assert_eq!(report.filled_qty, Quantity::from("100"));
2370 assert_eq!(
2371 report.avg_px,
2372 Some(Decimal::from_str_exact("50006.0").unwrap())
2373 );
2374 }
2375
2376 #[rstest]
2377 fn test_synthesize_filled_order_status_report_requires_full_fill_size() {
2378 let order = make_order("O-123457");
2379 let cmd = make_cmd(Some("O-123457"), Some("KRAKEN-790"));
2380 let fills = vec![make_fill(
2381 "KRAKEN-790",
2382 Some("O-123457"),
2383 "40",
2384 "50000.0",
2385 1,
2386 )];
2387
2388 assert!(synthesize_filled_order_status_report(&cmd, &order, &fills).is_none());
2389 }
2390
2391 #[rstest]
2392 fn test_synthesize_filled_order_status_report_matches_truncated_client_order_id() {
2393 let long_client_order_id = "O202602270023210040011";
2394 let order = make_order(long_client_order_id);
2395 let cmd = make_cmd(Some(long_client_order_id), None);
2396 let fills = vec![make_fill(
2397 "KRAKEN-791",
2398 Some(truncate_cl_ord_id(&ClientOrderId::from(long_client_order_id)).as_str()),
2399 "100",
2400 "50000.0",
2401 1,
2402 )];
2403
2404 let report = synthesize_filled_order_status_report(&cmd, &order, &fills)
2405 .expect("expected a filled report");
2406
2407 assert_eq!(
2408 report.client_order_id,
2409 Some(ClientOrderId::from(long_client_order_id))
2410 );
2411 assert_eq!(report.venue_order_id, VenueOrderId::from("KRAKEN-791"));
2412 assert_eq!(report.order_status, OrderStatus::Filled);
2413 }
2414}