1use std::sync::Arc;
23
24use ahash::AHashMap;
25use nautilus_core::{AtomicMap, UUID4, UnixNanos};
26use nautilus_live::ExecutionEventEmitter;
27use nautilus_model::{
28 enums::{OrderSide, OrderStatus, OrderType, TimeInForce},
29 events::{OrderCanceled, OrderEventAny, OrderUpdated},
30 identifiers::{AccountId, ClientOrderId, InstrumentId, VenueOrderId},
31 instruments::{Instrument, InstrumentAny},
32 reports::OrderStatusReport,
33 types::{Price, Quantity},
34};
35
36use super::{
37 DeltaSnapshot, OrderIdentity, WsDispatchState, ensure_accepted_emitted,
38 fill_report_to_order_filled, resolve_client_order_id,
39};
40use crate::{
41 common::lookup_instrument_in_snapshot,
42 websocket::futures::{
43 messages::{
44 KrakenFuturesFill, KrakenFuturesFillsDelta, KrakenFuturesOpenOrdersCancel,
45 KrakenFuturesOpenOrdersDelta,
46 },
47 parse::{parse_futures_ws_fill_report, parse_futures_ws_order_status_report},
48 },
49};
50
51#[expect(clippy::too_many_arguments)]
58pub fn open_orders_delta(
59 delta: &KrakenFuturesOpenOrdersDelta,
60 state: &WsDispatchState,
61 emitter: &ExecutionEventEmitter,
62 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
63 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
64 order_instrument_map: &Arc<AtomicMap<String, InstrumentId>>,
65 venue_client_map: &Arc<AtomicMap<String, ClientOrderId>>,
66 venue_order_qty: &Arc<AtomicMap<String, Quantity>>,
67 account_id: AccountId,
68 ts_init: UnixNanos,
69) {
70 if delta.is_fill_driven_cancel() {
71 log::debug!(
72 "Skipping fill-driven open_orders delta: order_id={}, reason={:?}",
73 delta.order.order_id,
74 delta.reason,
75 );
76 return;
77 }
78
79 let product_id = delta.order.instrument.as_str();
80 let instruments = instruments.load();
81 let Some(instrument) = lookup_instrument_in_snapshot(&instruments, product_id) else {
82 log::warn!("No instrument for product_id: {product_id}");
83 return;
84 };
85
86 order_instrument_map.insert(delta.order.order_id.clone(), instrument.id());
90 let qty = Quantity::new(delta.order.qty, instrument.size_precision());
91 venue_order_qty.insert(delta.order.order_id.clone(), qty);
92
93 let resolved_id = delta
94 .order
95 .cli_ord_id
96 .as_ref()
97 .map(|id| resolve_client_order_id(id, truncated_id_map));
98
99 if let Some(cid) = resolved_id
104 && state.filled_orders.contains(&cid)
105 {
106 log::debug!(
107 "Skipping stale open_orders delta for filled order: cid={cid}, order_id={}",
108 delta.order.order_id,
109 );
110 return;
111 }
112
113 if let Some(client_order_id) = resolved_id {
114 venue_client_map.insert(delta.order.order_id.clone(), client_order_id);
115
116 if let Some(identity) = state.lookup_identity(&client_order_id) {
117 delta_tracked(
118 delta,
119 client_order_id,
120 &identity,
121 instrument,
122 state,
123 emitter,
124 account_id,
125 ts_init,
126 );
127 return;
128 }
129 }
130
131 match parse_futures_ws_order_status_report(
133 &delta.order,
134 delta.is_cancel,
135 delta.reason.as_deref(),
136 instrument,
137 account_id,
138 ts_init,
139 ) {
140 Ok(mut report) => {
141 if let Some(cid) = resolved_id {
142 report = report.with_client_order_id(cid);
143 }
144 emitter.send_order_status_report(report);
145 }
146 Err(e) => log::error!("Failed to parse futures order status report: {e}"),
147 }
148}
149
150#[expect(clippy::too_many_arguments)]
151fn delta_tracked(
152 delta: &KrakenFuturesOpenOrdersDelta,
153 client_order_id: ClientOrderId,
154 identity: &OrderIdentity,
155 instrument: &InstrumentAny,
156 state: &WsDispatchState,
157 emitter: &ExecutionEventEmitter,
158 account_id: AccountId,
159 ts_init: UnixNanos,
160) {
161 let venue_order_id = VenueOrderId::new(&delta.order.order_id);
162 let ts_event = millis_to_nanos(delta.order.last_update_time);
163 let new_filled = Quantity::new(delta.order.filled, instrument.size_precision());
164
165 if delta.is_cancel {
166 ensure_accepted_emitted(
167 client_order_id,
168 venue_order_id,
169 account_id,
170 identity,
171 state,
172 emitter,
173 ts_event,
174 ts_init,
175 );
176 let canceled = OrderCanceled::new(
177 emitter.trader_id(),
178 identity.strategy_id,
179 identity.instrument_id,
180 client_order_id,
181 UUID4::new(),
182 ts_event,
183 ts_init,
184 false,
185 Some(venue_order_id),
186 Some(account_id),
187 );
188 emitter.send_order_event(OrderEventAny::Canceled(canceled));
189 state.cleanup_terminal(&client_order_id);
190 return;
191 }
192
193 let already_accepted = state.emitted_accepted.contains(&client_order_id);
194 ensure_accepted_emitted(
195 client_order_id,
196 venue_order_id,
197 account_id,
198 identity,
199 state,
200 emitter,
201 ts_event,
202 ts_init,
203 );
204
205 let qty = Quantity::new(delta.order.qty, instrument.size_precision());
206 let snapshot = DeltaSnapshot::new(
207 qty,
208 new_filled,
209 delta.order.limit_price,
210 delta.order.stop_price,
211 );
212
213 if !already_accepted {
214 state.record_delta_snapshot(client_order_id, snapshot);
216 return;
217 }
218
219 let previous = state.previous_delta_snapshot(&client_order_id);
226 state.record_delta_snapshot(client_order_id, snapshot);
227
228 let non_fill_changed = previous.is_some_and(|prev| !snapshot.non_fill_fields_match(&prev));
229 if !non_fill_changed {
230 return;
231 }
232
233 state.update_identity_quantity(&client_order_id, qty);
236 let updated = OrderUpdated::new(
237 emitter.trader_id(),
238 identity.strategy_id,
239 identity.instrument_id,
240 client_order_id,
241 qty,
242 UUID4::new(),
243 ts_event,
244 ts_init,
245 false,
246 Some(venue_order_id),
247 Some(account_id),
248 delta
249 .order
250 .limit_price
251 .map(|p| Price::new(p, instrument.price_precision())),
252 delta
253 .order
254 .stop_price
255 .map(|p| Price::new(p, instrument.price_precision())),
256 None,
257 false,
258 );
259 emitter.send_order_event(OrderEventAny::Updated(updated));
260}
261
262#[expect(clippy::too_many_arguments)]
264pub fn open_orders_cancel(
265 cancel: &KrakenFuturesOpenOrdersCancel,
266 state: &WsDispatchState,
267 emitter: &ExecutionEventEmitter,
268 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
269 order_instrument_map: &Arc<AtomicMap<String, InstrumentId>>,
270 venue_client_map: &Arc<AtomicMap<String, ClientOrderId>>,
271 venue_order_qty: &Arc<AtomicMap<String, Quantity>>,
272 account_id: AccountId,
273 ts_init: UnixNanos,
274) {
275 if let Some(ref reason) = cancel.reason
277 && (reason == "full_fill" || reason == "partial_fill")
278 {
279 log::debug!(
280 "Skipping fill-driven cancel: order_id={}, reason={reason}",
281 cancel.order_id,
282 );
283 return;
284 }
285
286 let venue_order_id = VenueOrderId::new(&cancel.order_id);
287 let resolved_id = cancel
288 .cli_ord_id
289 .as_ref()
290 .map(|id| resolve_client_order_id(id, truncated_id_map))
291 .or_else(|| venue_client_map.load().get(&cancel.order_id).copied());
292
293 if let Some(client_order_id) = resolved_id
294 && let Some(identity) = state.lookup_identity(&client_order_id)
295 {
296 let ts_event = ts_init;
297 ensure_accepted_emitted(
298 client_order_id,
299 venue_order_id,
300 account_id,
301 &identity,
302 state,
303 emitter,
304 ts_event,
305 ts_init,
306 );
307 let canceled = OrderCanceled::new(
308 emitter.trader_id(),
309 identity.strategy_id,
310 identity.instrument_id,
311 client_order_id,
312 UUID4::new(),
313 ts_event,
314 ts_init,
315 false,
316 Some(venue_order_id),
317 Some(account_id),
318 );
319 emitter.send_order_event(OrderEventAny::Canceled(canceled));
320 state.cleanup_terminal(&client_order_id);
321 return;
322 }
323
324 let Some(instrument_id) = order_instrument_map.load().get(&cancel.order_id).copied() else {
326 log::warn!(
327 "Cannot resolve instrument for cancel: order_id={}, \
328 order not seen in previous delta",
329 cancel.order_id
330 );
331 return;
332 };
333
334 let Some(quantity) = venue_order_qty.load().get(&cancel.order_id).copied() else {
335 log::warn!(
336 "Cannot resolve quantity for cancel: order_id={}, skipping",
337 cancel.order_id
338 );
339 return;
340 };
341
342 let report = OrderStatusReport::new(
343 account_id,
344 instrument_id,
345 resolved_id,
346 venue_order_id,
347 OrderSide::NoOrderSide,
348 OrderType::Limit,
349 TimeInForce::Gtc,
350 OrderStatus::Canceled,
351 quantity,
352 Quantity::zero(0),
353 ts_init,
354 ts_init,
355 ts_init,
356 None,
357 );
358 let report = if let Some(ref reason) = cancel.reason
359 && !reason.is_empty()
360 {
361 report.with_cancel_reason(reason.clone())
362 } else {
363 report
364 };
365 emitter.send_order_status_report(report);
366}
367
368#[expect(clippy::too_many_arguments)]
370pub fn fills_delta(
371 fills_delta: &KrakenFuturesFillsDelta,
372 state: &WsDispatchState,
373 emitter: &ExecutionEventEmitter,
374 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
375 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
376 venue_client_map: &Arc<AtomicMap<String, ClientOrderId>>,
377 account_id: AccountId,
378 ts_init: UnixNanos,
379) {
380 let instruments = instruments.load();
381 let venue_clients = venue_client_map.load();
382
383 for fill in &fills_delta.fills {
384 single_fill(
385 fill,
386 state,
387 emitter,
388 &instruments,
389 truncated_id_map,
390 &venue_clients,
391 account_id,
392 ts_init,
393 );
394 }
395}
396
397#[expect(clippy::too_many_arguments)]
398fn single_fill(
399 fill: &KrakenFuturesFill,
400 state: &WsDispatchState,
401 emitter: &ExecutionEventEmitter,
402 instruments: &AHashMap<InstrumentId, InstrumentAny>,
403 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
404 venue_client_map: &AHashMap<String, ClientOrderId>,
405 account_id: AccountId,
406 ts_init: UnixNanos,
407) {
408 let product_id = match &fill.instrument {
409 Some(id) => id.as_str(),
410 None => {
411 log::warn!("Fill missing instrument field: fill_id={}", fill.fill_id);
412 return;
413 }
414 };
415
416 let Some(instrument) = lookup_instrument_in_snapshot(instruments, product_id) else {
417 log::warn!("No instrument for product_id: {product_id}");
418 return;
419 };
420
421 let mut report = match parse_futures_ws_fill_report(fill, instrument, account_id, ts_init) {
422 Ok(report) => report,
423 Err(e) => {
424 log::error!("Failed to parse futures fill report: {e}");
425 return;
426 }
427 };
428
429 let resolved_id = fill
430 .cli_ord_id
431 .as_deref()
432 .filter(|s| !s.is_empty())
433 .map(|id| resolve_client_order_id(id, truncated_id_map))
434 .or_else(|| venue_client_map.get(&fill.order_id).copied());
435
436 if let Some(cid) = resolved_id
437 && state.filled_orders.contains(&cid)
438 {
439 log::debug!(
440 "Skipping stale fill for filled order: cid={cid}, order_id={}",
441 fill.order_id,
442 );
443 return;
444 }
445
446 if let Some(client_order_id) = resolved_id {
447 report.client_order_id = Some(client_order_id);
448
449 if let Some(identity) = state.lookup_identity(&client_order_id) {
450 if state.check_and_insert_trade(report.trade_id) {
451 log::debug!(
452 "Skipping duplicate fill for {client_order_id}: trade_id={}",
453 report.trade_id
454 );
455 return;
456 }
457 ensure_accepted_emitted(
458 client_order_id,
459 report.venue_order_id,
460 account_id,
461 &identity,
462 state,
463 emitter,
464 report.ts_event,
465 ts_init,
466 );
467 let filled = fill_report_to_order_filled(
468 &report,
469 emitter.trader_id(),
470 &identity,
471 instrument.quote_currency(),
472 client_order_id,
473 );
474 emitter.send_order_event(OrderEventAny::Filled(filled));
475
476 let previous = state
478 .previous_filled_qty(&client_order_id)
479 .unwrap_or_else(|| Quantity::zero(instrument.size_precision()));
480 let cumulative = previous + report.last_qty;
481 state.record_filled_qty(client_order_id, cumulative);
482
483 if cumulative >= identity.quantity {
484 state.insert_filled(client_order_id);
485 state.cleanup_terminal(&client_order_id);
486 }
487 return;
488 }
489 }
490
491 if state.check_and_insert_trade(report.trade_id) {
493 log::debug!(
494 "Skipping duplicate external fill: trade_id={}",
495 report.trade_id
496 );
497 return;
498 }
499 emitter.send_fill_report(report);
500}
501
502#[inline]
503fn millis_to_nanos(millis: i64) -> UnixNanos {
504 UnixNanos::from((millis as u64) * 1_000_000)
505}