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, parse::fee_currency};
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
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 <= Decimal::ZERO {
159 continue;
160 }
161 let price = Price::from_decimal_dp(level.price, price_precision)?;
162 let size = Quantity::from_decimal_dp(level.qty, size_precision)?;
163 let order_id = price.raw() as u64;
164
165 let order = BookOrder::new(OrderSide::Sell, price, size, order_id);
166 deltas.push(OrderBookDelta::new(
167 instrument_id,
168 BookAction::Add,
169 order,
170 0,
171 seq,
172 ts_event,
173 ts_init,
174 ));
175 seq += 1;
176 }
177
178 Ok(deltas)
179}
180
181pub fn parse_futures_ws_book_delta(
182 delta: &KrakenFuturesBookDelta,
183 instrument: &InstrumentAny,
184 sequence: u64,
185 ts_init: UnixNanos,
186) -> anyhow::Result<OrderBookDelta> {
187 let price_precision = instrument.price_precision();
188 let size_precision = instrument.size_precision();
189
190 let price = Price::from_decimal_dp(delta.price, price_precision)?;
191 let size = Quantity::from_decimal_dp(delta.qty, size_precision)?;
192
193 let action = if size.is_zero() {
194 BookAction::Delete
195 } else {
196 BookAction::Update
197 };
198
199 let side = match delta.side {
200 KrakenOrderSide::Buy => OrderSide::Buy,
201 KrakenOrderSide::Sell => OrderSide::Sell,
202 };
203
204 let order_id = price.raw() as u64;
205
206 let order = BookOrder::new(side, price, size, order_id);
207 let ts_event = millis_to_nanos(delta.timestamp);
208
209 Ok(OrderBookDelta::new(
210 instrument.id(),
211 action,
212 order,
213 0,
214 sequence,
215 ts_event,
216 ts_init,
217 ))
218}
219
220fn parse_ws_direction(direction: i32) -> OrderSide {
221 if direction == 0 {
222 OrderSide::Buy
223 } else {
224 OrderSide::Sell
225 }
226}
227
228fn infer_order_status(order: &KrakenFuturesOpenOrder, is_cancel: bool) -> OrderStatus {
229 if order.filled >= order.qty && order.qty > Decimal::ZERO {
230 OrderStatus::Filled
231 } else if is_cancel {
232 OrderStatus::Canceled
233 } else if order.filled > Decimal::ZERO {
234 OrderStatus::PartiallyFilled
235 } else {
236 OrderStatus::Accepted
237 }
238}
239
240pub fn parse_futures_ws_order_status_report(
241 order: &KrakenFuturesOpenOrder,
242 is_cancel: bool,
243 reason: Option<&str>,
244 instrument: &InstrumentAny,
245 account_id: AccountId,
246 ts_init: UnixNanos,
247) -> anyhow::Result<OrderStatusReport> {
248 let venue_order_id = VenueOrderId::new(&order.order_id);
249 let order_side = parse_ws_direction(order.direction);
250 let order_type = OrderType::from(order.order_type);
251 let order_type = if order_type == OrderType::MarketIfTouched && order.limit_price.is_some() {
252 OrderType::LimitIfTouched
253 } else {
254 order_type
255 };
256 let order_status = infer_order_status(order, is_cancel);
257
258 let price_precision = instrument.price_precision();
259 let size_precision = instrument.size_precision();
260
261 let quantity = Quantity::from_decimal_dp(order.qty, size_precision)
262 .context("Failed to parse order qty")?;
263 let filled_qty = Quantity::from_decimal_dp(order.filled, size_precision)
264 .context("Failed to parse order filled")?;
265
266 let ts_accepted = millis_to_nanos(order.time);
267 let ts_last = millis_to_nanos(order.last_update_time);
268
269 let mut report = OrderStatusReport {
270 account_id,
271 instrument_id: instrument.id(),
272 client_order_id: order.cli_ord_id.as_ref().map(ClientOrderId::new),
273 venue_order_id,
274 order_side: order_side.into(),
275 order_type,
276 time_in_force: TimeInForce::Gtc,
277 order_status,
278 quantity,
279 filled_qty,
280 report_id: UUID4::new(),
281 ts_accepted,
282 ts_last,
283 ts_init,
284 order_list_id: None,
285 venue_position_id: None,
286 linked_order_ids: None,
287 parent_order_id: None,
288 contingency_type: None,
289 expire_time: None,
290 price: None,
291 activation_price: None,
292 trigger_price: None,
293 trigger_type: None,
294 limit_offset: None,
295 trailing_offset: None,
296 trailing_offset_type: None,
297 display_qty: None,
298 avg_px: None,
299 post_only: false,
300 reduce_only: order.reduce_only,
301 cancel_reason: None,
302 ts_triggered: None,
303 };
304
305 if let Some(px) = order.limit_price {
306 report.price = Some(Price::from_decimal_dp(px, price_precision)?);
307 }
308
309 if let Some(px) = order.stop_price {
310 report.trigger_price = Some(Price::from_decimal_dp(px, price_precision)?);
311 report.trigger_type = Some(order.trigger_signal.as_deref().map_or(
312 TriggerType::Default,
313 |s| match s {
314 "mark" | "mark_price" => TriggerType::MarkPrice,
315 "spot" | "spot_price" | "index" | "index_price" => TriggerType::IndexPrice,
316 _ => TriggerType::LastPrice,
317 },
318 ));
319 }
320
321 if let Some(reason) = reason
322 && !reason.is_empty()
323 {
324 report.cancel_reason = Some(reason.to_string());
325 }
326
327 Ok(report)
328}
329
330pub fn parse_futures_ws_fill_report(
331 fill: &KrakenFuturesFill,
332 instrument: &InstrumentAny,
333 account_id: AccountId,
334 ts_init: UnixNanos,
335) -> anyhow::Result<FillReport> {
336 let price_precision = instrument.price_precision();
337 let size_precision = instrument.size_precision();
338
339 let venue_order_id = VenueOrderId::new(&fill.order_id);
340 let trade_id = TradeId::new(&fill.fill_id);
341 let order_side = if fill.buy {
342 OrderSide::Buy
343 } else {
344 OrderSide::Sell
345 };
346
347 let last_qty =
348 Quantity::from_decimal_dp(fill.qty, size_precision).context("Failed to parse fill qty")?;
349 let last_px = Price::from_decimal_dp(fill.price, price_precision)
350 .context("Failed to parse fill price")?;
351
352 let liquidity_side = fill.fill_type.into();
353
354 let fee = fill.fee_paid.unwrap_or(Decimal::ZERO);
355 let commission_currency = fill.fee_currency.as_deref().map_or_else(
357 || instrument.quote_currency(),
358 |reported| fee_currency(reported, instrument),
359 );
360 let commission = Money::from_decimal(fee, commission_currency)?;
361
362 let ts_event = millis_to_nanos(fill.time);
363
364 let client_order_id = fill
365 .cli_ord_id
366 .as_ref()
367 .filter(|s| !s.is_empty())
368 .map(ClientOrderId::new);
369
370 Ok(FillReport::new(
371 account_id,
372 instrument.id(),
373 venue_order_id,
374 trade_id,
375 order_side,
376 last_qty,
377 last_px,
378 commission,
379 liquidity_side,
380 client_order_id,
381 None, ts_event,
383 ts_init,
384 None, ))
386}
387
388pub fn parse_futures_ws_mark_price(
389 ticker: &KrakenFuturesTickerData,
390 instrument: &InstrumentAny,
391 ts_init: UnixNanos,
392) -> Option<MarkPriceUpdate> {
393 let mark_price = ticker.mark_price?;
394 let price = Price::from_decimal_dp(mark_price, instrument.price_precision()).ok()?;
395 let ts_event = ticker.time.map_or(ts_init, millis_to_nanos);
396 Some(MarkPriceUpdate::new(
397 instrument.id(),
398 price,
399 ts_event,
400 ts_init,
401 ))
402}
403
404pub fn parse_futures_ws_index_price(
405 ticker: &KrakenFuturesTickerData,
406 instrument: &InstrumentAny,
407 ts_init: UnixNanos,
408) -> Option<IndexPriceUpdate> {
409 let index = ticker.index?;
410 let price = Price::from_decimal_dp(index, instrument.price_precision()).ok()?;
411 let ts_event = ticker.time.map_or(ts_init, millis_to_nanos);
412 Some(IndexPriceUpdate::new(
413 instrument.id(),
414 price,
415 ts_event,
416 ts_init,
417 ))
418}
419
420pub fn parse_futures_ws_funding_rate(
421 ticker: &KrakenFuturesTickerData,
422 instrument: &InstrumentAny,
423 ts_init: UnixNanos,
424) -> Option<FundingRateUpdate> {
425 let rate = ticker.relative_funding_rate?;
426 let ts_event = ticker.time.map_or(ts_init, millis_to_nanos);
427 let next_funding_ns = ticker
428 .next_funding_rate_time
429 .map(|t| millis_to_nanos(t as i64));
430 Some(FundingRateUpdate::new(
431 instrument.id(),
432 rate,
433 None,
434 next_funding_ns,
435 ts_event,
436 ts_init,
437 ))
438}
439
440#[cfg(test)]
441mod tests {
442 use nautilus_model::{
443 enums::{CurrencyType, LiquiditySide, OrderSide},
444 identifiers::{InstrumentId, Symbol},
445 instruments::crypto_perpetual::CryptoPerpetual,
446 types::Currency,
447 };
448 use rstest::rstest;
449 use rust_decimal_macros::dec;
450
451 use super::*;
452 use crate::common::{
453 consts::KRAKEN_VENUE,
454 enums::{KrakenFillType, KrakenFuturesOrderType},
455 };
456
457 const TS: UnixNanos = UnixNanos::new(1_700_000_000_000_000_000);
458
459 fn create_mock_perp() -> InstrumentAny {
460 let instrument_id = InstrumentId::new(Symbol::new("PI_XBTUSD"), *KRAKEN_VENUE);
461 InstrumentAny::CryptoPerpetual(
462 CryptoPerpetual::builder()
463 .instrument_id(instrument_id)
464 .raw_symbol(Symbol::new("PI_XBTUSD"))
465 .base_currency(Currency::BTC())
466 .quote_currency(Currency::USD())
467 .settlement_currency(Currency::USD())
468 .is_inverse(false)
469 .price_precision(1)
470 .size_precision(0)
471 .price_increment(Price::from("0.5"))
472 .size_increment(Quantity::from("1"))
473 .ts_event(TS)
474 .ts_init(TS)
475 .build()
476 .unwrap(),
477 )
478 }
479
480 #[rstest]
481 fn test_parse_futures_ws_quote_tick() {
482 let json = include_str!("../../../test_data/ws_futures_ticker.json");
483 let ticker: KrakenFuturesTickerData = serde_json::from_str(json).unwrap();
484 let instrument = create_mock_perp();
485
486 let quote = parse_futures_ws_quote_tick(&ticker, &instrument, TS).unwrap();
487
488 assert_eq!(quote.instrument_id, instrument.id());
489 assert_eq!(quote.bid_price, Price::from("21978.5"));
490 assert_eq!(quote.ask_price, Price::from("21987.0"));
491 assert_eq!(quote.bid_size, Quantity::from("2536.0"));
492 assert_eq!(quote.ask_size, Quantity::from("13948.0"));
493 }
494
495 #[rstest]
496 fn test_parse_futures_ws_trade_tick() {
497 let json = include_str!("../../../test_data/ws_futures_trade.json");
498 let trade: KrakenFuturesTradeData = serde_json::from_str(json).unwrap();
499 let instrument = create_mock_perp();
500
501 let tick = parse_futures_ws_trade_tick(&trade, &instrument, TS).unwrap();
502
503 assert_eq!(tick.instrument_id, instrument.id());
504 assert_eq!(tick.price, Price::from("34969.5"));
505 assert_eq!(tick.size, Quantity::from("15000.0"));
506 assert_eq!(tick.aggressor_side, AggressorSide::Sell);
507 }
508
509 #[rstest]
510 fn test_parse_futures_ws_book_snapshot() {
511 let json = include_str!("../../../test_data/ws_futures_book_snapshot.json");
512 let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
513 let instrument = create_mock_perp();
514
515 let deltas = parse_futures_ws_book_snapshot_deltas(&snapshot, &instrument, 0, TS).unwrap();
516
517 assert_eq!(deltas.len(), 5);
519 assert_eq!(deltas[0].action, BookAction::Clear);
520 assert_eq!(deltas[1].action, BookAction::Add);
521 assert_eq!(deltas[1].order.side, OrderSide::Buy.into());
522 assert_eq!(deltas[3].order.side, OrderSide::Sell.into());
523 }
524
525 #[rstest]
526 fn test_parse_futures_ws_book_snapshot_skips_zero_qty() {
527 let json = include_str!("../../../test_data/ws_futures_book_snapshot_with_zero_qty.json");
528 let snapshot: KrakenFuturesBookSnapshot = serde_json::from_str(json).unwrap();
529 let instrument = create_mock_perp();
530
531 let deltas = parse_futures_ws_book_snapshot_deltas(&snapshot, &instrument, 0, TS).unwrap();
532
533 assert_eq!(deltas.len(), 4);
535 assert_eq!(deltas[0].action, BookAction::Clear);
536 assert_eq!(deltas[1].order.side, OrderSide::Buy.into());
537 assert_eq!(deltas[1].order.price, Price::from("34892.5"));
538 assert_eq!(deltas[2].order.side, OrderSide::Buy.into());
539 assert_eq!(deltas[2].order.price, Price::from("34891.5"));
540 assert_eq!(deltas[3].order.side, OrderSide::Sell.into());
541 assert_eq!(deltas[3].order.price, Price::from("34912.0"));
542 }
543
544 #[rstest]
545 fn test_parse_futures_ws_book_delta() {
546 let json = include_str!("../../../test_data/ws_futures_book_delta.json");
547 let delta_msg: KrakenFuturesBookDelta = serde_json::from_str(json).unwrap();
548 let instrument = create_mock_perp();
549
550 let delta = parse_futures_ws_book_delta(&delta_msg, &instrument, 10, TS).unwrap();
551
552 assert_eq!(delta.instrument_id, instrument.id());
553 assert_eq!(delta.order.side, OrderSide::Sell.into());
554 assert_eq!(delta.action, BookAction::Delete); assert_eq!(delta.sequence, 10);
556 }
557
558 #[rstest]
559 fn test_parse_futures_ws_order_status_report_new_order() {
560 let order = KrakenFuturesOpenOrder {
561 instrument: ustr::Ustr::from("PI_XBTUSD"),
562 time: 1700000000000,
563 last_update_time: 1700000000100,
564 qty: dec!(1000),
565 filled: Decimal::ZERO,
566 limit_price: Some(dec!(35000)),
567 stop_price: None,
568 order_type: KrakenFuturesOrderType::Limit,
569 order_id: "abc-123".to_string(),
570 cli_ord_id: Some("my-order-1".to_string()),
571 direction: 0,
572 reduce_only: false,
573 trigger_signal: None,
574 };
575 let instrument = create_mock_perp();
576 let account_id = AccountId::from("KRAKEN-001");
577
578 let report =
579 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
580 .unwrap();
581
582 assert_eq!(report.order_status, OrderStatus::Accepted);
583 assert_eq!(report.order_side, OrderSide::Buy.into());
584 assert_eq!(report.order_type, OrderType::Limit);
585 assert_eq!(report.quantity.as_decimal(), dec!(1000));
586 assert_eq!(report.filled_qty.as_decimal(), Decimal::ZERO);
587 assert_eq!(report.price.unwrap().as_decimal(), dec!(35000));
588 }
589
590 #[rstest]
591 fn test_parse_futures_ws_order_status_report_canceled() {
592 let order = KrakenFuturesOpenOrder {
593 instrument: ustr::Ustr::from("PI_XBTUSD"),
594 time: 1700000000000,
595 last_update_time: 1700000001000,
596 qty: dec!(1000),
597 filled: Decimal::ZERO,
598 limit_price: Some(dec!(35000)),
599 stop_price: None,
600 order_type: KrakenFuturesOrderType::Limit,
601 order_id: "abc-123".to_string(),
602 cli_ord_id: None,
603 direction: 1,
604 reduce_only: false,
605 trigger_signal: None,
606 };
607 let instrument = create_mock_perp();
608 let account_id = AccountId::from("KRAKEN-001");
609
610 let report = parse_futures_ws_order_status_report(
611 &order,
612 true,
613 Some("cancelled_by_user"),
614 &instrument,
615 account_id,
616 TS,
617 )
618 .unwrap();
619
620 assert_eq!(report.order_status, OrderStatus::Canceled);
621 assert_eq!(report.order_side, OrderSide::Sell.into());
622 assert_eq!(report.cancel_reason.as_deref(), Some("cancelled_by_user"));
623 }
624
625 #[rstest]
626 fn test_parse_futures_ws_order_status_report_market_if_touched() {
627 let order = KrakenFuturesOpenOrder {
628 instrument: ustr::Ustr::from("PI_XBTUSD"),
629 time: 1700000000000,
630 last_update_time: 1700000000100,
631 qty: dec!(500),
632 filled: Decimal::ZERO,
633 limit_price: None,
634 stop_price: Some(dec!(36000)),
635 order_type: KrakenFuturesOrderType::TakeProfit,
636 order_id: "tp-001".to_string(),
637 cli_ord_id: Some("my-tp-1".to_string()),
638 direction: 0,
639 reduce_only: true,
640 trigger_signal: None,
641 };
642 let instrument = create_mock_perp();
643 let account_id = AccountId::from("KRAKEN-001");
644
645 let report =
646 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
647 .unwrap();
648
649 assert_eq!(report.order_type, OrderType::MarketIfTouched);
650 assert_eq!(report.trigger_price.unwrap().as_decimal(), dec!(36000));
651 assert!(report.price.is_none());
652 assert!(report.reduce_only);
653 }
654
655 #[rstest]
656 fn test_parse_futures_ws_order_status_report_limit_if_touched() {
657 let order = KrakenFuturesOpenOrder {
658 instrument: ustr::Ustr::from("PI_XBTUSD"),
659 time: 1700000000000,
660 last_update_time: 1700000000100,
661 qty: dec!(500),
662 filled: Decimal::ZERO,
663 limit_price: Some(dec!(35500)),
664 stop_price: Some(dec!(36000)),
665 order_type: KrakenFuturesOrderType::TakeProfit,
666 order_id: "tpl-001".to_string(),
667 cli_ord_id: Some("my-tpl-1".to_string()),
668 direction: 1,
669 reduce_only: false,
670 trigger_signal: None,
671 };
672 let instrument = create_mock_perp();
673 let account_id = AccountId::from("KRAKEN-001");
674
675 let report =
676 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
677 .unwrap();
678
679 assert_eq!(report.order_type, OrderType::LimitIfTouched);
680 assert_eq!(report.trigger_price.unwrap().as_decimal(), dec!(36000));
681 assert_eq!(report.price.unwrap().as_decimal(), dec!(35500));
682 assert_eq!(report.order_side, OrderSide::Sell.into());
683 }
684
685 #[rstest]
686 fn test_parse_futures_ws_order_status_report_spot_trigger_signal() {
687 let order = KrakenFuturesOpenOrder {
688 instrument: ustr::Ustr::from("PI_XBTUSD"),
689 time: 1700000000000,
690 last_update_time: 1700000000100,
691 qty: dec!(500),
692 filled: Decimal::ZERO,
693 limit_price: None,
694 stop_price: Some(dec!(36000)),
695 order_type: KrakenFuturesOrderType::TakeProfit,
696 order_id: "tp-spot-001".to_string(),
697 cli_ord_id: Some("my-tp-spot-1".to_string()),
698 direction: 0,
699 reduce_only: false,
700 trigger_signal: Some("spot".to_string()),
701 };
702 let instrument = create_mock_perp();
703 let account_id = AccountId::from("KRAKEN-001");
704
705 let report =
706 parse_futures_ws_order_status_report(&order, false, None, &instrument, account_id, TS)
707 .unwrap();
708
709 assert_eq!(report.trigger_type, Some(TriggerType::IndexPrice));
710 }
711
712 #[rstest]
713 #[case::taker(KrakenFillType::Taker, LiquiditySide::Taker)]
714 #[case::assignee(KrakenFillType::Assignee, LiquiditySide::NoLiquiditySide)]
715 fn test_parse_futures_ws_fill_report(
716 #[case] fill_type: KrakenFillType,
717 #[case] expected_liquidity_side: LiquiditySide,
718 ) {
719 let json = include_str!("../../../test_data/ws_futures_fills_delta.json");
720 let fills_delta: super::super::messages::KrakenFuturesFillsDelta =
721 serde_json::from_str(json).unwrap();
722 let mut fill = fills_delta.fills[0].clone();
723 fill.fill_type = fill_type;
724
725 let instrument_id = InstrumentId::new(Symbol::new("PF_ETHUSD"), *KRAKEN_VENUE);
726 let usd = Currency::new("USD", 6, 0, "USD", CurrencyType::Fiat);
727 let instrument = InstrumentAny::CryptoPerpetual(
728 CryptoPerpetual::builder()
729 .instrument_id(instrument_id)
730 .raw_symbol(Symbol::new("PF_ETHUSD"))
731 .base_currency(Currency::ETH())
732 .quote_currency(usd)
733 .settlement_currency(usd)
734 .is_inverse(false)
735 .price_precision(1)
736 .size_precision(3)
737 .price_increment(Price::from("0.5"))
738 .size_increment(Quantity::from("0.001"))
739 .ts_event(TS)
740 .ts_init(TS)
741 .build()
742 .unwrap(),
743 );
744
745 let account_id = AccountId::from("KRAKEN-001");
746 let report = parse_futures_ws_fill_report(&fill, &instrument, account_id, TS).unwrap();
747
748 assert_eq!(report.instrument_id, instrument_id);
749 assert_eq!(report.order_side, OrderSide::Buy);
750 assert_eq!(report.last_px.as_decimal(), dec!(3162));
751 assert_eq!(report.last_qty.as_decimal(), dec!(0.001));
752 assert_eq!(report.liquidity_side, expected_liquidity_side);
753 assert_eq!(report.commission.as_decimal(), dec!(0.001581));
754 assert_eq!(report.commission.currency.code, "USD");
756 }
757
758 #[rstest]
763 fn test_parse_futures_ws_fill_report_honors_the_reported_fee_currency() {
764 let json = include_str!("../../../test_data/ws_futures_fills_delta.json");
765 let fills_delta: super::super::messages::KrakenFuturesFillsDelta =
766 serde_json::from_str(json).unwrap();
767 let mut fill = fills_delta.fills[0].clone();
768 fill.fee_currency = Some("XXBT".to_string());
769
770 let instrument_id = InstrumentId::new(Symbol::new("PI_XBTUSD"), *KRAKEN_VENUE);
771 let usd = Currency::new("USD", 6, 0, "USD", CurrencyType::Fiat);
772 let instrument = InstrumentAny::CryptoPerpetual(
773 CryptoPerpetual::builder()
774 .instrument_id(instrument_id)
775 .raw_symbol(Symbol::new("PI_XBTUSD"))
776 .base_currency(Currency::BTC())
777 .quote_currency(usd)
778 .settlement_currency(Currency::BTC())
779 .is_inverse(true)
780 .price_precision(1)
781 .size_precision(3)
782 .price_increment(Price::from("0.5"))
783 .size_increment(Quantity::from("0.001"))
784 .ts_event(TS)
785 .ts_init(TS)
786 .build()
787 .unwrap(),
788 );
789
790 let report =
791 parse_futures_ws_fill_report(&fill, &instrument, AccountId::from("KRAKEN-001"), TS)
792 .unwrap();
793
794 assert_eq!(report.commission.currency.code, "BTC");
796 assert_eq!(report.commission.as_decimal(), dec!(0.001581));
797 }
798}