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 events::OrderDeniedReason,
51 identifiers::{
52 AccountId, ClientId, ClientOrderId, InstrumentId, StrategyId, TradeId, Venue, VenueOrderId,
53 },
54 instruments::{Instrument, InstrumentAny},
55 orders::{Order, OrderAny},
56 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
57 types::{AccountBalance, MarginBalance, Quantity},
58};
59use rust_decimal::Decimal;
60use tokio_util::sync::CancellationToken;
61
62use super::{
63 absent_unless_unread, cached_order_for_status_command, command_failure_from_cancel_error,
64 command_failure_from_futures_batch_error, command_failure_from_futures_batch_item,
65 command_failure_from_modify_error, command_failure_from_submit_error, earliest_venue_time,
66 status_command_label,
67};
68use crate::{
69 common::{
70 consts::{KRAKEN_FUTURES_MAX_CLIENT_ORDER_ID_LEN, KRAKEN_VENUE},
71 credential::KrakenCredential,
72 enums::{KrakenApiResult, KrakenProductType, KrakenSendStatus, product_type_from_symbol},
73 parse::truncate_cl_ord_id,
74 },
75 config::KrakenExecutionClientConfig,
76 http::{
77 KrakenFuturesHttpClient, KrakenHttpError,
78 futures::{
79 client::{KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND, order_identifier, supersedes},
80 models::FuturesBatchCancelStatus,
81 query::KrakenFuturesBatchCancelItem,
82 },
83 },
84 websocket::{
85 dispatch::{self, OrderIdentity, WsDispatchState},
86 futures::{client::KrakenFuturesWebSocketClient, messages::KrakenFuturesWsMessage},
87 },
88};
89
90const FUTURES_BATCH_CANCEL_LIMIT: usize = 50;
91
92const FUTURES_ORDERS_STATUS_LIMIT: usize = 50;
94
95const ORDERS_STATUS_CLOCK_SKEW: DurationNanos = DurationNanos::from_secs(5);
99
100#[allow(dead_code)]
105#[derive(Debug)]
106pub struct KrakenFuturesExecutionClient {
107 core: ExecutionClientCore,
108 clock: &'static AtomicTime,
109 config: KrakenExecutionClientConfig,
110 emitter: ExecutionEventEmitter,
111 http: KrakenFuturesHttpClient,
112 ws: KrakenFuturesWebSocketClient,
113 cancellation_token: CancellationToken,
114 session_tasks: TaskGroup,
115 pending_tasks: TaskGroup,
116 instruments: Arc<AtomicMap<InstrumentId, InstrumentAny>>,
117 truncated_id_map: Arc<AtomicMap<String, ClientOrderId>>,
118 order_instrument_map: Arc<AtomicMap<String, InstrumentId>>,
119 venue_client_map: Arc<AtomicMap<String, ClientOrderId>>,
120 venue_order_qty: Arc<AtomicMap<String, Quantity>>,
121 ws_dispatch_state: Arc<WsDispatchState>,
122}
123
124impl KrakenFuturesExecutionClient {
125 pub fn new(
131 core: ExecutionClientCore,
132 config: KrakenExecutionClientConfig,
133 ) -> anyhow::Result<Self> {
134 config.validate()?;
135
136 let clock = get_atomic_clock_realtime();
137 let emitter = ExecutionEventEmitter::new(
138 clock,
139 core.trader_id,
140 core.account_id,
141 AccountType::Margin,
142 None,
143 );
144
145 let session_tasks = TaskGroup::new();
146 let cancellation_token = session_tasks.cancellation_token();
147 let pending_tasks = TaskGroup::new();
148 let api_key = config.api_key.expose_secret().to_owned();
149 let api_secret = config.api_secret.expose_secret().to_owned();
150 let proxy_url = config
151 .proxy_url
152 .as_ref()
153 .map(|value| value.expose_secret().to_owned());
154
155 let http = KrakenFuturesHttpClient::with_credentials(
156 api_key.clone(),
157 api_secret.clone(),
158 config.environment,
159 config.base_url.clone(),
160 config.timeout_secs,
161 Some(config.max_retries),
162 None,
163 None,
164 proxy_url.clone(),
165 config
166 .max_requests_per_second
167 .unwrap_or(KRAKEN_FUTURES_DEFAULT_RATE_LIMIT_PER_SECOND),
168 )?;
169
170 let credential = KrakenCredential::new(api_key, api_secret);
171 let ws = KrakenFuturesWebSocketClient::with_credentials(
172 config.ws_url(),
173 config.heartbeat_interval_secs,
174 Some(credential),
175 config.auth_timeout_secs,
176 config.transport_backend,
177 proxy_url,
178 )
179 .with_socket_control(SocketControl::new(
180 core.client_id,
181 Some(*KRAKEN_VENUE),
182 "kraken-futures-user-streams",
183 ));
184
185 Ok(Self {
186 core,
187 clock,
188 config,
189 emitter,
190 http,
191 ws,
192 cancellation_token,
193 session_tasks,
194 pending_tasks,
195 instruments: Arc::new(AtomicMap::new()),
196 truncated_id_map: Arc::new(AtomicMap::new()),
197 order_instrument_map: Arc::new(AtomicMap::new()),
198 venue_client_map: Arc::new(AtomicMap::new()),
199 venue_order_qty: Arc::new(AtomicMap::new()),
200 ws_dispatch_state: Arc::new(WsDispatchState::new()),
201 })
202 }
203
204 fn register_order_identity(&self, order: &OrderAny) {
205 self.ws_dispatch_state.register_identity(
206 order.client_order_id(),
207 OrderIdentity {
208 strategy_id: order.strategy_id(),
209 instrument_id: order.instrument_id(),
210 order_side: order.order_side(),
211 order_type: order.order_type(),
212 quantity: order.quantity(),
213 },
214 );
215 }
216
217 #[must_use]
219 pub fn clock(&self) -> &'static AtomicTime {
220 self.clock
221 }
222
223 #[must_use]
225 pub fn emitter(&self) -> &ExecutionEventEmitter {
226 &self.emitter
227 }
228
229 fn spawn_task<F>(&self, description: &'static str, fut: F)
230 where
231 F: Future<Output = anyhow::Result<()>> + Send + 'static,
232 {
233 let future = async move {
234 if let Err(e) = fut.await {
235 log::warn!("{description} failed: {e:?}");
236 }
237 };
238
239 if let Err(e) = self.pending_tasks.spawn(future) {
240 log::warn!("Skipping Kraken Futures {description} after shutdown began: {e}");
241 }
242 }
243
244 async fn finish_tasks(&self) -> anyhow::Result<()> {
245 let (session_result, pending_result) = tokio::join!(
246 self.session_tasks
247 .finish_shutdown(Duration::from_secs(1), Duration::from_secs(2)),
248 self.pending_tasks
249 .finish_shutdown(Duration::from_secs(2), Duration::from_secs(2)),
250 );
251 session_result.context("failed to finish Kraken Futures execution session tasks")?;
252 pending_result.context("failed to finish Kraken Futures execution command tasks")?;
253 Ok(())
254 }
255
256 async fn prepare_task_groups(&mut self) -> anyhow::Result<()> {
257 if !self.session_tasks.is_open() || !self.pending_tasks.is_open() {
258 self.session_tasks.begin_shutdown();
259 self.pending_tasks.begin_shutdown();
260 self.finish_tasks().await?;
261 self.session_tasks
262 .start_generation()
263 .context("failed to start Kraken Futures execution session task generation")?;
264 self.pending_tasks
265 .start_generation()
266 .context("failed to start Kraken Futures execution command task generation")?;
267 self.cancellation_token = self.session_tasks.cancellation_token();
268 }
269 Ok(())
270 }
271
272 async fn teardown_partial_connect(&mut self) -> anyhow::Result<()> {
273 self.http.cancel_all_requests();
274 self.pending_tasks.begin_shutdown();
275 let ws_result = self.ws.close().await;
276 self.session_tasks.begin_shutdown();
277 let tasks_result = self.finish_tasks().await;
278 self.core.set_disconnected();
279 tasks_result?;
280 Ok(ws_result?)
281 }
282
283 fn submit_single_order(&self, order: &OrderAny, task_name: &'static str) {
284 if order.is_closed() {
285 log::warn!(
286 "Cannot submit closed order: client_order_id={}",
287 order.client_order_id()
288 );
289 return;
290 }
291
292 if let Err(reason) = validate_order(order) {
293 self.emitter.emit_order_denied(order, &reason.to_string());
294 return;
295 }
296
297 let account_id = self.core.account_id;
298 let client_order_id = order.client_order_id();
299 let strategy_id = order.strategy_id();
300 let instrument_id = order.instrument_id();
301 let order_side = order.order_side();
302 let order_type = order.order_type();
303 let quantity = order.quantity();
304 let time_in_force = order.time_in_force();
305 let price = order.price();
306 let trigger_price = order.trigger_price();
307 let trigger_type = order.trigger_type();
308 let is_reduce_only = order.is_reduce_only();
309 let is_post_only = order.is_post_only();
310
311 log::debug!("OrderSubmitted: client_order_id={client_order_id}");
312 self.register_order_identity(order);
313 self.emitter.emit_order_submitted(order);
314
315 let http = self.http.clone();
316 let emitter = self.emitter.clone();
317 let clock = self.clock;
318 let dispatch_state = self.ws_dispatch_state.clone();
319
320 self.spawn_task(task_name, async move {
321 let result = http
322 .submit_order(
323 account_id,
324 instrument_id,
325 client_order_id,
326 order_side,
327 order_type,
328 quantity,
329 time_in_force,
330 price,
331 trigger_price,
332 trigger_type,
333 is_reduce_only,
334 is_post_only,
335 )
336 .await;
337
338 match result {
339 Ok(_) => {}
340 Err(e) => match command_failure_from_submit_error(&e) {
341 CommandFailure::Ambiguous(reason) => {
342 log::warn!(
343 "{task_name} outcome is ambiguous for client_order_id={client_order_id}: {reason}"
344 );
345 }
346 CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason) => {
347 let ts_event = clock.get_time_ns();
348 let error_msg = format!("{task_name} error: {reason}");
349 let due_post_only = error_msg.contains("POST_ONLY_REJECTED");
350 dispatch_state.cleanup_terminal(&client_order_id);
351 emitter.emit_order_rejected_event(
352 strategy_id,
353 instrument_id,
354 client_order_id,
355 &error_msg,
356 ts_event,
357 due_post_only,
358 );
359 }
360 },
361 }
362 Ok(())
363 });
364 }
365
366 fn cancel_single_order(&self, cmd: &CancelOrder) {
367 let account_id = self.core.account_id;
368 let client_order_id = cmd.client_order_id;
369 let venue_order_id = cmd.venue_order_id;
370 let strategy_id = cmd.strategy_id;
371 let instrument_id = cmd.instrument_id;
372
373 log::debug!(
374 "Canceling order: venue_order_id={venue_order_id:?}, client_order_id={client_order_id}"
375 );
376
377 let http = self.http.clone();
378 let emitter = self.emitter.clone();
379 let clock = self.clock;
380
381 self.spawn_task("cancel_order", async move {
382 if let Err(failure) = cancel_order_for_futures(
383 &http,
384 account_id,
385 instrument_id,
386 Some(client_order_id),
387 venue_order_id,
388 )
389 .await
390 {
391 handle_cancel_failure(
392 &emitter,
393 clock,
394 strategy_id,
395 instrument_id,
396 client_order_id,
397 venue_order_id,
398 failure,
399 );
400 }
401 Ok(())
402 });
403 }
404
405 fn spawn_message_handler(&mut self) -> anyhow::Result<()> {
406 let mut rx = self
407 .ws
408 .take_output_rx()
409 .context("Failed to take futures WebSocket output receiver")?;
410 let emitter = self.emitter.clone();
411 let instruments = self.instruments.clone();
412 let truncated_id_map = self.truncated_id_map.clone();
413 let order_instrument_map = self.order_instrument_map.clone();
414 let venue_client_map = self.venue_client_map.clone();
415 let venue_order_qty = self.venue_order_qty.clone();
416 let dispatch_state = self.ws_dispatch_state.clone();
417 let account_id = self.core.account_id;
418 let clock = self.clock;
419 let cancellation_token = self.cancellation_token.clone();
420
421 let future = async move {
422 loop {
423 tokio::select! {
424 () = cancellation_token.cancelled() => {
425 log::debug!("Futures execution message handler cancelled");
426 break;
427 }
428 msg = rx.recv() => {
429 match msg {
430 Some(ws_msg) => {
431 Self::handle_ws_message(
432 ws_msg,
433 &emitter,
434 &dispatch_state,
435 &instruments,
436 &truncated_id_map,
437 &order_instrument_map,
438 &venue_client_map,
439 &venue_order_qty,
440 account_id,
441 clock,
442 );
443 }
444 None => {
445 log::debug!("Futures execution WebSocket stream ended");
446 break;
447 }
448 }
449 }
450 }
451 }
452 };
453
454 self.session_tasks
455 .spawn(future)
456 .context("failed to register Kraken Futures execution stream task")
457 }
458
459 #[expect(clippy::too_many_arguments)]
460 fn handle_ws_message(
461 msg: KrakenFuturesWsMessage,
462 emitter: &ExecutionEventEmitter,
463 dispatch_state: &Arc<WsDispatchState>,
464 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
465 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
466 order_instrument_map: &Arc<AtomicMap<String, InstrumentId>>,
467 venue_client_map: &Arc<AtomicMap<String, ClientOrderId>>,
468 venue_order_qty: &Arc<AtomicMap<String, Quantity>>,
469 account_id: AccountId,
470 clock: &'static AtomicTime,
471 ) {
472 let ts_init = clock.get_time_ns();
473
474 match msg {
475 KrakenFuturesWsMessage::OpenOrdersDelta(delta) => {
476 dispatch::futures::open_orders_delta(
477 &delta,
478 dispatch_state,
479 emitter,
480 instruments,
481 truncated_id_map,
482 order_instrument_map,
483 venue_client_map,
484 venue_order_qty,
485 account_id,
486 ts_init,
487 );
488 }
489 KrakenFuturesWsMessage::OpenOrdersCancel(cancel) => {
490 dispatch::futures::open_orders_cancel(
491 &cancel,
492 dispatch_state,
493 emitter,
494 truncated_id_map,
495 order_instrument_map,
496 venue_client_map,
497 venue_order_qty,
498 account_id,
499 ts_init,
500 );
501 }
502 KrakenFuturesWsMessage::FillsDelta(fills_delta) => {
503 dispatch::futures::fills_delta(
504 &fills_delta,
505 dispatch_state,
506 emitter,
507 instruments,
508 truncated_id_map,
509 venue_client_map,
510 account_id,
511 ts_init,
512 );
513 }
514 KrakenFuturesWsMessage::Challenge(challenge) => {
515 log::debug!("Received challenge: length={}", challenge.len());
516 }
517 KrakenFuturesWsMessage::Reconnected => {
518 log::info!("Futures execution WebSocket reconnected");
519 }
520 KrakenFuturesWsMessage::Ticker(_)
521 | KrakenFuturesWsMessage::Trade(_)
522 | KrakenFuturesWsMessage::BookSnapshot(_)
523 | KrakenFuturesWsMessage::BookDelta(_) => {}
524 }
525 }
526
527 async fn await_account_registered(&self, timeout_secs: f64) -> anyhow::Result<()> {
528 let account_id = self.core.account_id;
529
530 if self.core.cache().account(&account_id).is_some() {
531 log::info!("Account {account_id} registered");
532 return Ok(());
533 }
534
535 let start = Instant::now();
536 let timeout = Duration::from_secs_f64(timeout_secs);
537 let interval = Duration::from_millis(10);
538
539 loop {
540 tokio::time::sleep(interval).await;
541
542 if self.core.cache().account(&account_id).is_some() {
543 log::info!("Account {account_id} registered");
544 return Ok(());
545 }
546
547 if start.elapsed() >= timeout {
548 anyhow::bail!(
549 "Timeout waiting for account {account_id} to be registered after {timeout_secs}s"
550 );
551 }
552 }
553 }
554
555 fn modify_single_order(&self, cmd: &ModifyOrder) {
556 let client_order_id = cmd.client_order_id;
557 let venue_order_id = cmd.venue_order_id;
558 let strategy_id = cmd.strategy_id;
559 let instrument_id = cmd.instrument_id;
560 let quantity = cmd.quantity;
561 let price = cmd.price;
562
563 log::debug!(
564 "Modifying order: venue_order_id={venue_order_id:?}, client_order_id={client_order_id}"
565 );
566
567 let http = self.http.clone();
568 let emitter = self.emitter.clone();
569 let clock = self.clock;
570
571 self.spawn_task("modify_order", async move {
572 match http
573 .modify_order(
574 instrument_id,
575 Some(client_order_id),
576 venue_order_id,
577 quantity,
578 price,
579 None,
580 )
581 .await
582 {
583 Ok(_) => {}
584 Err(e) => match command_failure_from_modify_error(&e) {
585 CommandFailure::Ambiguous(reason) => {
586 log::warn!(
587 "modify_order outcome is ambiguous for client_order_id={client_order_id}: {reason}"
588 );
589 }
590 CommandFailure::NotSent(reason) | CommandFailure::VenueRejected(reason) => {
591 let ts_event = clock.get_time_ns();
592 emitter.emit_order_modify_rejected_event(
593 strategy_id,
594 instrument_id,
595 client_order_id,
596 venue_order_id,
597 &format!("modify-order error: {reason}"),
598 ts_event,
599 );
600 }
601 },
602 }
603 Ok(())
604 });
605 }
606}
607
608#[async_trait(?Send)]
609impl ExecutionClient for KrakenFuturesExecutionClient {
610 fn is_connected(&self) -> bool {
611 self.core.is_connected()
612 }
613
614 fn client_id(&self) -> ClientId {
615 self.core.client_id
616 }
617
618 fn account_id(&self) -> AccountId {
619 self.core.account_id
620 }
621
622 fn venue(&self) -> Venue {
623 *KRAKEN_VENUE
624 }
625
626 fn oms_type(&self) -> OmsType {
627 self.core.oms_type
628 }
629
630 fn get_account(&self) -> Option<AccountAny> {
631 self.core.cache().account_owned(&self.core.account_id)
632 }
633
634 fn generate_account_state(
635 &self,
636 balances: Vec<AccountBalance>,
637 margins: Vec<MarginBalance>,
638 reported: bool,
639 ts_event: UnixNanos,
640 info: Option<Params>,
641 ) -> anyhow::Result<()> {
642 self.emitter
643 .emit_account_state(balances, margins, reported, ts_event, info);
644 Ok(())
645 }
646
647 fn start(&mut self) -> anyhow::Result<()> {
648 if self.core.is_started() {
649 return Ok(());
650 }
651
652 self.emitter.set_sender(get_exec_event_sender());
653 self.core.set_started();
654
655 log::info!(
656 "Started: client_id={}, account_id={}, product_type=Futures, environment={:?}",
657 self.core.client_id,
658 self.core.account_id,
659 self.config.environment
660 );
661 Ok(())
662 }
663
664 fn stop(&mut self) -> anyhow::Result<()> {
665 if self.core.is_stopped() {
666 return Ok(());
667 }
668
669 self.http.cancel_all_requests();
670 self.session_tasks.begin_shutdown();
671 self.pending_tasks.begin_shutdown();
672 self.ws.begin_shutdown();
673 self.core.set_stopped();
674 self.core.set_disconnected();
675 log::info!("Stopped: client_id={}", self.core.client_id);
676 Ok(())
677 }
678
679 async fn connect(&mut self) -> anyhow::Result<()> {
680 if self.core.is_connected() && self.session_tasks.is_open() && self.pending_tasks.is_open()
681 {
682 return Ok(());
683 }
684
685 self.http.reset_cancellation_token();
686 self.prepare_task_groups().await?;
687
688 if !self.core.instruments_initialized() {
689 let instruments = self
690 .http
691 .request_instruments()
692 .await
693 .context("Failed to load Kraken futures instruments")?;
694 log::debug!("Loaded {} Futures instruments", instruments.len());
695 self.http.cache_instruments(&instruments);
696 self.core.set_instruments_initialized();
697 }
698
699 self.instruments.rcu(|m| {
700 for instrument in self.http.instruments_cache.load().values() {
701 m.insert(instrument.id(), instrument.clone());
702 }
703 });
704
705 let session_result = async {
706 self.ws
707 .connect()
708 .await
709 .context("Failed to connect futures WebSocket")?;
710 self.ws
711 .wait_until_active(10.0)
712 .await
713 .context("Futures WebSocket failed to become active")?;
714
715 self.ws
716 .authenticate()
717 .await
718 .context("Failed to authenticate futures WebSocket")?;
719
720 let account_state = self
721 .http
722 .request_account_state(self.core.account_id)
723 .await
724 .context("Failed to request Kraken futures account state")?;
725
726 if !account_state.balances.is_empty() {
727 log::debug!(
728 "Received account state with {} balance(s)",
729 account_state.balances.len()
730 );
731 }
732 self.emitter.send_account_state(account_state);
733 self.await_account_registered(30.0).await?;
734
735 self.spawn_message_handler()?;
736
737 self.ws
738 .subscribe_executions()
739 .await
740 .context("Failed to subscribe to executions")?;
741
742 log::debug!("Futures WebSocket authenticated and subscribed to executions");
743
744 Ok::<(), anyhow::Error>(())
745 }
746 .await;
747
748 if let Err(e) = session_result {
749 if let Err(teardown_error) = self.teardown_partial_connect().await {
750 return Err(e.context(format!(
751 "Kraken Futures execution startup teardown failed: {teardown_error}"
752 )));
753 }
754 return Err(e);
755 }
756
757 self.core.set_connected();
758 log::info!("Connected: client_id={}", self.core.client_id);
759 Ok(())
760 }
761
762 async fn disconnect(&mut self) -> anyhow::Result<()> {
763 self.teardown_partial_connect().await?;
764 log::info!("Disconnected: client_id={}", self.core.client_id);
765 Ok(())
766 }
767
768 async fn generate_order_status_report(
769 &self,
770 cmd: &GenerateOrderStatusReport,
771 ) -> anyhow::Result<Option<OrderStatusReport>> {
772 log::debug!(
773 "Generating order status report: venue_order_id={:?}, client_order_id={:?}",
774 cmd.venue_order_id,
775 cmd.client_order_id
776 );
777
778 let account_id = self.core.account_id;
779 let cached = cached_order_for_status_command(&self.core.cache(), cmd);
780 let queried = QueriedOrder::new(cmd, cached.as_ref());
781
782 let (mut reports, coverage) = self
785 .http
786 .request_order_status_reports_checked(account_id, cmd.instrument_id, None, None, false)
787 .await?;
788 name_reports_by_cached_order(&self.core.cache(), &mut reports);
789
790 let matched = queried.find_report(&self.core.cache(), &reports)?;
791 let mut stray_rows = queried.has_stray_report(&self.core.cache(), &reports);
792
793 if let Some(mut report) = matched {
794 if is_unpriced_terminal_report(&report) {
798 let fills = self
799 .http
800 .request_fill_reports(account_id, Some(report.instrument_id), None, None)
801 .await;
802 let page = fills.as_deref().unwrap_or_default();
803
804 if let Err(covered) = price_from_fills(&mut report, cached.as_ref(), page) {
805 fills?;
806 anyhow::bail!(
807 "Order {} executed per the order history with fills covering {covered} \
808 of {}; deferring until the fills feed prices it",
809 report.venue_order_id,
810 report.filled_qty,
811 );
812 }
813 }
814
815 return Ok(Some(report));
816 }
817
818 let Some(order) = cached else {
821 if stray_rows {
822 anyhow::bail!(
823 "No report names order {:?}, but a venue row of another venue order carries \
824 its client order ID; deferring rather than reporting it missing",
825 queried.client_order_id,
826 );
827 }
828 return absent_unless_unread(
829 &coverage,
830 cmd.venue_order_id,
831 None,
832 &status_command_label(cmd, cmd.venue_order_id),
833 );
834 };
835
836 let order_ids: Vec<String> = queried
838 .venue_order_id
839 .map(|id| id.to_string())
840 .into_iter()
841 .collect();
842 let cli_ord_ids: Vec<String> = venue_client_order_ids(&order.client_order_id()).collect();
843
844 let mut recent_reports = self
845 .http
846 .request_orders_status_reports(account_id, &order_ids, &cli_ord_ids)
847 .await?;
848 name_reports_by_cached_order(&self.core.cache(), &mut recent_reports);
849
850 let matched_recent = queried.find_report(&self.core.cache(), &recent_reports)?;
851 stray_rows |= queried.has_stray_report(&self.core.cache(), &recent_reports);
852
853 if matched_recent
855 .as_ref()
856 .is_some_and(|report| report.order_status != OrderStatus::Filled)
857 {
858 return Ok(matched_recent);
859 }
860
861 let now = Timestamp::now();
862 let start = now - Duration::from_secs(5 * 60);
863 let mut fills = self
864 .http
865 .request_fill_reports(
866 account_id,
867 Some(order.instrument_id()),
868 Some(start),
869 Some(now),
870 )
871 .await?;
872 name_fills_by_cached_order(&self.core.cache(), &mut fills);
873
874 let synthesized =
875 synthesize_filled_order_status_report(&self.core.cache(), &queried, &order, &fills);
876
877 match (synthesized, matched_recent) {
878 (Some(report), _) => Ok(Some(report)),
879 (None, Some(_)) => anyhow::bail!(
881 "Order {} fully executed in the orders-status window without visible \
882 fills; deferring until the fills feed prices it",
883 order.client_order_id(),
884 ),
885 (None, None) if stray_rows || queried.has_stray_fill(&self.core.cache(), &fills) => {
886 anyhow::bail!(
887 "No report names order {}, yet a venue row or fill that may be its own carries \
888 its client order ID; deferring rather than reporting it missing",
889 order.client_order_id(),
890 )
891 }
892 (None, None) => absent_unless_unread(
893 &coverage,
894 cmd.venue_order_id.or(order.venue_order_id()),
895 Some(earliest_venue_time(&order)),
896 order.client_order_id().as_str(),
897 ),
898 }
899 }
900
901 async fn generate_order_status_reports(
902 &self,
903 cmd: &GenerateOrderStatusReports,
904 ) -> anyhow::Result<Vec<OrderStatusReport>> {
905 log::debug!(
906 "Generating order status reports: instrument_id={:?}, open_only={}",
907 cmd.instrument_id,
908 cmd.open_only
909 );
910
911 let account_id = self.core.account_id;
912 let start = cmd.start.map(Timestamp::from);
913 let end = cmd.end.map(Timestamp::from);
914 let mut reports = self
915 .http
916 .request_order_status_reports(account_id, cmd.instrument_id, start, end, cmd.open_only)
917 .await?;
918
919 if reports.iter().any(is_unpriced_terminal_report) {
924 let fills = match self
927 .http
928 .request_fill_reports(account_id, cmd.instrument_id, None, None)
929 .await
930 {
931 Ok(fills) => fills,
932 Err(e) => {
933 log::warn!("Failed to read fills to price executed history orders: {e}");
934 Vec::new()
935 }
936 };
937 let cache = self.core.cache();
938 reports.retain_mut(|report| {
939 if !is_unpriced_terminal_report(report) {
940 return true;
941 }
942
943 let cached = cached_order_for_report(&cache, report);
944
945 match price_from_fills(report, cached.as_ref(), &fills) {
946 Ok(()) => true,
947 Err(covered) => {
948 log::warn!(
949 "Withholding executed order {} from the bulk response: fills cover \
950 {covered} of {}",
951 report.venue_order_id,
952 report.filled_qty,
953 );
954 false
955 }
956 }
957 });
958 }
959
960 if cmd.open_only {
961 let (extension, _) = self
962 .reports_for_open_orders_absent_from_venue(account_id, cmd.instrument_id, &reports)
963 .await?;
964
965 let (window_only, window_stated) = fold_window_into_venue_read(&mut reports, extension);
966
967 reports.retain(|report| {
971 let withhold = window_stated.contains(&report.venue_order_id)
972 && report.order_status == OrderStatus::Filled;
973
974 if withhold {
975 log::warn!(
976 "Withholding fully executed order {} from the bulk response: the \
977 orders-status window carries no fill price",
978 report.venue_order_id,
979 );
980 }
981 !withhold
982 });
983
984 for report in window_only {
985 if report.order_status == OrderStatus::Filled {
986 log::warn!(
987 "Withholding fully executed order {} from the bulk response: the \
988 orders-status window carries no fill price",
989 report.venue_order_id,
990 );
991 continue;
992 }
993
994 reports.push(report);
995 }
996 }
997
998 name_reports_by_cached_order(&self.core.cache(), &mut reports);
999 Ok(reports)
1000 }
1001
1002 async fn generate_fill_reports(
1003 &self,
1004 cmd: GenerateFillReports,
1005 ) -> anyhow::Result<Vec<FillReport>> {
1006 log::debug!(
1007 "Generating fill reports: instrument_id={:?}",
1008 cmd.instrument_id
1009 );
1010
1011 let account_id = self.core.account_id;
1012 let start = cmd.start.map(Timestamp::from);
1013 let end = cmd.end.map(Timestamp::from);
1014 let mut reports = self
1015 .http
1016 .request_fill_reports(account_id, cmd.instrument_id, start, end)
1017 .await?;
1018
1019 if let Some(venue_order_id) = cmd.venue_order_id {
1020 reports.retain(|report| report.venue_order_id == venue_order_id);
1021 }
1022
1023 name_fills_by_cached_order(&self.core.cache(), &mut reports);
1024 Ok(reports)
1025 }
1026
1027 async fn generate_position_status_reports(
1028 &self,
1029 cmd: &GeneratePositionStatusReports,
1030 ) -> anyhow::Result<Vec<PositionStatusReport>> {
1031 log::debug!(
1032 "Generating position status reports: instrument_id={:?}",
1033 cmd.instrument_id
1034 );
1035
1036 let account_id = self.core.account_id;
1037 self.http
1038 .request_position_status_reports(account_id, cmd.instrument_id)
1039 .await
1040 }
1041
1042 async fn generate_mass_status(
1043 &self,
1044 lookback_mins: Option<u64>,
1045 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
1046 log::debug!("Generating mass status: lookback_mins={lookback_mins:?}");
1047
1048 let ts_init = self.clock.get_time_ns();
1049 let lookback_start = lookback_mins.map(|mins| {
1052 let nanos = mins.saturating_mul(60).saturating_mul(1_000_000_000);
1053 ts_init.saturating_sub(DurationNanos::new(nanos))
1054 });
1055 let start = lookback_start.map(Timestamp::from);
1056 let account_id = self.core.account_id;
1057
1058 let (mut order_reports, orders_complete) = match self
1064 .http
1065 .request_order_status_reports_checked(account_id, None, start, None, false)
1066 .await
1067 {
1068 Ok((reports, coverage)) => (reports, coverage.is_complete()),
1069 Err(e) if is_refused_history_read(&e) => {
1070 log::warn!(
1071 "Order history read refused for mass status, reading open orders only and \
1072 marking the set incomplete: {e}"
1073 );
1074 let (reports, _) = self
1075 .http
1076 .request_order_status_reports_checked(account_id, None, start, None, true)
1077 .await?;
1078 (reports, false)
1079 }
1080 Err(e) => return Err(e),
1081 };
1082 let mut from_venue_read: HashSet<VenueOrderId> = order_reports
1085 .iter()
1086 .map(|report| report.venue_order_id)
1087 .collect();
1088
1089 let (extension, mut extension_withheld) = self
1090 .reports_for_open_orders_absent_from_venue(account_id, None, &order_reports)
1091 .await?;
1092
1093 let (window_only, window_stated) =
1094 fold_window_into_venue_read(&mut order_reports, extension);
1095
1096 order_reports.retain(|report| {
1101 let withhold = window_stated.contains(&report.venue_order_id)
1102 && report.order_status == OrderStatus::Filled;
1103
1104 if withhold {
1105 log::warn!(
1106 "Withholding fully executed order {} from mass status: the orders-status \
1107 window carries no fill price",
1108 report.venue_order_id,
1109 );
1110 extension_withheld = true;
1111 }
1112 !withhold
1113 });
1114
1115 for venue_order_id in &window_stated {
1116 from_venue_read.remove(venue_order_id);
1117 }
1118
1119 for report in window_only {
1120 if report.order_status == OrderStatus::Filled {
1121 log::warn!(
1122 "Withholding fully executed order {} from mass status: the orders-status \
1123 window carries no fill price",
1124 report.venue_order_id,
1125 );
1126 extension_withheld = true;
1127 continue;
1128 }
1129
1130 order_reports.push(report);
1131 }
1132
1133 let orders_complete = orders_complete && !extension_withheld;
1134
1135 let (mut fill_reports, fills_complete) = self
1136 .http
1137 .request_fill_reports_checked(account_id, None, start, None)
1138 .await?;
1139
1140 let mut withheld = false;
1152 let mut withheld_fills: HashSet<VenueOrderId> = HashSet::new();
1153 {
1154 let cache = self.core.cache();
1155 order_reports.retain_mut(|report| {
1156 if !from_venue_read.contains(&report.venue_order_id)
1157 || !is_unpriced_terminal_report(report)
1158 {
1159 return true;
1160 }
1161
1162 let cached = cached_order_for_report(&cache, report);
1163
1164 match price_from_fills(report, cached.as_ref(), &fill_reports) {
1165 Ok(()) => true,
1166 Err(covered) => {
1167 log::warn!(
1168 "Withholding executed order {} from mass status: fills cover \
1169 {covered} of {}; {}",
1170 report.venue_order_id,
1171 report.filled_qty,
1172 if cached.is_some() {
1173 "its page fills stay for the cached order"
1174 } else {
1175 "its page fills are withheld with it"
1176 },
1177 );
1178 withheld = true;
1179
1180 if cached.is_none() {
1181 withheld_fills.insert(report.venue_order_id);
1182 }
1183 false
1184 }
1185 }
1186 });
1187 }
1188 fill_reports.retain(|fill| !withheld_fills.contains(&fill.venue_order_id));
1189 let orders_complete = orders_complete && !withheld;
1190
1191 let position_reports = self
1192 .http
1193 .request_position_status_reports(account_id, None)
1194 .await?;
1195
1196 let orders_complete = if lookback_start.is_none() {
1211 let held: HashSet<InstrumentId> = position_reports
1212 .iter()
1213 .map(|report| report.instrument_id)
1214 .collect();
1215 let netted: HashSet<VenueOrderId> = {
1216 let cache = self.core.cache();
1217 order_reports
1218 .iter()
1219 .filter(|report| {
1220 from_venue_read.contains(&report.venue_order_id)
1221 && !held.contains(&report.instrument_id)
1222 && cached_order_for_report(&cache, report).is_none()
1223 })
1224 .map(|report| report.venue_order_id)
1225 .collect()
1226 };
1227 let candidates: HashMap<VenueOrderId, InstrumentId> = order_reports
1228 .iter()
1229 .filter(|report| {
1230 netted.contains(&report.venue_order_id)
1231 && matches!(
1232 report.order_status,
1233 OrderStatus::Filled
1234 | OrderStatus::Canceled
1235 | OrderStatus::Expired
1236 | OrderStatus::Voided
1237 )
1238 })
1239 .map(|report| (report.venue_order_id, report.instrument_id))
1240 .collect();
1241
1242 let mut net: HashMap<InstrumentId, Decimal> = HashMap::new();
1243
1244 for fill in fill_reports
1245 .iter()
1246 .filter(|fill| netted.contains(&fill.venue_order_id))
1247 {
1248 let qty = fill.last_qty.as_decimal();
1249 let signed = match fill.order_side {
1250 OrderSide::Buy => qty,
1251 OrderSide::Sell => -qty,
1252 };
1253 *net.entry(fill.instrument_id).or_default() += signed;
1254 }
1255 net.retain(|instrument_id, net| {
1256 !net.is_zero() && candidates.values().any(|id| id == instrument_id)
1257 });
1258
1259 if net.is_empty() {
1260 orders_complete
1261 } else {
1262 for (instrument_id, net) in &net {
1263 log::warn!(
1264 "Withholding terminal orders on {instrument_id} from mass status: the venue \
1265 reports it flat but the fills of orders the cache does not hold net to \
1266 {net}, so reconciliation would open a position; marking the set incomplete",
1267 );
1268 }
1269 let withheld_flat: HashSet<VenueOrderId> = candidates
1270 .iter()
1271 .filter(|(_, instrument_id)| net.contains_key(instrument_id))
1272 .map(|(venue_order_id, _)| *venue_order_id)
1273 .collect();
1274 order_reports.retain(|report| !withheld_flat.contains(&report.venue_order_id));
1275 fill_reports.retain(|fill| !withheld_flat.contains(&fill.venue_order_id));
1276 false
1277 }
1278 } else {
1279 orders_complete
1280 };
1281
1282 let mut mass_status = ExecutionMassStatus::new(
1283 self.core.client_id,
1284 self.core.account_id,
1285 *KRAKEN_VENUE,
1286 ts_init,
1287 None,
1288 );
1289 {
1290 let cache = self.core.cache();
1291 name_reports_by_cached_order(&cache, &mut order_reports);
1292 name_fills_by_cached_order(&cache, &mut fill_reports);
1293 }
1294 mass_status.add_order_reports(order_reports);
1295 mass_status.add_fill_reports(fill_reports);
1296 mass_status.add_position_reports(position_reports);
1297 mass_status.set_report_window(lookback_start, orders_complete && fills_complete);
1299
1300 Ok(Some(mass_status))
1301 }
1302
1303 fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
1304 log::debug!("Querying account: {cmd}");
1305
1306 let account_id = self.core.account_id;
1307 let http = self.http.clone();
1308 let emitter = self.emitter.clone();
1309
1310 self.spawn_task("query_account", async move {
1311 let account_state = http.request_account_state(account_id).await?;
1312 emitter.emit_account_state(
1313 account_state.balances.clone(),
1314 account_state.margins.clone(),
1315 account_state.is_reported,
1316 account_state.ts_event,
1317 account_state.info,
1318 );
1319 Ok(())
1320 });
1321
1322 Ok(())
1323 }
1324
1325 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
1326 log::debug!("Querying order: {cmd}");
1327
1328 let venue_order_id = cmd
1329 .venue_order_id
1330 .context("venue_order_id required for query_order")?;
1331 let cached_client_order_id = self.core.cache().client_order_id(&venue_order_id).copied();
1333 let account_id = self.core.account_id;
1334 let instrument_id = cmd.instrument_id;
1335 let http = self.http.clone();
1336 let emitter = self.emitter.clone();
1337
1338 self.spawn_task("query_order", async move {
1339 let reports = http
1342 .request_order_status_reports(account_id, Some(instrument_id), None, None, true)
1343 .await
1344 .context("Failed to query order")?;
1345
1346 match reports
1347 .into_iter()
1348 .find(|r| r.venue_order_id == venue_order_id)
1349 {
1350 Some(mut report) => {
1351 if let Some(client_order_id) = cached_client_order_id {
1352 report.client_order_id = Some(client_order_id);
1353 }
1354 emitter.send_order_status_report(report);
1355 }
1356 None => log::warn!(
1357 "No open order {venue_order_id} found for {instrument_id}; no report emitted"
1358 ),
1359 }
1360 Ok(())
1361 });
1362
1363 Ok(())
1364 }
1365
1366 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
1367 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
1368 self.submit_single_order(&order, "submit_order");
1369 Ok(())
1370 }
1371
1372 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
1373 let orders = self.core.get_orders_for_list(&cmd.order_list)?;
1374
1375 log::debug!(
1376 "Submitting order list: order_list_id={}, count={}",
1377 cmd.order_list.id,
1378 orders.len()
1379 );
1380
1381 if let Some(reason) = orders
1383 .iter()
1384 .filter(|order| !order.is_closed())
1385 .find_map(|order| validate_order(order).err())
1386 {
1387 for order in orders.iter().filter(|order| !order.is_closed()) {
1388 self.emitter.emit_order_denied(order, &reason.to_string());
1389 }
1390 return Ok(());
1391 }
1392
1393 let mut order_tuples = Vec::with_capacity(orders.len());
1394 let mut order_meta = Vec::with_capacity(orders.len());
1395
1396 for order in &orders {
1397 if order.is_closed() {
1398 log::warn!(
1399 "Cannot submit closed order: client_order_id={}",
1400 order.client_order_id()
1401 );
1402 continue;
1403 }
1404
1405 if order.order_type() == OrderType::Market {
1408 self.submit_single_order(order, "submit_order_list");
1409 continue;
1410 }
1411
1412 let client_order_id = order.client_order_id();
1413 self.register_order_identity(order);
1414 self.emitter.emit_order_submitted(order);
1415
1416 order_tuples.push((
1417 order.instrument_id(),
1418 client_order_id,
1419 order.order_side(),
1420 order.order_type(),
1421 order.quantity(),
1422 order.time_in_force(),
1423 order.price(),
1424 order.trigger_price(),
1425 order.trigger_type(),
1426 order.is_reduce_only(),
1427 order.is_post_only(),
1428 ));
1429
1430 order_meta.push((order.strategy_id(), order.instrument_id(), client_order_id));
1431 }
1432
1433 if order_tuples.is_empty() {
1434 return Ok(());
1435 }
1436
1437 let http = self.http.clone();
1438 let emitter = self.emitter.clone();
1439 let clock = self.clock;
1440 let dispatch_state = self.ws_dispatch_state.clone();
1441
1442 self.spawn_task("submit_order_list", async move {
1443 let results = http.send_order_batches(order_tuples).await;
1444 for (result, (strategy_id, instrument_id, client_order_id)) in
1445 results.into_iter().zip(&order_meta)
1446 {
1447 let outcome = match result {
1448 Ok(item) => command_failure_from_futures_batch_item(item),
1449 Err(e) => Err(command_failure_from_futures_batch_error(&e)),
1450 };
1451
1452 match outcome {
1453 Ok(()) => {}
1454 Err(CommandFailure::Ambiguous(reason)) => {
1455 log::warn!(
1456 "submit_order_list outcome is ambiguous for client_order_id={client_order_id}: {reason}"
1457 );
1458 }
1459 Err(
1460 CommandFailure::NotSent(reason)
1461 | CommandFailure::VenueRejected(reason),
1462 ) => {
1463 let ts_event = clock.get_time_ns();
1464 let error_msg =
1465 format!("submit_order_list batch item rejected: {reason}");
1466 dispatch_state.cleanup_terminal(client_order_id);
1467 emitter.emit_order_rejected_event(
1468 *strategy_id,
1469 *instrument_id,
1470 *client_order_id,
1471 &error_msg,
1472 ts_event,
1473 reason == "postWouldExecute",
1474 );
1475 }
1476 }
1477 }
1478 Ok(())
1479 });
1480
1481 Ok(())
1482 }
1483
1484 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
1485 self.modify_single_order(&cmd);
1486 Ok(())
1487 }
1488
1489 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
1490 self.cancel_single_order(&cmd);
1491 Ok(())
1492 }
1493
1494 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
1495 let instrument_id = cmd.instrument_id;
1496
1497 if cmd.order_side.is_none() {
1498 log::debug!("Canceling all orders: instrument_id={instrument_id} (bulk)");
1499
1500 let http = self.http.clone();
1501 let symbol = instrument_id.symbol.to_string();
1502
1503 self.spawn_task("cancel_all_orders", async move {
1504 match http.inner.cancel_all_orders(Some(symbol)).await {
1505 Ok(response) => {
1506 if response.result != KrakenApiResult::Success
1507 && response.cancel_status.cancelled_orders.is_empty()
1508 {
1509 log::warn!(
1510 "Cancel-all failed without per-order results, awaiting reconciliation: status={}",
1511 response.cancel_status.status
1512 );
1513 }
1514 }
1515 Err(e) => match command_failure_from_cancel_error(e) {
1516 CommandFailure::NotSent(reason) => {
1517 log::warn!("Cancel-all failed local validation: {reason}");
1518 }
1519 CommandFailure::Ambiguous(reason)
1520 | CommandFailure::VenueRejected(reason) => {
1521 log::warn!(
1522 "Cancel-all ambiguous failure, awaiting reconciliation: {reason}"
1523 );
1524 }
1525 },
1526 }
1527 Ok(())
1528 });
1529
1530 return Ok(());
1531 }
1532
1533 log::debug!(
1534 "Canceling all orders: instrument_id={instrument_id}, side={:?}",
1535 cmd.order_side
1536 );
1537
1538 let cancels: Vec<CancelOrder> = {
1541 let cache = self.core.cache();
1542 let ts_init = self.clock.get_time_ns();
1543 let correlation_id = cmd.correlation_id.or(Some(cmd.command_id));
1544
1545 let mut seen = HashSet::new();
1548
1549 cache
1550 .orders_open(None, Some(&instrument_id), None, None, None)
1551 .into_iter()
1552 .chain(cache.orders_inflight(None, Some(&instrument_id), None, None, None))
1553 .filter(|order| Some(order.order_side()) == cmd.order_side)
1554 .filter(|order| seen.insert(order.client_order_id()))
1555 .map(|order| {
1556 CancelOrder::new(
1557 cmd.trader_id,
1558 cmd.client_id,
1559 order.strategy_id(),
1561 order.instrument_id(),
1562 order.client_order_id(),
1563 order.venue_order_id(),
1564 UUID4::new(),
1565 ts_init,
1566 cmd.params.clone(),
1567 correlation_id,
1568 )
1569 })
1570 .collect()
1571 };
1572
1573 if cancels.is_empty() {
1574 log::debug!("No open orders to cancel for {instrument_id}");
1575 return Ok(());
1576 }
1577
1578 let http = self.http.clone();
1579 let emitter = self.emitter.clone();
1580 let clock = self.clock;
1581
1582 self.spawn_task("cancel_all_orders_by_side", async move {
1583 batch_cancel_orders_for_futures(&http, &emitter, clock, &cancels).await;
1584 Ok(())
1585 });
1586
1587 Ok(())
1588 }
1589
1590 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
1591 log::debug!(
1592 "Batch canceling orders: instrument_id={}, count={}",
1593 cmd.instrument_id,
1594 cmd.cancels.len()
1595 );
1596
1597 let http = self.http.clone();
1598 let emitter = self.emitter.clone();
1599 let clock = self.clock;
1600 let cancels = cmd.cancels;
1601
1602 self.spawn_task("batch_cancel_orders", async move {
1603 batch_cancel_orders_for_futures(&http, &emitter, clock, &cancels).await;
1604 Ok(())
1605 });
1606
1607 Ok(())
1608 }
1609}
1610
1611#[derive(Debug, Clone)]
1612struct CancelRequestContext {
1613 strategy_id: StrategyId,
1614 instrument_id: InstrumentId,
1615 client_order_id: ClientOrderId,
1616 cli_ord_id: String,
1619 venue_order_id: Option<VenueOrderId>,
1620}
1621
1622async fn cancel_order_for_futures(
1623 http: &KrakenFuturesHttpClient,
1624 _account_id: AccountId,
1625 instrument_id: InstrumentId,
1626 client_order_id: Option<ClientOrderId>,
1627 venue_order_id: Option<VenueOrderId>,
1628) -> Result<(), CommandFailure> {
1629 http.get_cached_instrument(&instrument_id.symbol.inner())
1630 .ok_or_else(|| {
1631 CommandFailure::not_sent(InstrumentLookupError::not_found(instrument_id).to_string())
1632 })?;
1633
1634 let (order_id, cli_ord_id) =
1635 order_identifier(client_order_id.as_ref(), venue_order_id.as_ref());
1636
1637 if order_id.is_none() && cli_ord_id.is_none() {
1638 return Err(CommandFailure::not_sent(
1639 "Either client_order_id or venue_order_id must be provided",
1640 ));
1641 }
1642
1643 let response = http
1644 .inner
1645 .cancel_order(order_id, cli_ord_id)
1646 .await
1647 .map_err(command_failure_from_cancel_error)?;
1648
1649 if response.result != KrakenApiResult::Success
1650 || response.cancel_status.status != KrakenSendStatus::Cancelled
1651 {
1652 return Err(CommandFailure::venue_rejected(format!(
1653 "cancel-order rejected: status={}",
1654 response.cancel_status.status
1655 )));
1656 }
1657
1658 Ok(())
1659}
1660
1661async fn batch_cancel_orders_for_futures(
1662 http: &KrakenFuturesHttpClient,
1663 emitter: &ExecutionEventEmitter,
1664 clock: &'static AtomicTime,
1665 cancels: &[CancelOrder],
1666) {
1667 let mut contexts = Vec::new();
1668 let mut items = Vec::new();
1669
1670 for cancel in cancels {
1671 match batch_cancel_item_for_futures(http, cancel) {
1672 Ok((context, item)) => {
1673 contexts.push(context);
1674 items.push(item);
1675 }
1676 Err(CommandFailure::NotSent(reason)) => {
1677 log::warn!(
1678 "Batch cancel command failed local validation for {}: {reason}",
1679 cancel.client_order_id
1680 );
1681 }
1682 Err(CommandFailure::Ambiguous(reason) | CommandFailure::VenueRejected(reason)) => {
1683 log::warn!(
1684 "Batch cancel command ambiguous failure for {}, awaiting reconciliation: {reason}",
1685 cancel.client_order_id
1686 );
1687 }
1688 }
1689 }
1690
1691 for (item_chunk, context_chunk) in items
1692 .chunks(FUTURES_BATCH_CANCEL_LIMIT)
1693 .zip(contexts.chunks(FUTURES_BATCH_CANCEL_LIMIT))
1694 {
1695 let response = match http
1696 .inner
1697 .cancel_order_items_batch(item_chunk.to_vec())
1698 .await
1699 {
1700 Ok(response) => response,
1701 Err(e) => {
1702 match command_failure_from_cancel_error(e) {
1703 CommandFailure::NotSent(reason) => {
1704 log::warn!("Batch cancel failed local validation: {reason}");
1705 }
1706 CommandFailure::Ambiguous(reason) | CommandFailure::VenueRejected(reason) => {
1707 log::warn!(
1708 "Batch cancel failed without per-order results, awaiting reconciliation: {reason}"
1709 );
1710 }
1711 }
1712 continue;
1713 }
1714 };
1715
1716 if response.batch_status.is_empty() {
1717 if response.result != KrakenApiResult::Success {
1718 let reason = response.error.as_deref().unwrap_or("Unknown error");
1719 log::warn!(
1720 "Batch cancel failed without per-order results, awaiting reconciliation: {reason}"
1721 );
1722 }
1723 continue;
1724 }
1725
1726 if response.batch_status.len() != context_chunk.len() {
1727 log::warn!(
1728 "Batch cancel returned {} per-order result(s) for {} request(s); unmatched results await reconciliation",
1729 response.batch_status.len(),
1730 context_chunk.len()
1731 );
1732 }
1733
1734 for (index, status) in response.batch_status.iter().enumerate() {
1735 let Some(cancel_status) = batch_cancel_status(status) else {
1736 log::warn!("Batch cancel result without status at index {index}");
1737 continue;
1738 };
1739
1740 if cancel_status == KrakenSendStatus::Cancelled {
1741 continue;
1742 }
1743
1744 let Some(context) = batch_cancel_context(status, context_chunk, index) else {
1745 log::warn!(
1746 "Batch cancel rejected item without matching request context at index {index}: status={cancel_status}"
1747 );
1748 continue;
1749 };
1750
1751 emitter.emit_order_cancel_rejected_event(
1752 context.strategy_id,
1753 context.instrument_id,
1754 context.client_order_id,
1755 context.venue_order_id,
1756 &format!("batch-cancel rejected: status={cancel_status}"),
1757 clock.get_time_ns(),
1758 );
1759 }
1760 }
1761}
1762
1763fn batch_cancel_item_for_futures(
1764 http: &KrakenFuturesHttpClient,
1765 cancel: &CancelOrder,
1766) -> Result<(CancelRequestContext, KrakenFuturesBatchCancelItem), CommandFailure> {
1767 http.get_cached_instrument(&cancel.instrument_id.symbol.inner())
1768 .ok_or_else(|| {
1769 CommandFailure::not_sent(
1770 InstrumentLookupError::not_found(cancel.instrument_id).to_string(),
1771 )
1772 })?;
1773
1774 let cli_ord_id = cancel.client_order_id.to_string();
1775 let item = if let Some(venue_order_id) = cancel.venue_order_id {
1776 KrakenFuturesBatchCancelItem::from_order_id(venue_order_id.to_string())
1777 } else {
1778 KrakenFuturesBatchCancelItem::from_client_order_id(cli_ord_id.clone())
1779 };
1780
1781 Ok((
1782 CancelRequestContext {
1783 strategy_id: cancel.strategy_id,
1784 instrument_id: cancel.instrument_id,
1785 client_order_id: cancel.client_order_id,
1786 cli_ord_id,
1787 venue_order_id: cancel.venue_order_id,
1788 },
1789 item,
1790 ))
1791}
1792
1793fn batch_cancel_status(status: &FuturesBatchCancelStatus) -> Option<KrakenSendStatus> {
1794 status
1795 .cancel_status
1796 .as_ref()
1797 .map(|cancel_status| cancel_status.status)
1798 .or(status.status)
1799}
1800
1801fn batch_cancel_context<'a>(
1802 status: &FuturesBatchCancelStatus,
1803 contexts: &'a [CancelRequestContext],
1804 index: usize,
1805) -> Option<&'a CancelRequestContext> {
1806 if let Some(order_id) = status.order_id.as_deref()
1807 && let Some(context) = contexts.iter().find(|context| {
1808 context
1809 .venue_order_id
1810 .is_some_and(|venue_order_id| venue_order_id.as_str() == order_id)
1811 })
1812 {
1813 return Some(context);
1814 }
1815
1816 if let Some(cli_ord_id) = status.cli_ord_id.as_deref()
1817 && let Some(context) = contexts
1818 .iter()
1819 .find(|context| context.cli_ord_id == cli_ord_id)
1820 {
1821 return Some(context);
1822 }
1823
1824 if index < contexts.len() && status.order_id.is_none() && status.cli_ord_id.is_none() {
1825 return contexts.get(index);
1826 }
1827
1828 None
1829}
1830
1831fn handle_cancel_failure(
1832 emitter: &ExecutionEventEmitter,
1833 clock: &'static AtomicTime,
1834 strategy_id: StrategyId,
1835 instrument_id: InstrumentId,
1836 client_order_id: ClientOrderId,
1837 venue_order_id: Option<VenueOrderId>,
1838 failure: CommandFailure,
1839) {
1840 match failure {
1841 CommandFailure::VenueRejected(reason) => {
1842 emitter.emit_order_cancel_rejected_event(
1843 strategy_id,
1844 instrument_id,
1845 client_order_id,
1846 venue_order_id,
1847 &reason,
1848 clock.get_time_ns(),
1849 );
1850 }
1851 CommandFailure::NotSent(reason) => {
1852 log::warn!("Cancel command failed local validation for {client_order_id}: {reason}");
1853 }
1854 CommandFailure::Ambiguous(reason) => {
1855 log::warn!(
1856 "Ambiguous cancel failure for {client_order_id}, awaiting reconciliation: {reason}"
1857 );
1858 }
1859 }
1860}
1861
1862impl KrakenFuturesExecutionClient {
1863 async fn reports_for_open_orders_absent_from_venue(
1880 &self,
1881 account_id: AccountId,
1882 instrument_id: Option<InstrumentId>,
1883 reported: &[OrderStatusReport],
1884 ) -> anyhow::Result<(Vec<OrderStatusReport>, bool)> {
1885 let mut order_ids = Vec::new();
1886 let mut queried_by_cli_ord_id = Vec::new();
1887
1888 {
1889 let cache = self.core.cache();
1890 for order in cache.orders_open(
1891 Some(&*KRAKEN_VENUE),
1892 instrument_id.as_ref(),
1893 None,
1894 None,
1895 None,
1896 ) {
1897 if product_type_from_symbol(order.instrument_id().symbol.inner().as_str())
1899 != KrakenProductType::Futures
1900 {
1901 continue;
1902 }
1903
1904 match order.venue_order_id() {
1905 Some(venue_order_id) => {
1906 let already_reported = reported
1907 .iter()
1908 .any(|report| report.venue_order_id == venue_order_id);
1909 if !already_reported {
1910 order_ids.push(venue_order_id.to_string());
1911 }
1912 }
1913 None => {
1914 queried_by_cli_ord_id.push(HeldOrder {
1915 client_order_id: order.client_order_id(),
1916 instrument_id: order.instrument_id(),
1917 side: order.order_side(),
1918 ts_init: order.ts_init(),
1919 });
1920 }
1921 }
1922 }
1923 }
1924
1925 if order_ids.is_empty() && queried_by_cli_ord_id.is_empty() {
1926 return Ok((Vec::new(), false));
1927 }
1928
1929 let cli_ord_ids: Vec<String> = queried_by_cli_ord_id
1930 .iter()
1931 .flat_map(|order| venue_client_order_ids(&order.client_order_id))
1932 .collect();
1933
1934 log::debug!(
1935 "Resolving {} venue order ID(s) and {} client order ID(s) from the orders-status window",
1936 order_ids.len(),
1937 cli_ord_ids.len(),
1938 );
1939
1940 let mut reports = Vec::new();
1941 for chunk in order_ids.chunks(FUTURES_ORDERS_STATUS_LIMIT) {
1942 reports.extend(
1943 self.http
1944 .request_orders_status_reports(account_id, chunk, &[])
1945 .await?,
1946 );
1947 }
1948
1949 for chunk in cli_ord_ids.chunks(FUTURES_ORDERS_STATUS_LIMIT) {
1950 reports.extend(
1951 self.http
1952 .request_orders_status_reports(account_id, &[], chunk)
1953 .await?,
1954 );
1955 }
1956
1957 let (mut attributed, withheld) = attribute_orders_status_reports(
1958 &self.core.cache(),
1959 &queried_by_cli_ord_id,
1960 latest_report_per_order(reports),
1961 );
1962
1963 if let Some(instrument_id) = instrument_id {
1966 attributed.retain(|report| report.instrument_id == instrument_id);
1967 }
1968
1969 Ok((attributed, withheld))
1970 }
1971}
1972
1973fn validate_order(order: &OrderAny) -> Result<(), OrderDeniedReason> {
1975 let len = order.client_order_id().as_str().len();
1976
1977 if len > KRAKEN_FUTURES_MAX_CLIENT_ORDER_ID_LEN {
1978 return Err(OrderDeniedReason::InvalidClientOrderId {
1979 detail: format!(
1980 "Kraken Futures accepts a client order ID of at most \
1981 {KRAKEN_FUTURES_MAX_CLIENT_ORDER_ID_LEN} characters, was {len}"
1982 ),
1983 });
1984 }
1985
1986 Ok(())
1987}
1988
1989fn cached_order_for_report(cache: &Cache, report: &OrderStatusReport) -> Option<OrderAny> {
1991 let client_order_id = cache.client_order_id(&report.venue_order_id)?;
1992 cache.order(client_order_id).map(|order| order.clone())
1993}
1994
1995struct QueriedOrder {
2010 venue_order_id: Option<VenueOrderId>,
2011 client_order_id: Option<ClientOrderId>,
2012 cached: Option<(InstrumentId, OrderSide, UnixNanos)>,
2013}
2014
2015impl QueriedOrder {
2016 fn new(cmd: &GenerateOrderStatusReport, cached: Option<&OrderAny>) -> Self {
2017 Self {
2018 venue_order_id: cmd
2019 .venue_order_id
2020 .or_else(|| cached.and_then(Order::venue_order_id)),
2021 client_order_id: cmd
2022 .client_order_id
2023 .or_else(|| cached.map(Order::client_order_id)),
2024 cached: cached
2025 .map(|order| (order.instrument_id(), order.order_side(), order.ts_init())),
2026 }
2027 }
2028
2029 fn echoes(
2032 &self,
2033 echoed: Option<ClientOrderId>,
2034 instrument_id: InstrumentId,
2035 side: OrderSide,
2036 placed: UnixNanos,
2037 ) -> bool {
2038 let echoes_id = echoed
2039 .zip(self.client_order_id)
2040 .is_some_and(|(echoed, queried)| {
2041 venue_client_order_ids(&queried).any(|id| id == echoed.as_str())
2042 });
2043
2044 echoes_id
2045 && self
2046 .cached
2047 .is_none_or(|(order_instrument, order_side, created)| {
2048 instrument_id == order_instrument
2049 && side == order_side
2050 && placed.saturating_add(ORDERS_STATUS_CLOCK_SKEW) >= created
2051 })
2052 }
2053
2054 fn indexed_for_another(&self, cache: &Cache, venue_order_id: &VenueOrderId) -> bool {
2056 cache
2057 .client_order_id(venue_order_id)
2058 .is_some_and(|owner| Some(*owner) != self.client_order_id)
2059 }
2060
2061 fn echoes_report(&self, cache: &Cache, report: &OrderStatusReport) -> bool {
2062 !self.indexed_for_another(cache, &report.venue_order_id)
2063 && report.order_side.is_some_and(|side| {
2064 self.echoes(
2065 report.client_order_id,
2066 report.instrument_id,
2067 side,
2068 report.ts_accepted,
2069 )
2070 })
2071 }
2072
2073 fn echoes_fill(&self, cache: &Cache, fill: &FillReport) -> bool {
2074 !self.indexed_for_another(cache, &fill.venue_order_id)
2075 && self.echoes(
2076 fill.client_order_id,
2077 fill.instrument_id,
2078 fill.order_side,
2079 fill.ts_event,
2080 )
2081 }
2082
2083 fn echo_is_another_order(
2087 &self,
2088 cache: &Cache,
2089 echoed: Option<ClientOrderId>,
2090 instrument_id: InstrumentId,
2091 side: OrderSide,
2092 placed: UnixNanos,
2093 ) -> bool {
2094 echoed
2095 .filter(|echoed| Some(*echoed) != self.client_order_id)
2096 .and_then(|echoed| cache.order(&echoed))
2097 .is_some_and(|other| {
2098 other.ts_submitted().is_some()
2099 && other.venue_order_id().is_none()
2100 && other.instrument_id() == instrument_id
2101 && other.order_side() == side
2102 && placed.saturating_add(ORDERS_STATUS_CLOCK_SKEW) >= other.ts_init()
2103 })
2104 }
2105
2106 fn report_may_be_another_order(&self, cache: &Cache, report: &OrderStatusReport) -> bool {
2107 report.order_side.is_some_and(|side| {
2108 self.echo_is_another_order(
2109 cache,
2110 report.client_order_id,
2111 report.instrument_id,
2112 side,
2113 report.ts_accepted,
2114 )
2115 })
2116 }
2117
2118 fn fill_may_be_another_order(&self, cache: &Cache, fill: &FillReport) -> bool {
2119 self.echo_is_another_order(
2120 cache,
2121 fill.client_order_id,
2122 fill.instrument_id,
2123 fill.order_side,
2124 fill.ts_event,
2125 )
2126 }
2127
2128 fn find_report(
2136 &self,
2137 cache: &Cache,
2138 reports: &[OrderStatusReport],
2139 ) -> anyhow::Result<Option<OrderStatusReport>> {
2140 if let Some(venue_order_id) = self.venue_order_id {
2141 return Ok(reports
2142 .iter()
2143 .find(|report| report.venue_order_id == venue_order_id)
2144 .cloned());
2145 }
2146
2147 let mut candidates = reports
2148 .iter()
2149 .filter(|report| self.echoes_report(cache, report));
2150 let Some(first) = candidates.next() else {
2151 return Ok(None);
2152 };
2153
2154 if candidates.any(|report| report.venue_order_id != first.venue_order_id) {
2155 anyhow::bail!(
2156 "Reports of several venue orders carry the client order ID of order {:?}; \
2157 deferring rather than taking one of them",
2158 self.client_order_id,
2159 );
2160 }
2161
2162 if let Some(shared) = reports.iter().find(|report| {
2163 self.echoes_report(cache, report) && self.report_may_be_another_order(cache, report)
2164 }) {
2165 anyhow::bail!(
2166 "Report of venue order {} carries {:?}, the full ID of another cached order and a \
2167 form of order {:?}; deferring until the cache ties the venue order to one of them",
2168 shared.venue_order_id,
2169 shared.client_order_id,
2170 self.client_order_id,
2171 );
2172 }
2173
2174 let mut report = first.clone();
2175 report.client_order_id = self.client_order_id;
2176 Ok(Some(report))
2177 }
2178
2179 fn has_stray_report(&self, cache: &Cache, reports: &[OrderStatusReport]) -> bool {
2182 self.venue_order_id.is_some_and(|venue_order_id| {
2183 reports.iter().any(|report| {
2184 report.venue_order_id != venue_order_id && self.echoes_report(cache, report)
2185 })
2186 })
2187 }
2188
2189 fn has_stray_fill(&self, cache: &Cache, fills: &[FillReport]) -> bool {
2193 fills.iter().any(|fill| {
2194 Some(fill.venue_order_id) != self.venue_order_id && self.echoes_fill(cache, fill)
2195 })
2196 }
2197}
2198
2199fn venue_client_order_ids(client_order_id: &ClientOrderId) -> impl Iterator<Item = String> {
2203 let full = client_order_id.to_string();
2204 let shortened = truncate_cl_ord_id(client_order_id);
2205 let shortened = (shortened != full).then_some(shortened);
2206 std::iter::once(full).chain(shortened)
2207}
2208
2209#[derive(Clone, Copy, Debug)]
2211struct HeldOrder {
2212 client_order_id: ClientOrderId,
2213 instrument_id: InstrumentId,
2214 side: OrderSide,
2215 ts_init: UnixNanos,
2216}
2217
2218fn name_reports_by_cached_order(cache: &Cache, reports: &mut [OrderStatusReport]) {
2222 for report in reports {
2223 if let Some(client_order_id) = cache.client_order_id(&report.venue_order_id) {
2224 report.client_order_id = Some(*client_order_id);
2225 }
2226 }
2227}
2228
2229fn name_fills_by_cached_order(cache: &Cache, fills: &mut [FillReport]) {
2232 for fill in fills {
2233 if let Some(client_order_id) = cache.client_order_id(&fill.venue_order_id) {
2234 fill.client_order_id = Some(*client_order_id);
2235 }
2236 }
2237}
2238
2239fn fold_window_into_venue_read(
2250 venue_read: &mut [OrderStatusReport],
2251 window: Vec<OrderStatusReport>,
2252) -> (Vec<OrderStatusReport>, HashSet<VenueOrderId>) {
2253 let positions: HashMap<VenueOrderId, usize> = venue_read
2254 .iter()
2255 .enumerate()
2256 .map(|(position, report)| (report.venue_order_id, position))
2257 .collect();
2258 let mut window_stated = HashSet::new();
2259
2260 let window_only = window
2261 .into_iter()
2262 .filter_map(|copy| {
2263 let Some(&position) = positions.get(©.venue_order_id) else {
2264 return Some(copy);
2265 };
2266 let report = &mut venue_read[position];
2267 report.client_order_id = copy.client_order_id;
2268
2269 if !report.order_status.is_closed() && supersedes(©, report) {
2270 report.order_status = copy.order_status;
2271 report.quantity = copy.quantity;
2272 report.filled_qty = copy.filled_qty;
2273 report.price = copy.price;
2274 report.avg_px = copy.avg_px;
2275 report.cancel_reason = copy.cancel_reason;
2276 report.ts_last = copy.ts_last;
2277 window_stated.insert(copy.venue_order_id);
2278 }
2279 None
2280 })
2281 .collect();
2282
2283 (window_only, window_stated)
2284}
2285
2286fn latest_report_per_order(reports: Vec<OrderStatusReport>) -> Vec<OrderStatusReport> {
2293 let mut latest: IndexMap<VenueOrderId, OrderStatusReport> =
2294 IndexMap::with_capacity(reports.len());
2295
2296 for report in reports {
2297 match latest.get(&report.venue_order_id) {
2298 Some(existing) if !supersedes(&report, existing) => {}
2299 _ => {
2300 latest.insert(report.venue_order_id, report);
2301 }
2302 }
2303 }
2304
2305 latest.into_values().collect()
2306}
2307
2308fn attribute_orders_status_reports(
2324 cache: &Cache,
2325 queried_by_cli_ord_id: &[HeldOrder],
2326 reports: Vec<OrderStatusReport>,
2327) -> (Vec<OrderStatusReport>, bool) {
2328 let mut held: HashMap<String, Option<HeldOrder>> = HashMap::new();
2330
2331 for order in queried_by_cli_ord_id {
2332 for echoed in venue_client_order_ids(&order.client_order_id) {
2333 held.entry(echoed)
2334 .and_modify(|named| *named = None)
2335 .or_insert(Some(*order));
2336 }
2337 }
2338
2339 let indexed =
2340 |report: &OrderStatusReport| cache.client_order_id(&report.venue_order_id).copied();
2341
2342 let mut unindexed: HashMap<ClientOrderId, HashSet<VenueOrderId>> = HashMap::new();
2343
2344 for report in &reports {
2345 if indexed(report).is_none()
2346 && let Some(echoed) = report.client_order_id
2347 {
2348 unindexed
2349 .entry(echoed)
2350 .or_default()
2351 .insert(report.venue_order_id);
2352 }
2353 }
2354
2355 let by_echoed_id = |report: &OrderStatusReport| -> Result<ClientOrderId, &'static str> {
2356 let echoed = report
2357 .client_order_id
2358 .ok_or("it carries no client order ID")?;
2359 if unindexed
2360 .get(&echoed)
2361 .is_none_or(|venue_order_ids| venue_order_ids.len() != 1)
2362 {
2363 return Err("several reports carry its client order ID");
2364 }
2365 let order = held
2366 .get(echoed.as_str())
2367 .ok_or("no order queried by client order ID holds its client order ID")?
2368 .ok_or("several queried orders hold its client order ID")?;
2369
2370 if report.instrument_id != order.instrument_id || report.order_side != Some(order.side) {
2371 return Err("its instrument or side is not the queried order's");
2372 }
2373
2374 if report.ts_accepted.saturating_add(ORDERS_STATUS_CLOCK_SKEW) < order.ts_init {
2375 return Err(
2376 "the venue placed it before the queried order's creation, beyond the clock skew \
2377 allowance; check the node clock if the order is recent",
2378 );
2379 }
2380
2381 Ok(order.client_order_id)
2382 };
2383
2384 let named: Vec<(OrderStatusReport, Result<ClientOrderId, &'static str>, bool)> = reports
2385 .into_iter()
2386 .map(|report| match indexed(&report) {
2387 Some(client_order_id) => (report, Ok(client_order_id), false),
2388 None => {
2389 let named = by_echoed_id(&report);
2390 (report, named, true)
2391 }
2392 })
2393 .collect();
2394
2395 let mut named_by_echo: HashMap<ClientOrderId, usize> = HashMap::new();
2397
2398 for (_, named, by_echo) in &named {
2399 if *by_echo && let Ok(client_order_id) = named {
2400 *named_by_echo.entry(*client_order_id).or_default() += 1;
2401 }
2402 }
2403
2404 let mut attributed = Vec::with_capacity(named.len());
2405 let mut withheld = false;
2406
2407 for (mut report, named, by_echo) in named {
2408 let named = match named {
2409 Ok(client_order_id)
2410 if by_echo && named_by_echo.get(&client_order_id).is_some_and(|n| *n > 1) =>
2411 {
2412 Err("several reports name the queried order it would be")
2413 }
2414 named => named,
2415 };
2416
2417 match named {
2418 Ok(client_order_id) => {
2419 report.client_order_id = Some(client_order_id);
2420 attributed.push(report);
2421 }
2422 Err(reason) => {
2423 log::warn!(
2424 "Withholding orders-status report {} with client order ID {:?}: {reason}",
2425 report.venue_order_id,
2426 report.client_order_id,
2427 );
2428 withheld = true;
2429 }
2430 }
2431 }
2432
2433 (attributed, withheld)
2434}
2435
2436fn is_refused_history_read(error: &anyhow::Error) -> bool {
2443 error
2444 .chain()
2445 .filter_map(|cause| cause.downcast_ref::<KrakenHttpError>())
2446 .any(|e| match e {
2447 KrakenHttpError::ApiError(_) => true,
2448 KrakenHttpError::NetworkError(message) => message.starts_with("HTTP error 429"),
2449 _ => false,
2450 })
2451}
2452
2453fn is_unpriced_terminal_report(report: &OrderStatusReport) -> bool {
2458 matches!(
2459 report.order_status,
2460 OrderStatus::Filled | OrderStatus::Canceled | OrderStatus::Expired | OrderStatus::Voided
2461 ) && !report.filled_qty.is_zero()
2462 && report.avg_px.is_none()
2463}
2464
2465fn price_from_fills(
2479 report: &mut OrderStatusReport,
2480 cached: Option<&OrderAny>,
2481 fills: &[FillReport],
2482) -> Result<(), Decimal> {
2483 let target = report.filled_qty.as_decimal();
2484 let order_fills: Vec<&FillReport> = fills
2485 .iter()
2486 .filter(|fill| fill.venue_order_id == report.venue_order_id)
2487 .collect();
2488 let total = |fills: &mut dyn Iterator<Item = &&FillReport>| {
2489 fills.fold((Decimal::ZERO, Decimal::ZERO), |(qty, notional), fill| {
2490 let fill_qty = fill.last_qty.as_decimal();
2491 (
2492 qty + fill_qty,
2493 notional + fill_qty * fill.last_px.as_decimal(),
2494 )
2495 })
2496 };
2497
2498 let (page_qty, page_notional) = total(&mut order_fills.iter());
2499
2500 if !target.is_zero() && page_qty == target {
2501 report.avg_px = Some(page_notional / target);
2502 return Ok(());
2503 }
2504
2505 let Some(order) = cached else {
2506 return Err(page_qty);
2507 };
2508 let recorded: HashSet<TradeId> = order.trade_ids().into_iter().copied().collect();
2509 let (new_qty, new_notional) = total(
2510 &mut order_fills
2511 .iter()
2512 .filter(|fill| !recorded.contains(&fill.trade_id)),
2513 );
2514 let cached_qty = order.filled_qty().as_decimal();
2515 let covered = cached_qty + new_qty;
2516
2517 if !target.is_zero() && covered == target {
2518 let cached_notional = order.avg_px().unwrap_or(Decimal::ZERO) * cached_qty;
2519 report.avg_px = Some((cached_notional + new_notional) / target);
2520 return Ok(());
2521 }
2522
2523 Err(covered.max(page_qty))
2524}
2525
2526fn synthesize_filled_order_status_report(
2527 cache: &Cache,
2528 queried: &QueriedOrder,
2529 order: &OrderAny,
2530 fills: &[FillReport],
2531) -> Option<OrderStatusReport> {
2532 let venue_order_id = queried.venue_order_id;
2533
2534 let mut matched: Vec<&FillReport> = match venue_order_id {
2538 Some(venue_order_id) => fills
2539 .iter()
2540 .filter(|fill| fill.venue_order_id == venue_order_id)
2541 .collect(),
2542 None => {
2543 let matched: Vec<&FillReport> = fills
2544 .iter()
2545 .filter(|fill| queried.echoes_fill(cache, fill))
2546 .collect();
2547 let first_venue_order_id = matched.first()?.venue_order_id;
2548
2549 if matched.iter().any(|fill| {
2550 fill.venue_order_id != first_venue_order_id
2551 || queried.fill_may_be_another_order(cache, fill)
2552 }) {
2553 return None;
2554 }
2555 matched
2556 }
2557 };
2558
2559 if matched.is_empty() {
2560 return None;
2561 }
2562
2563 matched.sort_by_key(|fill| fill.ts_event);
2564 let first_fill = *matched.first()?;
2565 let last_fill = *matched.last()?;
2566
2567 let total_filled = matched
2568 .iter()
2569 .fold(Decimal::ZERO, |acc, fill| acc + fill.last_qty.as_decimal());
2570 if total_filled < order.quantity().as_decimal() {
2571 return None;
2572 }
2573
2574 let total_notional = matched.iter().fold(Decimal::ZERO, |acc, fill| {
2575 acc + fill.last_qty.as_decimal() * fill.last_px.as_decimal()
2576 });
2577 let avg_px = if total_filled.is_zero() {
2578 None
2579 } else {
2580 Some(total_notional / total_filled)
2581 };
2582 let venue_order_id = venue_order_id.unwrap_or(first_fill.venue_order_id);
2583
2584 let mut report = OrderStatusReport::new(
2585 first_fill.account_id,
2586 order.instrument_id(),
2587 Some(order.client_order_id()),
2588 venue_order_id,
2589 order.order_side().into(),
2590 order.order_type(),
2591 order.time_in_force(),
2592 OrderStatus::Filled,
2593 order.quantity(),
2594 order.quantity(),
2595 first_fill.ts_event,
2596 last_fill.ts_event,
2597 last_fill.ts_init,
2598 None,
2599 );
2600 report.order_list_id = order.order_list_id();
2601 report.venue_position_id = matched.iter().rev().find_map(|fill| fill.venue_position_id);
2602 report.linked_order_ids = order
2603 .linked_order_ids()
2604 .map(|linked_order_ids| linked_order_ids.to_vec());
2605 report.parent_order_id = order.parent_order_id();
2606 report.expire_time = order.expire_time();
2607 report.price = order.price();
2608 report.trigger_price = order.trigger_price();
2609 report.trigger_type = order.trigger_type();
2610 report.avg_px = avg_px;
2611 report.display_qty = order.display_qty();
2612 report.post_only = order.is_post_only();
2613 report.reduce_only = order.is_reduce_only();
2614 Some(report)
2615}
2616
2617#[cfg(test)]
2618mod tests {
2619 use nautilus_core::{UUID4, UnixNanos};
2620 use nautilus_model::{
2621 enums::{LiquiditySide, OrderSide, OrderType, TimeInForce},
2622 identifiers::{
2623 AccountId, ClientOrderId, InstrumentId, StrategyId, TradeId, TraderId, VenueOrderId,
2624 },
2625 orders::OrderTestBuilder,
2626 reports::FillReport,
2627 types::{Currency, Money, Price, Quantity},
2628 };
2629 use rstest::rstest;
2630
2631 use super::*;
2632
2633 const TEST_INSTRUMENT_ID: &str = "PF_XBTUSD.KRAKEN";
2634
2635 #[tokio::test]
2636 async fn test_cancel_order_for_futures_missing_cached_instrument_returns_canonical_error() {
2637 let http = KrakenFuturesHttpClient::default();
2638 let instrument_id = InstrumentId::from(TEST_INSTRUMENT_ID);
2639 let client_order_id = ClientOrderId::from("C-001");
2640 let venue_order_id = VenueOrderId::from("V-001");
2641
2642 let result = cancel_order_for_futures(
2643 &http,
2644 AccountId::from("KRAKEN-001"),
2645 instrument_id,
2646 Some(client_order_id),
2647 Some(venue_order_id),
2648 )
2649 .await;
2650
2651 match result {
2652 Err(CommandFailure::NotSent(reason)) => {
2653 assert_eq!(
2654 reason,
2655 InstrumentLookupError::not_found(instrument_id).to_string()
2656 );
2657 }
2658 _ => panic!("Expected local validation failure"),
2659 }
2660 }
2661
2662 #[rstest]
2663 fn test_batch_cancel_item_for_futures_missing_cached_instrument_returns_canonical_error() {
2664 let http = KrakenFuturesHttpClient::default();
2665 let instrument_id = InstrumentId::from(TEST_INSTRUMENT_ID);
2666 let cancel = CancelOrder::new(
2667 TraderId::from("TESTER-001"),
2668 None,
2669 StrategyId::from("S-001"),
2670 instrument_id,
2671 ClientOrderId::from("C-001"),
2672 Some(VenueOrderId::from("V-001")),
2673 UUID4::new(),
2674 UnixNanos::default(),
2675 None,
2676 None,
2677 );
2678
2679 let result = batch_cancel_item_for_futures(&http, &cancel);
2680
2681 match result {
2682 Err(CommandFailure::NotSent(reason)) => {
2683 assert_eq!(
2684 reason,
2685 InstrumentLookupError::not_found(instrument_id).to_string()
2686 );
2687 }
2688 _ => panic!("Expected local validation failure"),
2689 }
2690 }
2691
2692 fn make_fill(
2693 venue_order_id: &str,
2694 client_order_id: Option<&str>,
2695 quantity: &str,
2696 price: &str,
2697 ts_event: u64,
2698 ) -> FillReport {
2699 FillReport::new(
2700 AccountId::from("KRAKEN-001"),
2701 InstrumentId::from(TEST_INSTRUMENT_ID),
2702 VenueOrderId::from(venue_order_id),
2703 TradeId::from(format!("T-{ts_event}").as_str()),
2704 OrderSide::Buy,
2705 Quantity::from(quantity),
2706 Price::from(price),
2707 Money::from_decimal(Decimal::ZERO, Currency::USD()).unwrap(),
2708 LiquiditySide::Taker,
2709 client_order_id.map(ClientOrderId::from),
2710 None,
2711 UnixNanos::from(ts_event),
2712 UnixNanos::from(ts_event),
2713 None,
2714 )
2715 }
2716
2717 fn make_cmd(
2718 client_order_id: Option<&str>,
2719 venue_order_id: Option<&str>,
2720 ) -> GenerateOrderStatusReport {
2721 GenerateOrderStatusReport::new(
2722 UUID4::new(),
2723 UnixNanos::default(),
2724 Some(InstrumentId::from(TEST_INSTRUMENT_ID)),
2725 client_order_id.map(ClientOrderId::from),
2726 venue_order_id.map(VenueOrderId::from),
2727 None,
2728 None,
2729 )
2730 }
2731
2732 fn make_order(client_order_id: &str) -> OrderAny {
2733 OrderTestBuilder::new(OrderType::Market)
2734 .instrument_id(InstrumentId::from(TEST_INSTRUMENT_ID))
2735 .client_order_id(ClientOrderId::from(client_order_id))
2736 .side(OrderSide::Buy)
2737 .quantity(Quantity::from("100"))
2738 .time_in_force(TimeInForce::Ioc)
2739 .build()
2740 }
2741
2742 #[rstest]
2743 fn test_synthesize_filled_order_status_report_matches_full_fill_by_venue_order_id() {
2744 let order = make_order("O-123456");
2745 let cmd = make_cmd(Some("O-123456"), Some("KRAKEN-789"));
2746 let fills = vec![
2747 make_fill("KRAKEN-789", Some("O-123456"), "40", "50000.0", 1),
2748 make_fill("KRAKEN-789", Some("O-123456"), "60", "50010.0", 2),
2749 make_fill("KRAKEN-OTHER", Some("O-123456"), "999", "1.0", 3),
2750 ];
2751
2752 let report = synthesize_filled_order_status_report(
2753 &Cache::default(),
2754 &QueriedOrder::new(&cmd, Some(&order)),
2755 &order,
2756 &fills,
2757 )
2758 .expect("expected a filled report");
2759
2760 assert_eq!(report.venue_order_id, VenueOrderId::from("KRAKEN-789"));
2761 assert_eq!(
2762 report.client_order_id,
2763 Some(ClientOrderId::from("O-123456"))
2764 );
2765 assert_eq!(report.order_status, OrderStatus::Filled);
2766 assert_eq!(report.order_type, OrderType::Market);
2767 assert_eq!(report.time_in_force, TimeInForce::Ioc);
2768 assert_eq!(report.quantity, Quantity::from("100"));
2769 assert_eq!(report.filled_qty, Quantity::from("100"));
2770 assert_eq!(
2771 report.avg_px,
2772 Some(Decimal::from_str_exact("50006.0").unwrap())
2773 );
2774 }
2775
2776 #[rstest]
2777 fn test_synthesize_filled_order_status_report_requires_full_fill_size() {
2778 let order = make_order("O-123457");
2779 let cmd = make_cmd(Some("O-123457"), Some("KRAKEN-790"));
2780 let fills = vec![make_fill(
2781 "KRAKEN-790",
2782 Some("O-123457"),
2783 "40",
2784 "50000.0",
2785 1,
2786 )];
2787
2788 assert!(
2789 synthesize_filled_order_status_report(
2790 &Cache::default(),
2791 &QueriedOrder::new(&cmd, Some(&order)),
2792 &order,
2793 &fills
2794 )
2795 .is_none()
2796 );
2797 }
2798
2799 #[rstest]
2805 #[case::full_id_on_one_venue_order(&[("KRAKEN-791", false)], 0, Some("KRAKEN-791"))]
2806 #[case::shortened_id_on_one_venue_order(&[("KRAKEN-791", true)], 0, Some("KRAKEN-791"))]
2807 #[case::full_id_on_two_venue_orders(&[("KRAKEN-791", false), ("KRAKEN-792", false)], 0, None)]
2808 #[case::executed_before_the_order(&[("KRAKEN-791", true)], 10_000_000_000, None)]
2809 fn test_synthesize_filled_order_status_report_without_a_venue_order_id(
2810 #[case] fills: &[(&str, bool)],
2811 #[case] created_ns: u64,
2812 #[case] expected_venue_order_id: Option<&str>,
2813 ) {
2814 let long_client_order_id = "O-20260227-002321-004-001-1";
2815 let shortened = truncate_cl_ord_id(&ClientOrderId::from(long_client_order_id));
2816 let order = OrderTestBuilder::new(OrderType::Market)
2817 .instrument_id(InstrumentId::from(TEST_INSTRUMENT_ID))
2818 .client_order_id(ClientOrderId::from(long_client_order_id))
2819 .side(OrderSide::Buy)
2820 .quantity(Quantity::from("100"))
2821 .time_in_force(TimeInForce::Ioc)
2822 .ts_init(UnixNanos::from(created_ns))
2823 .build();
2824 let cmd = make_cmd(Some(long_client_order_id), None);
2825 let quantity = format!("{}", 100 / fills.len());
2826 let fills: Vec<FillReport> = fills
2827 .iter()
2828 .enumerate()
2829 .map(|(i, (venue_order_id, shortened_echo))| {
2830 let echoed = if *shortened_echo {
2831 shortened.as_str()
2832 } else {
2833 long_client_order_id
2834 };
2835 make_fill(
2836 venue_order_id,
2837 Some(echoed),
2838 &quantity,
2839 "50000.0",
2840 i as u64 + 1,
2841 )
2842 })
2843 .collect();
2844
2845 let report = synthesize_filled_order_status_report(
2846 &Cache::default(),
2847 &QueriedOrder::new(&cmd, Some(&order)),
2848 &order,
2849 &fills,
2850 );
2851
2852 assert_eq!(
2853 report.as_ref().map(|report| report.venue_order_id),
2854 expected_venue_order_id.map(VenueOrderId::from)
2855 );
2856
2857 if let Some(report) = report {
2858 assert_eq!(
2859 report.client_order_id,
2860 Some(ClientOrderId::from(long_client_order_id))
2861 );
2862 assert_eq!(report.order_status, OrderStatus::Filled);
2863 }
2864 }
2865}