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, 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    // Leading CLEAR delta to reset the book
134    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, // venue_position_id
378        ts_event,
379        ts_init,
380        None, // report_id
381    ))
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, // info
479            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        // CLEAR + 2 bids + 2 asks = 5
522        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        // CLEAR + 2 bids (skipped qty=0) + 1 ask (skipped qty=0) = 4
538        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); // qty=0
559        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, // info
750            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}