1use anyhow::Context;
19use nautilus_core::{UUID4, datetime::NANOSECONDS_IN_MILLISECOND, nanos::UnixNanos};
20use nautilus_model::{
21 data::{
22 BookOrder, FundingRateUpdate, IndexPriceUpdate, MarkPriceUpdate, OrderBookDelta, QuoteTick,
23 TradeTick,
24 },
25 enums::{
26 AggressorSide, BookAction, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType,
27 },
28 identifiers::{AccountId, ClientOrderId, TradeId, VenueOrderId},
29 instruments::{Instrument, any::InstrumentAny},
30 reports::{FillReport, OrderStatusReport},
31 types::{Money, Price, Quantity},
32};
33use rust_decimal::Decimal;
34
35use super::messages::{
36 KrakenFuturesBookDelta, KrakenFuturesBookSnapshot, KrakenFuturesFill, KrakenFuturesOpenOrder,
37 KrakenFuturesTickerData, KrakenFuturesTradeData,
38};
39use crate::common::enums::KrakenOrderSide;
40
41fn millis_to_nanos(millis: i64) -> UnixNanos {
42 UnixNanos::from((millis as u64) * NANOSECONDS_IN_MILLISECOND)
43}
44
45pub fn parse_futures_ws_quote_tick(
46 ticker: &KrakenFuturesTickerData,
47 instrument: &InstrumentAny,
48 ts_init: UnixNanos,
49) -> anyhow::Result<QuoteTick> {
50 let price_precision = instrument.price_precision();
51 let size_precision = instrument.size_precision();
52
53 let bid = ticker.bid.context("Ticker missing bid")?;
54 let ask = ticker.ask.context("Ticker missing ask")?;
55 let bid_size = ticker.bid_size.unwrap_or(Decimal::ZERO);
56 let ask_size = ticker.ask_size.unwrap_or(Decimal::ZERO);
57
58 let bid_price =
59 Price::from_decimal_dp(bid, price_precision).context("Failed to construct bid Price")?;
60 let ask_price =
61 Price::from_decimal_dp(ask, price_precision).context("Failed to construct ask Price")?;
62 let bid_qty = Quantity::from_decimal_dp(bid_size, size_precision)
63 .context("Failed to construct bid Quantity")?;
64 let ask_qty = Quantity::from_decimal_dp(ask_size, size_precision)
65 .context("Failed to construct ask Quantity")?;
66
67 let ts_event = ticker.time.map_or(ts_init, millis_to_nanos);
68
69 Ok(QuoteTick::new(
70 instrument.id(),
71 bid_price,
72 ask_price,
73 bid_qty,
74 ask_qty,
75 ts_event,
76 ts_init,
77 ))
78}
79
80pub fn parse_futures_ws_trade_tick(
81 trade: &KrakenFuturesTradeData,
82 instrument: &InstrumentAny,
83 ts_init: UnixNanos,
84) -> anyhow::Result<TradeTick> {
85 let price_precision = instrument.price_precision();
86 let size_precision = instrument.size_precision();
87
88 let price = Price::from_decimal_dp(trade.price, price_precision)
89 .context("Failed to construct trade Price")?;
90 let size = Quantity::from_decimal_dp(trade.qty, size_precision)
91 .context("Failed to construct trade Quantity")?;
92
93 let aggressor = match trade.side {
94 KrakenOrderSide::Buy => AggressorSide::Buy,
95 KrakenOrderSide::Sell => AggressorSide::Sell,
96 };
97
98 let trade_id = trade
99 .uid
100 .as_deref()
101 .map_or_else(|| TradeId::new(trade.seq.to_string()), TradeId::new);
102
103 let ts_event = millis_to_nanos(trade.time);
104
105 TradeTick::new_checked(
106 instrument.id(),
107 price,
108 size,
109 aggressor,
110 trade_id,
111 ts_event,
112 ts_init,
113 )
114 .context("Failed to construct TradeTick from Kraken futures trade")
115}
116
117pub fn parse_futures_ws_book_snapshot_deltas(
118 snapshot: &KrakenFuturesBookSnapshot,
119 instrument: &InstrumentAny,
120 sequence: u64,
121 ts_init: UnixNanos,
122) -> anyhow::Result<Vec<OrderBookDelta>> {
123 let instrument_id = instrument.id();
124 let price_precision = instrument.price_precision();
125 let size_precision = instrument.size_precision();
126 let ts_event = millis_to_nanos(snapshot.timestamp);
127
128 let capacity = snapshot.bids.len() + snapshot.asks.len() + 1;
129 let mut deltas = Vec::with_capacity(capacity);
130 let mut seq = sequence;
131
132 deltas.push(OrderBookDelta::clear(instrument_id, seq, ts_event, ts_init));
134 seq += 1;
135
136 for level in &snapshot.bids {
137 if level.qty <= Decimal::ZERO {
138 continue;
139 }
140 let price = Price::from_decimal_dp(level.price, price_precision)?;
141 let size = Quantity::from_decimal_dp(level.qty, size_precision)?;
142 let order_id = price.raw as u64;
143 let order = BookOrder::new(OrderSide::Buy, price, size, order_id);
144 deltas.push(OrderBookDelta::new(
145 instrument_id,
146 BookAction::Add,
147 order,
148 0,
149 seq,
150 ts_event,
151 ts_init,
152 ));
153 seq += 1;
154 }
155
156 for level in &snapshot.asks {
157 if level.qty <= Decimal::ZERO {
158 continue;
159 }
160 let price = Price::from_decimal_dp(level.price, price_precision)?;
161 let size = Quantity::from_decimal_dp(level.qty, size_precision)?;
162 let order_id = price.raw as u64;
163 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
164 deltas.push(OrderBookDelta::new(
165 instrument_id,
166 BookAction::Add,
167 order,
168 0,
169 seq,
170 ts_event,
171 ts_init,
172 ));
173 seq += 1;
174 }
175
176 Ok(deltas)
177}
178
179pub fn parse_futures_ws_book_delta(
180 delta: &KrakenFuturesBookDelta,
181 instrument: &InstrumentAny,
182 sequence: u64,
183 ts_init: UnixNanos,
184) -> anyhow::Result<OrderBookDelta> {
185 let price_precision = instrument.price_precision();
186 let size_precision = instrument.size_precision();
187
188 let price = Price::from_decimal_dp(delta.price, price_precision)?;
189 let size = Quantity::from_decimal_dp(delta.qty, size_precision)?;
190
191 let action = if size.raw == 0 {
192 BookAction::Delete
193 } else {
194 BookAction::Update
195 };
196
197 let side = match delta.side {
198 KrakenOrderSide::Buy => OrderSide::Buy,
199 KrakenOrderSide::Sell => OrderSide::Sell,
200 };
201
202 let order_id = price.raw as u64;
203 let order = BookOrder::new(side, price, size, order_id);
204 let ts_event = millis_to_nanos(delta.timestamp);
205
206 Ok(OrderBookDelta::new(
207 instrument.id(),
208 action,
209 order,
210 0,
211 sequence,
212 ts_event,
213 ts_init,
214 ))
215}
216
217fn parse_ws_direction(direction: i32) -> OrderSide {
218 if direction == 0 {
219 OrderSide::Buy
220 } else {
221 OrderSide::Sell
222 }
223}
224
225fn infer_order_status(order: &KrakenFuturesOpenOrder, is_cancel: bool) -> OrderStatus {
226 if order.filled >= order.qty && order.qty > Decimal::ZERO {
227 OrderStatus::Filled
228 } else if is_cancel {
229 OrderStatus::Canceled
230 } else if order.filled > Decimal::ZERO {
231 OrderStatus::PartiallyFilled
232 } else {
233 OrderStatus::Accepted
234 }
235}
236
237pub fn parse_futures_ws_order_status_report(
238 order: &KrakenFuturesOpenOrder,
239 is_cancel: bool,
240 reason: Option<&str>,
241 instrument: &InstrumentAny,
242 account_id: AccountId,
243 ts_init: UnixNanos,
244) -> anyhow::Result<OrderStatusReport> {
245 let venue_order_id = VenueOrderId::new(&order.order_id);
246 let order_side = parse_ws_direction(order.direction);
247 let order_type = OrderType::from(order.order_type);
248 let order_type = if order_type == OrderType::MarketIfTouched && order.limit_price.is_some() {
249 OrderType::LimitIfTouched
250 } else {
251 order_type
252 };
253 let order_status = infer_order_status(order, is_cancel);
254
255 let price_precision = instrument.price_precision();
256 let size_precision = instrument.size_precision();
257
258 let quantity = Quantity::from_decimal_dp(order.qty, size_precision)
259 .context("Failed to parse order qty")?;
260 let filled_qty = Quantity::from_decimal_dp(order.filled, size_precision)
261 .context("Failed to parse order filled")?;
262
263 let ts_accepted = millis_to_nanos(order.time);
264 let ts_last = millis_to_nanos(order.last_update_time);
265
266 let mut report = OrderStatusReport {
267 account_id,
268 instrument_id: instrument.id(),
269 client_order_id: order.cli_ord_id.as_ref().map(ClientOrderId::new),
270 venue_order_id,
271 order_side: order_side.into(),
272 order_type,
273 time_in_force: TimeInForce::Gtc,
274 order_status,
275 quantity,
276 filled_qty,
277 report_id: UUID4::new(),
278 ts_accepted,
279 ts_last,
280 ts_init,
281 order_list_id: None,
282 venue_position_id: None,
283 linked_order_ids: None,
284 parent_order_id: None,
285 contingency_type: None,
286 expire_time: None,
287 price: None,
288 activation_price: None,
289 trigger_price: None,
290 trigger_type: None,
291 limit_offset: None,
292 trailing_offset: None,
293 trailing_offset_type: None,
294 display_qty: None,
295 avg_px: None,
296 post_only: false,
297 reduce_only: order.reduce_only,
298 cancel_reason: None,
299 ts_triggered: None,
300 };
301
302 if let Some(px) = order.limit_price {
303 report.price = Some(Price::from_decimal_dp(px, price_precision)?);
304 }
305
306 if let Some(px) = order.stop_price {
307 report.trigger_price = Some(Price::from_decimal_dp(px, price_precision)?);
308 report.trigger_type = Some(order.trigger_signal.as_deref().map_or(
309 TriggerType::Default,
310 |s| match s {
311 "mark" | "mark_price" => TriggerType::MarkPrice,
312 "spot" | "spot_price" | "index" | "index_price" => TriggerType::IndexPrice,
313 _ => TriggerType::LastPrice,
314 },
315 ));
316 }
317
318 if let Some(reason) = reason
319 && !reason.is_empty()
320 {
321 report.cancel_reason = Some(reason.to_string());
322 }
323
324 Ok(report)
325}
326
327pub fn parse_futures_ws_fill_report(
328 fill: &KrakenFuturesFill,
329 instrument: &InstrumentAny,
330 account_id: AccountId,
331 ts_init: UnixNanos,
332) -> anyhow::Result<FillReport> {
333 let price_precision = instrument.price_precision();
334 let size_precision = instrument.size_precision();
335
336 let venue_order_id = VenueOrderId::new(&fill.order_id);
337 let trade_id = TradeId::new(&fill.fill_id);
338 let order_side = if fill.buy {
339 OrderSide::Buy
340 } else {
341 OrderSide::Sell
342 };
343
344 let last_qty =
345 Quantity::from_decimal_dp(fill.qty, size_precision).context("Failed to parse fill qty")?;
346 let last_px = Price::from_decimal_dp(fill.price, price_precision)
347 .context("Failed to parse fill price")?;
348
349 let liquidity_side = fill.fill_type.into();
350
351 let fee = fill.fee_paid.unwrap_or(Decimal::ZERO);
352 let commission_currency = instrument.quote_currency();
353 let commission = Money::from_decimal(fee, commission_currency)?;
354
355 let ts_event = millis_to_nanos(fill.time);
356
357 let client_order_id = fill
358 .cli_ord_id
359 .as_ref()
360 .filter(|s| !s.is_empty())
361 .map(ClientOrderId::new);
362
363 Ok(FillReport::new(
364 account_id,
365 instrument.id(),
366 venue_order_id,
367 trade_id,
368 order_side,
369 last_qty,
370 last_px,
371 commission,
372 liquidity_side,
373 client_order_id,
374 None, ts_event,
376 ts_init,
377 None, ))
379}
380
381pub fn parse_futures_ws_mark_price(
382 ticker: &KrakenFuturesTickerData,
383 instrument: &InstrumentAny,
384 ts_init: UnixNanos,
385) -> Option<MarkPriceUpdate> {
386 let mark_price = ticker.mark_price?;
387 let price = Price::from_decimal_dp(mark_price, instrument.price_precision()).ok()?;
388 let ts_event = ticker.time.map_or(ts_init, millis_to_nanos);
389 Some(MarkPriceUpdate::new(
390 instrument.id(),
391 price,
392 ts_event,
393 ts_init,
394 ))
395}
396
397pub fn parse_futures_ws_index_price(
398 ticker: &KrakenFuturesTickerData,
399 instrument: &InstrumentAny,
400 ts_init: UnixNanos,
401) -> Option<IndexPriceUpdate> {
402 let index = ticker.index?;
403 let price = Price::from_decimal_dp(index, instrument.price_precision()).ok()?;
404 let ts_event = ticker.time.map_or(ts_init, millis_to_nanos);
405 Some(IndexPriceUpdate::new(
406 instrument.id(),
407 price,
408 ts_event,
409 ts_init,
410 ))
411}
412
413pub fn parse_futures_ws_funding_rate(
414 ticker: &KrakenFuturesTickerData,
415 instrument: &InstrumentAny,
416 ts_init: UnixNanos,
417) -> Option<FundingRateUpdate> {
418 let rate = ticker.relative_funding_rate?;
419 let ts_event = ticker.time.map_or(ts_init, millis_to_nanos);
420 let next_funding_ns = ticker
421 .next_funding_rate_time
422 .map(|t| millis_to_nanos(t as i64));
423 Some(FundingRateUpdate::new(
424 instrument.id(),
425 rate,
426 None,
427 next_funding_ns,
428 ts_event,
429 ts_init,
430 ))
431}
432
433#[cfg(test)]
434mod tests {
435 use nautilus_model::{
436 enums::{CurrencyType, LiquiditySide, OrderSide},
437 identifiers::{InstrumentId, Symbol},
438 instruments::crypto_perpetual::CryptoPerpetual,
439 types::Currency,
440 };
441 use rstest::rstest;
442 use rust_decimal_macros::dec;
443
444 use super::*;
445 use crate::common::{
446 consts::KRAKEN_VENUE,
447 enums::{KrakenFillType, KrakenFuturesOrderType},
448 };
449
450 const TS: UnixNanos = UnixNanos::new(1_700_000_000_000_000_000);
451
452 fn create_mock_perp() -> InstrumentAny {
453 let instrument_id = InstrumentId::new(Symbol::new("PI_XBTUSD"), *KRAKEN_VENUE);
454 InstrumentAny::CryptoPerpetual(
455 CryptoPerpetual::builder()
456 .instrument_id(instrument_id)
457 .raw_symbol(Symbol::new("PI_XBTUSD"))
458 .base_currency(Currency::BTC())
459 .quote_currency(Currency::USD())
460 .settlement_currency(Currency::USD())
461 .is_inverse(false)
462 .price_precision(1)
463 .size_precision(0)
464 .price_increment(Price::from("0.5"))
465 .size_increment(Quantity::from("1"))
466 .ts_event(TS)
467 .ts_init(TS)
468 .build()
469 .unwrap(),
470 )
471 }
472
473 #[rstest]
474 fn test_parse_futures_ws_quote_tick() {
475 let json = include_str!("../../../test_data/ws_futures_ticker.json");
476 let ticker: KrakenFuturesTickerData = serde_json::from_str(json).unwrap();
477 let instrument = create_mock_perp();
478
479 let quote = parse_futures_ws_quote_tick(&ticker, &instrument, TS).unwrap();
480
481 assert_eq!(quote.instrument_id, instrument.id());
482 assert_eq!(quote.bid_price, Price::from("21978.5"));
483 assert_eq!(quote.ask_price, Price::from("21987.0"));
484 assert_eq!(quote.bid_size, Quantity::from("2536.0"));
485 assert_eq!(quote.ask_size, Quantity::from("13948.0"));
486 }
487
488 #[rstest]
489 fn test_parse_futures_ws_trade_tick() {
490 let json = include_str!("../../../test_data/ws_futures_trade.json");
491 let trade: KrakenFuturesTradeData = serde_json::from_str(json).unwrap();
492 let instrument = create_mock_perp();
493
494 let tick = parse_futures_ws_trade_tick(&trade, &instrument, TS).unwrap();
495
496 assert_eq!(tick.instrument_id, instrument.id());
497 assert_eq!(tick.price, Price::from("34969.5"));
498 assert_eq!(tick.size, Quantity::from("15000.0"));
499 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
500 }
501
502 #[rstest]
503 fn test_parse_futures_ws_book_snapshot() {
504 let json = include_str!("../../../test_data/ws_futures_book_snapshot.json");
505 let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
506 let instrument = create_mock_perp();
507
508 let deltas = parse_futures_ws_book_snapshot_deltas(&snapshot, &instrument, 0, TS).unwrap();
509
510 assert_eq!(deltas.len(), 5);
512 assert_eq!(deltas[0].action, BookAction::Clear);
513 assert_eq!(deltas[1].action, BookAction::Add);
514 assert_eq!(deltas[1].order.side, OrderSide::Buy.into());
515 assert_eq!(deltas[3].order.side, OrderSide::Sell.into());
516 }
517
518 #[rstest]
519 fn test_parse_futures_ws_book_snapshot_skips_zero_qty() {
520 let json = include_str!("../../../test_data/ws_futures_book_snapshot_with_zero_qty.json");
521 let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
522 let instrument = create_mock_perp();
523
524 let deltas = parse_futures_ws_book_snapshot_deltas(&snapshot, &instrument, 0, TS).unwrap();
525
526 assert_eq!(deltas.len(), 4);
528 assert_eq!(deltas[0].action, BookAction::Clear);
529 assert_eq!(deltas[1].order.side, OrderSide::Buy.into());
530 assert_eq!(deltas[1].order.price, Price::from("34892.5"));
531 assert_eq!(deltas[2].order.side, OrderSide::Buy.into());
532 assert_eq!(deltas[2].order.price, Price::from("34891.5"));
533 assert_eq!(deltas[3].order.side, OrderSide::Sell.into());
534 assert_eq!(deltas[3].order.price, Price::from("34912.0"));
535 }
536
537 #[rstest]
538 fn test_parse_futures_ws_book_delta() {
539 let json = include_str!("../../../test_data/ws_futures_book_delta.json");
540 let delta_msg: KrakenFuturesBookDelta = serde_json::from_str(json).unwrap();
541 let instrument = create_mock_perp();
542
543 let delta = parse_futures_ws_book_delta(&delta_msg, &instrument, 10, TS).unwrap();
544
545 assert_eq!(delta.instrument_id, instrument.id());
546 assert_eq!(delta.order.side, OrderSide::Sell.into());
547 assert_eq!(delta.action, BookAction::Delete); assert_eq!(delta.sequence, 10);
549 }
550
551 #[rstest]
552 fn test_parse_futures_ws_order_status_report_new_order() {
553 let order = KrakenFuturesOpenOrder {
554 instrument: ustr::Ustr::from("PI_XBTUSD"),
555 time: 1700000000000,
556 last_update_time: 1700000000100,
557 qty: dec!(1000),
558 filled: Decimal::ZERO,
559 limit_price: Some(dec!(35000)),
560 stop_price: None,
561 order_type: KrakenFuturesOrderType::Limit,
562 order_id: "abc-123".to_string(),
563 cli_ord_id: Some("my-order-1".to_string()),
564 direction: 0,
565 reduce_only: false,
566 trigger_signal: None,
567 };
568 let instrument = create_mock_perp();
569 let account_id = AccountId::from("KRAKEN-001");
570
571 let report =
572 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
573 .unwrap();
574
575 assert_eq!(report.order_status, OrderStatus::Accepted);
576 assert_eq!(report.order_side, OrderSide::Buy.into());
577 assert_eq!(report.order_type, OrderType::Limit);
578 assert_eq!(report.quantity.as_decimal(), dec!(1000));
579 assert_eq!(report.filled_qty.as_decimal(), Decimal::ZERO);
580 assert_eq!(report.price.unwrap().as_decimal(), dec!(35000));
581 }
582
583 #[rstest]
584 fn test_parse_futures_ws_order_status_report_canceled() {
585 let order = KrakenFuturesOpenOrder {
586 instrument: ustr::Ustr::from("PI_XBTUSD"),
587 time: 1700000000000,
588 last_update_time: 1700000001000,
589 qty: dec!(1000),
590 filled: Decimal::ZERO,
591 limit_price: Some(dec!(35000)),
592 stop_price: None,
593 order_type: KrakenFuturesOrderType::Limit,
594 order_id: "abc-123".to_string(),
595 cli_ord_id: None,
596 direction: 1,
597 reduce_only: false,
598 trigger_signal: None,
599 };
600 let instrument = create_mock_perp();
601 let account_id = AccountId::from("KRAKEN-001");
602
603 let report = parse_futures_ws_order_status_report(
604 &order,
605 true,
606 Some("cancelled_by_user"),
607 &instrument,
608 account_id,
609 TS,
610 )
611 .unwrap();
612
613 assert_eq!(report.order_status, OrderStatus::Canceled);
614 assert_eq!(report.order_side, OrderSide::Sell.into());
615 assert_eq!(report.cancel_reason.as_deref(), Some("cancelled_by_user"));
616 }
617
618 #[rstest]
619 fn test_parse_futures_ws_order_status_report_market_if_touched() {
620 let order = KrakenFuturesOpenOrder {
621 instrument: ustr::Ustr::from("PI_XBTUSD"),
622 time: 1700000000000,
623 last_update_time: 1700000000100,
624 qty: dec!(500),
625 filled: Decimal::ZERO,
626 limit_price: None,
627 stop_price: Some(dec!(36000)),
628 order_type: KrakenFuturesOrderType::TakeProfit,
629 order_id: "tp-001".to_string(),
630 cli_ord_id: Some("my-tp-1".to_string()),
631 direction: 0,
632 reduce_only: true,
633 trigger_signal: None,
634 };
635 let instrument = create_mock_perp();
636 let account_id = AccountId::from("KRAKEN-001");
637
638 let report =
639 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
640 .unwrap();
641
642 assert_eq!(report.order_type, OrderType::MarketIfTouched);
643 assert_eq!(report.trigger_price.unwrap().as_decimal(), dec!(36000));
644 assert!(report.price.is_none());
645 assert!(report.reduce_only);
646 }
647
648 #[rstest]
649 fn test_parse_futures_ws_order_status_report_limit_if_touched() {
650 let order = KrakenFuturesOpenOrder {
651 instrument: ustr::Ustr::from("PI_XBTUSD"),
652 time: 1700000000000,
653 last_update_time: 1700000000100,
654 qty: dec!(500),
655 filled: Decimal::ZERO,
656 limit_price: Some(dec!(35500)),
657 stop_price: Some(dec!(36000)),
658 order_type: KrakenFuturesOrderType::TakeProfit,
659 order_id: "tpl-001".to_string(),
660 cli_ord_id: Some("my-tpl-1".to_string()),
661 direction: 1,
662 reduce_only: false,
663 trigger_signal: None,
664 };
665 let instrument = create_mock_perp();
666 let account_id = AccountId::from("KRAKEN-001");
667
668 let report =
669 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
670 .unwrap();
671
672 assert_eq!(report.order_type, OrderType::LimitIfTouched);
673 assert_eq!(report.trigger_price.unwrap().as_decimal(), dec!(36000));
674 assert_eq!(report.price.unwrap().as_decimal(), dec!(35500));
675 assert_eq!(report.order_side, OrderSide::Sell.into());
676 }
677
678 #[rstest]
679 fn test_parse_futures_ws_order_status_report_spot_trigger_signal() {
680 let order = KrakenFuturesOpenOrder {
681 instrument: ustr::Ustr::from("PI_XBTUSD"),
682 time: 1700000000000,
683 last_update_time: 1700000000100,
684 qty: dec!(500),
685 filled: Decimal::ZERO,
686 limit_price: None,
687 stop_price: Some(dec!(36000)),
688 order_type: KrakenFuturesOrderType::TakeProfit,
689 order_id: "tp-spot-001".to_string(),
690 cli_ord_id: Some("my-tp-spot-1".to_string()),
691 direction: 0,
692 reduce_only: false,
693 trigger_signal: Some("spot".to_string()),
694 };
695 let instrument = create_mock_perp();
696 let account_id = AccountId::from("KRAKEN-001");
697
698 let report =
699 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
700 .unwrap();
701
702 assert_eq!(report.trigger_type, Some(TriggerType::IndexPrice));
703 }
704
705 #[rstest]
706 #[case::taker(KrakenFillType::Taker, LiquiditySide::Taker)]
707 #[case::assignee(KrakenFillType::Assignee, LiquiditySide::NoLiquiditySide)]
708 fn test_parse_futures_ws_fill_report(
709 #[case] fill_type: KrakenFillType,
710 #[case] expected_liquidity_side: LiquiditySide,
711 ) {
712 let json = include_str!("../../../test_data/ws_futures_fills_delta.json");
713 let fills_delta: super::super::messages::KrakenFuturesFillsDelta =
714 serde_json::from_str(json).unwrap();
715 let mut fill = fills_delta.fills[0].clone();
716 fill.fill_type = fill_type;
717
718 let instrument_id = InstrumentId::new(Symbol::new("PF_ETHUSD"), *KRAKEN_VENUE);
719 let usd = Currency::new("USD", 6, 0, "USD", CurrencyType::Fiat);
720 let instrument = InstrumentAny::CryptoPerpetual(
721 CryptoPerpetual::builder()
722 .instrument_id(instrument_id)
723 .raw_symbol(Symbol::new("PF_ETHUSD"))
724 .base_currency(Currency::ETH())
725 .quote_currency(usd)
726 .settlement_currency(usd)
727 .is_inverse(false)
728 .price_precision(1)
729 .size_precision(3)
730 .price_increment(Price::from("0.5"))
731 .size_increment(Quantity::from("0.001"))
732 .ts_event(TS)
733 .ts_init(TS)
734 .build()
735 .unwrap(),
736 );
737
738 let account_id = AccountId::from("KRAKEN-001");
739 let report = parse_futures_ws_fill_report(&fill, &instrument, account_id, TS).unwrap();
740
741 assert_eq!(report.instrument_id, instrument_id);
742 assert_eq!(report.order_side, OrderSide::Buy);
743 assert_eq!(report.last_px.as_decimal(), dec!(3162));
744 assert_eq!(report.last_qty.as_decimal(), dec!(0.001));
745 assert_eq!(report.liquidity_side, expected_liquidity_side);
746 assert_eq!(report.commission.as_decimal(), dec!(0.001581));
747 }
748}