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, TS,
479 TS,
480 ))
481 }
482
483 #[rstest]
484 fn test_parse_futures_ws_quote_tick() {
485 let json = include_str!("../../../test_data/ws_futures_ticker.json");
486 let ticker: KrakenFuturesTickerData = serde_json::from_str(json).unwrap();
487 let instrument = create_mock_perp();
488
489 let quote = parse_futures_ws_quote_tick(&ticker, &instrument, TS).unwrap();
490
491 assert_eq!(quote.instrument_id, instrument.id());
492 assert_eq!(quote.bid_price.as_f64(), 21978.5);
493 assert_eq!(quote.ask_price.as_f64(), 21987.0);
494 assert!(quote.bid_size.as_f64() > 0.0);
495 assert!(quote.ask_size.as_f64() > 0.0);
496 }
497
498 #[rstest]
499 fn test_parse_futures_ws_trade_tick() {
500 let json = include_str!("../../../test_data/ws_futures_trade.json");
501 let trade: KrakenFuturesTradeData = serde_json::from_str(json).unwrap();
502 let instrument = create_mock_perp();
503
504 let tick = parse_futures_ws_trade_tick(&trade, &instrument, TS).unwrap();
505
506 assert_eq!(tick.instrument_id, instrument.id());
507 assert_eq!(tick.price.as_f64(), 34969.5);
508 assert_eq!(tick.size.as_f64(), 15000.0);
509 assert_eq!(tick.aggressor_side, AggressorSide::Seller);
510 }
511
512 #[rstest]
513 fn test_parse_futures_ws_book_snapshot() {
514 let json = include_str!("../../../test_data/ws_futures_book_snapshot.json");
515 let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
516 let instrument = create_mock_perp();
517
518 let deltas = parse_futures_ws_book_snapshot_deltas(&snapshot, &instrument, 0, TS).unwrap();
519
520 assert_eq!(deltas.len(), 5);
522 assert_eq!(deltas[0].action, BookAction::Clear);
523 assert_eq!(deltas[1].action, BookAction::Add);
524 assert_eq!(deltas[1].order.side, OrderSide::Buy);
525 assert_eq!(deltas[3].order.side, OrderSide::Sell);
526 }
527
528 #[rstest]
529 fn test_parse_futures_ws_book_snapshot_skips_zero_qty() {
530 let json = include_str!("../../../test_data/ws_futures_book_snapshot_with_zero_qty.json");
531 let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
532 let instrument = create_mock_perp();
533
534 let deltas = parse_futures_ws_book_snapshot_deltas(&snapshot, &instrument, 0, TS).unwrap();
535
536 assert_eq!(deltas.len(), 4);
538 assert_eq!(deltas[0].action, BookAction::Clear);
539 assert_eq!(deltas[1].order.side, OrderSide::Buy);
540 assert_eq!(deltas[1].order.price.as_f64(), 34892.5);
541 assert_eq!(deltas[2].order.side, OrderSide::Buy);
542 assert_eq!(deltas[2].order.price.as_f64(), 34891.5);
543 assert_eq!(deltas[3].order.side, OrderSide::Sell);
544 assert_eq!(deltas[3].order.price.as_f64(), 34912.0);
545 }
546
547 #[rstest]
548 fn test_parse_futures_ws_book_delta() {
549 let json = include_str!("../../../test_data/ws_futures_book_delta.json");
550 let delta_msg: KrakenFuturesBookDelta = serde_json::from_str(json).unwrap();
551 let instrument = create_mock_perp();
552
553 let delta = parse_futures_ws_book_delta(&delta_msg, &instrument, 10, TS).unwrap();
554
555 assert_eq!(delta.instrument_id, instrument.id());
556 assert_eq!(delta.order.side, OrderSide::Sell);
557 assert_eq!(delta.action, BookAction::Delete); assert_eq!(delta.sequence, 10);
559 }
560
561 #[rstest]
562 fn test_parse_futures_ws_order_status_report_new_order() {
563 let order = KrakenFuturesOpenOrder {
564 instrument: ustr::Ustr::from("PI_XBTUSD"),
565 time: 1700000000000,
566 last_update_time: 1700000000100,
567 qty: 1000.0,
568 filled: 0.0,
569 limit_price: Some(35000.0),
570 stop_price: None,
571 order_type: KrakenFuturesOrderType::Limit,
572 order_id: "abc-123".to_string(),
573 cli_ord_id: Some("my-order-1".to_string()),
574 direction: 0,
575 reduce_only: false,
576 trigger_signal: None,
577 };
578 let instrument = create_mock_perp();
579 let account_id = AccountId::from("KRAKEN-001");
580
581 let report =
582 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
583 .unwrap();
584
585 assert_eq!(report.order_status, OrderStatus::Accepted);
586 assert_eq!(report.order_side, OrderSide::Buy);
587 assert_eq!(report.order_type, OrderType::Limit);
588 assert_eq!(report.quantity.as_f64(), 1000.0);
589 assert_eq!(report.filled_qty.as_f64(), 0.0);
590 assert_eq!(report.price.unwrap().as_f64(), 35000.0);
591 }
592
593 #[rstest]
594 fn test_parse_futures_ws_order_status_report_canceled() {
595 let order = KrakenFuturesOpenOrder {
596 instrument: ustr::Ustr::from("PI_XBTUSD"),
597 time: 1700000000000,
598 last_update_time: 1700000001000,
599 qty: 1000.0,
600 filled: 0.0,
601 limit_price: Some(35000.0),
602 stop_price: None,
603 order_type: KrakenFuturesOrderType::Limit,
604 order_id: "abc-123".to_string(),
605 cli_ord_id: None,
606 direction: 1,
607 reduce_only: false,
608 trigger_signal: None,
609 };
610 let instrument = create_mock_perp();
611 let account_id = AccountId::from("KRAKEN-001");
612
613 let report = parse_futures_ws_order_status_report(
614 &order,
615 true,
616 Some("cancelled_by_user"),
617 &instrument,
618 account_id,
619 TS,
620 )
621 .unwrap();
622
623 assert_eq!(report.order_status, OrderStatus::Canceled);
624 assert_eq!(report.order_side, OrderSide::Sell);
625 assert_eq!(report.cancel_reason.as_deref(), Some("cancelled_by_user"));
626 }
627
628 #[rstest]
629 fn test_parse_futures_ws_order_status_report_market_if_touched() {
630 let order = KrakenFuturesOpenOrder {
631 instrument: ustr::Ustr::from("PI_XBTUSD"),
632 time: 1700000000000,
633 last_update_time: 1700000000100,
634 qty: 500.0,
635 filled: 0.0,
636 limit_price: None,
637 stop_price: Some(36000.0),
638 order_type: KrakenFuturesOrderType::TakeProfit,
639 order_id: "tp-001".to_string(),
640 cli_ord_id: Some("my-tp-1".to_string()),
641 direction: 0,
642 reduce_only: true,
643 trigger_signal: None,
644 };
645 let instrument = create_mock_perp();
646 let account_id = AccountId::from("KRAKEN-001");
647
648 let report =
649 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
650 .unwrap();
651
652 assert_eq!(report.order_type, OrderType::MarketIfTouched);
653 assert_eq!(report.trigger_price.unwrap().as_f64(), 36000.0);
654 assert!(report.price.is_none());
655 assert!(report.reduce_only);
656 }
657
658 #[rstest]
659 fn test_parse_futures_ws_order_status_report_limit_if_touched() {
660 let order = KrakenFuturesOpenOrder {
661 instrument: ustr::Ustr::from("PI_XBTUSD"),
662 time: 1700000000000,
663 last_update_time: 1700000000100,
664 qty: 500.0,
665 filled: 0.0,
666 limit_price: Some(35500.0),
667 stop_price: Some(36000.0),
668 order_type: KrakenFuturesOrderType::TakeProfit,
669 order_id: "tpl-001".to_string(),
670 cli_ord_id: Some("my-tpl-1".to_string()),
671 direction: 1,
672 reduce_only: false,
673 trigger_signal: None,
674 };
675 let instrument = create_mock_perp();
676 let account_id = AccountId::from("KRAKEN-001");
677
678 let report =
679 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
680 .unwrap();
681
682 assert_eq!(report.order_type, OrderType::LimitIfTouched);
683 assert_eq!(report.trigger_price.unwrap().as_f64(), 36000.0);
684 assert_eq!(report.price.unwrap().as_f64(), 35500.0);
685 assert_eq!(report.order_side, OrderSide::Sell);
686 }
687
688 #[rstest]
689 fn test_parse_futures_ws_order_status_report_spot_trigger_signal() {
690 let order = KrakenFuturesOpenOrder {
691 instrument: ustr::Ustr::from("PI_XBTUSD"),
692 time: 1700000000000,
693 last_update_time: 1700000000100,
694 qty: 500.0,
695 filled: 0.0,
696 limit_price: None,
697 stop_price: Some(36000.0),
698 order_type: KrakenFuturesOrderType::TakeProfit,
699 order_id: "tp-spot-001".to_string(),
700 cli_ord_id: Some("my-tp-spot-1".to_string()),
701 direction: 0,
702 reduce_only: false,
703 trigger_signal: Some("spot".to_string()),
704 };
705 let instrument = create_mock_perp();
706 let account_id = AccountId::from("KRAKEN-001");
707
708 let report =
709 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
710 .unwrap();
711
712 assert_eq!(report.trigger_type, Some(TriggerType::IndexPrice));
713 }
714
715 #[rstest]
716 fn test_parse_futures_ws_fill_report() {
717 let json = include_str!("../../../test_data/ws_futures_fills_delta.json");
718 let fills_delta: super::super::messages::KrakenFuturesFillsDelta =
719 serde_json::from_str(json).unwrap();
720 let fill = &fills_delta.fills[0];
721
722 let instrument_id = InstrumentId::new(Symbol::new("PF_ETHUSD"), *KRAKEN_VENUE);
723 let usd = Currency::new("USD", 6, 0, "USD", CurrencyType::Fiat);
724 let instrument = InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
725 instrument_id,
726 Symbol::new("PF_ETHUSD"),
727 Currency::ETH(),
728 usd,
729 usd,
730 false,
731 1,
732 3,
733 Price::from("0.5"),
734 Quantity::from("0.001"),
735 None,
736 None,
737 None,
738 None,
739 None,
740 None,
741 None,
742 None,
743 None,
744 None,
745 None,
746 None,
747 None, TS,
749 TS,
750 ));
751
752 let account_id = AccountId::from("KRAKEN-001");
753 let report = parse_futures_ws_fill_report(fill, &instrument, account_id, TS).unwrap();
754
755 assert_eq!(report.instrument_id, instrument_id);
756 assert_eq!(report.order_side, OrderSide::Buy);
757 assert_eq!(report.last_px.as_f64(), 3162.0);
758 assert_eq!(report.last_qty.as_f64(), 0.001);
759 assert_eq!(report.liquidity_side, LiquiditySide::Taker);
760 assert_eq!(report.commission.as_f64(), 0.001581);
761 }
762}