1use std::sync::Arc;
24
25use nautilus_core::{AtomicMap, UUID4, UnixNanos};
26use nautilus_live::ExecutionEventEmitter;
27use nautilus_model::{
28 enums::OrderStatus,
29 events::{
30 OrderAccepted, OrderCanceled, OrderEventAny, OrderExpired, OrderTriggered, OrderUpdated,
31 },
32 identifiers::{AccountId, ClientOrderId, InstrumentId},
33 instruments::{Instrument, InstrumentAny},
34 reports::{FillReport, OrderStatusReport},
35 types::Quantity,
36};
37
38use super::{
39 OrderIdentity, WsDispatchState, ensure_accepted_emitted, fill_report_to_order_filled,
40 resolve_client_order_id,
41};
42use crate::{
43 common::lookup_instrument_in_snapshot,
44 websocket::spot_v2::{
45 enums::KrakenExecType,
46 messages::KrakenWsExecutionData,
47 parse::{parse_ws_fill_report, parse_ws_order_status_report},
48 },
49};
50
51#[expect(clippy::too_many_arguments)]
53pub fn execution(
54 exec: &KrakenWsExecutionData,
55 state: &WsDispatchState,
56 emitter: &ExecutionEventEmitter,
57 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
58 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
59 order_qty_cache: &Arc<AtomicMap<String, f64>>,
60 account_id: AccountId,
61 ts_init: UnixNanos,
62) {
63 execution_inner(
64 exec,
65 state,
66 emitter,
67 instruments,
68 truncated_id_map,
69 order_qty_cache,
70 account_id,
71 ts_init,
72 );
73
74 if is_terminal_exec_type(exec.exec_type) {
79 state.forget_order_symbol(&exec.order_id);
80 state.forget_order_client_id(&exec.order_id);
81 }
82}
83
84#[expect(clippy::too_many_arguments)]
85fn execution_inner(
86 exec: &KrakenWsExecutionData,
87 state: &WsDispatchState,
88 emitter: &ExecutionEventEmitter,
89 instruments: &Arc<AtomicMap<InstrumentId, InstrumentAny>>,
90 truncated_id_map: &Arc<AtomicMap<String, ClientOrderId>>,
91 order_qty_cache: &Arc<AtomicMap<String, f64>>,
92 account_id: AccountId,
93 ts_init: UnixNanos,
94) {
95 let cached_symbol;
100 let symbol = match exec.symbol.as_deref() {
101 Some(s) => {
102 state.cache_order_symbol(&exec.order_id, s);
103 s
104 }
105 None => match state.lookup_order_symbol(&exec.order_id) {
106 Some(s) => {
107 cached_symbol = s;
108 cached_symbol.as_str()
109 }
110 None => {
111 log::debug!(
112 "Execution message without symbol and no cached mapping: \
113 exec_type={:?}, order_id={}",
114 exec.exec_type,
115 exec.order_id
116 );
117 return;
118 }
119 },
120 };
121 let instruments = instruments.load();
122 let Some(instrument) = lookup_instrument_in_snapshot(&instruments, symbol) else {
123 log::warn!("No instrument for symbol: {symbol}");
124 return;
125 };
126
127 let cached_qty = exec
130 .cl_ord_id
131 .as_ref()
132 .and_then(|id| order_qty_cache.load().get(id).copied());
133 if let (Some(qty), Some(cl_ord_id)) = (exec.order_qty, &exec.cl_ord_id) {
134 order_qty_cache.insert(cl_ord_id.clone(), qty);
135 }
136
137 let resolved_id = match exec.cl_ord_id.as_ref() {
144 Some(id) => {
145 let cid = resolve_client_order_id(id, truncated_id_map);
146 state.cache_order_client_id(&exec.order_id, cid);
147 Some(cid)
148 }
149 None => state.lookup_order_client_id(&exec.order_id),
150 };
151
152 if let Some(cid) = resolved_id
155 && state.filled_orders.contains(&cid)
156 {
157 log::debug!(
158 "Skipping stale spot execution for filled order: cid={cid}, order_id={}",
159 exec.order_id,
160 );
161 return;
162 }
163
164 let identity = resolved_id.and_then(|cid| state.lookup_identity(&cid));
165
166 match parse_ws_order_status_report(exec, instrument, account_id, cached_qty, ts_init) {
168 Ok(mut report) => {
169 if let Some(cid) = resolved_id {
170 report = report.with_client_order_id(cid);
171 }
172
173 if let (Some(client_order_id), Some(identity)) = (resolved_id, identity.as_ref()) {
174 status_tracked(
175 &report,
176 exec.exec_type,
177 exec.exec_id.is_some(),
178 client_order_id,
179 identity,
180 state,
181 emitter,
182 account_id,
183 ts_init,
184 );
185 } else {
186 emitter.send_order_status_report(report);
187 }
188 }
189 Err(e) => log::error!("Failed to parse order status report: {e}"),
190 }
191
192 if exec.exec_id.is_some() {
194 match parse_ws_fill_report(exec, instrument, account_id, ts_init) {
195 Ok(mut report) => {
196 if let Some(cid) = resolved_id {
197 report.client_order_id = Some(cid);
198 }
199
200 if let (Some(client_order_id), Some(identity)) = (resolved_id, identity.as_ref()) {
201 fill_tracked(
202 &report,
203 client_order_id,
204 identity,
205 instrument,
206 state,
207 emitter,
208 account_id,
209 ts_init,
210 );
211 } else {
212 if state.check_and_insert_trade(report.trade_id) {
213 log::debug!(
214 "Skipping duplicate external spot fill: trade_id={}",
215 report.trade_id
216 );
217 return;
218 }
219 emitter.send_fill_report(report);
220 }
221 }
222 Err(e) => log::error!("Failed to parse fill report: {e}"),
223 }
224 }
225}
226
227#[expect(clippy::too_many_arguments)]
228fn status_tracked(
229 report: &OrderStatusReport,
230 exec_type: KrakenExecType,
231 has_fill: bool,
232 client_order_id: ClientOrderId,
233 identity: &OrderIdentity,
234 state: &WsDispatchState,
235 emitter: &ExecutionEventEmitter,
236 account_id: AccountId,
237 ts_init: UnixNanos,
238) {
239 let venue_order_id = report.venue_order_id;
240 let ts_event = report.ts_last;
241 let trader_id = emitter.trader_id();
242
243 if matches!(
247 exec_type,
248 KrakenExecType::Amended | KrakenExecType::Restated
249 ) && state.emitted_accepted.contains(&client_order_id)
250 {
251 state.update_identity_quantity(&client_order_id, report.quantity);
252 let updated = OrderUpdated::new(
253 trader_id,
254 identity.strategy_id,
255 identity.instrument_id,
256 client_order_id,
257 report.quantity,
258 UUID4::new(),
259 ts_event,
260 ts_init,
261 false,
262 Some(venue_order_id),
263 Some(account_id),
264 report.price,
265 report.trigger_price,
266 None,
267 false,
268 );
269 emitter.send_order_event(OrderEventAny::Updated(updated));
270 return;
271 }
272
273 match report.order_status {
274 OrderStatus::Accepted => {
275 if !state.insert_accepted(client_order_id) {
276 return;
279 }
280 let accepted = OrderAccepted::new(
281 trader_id,
282 identity.strategy_id,
283 identity.instrument_id,
284 client_order_id,
285 venue_order_id,
286 account_id,
287 UUID4::new(),
288 ts_event,
289 ts_init,
290 false,
291 );
292 emitter.send_order_event(OrderEventAny::Accepted(accepted));
293 }
294 OrderStatus::Triggered => {
295 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 triggered = OrderTriggered::new(
308 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::Triggered(triggered));
320 }
321 OrderStatus::PartiallyFilled => {
322 }
325 OrderStatus::Filled
326 if !has_fill => {
332 state.insert_filled(client_order_id);
333 state.cleanup_terminal(&client_order_id);
334 }
335 OrderStatus::Canceled => {
336 ensure_accepted_emitted(
337 client_order_id,
338 venue_order_id,
339 account_id,
340 identity,
341 state,
342 emitter,
343 ts_event,
344 ts_init,
345 );
346 let canceled = OrderCanceled::new(
347 trader_id,
348 identity.strategy_id,
349 identity.instrument_id,
350 client_order_id,
351 UUID4::new(),
352 ts_event,
353 ts_init,
354 false,
355 Some(venue_order_id),
356 Some(account_id),
357 );
358 emitter.send_order_event(OrderEventAny::Canceled(canceled));
359 state.cleanup_terminal(&client_order_id);
360 }
361 OrderStatus::Expired => {
362 ensure_accepted_emitted(
363 client_order_id,
364 venue_order_id,
365 account_id,
366 identity,
367 state,
368 emitter,
369 ts_event,
370 ts_init,
371 );
372 let expired = OrderExpired::new(
373 trader_id,
374 identity.strategy_id,
375 identity.instrument_id,
376 client_order_id,
377 UUID4::new(),
378 ts_event,
379 ts_init,
380 false,
381 Some(venue_order_id),
382 Some(account_id),
383 );
384 emitter.send_order_event(OrderEventAny::Expired(expired));
385 state.cleanup_terminal(&client_order_id);
386 }
387 _ => {}
388 }
389}
390
391#[expect(clippy::too_many_arguments)]
392fn fill_tracked(
393 report: &FillReport,
394 client_order_id: ClientOrderId,
395 identity: &OrderIdentity,
396 instrument: &InstrumentAny,
397 state: &WsDispatchState,
398 emitter: &ExecutionEventEmitter,
399 account_id: AccountId,
400 ts_init: UnixNanos,
401) {
402 if state.check_and_insert_trade(report.trade_id) {
403 log::debug!(
404 "Skipping duplicate spot fill for {client_order_id}: trade_id={}",
405 report.trade_id
406 );
407 return;
408 }
409
410 ensure_accepted_emitted(
411 client_order_id,
412 report.venue_order_id,
413 account_id,
414 identity,
415 state,
416 emitter,
417 report.ts_event,
418 ts_init,
419 );
420
421 let filled = fill_report_to_order_filled(
422 report,
423 emitter.trader_id(),
424 identity,
425 instrument.quote_currency(),
426 client_order_id,
427 );
428 emitter.send_order_event(OrderEventAny::Filled(filled));
429
430 let previous = state
431 .previous_filled_qty(&client_order_id)
432 .unwrap_or_else(|| Quantity::zero(instrument.size_precision()));
433 let cumulative = previous + report.last_qty;
434 state.record_filled_qty(client_order_id, cumulative);
435
436 if cumulative >= identity.quantity {
437 state.insert_filled(client_order_id);
438 state.cleanup_terminal(&client_order_id);
439 }
440}
441
442#[must_use]
445pub fn is_terminal_exec_type(exec_type: KrakenExecType) -> bool {
446 matches!(
447 exec_type,
448 KrakenExecType::Filled | KrakenExecType::Canceled | KrakenExecType::Expired
449 )
450}
451
452#[cfg(test)]
453mod tests {
454 use rstest::rstest;
455
456 use super::*;
457
458 #[rstest]
459 #[case::filled(KrakenExecType::Filled, true)]
460 #[case::canceled(KrakenExecType::Canceled, true)]
461 #[case::expired(KrakenExecType::Expired, true)]
462 #[case::new(KrakenExecType::New, false)]
463 #[case::trade(KrakenExecType::Trade, false)]
464 #[case::pending_new(KrakenExecType::PendingNew, false)]
465 fn test_is_terminal_exec_type(#[case] exec_type: KrakenExecType, #[case] expected: bool) {
466 assert_eq!(is_terminal_exec_type(exec_type), expected);
467 }
468}