Skip to main content

nautilus_kraken/websocket/futures/
parse.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! WebSocket message parsers for converting Kraken Futures streaming data to Nautilus domain models.
17
18use 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    // Leading CLEAR delta to reset the book
133    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, // venue_position_id
375        ts_event,
376        ts_init,
377        None, // report_id
378    ))
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        // CLEAR + 2 bids + 2 asks = 5
511        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        // CLEAR + 2 bids (skipped qty=0) + 1 ask (skipped qty=0) = 4
527        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); // qty=0
548        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}