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