nautilus_bitmex/http/
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//! Conversion routines that map BitMEX REST models into Nautilus domain structures.
17
18use nautilus_core::{UnixNanos, time::get_atomic_clock_realtime, uuid::UUID4};
19use nautilus_model::{
20    data::{Bar, BarType, TradeTick},
21    enums::{ContingencyType, OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType},
22    identifiers::{AccountId, ClientOrderId, OrderListId, Symbol, TradeId, VenueOrderId},
23    instruments::{CryptoFuture, CryptoPerpetual, CurrencyPair, Instrument, InstrumentAny},
24    reports::{FillReport, OrderStatusReport, PositionStatusReport},
25    types::{Currency, Money, Price, Quantity, fixed::FIXED_PRECISION},
26};
27use rust_decimal::{Decimal, prelude::FromPrimitive};
28use ustr::Ustr;
29use uuid::Uuid;
30
31use super::models::{
32    BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTrade, BitmexTradeBin,
33};
34use crate::common::{
35    enums::{BitmexExecInstruction, BitmexExecType, BitmexInstrumentState, BitmexInstrumentType},
36    parse::{
37        clean_reason, convert_contract_quantity, derive_contract_decimal_and_increment,
38        map_bitmex_currency, normalize_trade_bin_prices, normalize_trade_bin_volume,
39        parse_aggressor_side, parse_contracts_quantity, parse_instrument_id, parse_liquidity_side,
40        parse_optional_datetime_to_unix_nanos, parse_position_side,
41        parse_signed_contracts_quantity,
42    },
43};
44
45/// Result of attempting to parse a BitMEX instrument.
46#[derive(Debug)]
47pub enum InstrumentParseResult {
48    /// Successfully parsed into a Nautilus instrument.
49    Ok(Box<InstrumentAny>),
50    /// Instrument type is not yet supported (intentionally skipped).
51    Unsupported {
52        symbol: String,
53        instrument_type: BitmexInstrumentType,
54    },
55    /// Instrument is not tradeable (delisted, settled, unlisted).
56    Inactive {
57        symbol: String,
58        state: BitmexInstrumentState,
59    },
60    /// Failed to parse due to an error.
61    Failed {
62        symbol: String,
63        instrument_type: BitmexInstrumentType,
64        error: String,
65    },
66}
67
68/// Returns the appropriate position multiplier for a BitMEX instrument.
69///
70/// For inverse contracts, BitMEX uses `underlyingToSettleMultiplier` to define contract sizing,
71/// with fallback to `underlyingToPositionMultiplier` for older historical data.
72/// For linear contracts, BitMEX uses `underlyingToPositionMultiplier`.
73fn get_position_multiplier(definition: &BitmexInstrument) -> Option<f64> {
74    if definition.is_inverse {
75        definition
76            .underlying_to_settle_multiplier
77            .or(definition.underlying_to_position_multiplier)
78    } else {
79        definition.underlying_to_position_multiplier
80    }
81}
82
83/// Attempts to convert a BitMEX instrument record into a Nautilus instrument by type.
84#[must_use]
85pub fn parse_instrument_any(
86    instrument: &BitmexInstrument,
87    ts_init: UnixNanos,
88) -> InstrumentParseResult {
89    let symbol = instrument.symbol.to_string();
90    let instrument_type = instrument.instrument_type;
91
92    match instrument.state {
93        BitmexInstrumentState::Open | BitmexInstrumentState::Closed => {}
94        state @ (BitmexInstrumentState::Unlisted
95        | BitmexInstrumentState::Settled
96        | BitmexInstrumentState::Delisted) => {
97            return InstrumentParseResult::Inactive { symbol, state };
98        }
99    }
100
101    match instrument.instrument_type {
102        BitmexInstrumentType::Spot => match parse_spot_instrument(instrument, ts_init) {
103            Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
104            Err(e) => InstrumentParseResult::Failed {
105                symbol,
106                instrument_type,
107                error: e.to_string(),
108            },
109        },
110        BitmexInstrumentType::PerpetualContract | BitmexInstrumentType::PerpetualContractFx => {
111            // Handle both crypto and FX perpetuals the same way
112            match parse_perpetual_instrument(instrument, ts_init) {
113                Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
114                Err(e) => InstrumentParseResult::Failed {
115                    symbol,
116                    instrument_type,
117                    error: e.to_string(),
118                },
119            }
120        }
121        BitmexInstrumentType::Futures => match parse_futures_instrument(instrument, ts_init) {
122            Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
123            Err(e) => InstrumentParseResult::Failed {
124                symbol,
125                instrument_type,
126                error: e.to_string(),
127            },
128        },
129        BitmexInstrumentType::PredictionMarket => {
130            // Prediction markets work similarly to futures (bounded 0-100, cash settled)
131            match parse_futures_instrument(instrument, ts_init) {
132                Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
133                Err(e) => InstrumentParseResult::Failed {
134                    symbol,
135                    instrument_type,
136                    error: e.to_string(),
137                },
138            }
139        }
140        BitmexInstrumentType::BasketIndex
141        | BitmexInstrumentType::CryptoIndex
142        | BitmexInstrumentType::FxIndex
143        | BitmexInstrumentType::LendingIndex
144        | BitmexInstrumentType::VolatilityIndex
145        | BitmexInstrumentType::StockIndex
146        | BitmexInstrumentType::YieldIndex => {
147            // Parse index instruments as perpetuals for cache purposes
148            // They need to be in cache for WebSocket price updates
149            match parse_index_instrument(instrument, ts_init) {
150                Ok(inst) => InstrumentParseResult::Ok(Box::new(inst)),
151                Err(e) => InstrumentParseResult::Failed {
152                    symbol,
153                    instrument_type,
154                    error: e.to_string(),
155                },
156            }
157        }
158
159        // Explicitly list unsupported types for clarity
160        BitmexInstrumentType::StockPerpetual
161        | BitmexInstrumentType::CallOption
162        | BitmexInstrumentType::PutOption
163        | BitmexInstrumentType::SwapRate
164        | BitmexInstrumentType::ReferenceBasket
165        | BitmexInstrumentType::LegacyFutures
166        | BitmexInstrumentType::LegacyFuturesN
167        | BitmexInstrumentType::FuturesSpreads => InstrumentParseResult::Unsupported {
168            symbol,
169            instrument_type,
170        },
171    }
172}
173
174/// Parse a BitMEX index instrument into a Nautilus `InstrumentAny`.
175///
176/// Index instruments are parsed as perpetuals with minimal fields to support
177/// price update lookups in the WebSocket.
178///
179/// # Errors
180///
181/// Returns an error if values are out of valid range or cannot be parsed.
182pub fn parse_index_instrument(
183    definition: &BitmexInstrument,
184    ts_init: UnixNanos,
185) -> anyhow::Result<InstrumentAny> {
186    let instrument_id = parse_instrument_id(definition.symbol);
187    let raw_symbol = Symbol::new(definition.symbol);
188
189    let base_currency = Currency::USD();
190    let quote_currency = Currency::USD();
191    let settlement_currency = Currency::USD();
192
193    let price_increment = Price::from(definition.tick_size.to_string());
194    let size_increment = Quantity::from(1); // Indices don't have tradeable sizes
195
196    Ok(InstrumentAny::CryptoPerpetual(CryptoPerpetual::new(
197        instrument_id,
198        raw_symbol,
199        base_currency,
200        quote_currency,
201        settlement_currency,
202        false, // is_inverse
203        price_increment.precision,
204        size_increment.precision,
205        price_increment,
206        size_increment,
207        None, // multiplier
208        None, // lot_size
209        None, // max_quantity
210        None, // min_quantity
211        None, // max_notional
212        None, // min_notional
213        None, // max_price
214        None, // min_price
215        None, // margin_init
216        None, // margin_maint
217        None, // maker_fee
218        None, // taker_fee
219        ts_init,
220        ts_init,
221    )))
222}
223
224/// Parse a BitMEX spot instrument into a Nautilus `InstrumentAny`.
225///
226/// # Errors
227///
228/// Returns an error if values are out of valid range or cannot be parsed.
229pub fn parse_spot_instrument(
230    definition: &BitmexInstrument,
231    ts_init: UnixNanos,
232) -> anyhow::Result<InstrumentAny> {
233    let instrument_id = parse_instrument_id(definition.symbol);
234    let raw_symbol = Symbol::new(definition.symbol);
235    let base_currency = get_currency(&definition.underlying.to_uppercase());
236    let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
237
238    let price_increment = Price::from(definition.tick_size.to_string());
239
240    let max_scale = FIXED_PRECISION as u32;
241    let (contract_decimal, size_increment) =
242        derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
243
244    let min_quantity = convert_contract_quantity(
245        definition.lot_size,
246        contract_decimal,
247        max_scale,
248        "minimum quantity",
249    )?;
250
251    let taker_fee = definition
252        .taker_fee
253        .and_then(|fee| Decimal::try_from(fee).ok())
254        .unwrap_or(Decimal::ZERO);
255    let maker_fee = definition
256        .maker_fee
257        .and_then(|fee| Decimal::try_from(fee).ok())
258        .unwrap_or(Decimal::ZERO);
259
260    let margin_init = definition
261        .init_margin
262        .as_ref()
263        .and_then(|margin| Decimal::try_from(*margin).ok())
264        .unwrap_or(Decimal::ZERO);
265    let margin_maint = definition
266        .maint_margin
267        .as_ref()
268        .and_then(|margin| Decimal::try_from(*margin).ok())
269        .unwrap_or(Decimal::ZERO);
270
271    let lot_size =
272        convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
273    let max_quantity = convert_contract_quantity(
274        definition.max_order_qty,
275        contract_decimal,
276        max_scale,
277        "max quantity",
278    )?;
279    let max_notional: Option<Money> = None;
280    let min_notional: Option<Money> = None;
281    let max_price = definition
282        .max_price
283        .map(|price| Price::from(price.to_string()));
284    let min_price = None;
285    let ts_event = UnixNanos::from(definition.timestamp);
286
287    let instrument = CurrencyPair::new(
288        instrument_id,
289        raw_symbol,
290        base_currency,
291        quote_currency,
292        price_increment.precision,
293        size_increment.precision,
294        price_increment,
295        size_increment,
296        None, // multiplier
297        lot_size,
298        max_quantity,
299        min_quantity,
300        max_notional,
301        min_notional,
302        max_price,
303        min_price,
304        Some(margin_init),
305        Some(margin_maint),
306        Some(maker_fee),
307        Some(taker_fee),
308        ts_event,
309        ts_init,
310    );
311
312    Ok(InstrumentAny::CurrencyPair(instrument))
313}
314
315/// Parse a BitMEX perpetual instrument into a Nautilus `InstrumentAny`.
316///
317/// # Errors
318///
319/// Returns an error if values are out of valid range or cannot be parsed.
320pub fn parse_perpetual_instrument(
321    definition: &BitmexInstrument,
322    ts_init: UnixNanos,
323) -> anyhow::Result<InstrumentAny> {
324    let instrument_id = parse_instrument_id(definition.symbol);
325    let raw_symbol = Symbol::new(definition.symbol);
326    let base_currency = get_currency(&definition.underlying.to_uppercase());
327    let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
328    let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
329        || definition.quote_currency.to_uppercase(),
330        |s| s.to_uppercase(),
331    ));
332    let is_inverse = definition.is_inverse;
333
334    let price_increment = Price::from(definition.tick_size.to_string());
335
336    let max_scale = FIXED_PRECISION as u32;
337    let (contract_decimal, size_increment) =
338        derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
339
340    let lot_size =
341        convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
342
343    let taker_fee = definition
344        .taker_fee
345        .and_then(|fee| Decimal::try_from(fee).ok())
346        .unwrap_or(Decimal::ZERO);
347    let maker_fee = definition
348        .maker_fee
349        .and_then(|fee| Decimal::try_from(fee).ok())
350        .unwrap_or(Decimal::ZERO);
351
352    let margin_init = definition
353        .init_margin
354        .as_ref()
355        .and_then(|margin| Decimal::try_from(*margin).ok())
356        .unwrap_or(Decimal::ZERO);
357    let margin_maint = definition
358        .maint_margin
359        .as_ref()
360        .and_then(|margin| Decimal::try_from(*margin).ok())
361        .unwrap_or(Decimal::ZERO);
362
363    // TODO: How to handle negative multipliers?
364    let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
365    let max_quantity = convert_contract_quantity(
366        definition.max_order_qty,
367        contract_decimal,
368        max_scale,
369        "max quantity",
370    )?;
371    let min_quantity = lot_size;
372    let max_notional: Option<Money> = None;
373    let min_notional: Option<Money> = None;
374    let max_price = definition
375        .max_price
376        .map(|price| Price::from(price.to_string()));
377    let min_price = None;
378    let ts_event = UnixNanos::from(definition.timestamp);
379
380    let instrument = CryptoPerpetual::new(
381        instrument_id,
382        raw_symbol,
383        base_currency,
384        quote_currency,
385        settlement_currency,
386        is_inverse,
387        price_increment.precision,
388        size_increment.precision,
389        price_increment,
390        size_increment,
391        multiplier,
392        lot_size,
393        max_quantity,
394        min_quantity,
395        max_notional,
396        min_notional,
397        max_price,
398        min_price,
399        Some(margin_init),
400        Some(margin_maint),
401        Some(maker_fee),
402        Some(taker_fee),
403        ts_event,
404        ts_init,
405    );
406
407    Ok(InstrumentAny::CryptoPerpetual(instrument))
408}
409
410/// Parse a BitMEX futures instrument into a Nautilus `InstrumentAny`.
411///
412/// # Errors
413///
414/// Returns an error if values are out of valid range or cannot be parsed.
415pub fn parse_futures_instrument(
416    definition: &BitmexInstrument,
417    ts_init: UnixNanos,
418) -> anyhow::Result<InstrumentAny> {
419    let instrument_id = parse_instrument_id(definition.symbol);
420    let raw_symbol = Symbol::new(definition.symbol);
421    let underlying = get_currency(&definition.underlying.to_uppercase());
422    let quote_currency = get_currency(&definition.quote_currency.to_uppercase());
423    let settlement_currency = get_currency(&definition.settl_currency.as_ref().map_or_else(
424        || definition.quote_currency.to_uppercase(),
425        |s| s.to_uppercase(),
426    ));
427    let is_inverse = definition.is_inverse;
428
429    let ts_event = UnixNanos::from(definition.timestamp);
430    let activation_ns = definition
431        .listing
432        .as_ref()
433        .map_or(ts_event, |dt| UnixNanos::from(*dt));
434    let expiration_ns = parse_optional_datetime_to_unix_nanos(&definition.expiry, "expiry");
435    let price_increment = Price::from(definition.tick_size.to_string());
436
437    let max_scale = FIXED_PRECISION as u32;
438    let (contract_decimal, size_increment) =
439        derive_contract_decimal_and_increment(get_position_multiplier(definition), max_scale)?;
440
441    let lot_size =
442        convert_contract_quantity(definition.lot_size, contract_decimal, max_scale, "lot size")?;
443
444    let taker_fee = definition
445        .taker_fee
446        .and_then(|fee| Decimal::try_from(fee).ok())
447        .unwrap_or(Decimal::ZERO);
448    let maker_fee = definition
449        .maker_fee
450        .and_then(|fee| Decimal::try_from(fee).ok())
451        .unwrap_or(Decimal::ZERO);
452
453    let margin_init = definition
454        .init_margin
455        .as_ref()
456        .and_then(|margin| Decimal::try_from(*margin).ok())
457        .unwrap_or(Decimal::ZERO);
458    let margin_maint = definition
459        .maint_margin
460        .as_ref()
461        .and_then(|margin| Decimal::try_from(*margin).ok())
462        .unwrap_or(Decimal::ZERO);
463
464    // TODO: How to handle negative multipliers?
465    let multiplier = Some(Quantity::new_checked(definition.multiplier.abs(), 0)?);
466
467    let max_quantity = convert_contract_quantity(
468        definition.max_order_qty,
469        contract_decimal,
470        max_scale,
471        "max quantity",
472    )?;
473    let min_quantity = lot_size;
474    let max_notional: Option<Money> = None;
475    let min_notional: Option<Money> = None;
476    let max_price = definition
477        .max_price
478        .map(|price| Price::from(price.to_string()));
479    let min_price = None;
480    let instrument = CryptoFuture::new(
481        instrument_id,
482        raw_symbol,
483        underlying,
484        quote_currency,
485        settlement_currency,
486        is_inverse,
487        activation_ns,
488        expiration_ns,
489        price_increment.precision,
490        size_increment.precision,
491        price_increment,
492        size_increment,
493        multiplier,
494        lot_size,
495        max_quantity,
496        min_quantity,
497        max_notional,
498        min_notional,
499        max_price,
500        min_price,
501        Some(margin_init),
502        Some(margin_maint),
503        Some(maker_fee),
504        Some(taker_fee),
505        ts_event,
506        ts_init,
507    );
508
509    Ok(InstrumentAny::CryptoFuture(instrument))
510}
511
512/// Parse a BitMEX trade into a Nautilus `TradeTick`.
513///
514/// # Errors
515///
516/// Currently this function does not return errors as all fields are handled gracefully,
517/// but returns `Result` for future error handling compatibility.
518pub fn parse_trade(
519    trade: BitmexTrade,
520    price_precision: u8,
521    ts_init: UnixNanos,
522) -> anyhow::Result<TradeTick> {
523    let instrument_id = parse_instrument_id(trade.symbol);
524    let price = Price::new(trade.price, price_precision);
525    let size = Quantity::from(trade.size);
526    let aggressor_side = parse_aggressor_side(&trade.side);
527    let trade_id = TradeId::new(
528        trade
529            .trd_match_id
530            .map_or_else(|| Uuid::new_v4().to_string(), |uuid| uuid.to_string()),
531    );
532    let ts_event = UnixNanos::from(trade.timestamp);
533
534    Ok(TradeTick::new(
535        instrument_id,
536        price,
537        size,
538        aggressor_side,
539        trade_id,
540        ts_event,
541        ts_init,
542    ))
543}
544
545/// Converts a BitMEX trade-bin record into a Nautilus [`Bar`].
546///
547/// # Errors
548///
549/// Returns an error when required OHLC fields are missing from the payload.
550///
551/// # Panics
552///
553/// Panics if the bar type or price precision cannot be determined for the instrument, which
554/// indicates the instrument cache was not hydrated prior to parsing.
555pub fn parse_trade_bin(
556    bin: BitmexTradeBin,
557    instrument: &InstrumentAny,
558    bar_type: &BarType,
559    ts_init: UnixNanos,
560) -> anyhow::Result<Bar> {
561    let instrument_id = bar_type.instrument_id();
562    let price_precision = instrument.price_precision();
563
564    let open = bin
565        .open
566        .ok_or_else(|| anyhow::anyhow!("Trade bin missing open price for {instrument_id}"))?;
567    let high = bin
568        .high
569        .ok_or_else(|| anyhow::anyhow!("Trade bin missing high price for {instrument_id}"))?;
570    let low = bin
571        .low
572        .ok_or_else(|| anyhow::anyhow!("Trade bin missing low price for {instrument_id}"))?;
573    let close = bin
574        .close
575        .ok_or_else(|| anyhow::anyhow!("Trade bin missing close price for {instrument_id}"))?;
576
577    let open = Price::new(open, price_precision);
578    let high = Price::new(high, price_precision);
579    let low = Price::new(low, price_precision);
580    let close = Price::new(close, price_precision);
581
582    let (open, high, low, close) =
583        normalize_trade_bin_prices(open, high, low, close, &bin.symbol, Some(bar_type));
584
585    let volume_contracts = normalize_trade_bin_volume(bin.volume, &bin.symbol);
586    let volume = parse_contracts_quantity(volume_contracts, instrument);
587    let ts_event = UnixNanos::from(bin.timestamp);
588
589    Ok(Bar::new(
590        *bar_type, open, high, low, close, volume, ts_event, ts_init,
591    ))
592}
593
594/// Parse a BitMEX order into a Nautilus `OrderStatusReport`.
595///
596/// # BitMEX Response Quirks
597///
598/// BitMEX may omit `ord_status` in responses for completed orders. When this occurs,
599/// the parser defensively infers the status from `leaves_qty` and `cum_qty`:
600/// - `leaves_qty=0, cum_qty>0` -> `Filled`
601/// - `leaves_qty=0, cum_qty<=0` -> `Canceled`
602/// - Otherwise -> Returns error (unparsable)
603///
604/// # Errors
605///
606/// Returns an error if:
607/// - Order is missing `ord_status` and status cannot be inferred from quantity fields.
608/// - Order is missing `order_qty` and cannot be reconstructed from `cum_qty` + `leaves_qty`.
609///
610/// # Panics
611///
612/// Panics if:
613/// - Unsupported `ExecInstruction` type is encountered (other than `ParticipateDoNotInitiate` or `ReduceOnly`)
614pub fn parse_order_status_report(
615    order: &BitmexOrder,
616    instrument: &InstrumentAny,
617    ts_init: UnixNanos,
618) -> anyhow::Result<OrderStatusReport> {
619    let instrument_id = instrument.id();
620    let account_id = AccountId::new(format!("BITMEX-{}", order.account));
621    let venue_order_id = VenueOrderId::new(order.order_id.to_string());
622    let order_side: OrderSide = order
623        .side
624        .map_or(OrderSide::NoOrderSide, |side| side.into());
625
626    // BitMEX may not include ord_type in cancel responses,
627    // for robustness default to LIMIT if not provided.
628    let order_type: OrderType = order.ord_type.map_or(OrderType::Limit, |t| t.into());
629
630    // BitMEX may not include time_in_force in cancel responses,
631    // for robustness default to GTC if not provided.
632    let time_in_force: TimeInForce = order
633        .time_in_force
634        .and_then(|tif| tif.try_into().ok())
635        .unwrap_or(TimeInForce::Gtc);
636
637    // BitMEX may omit ord_status in responses for completed orders
638    // Defensively infer from leaves_qty, cum_qty, and working_indicator when possible
639    let order_status: OrderStatus = if let Some(status) = order.ord_status.as_ref() {
640        (*status).into()
641    } else {
642        // Infer status from quantity fields and working indicator
643        match (order.leaves_qty, order.cum_qty, order.working_indicator) {
644            (Some(0), Some(cum), _) if cum > 0 => {
645                log::debug!(
646                    "Inferred Filled from missing ordStatus (leaves_qty=0, cum_qty>0): order_id={:?}, client_order_id={:?}, cum_qty={}",
647                    order.order_id,
648                    order.cl_ord_id,
649                    cum,
650                );
651                OrderStatus::Filled
652            }
653            (Some(0), _, _) => {
654                log::debug!(
655                    "Inferred Canceled from missing ordStatus (leaves_qty=0, cum_qty<=0): order_id={:?}, client_order_id={:?}, cum_qty={:?}",
656                    order.order_id,
657                    order.cl_ord_id,
658                    order.cum_qty,
659                );
660                OrderStatus::Canceled
661            }
662            // BitMEX cancel responses may omit all quantity fields but include working_indicator
663            (None, None, Some(false)) => {
664                log::debug!(
665                    "Inferred Canceled from missing ordStatus with working_indicator=false: order_id={:?}, client_order_id={:?}",
666                    order.order_id,
667                    order.cl_ord_id,
668                );
669                OrderStatus::Canceled
670            }
671            _ => {
672                let order_json = serde_json::to_string(order)?;
673                anyhow::bail!(
674                    "Order missing ord_status and cannot infer (order_id={}, client_order_id={:?}, leaves_qty={:?}, cum_qty={:?}, working_indicator={:?}, order_json={})",
675                    order.order_id,
676                    order.cl_ord_id,
677                    order.leaves_qty,
678                    order.cum_qty,
679                    order.working_indicator,
680                    order_json
681                );
682            }
683        }
684    };
685
686    // Try to get order_qty, or reconstruct from cum_qty + leaves_qty
687    let (quantity, filled_qty) = if let Some(qty) = order.order_qty {
688        let quantity = parse_signed_contracts_quantity(qty, instrument);
689        let filled_qty = parse_signed_contracts_quantity(order.cum_qty.unwrap_or(0), instrument);
690        (quantity, filled_qty)
691    } else if let (Some(cum), Some(leaves)) = (order.cum_qty, order.leaves_qty) {
692        log::debug!(
693            "Reconstructing order_qty from cum_qty + leaves_qty: order_id={:?}, client_order_id={:?}, cum_qty={}, leaves_qty={}",
694            order.order_id,
695            order.cl_ord_id,
696            cum,
697            leaves,
698        );
699        let quantity = parse_signed_contracts_quantity(cum + leaves, instrument);
700        let filled_qty = parse_signed_contracts_quantity(cum, instrument);
701        (quantity, filled_qty)
702    } else if order_status == OrderStatus::Canceled || order_status == OrderStatus::Rejected {
703        // For canceled/rejected orders, both quantities will be reconciled from cache
704        // BitMEX sometimes omits all quantity fields in cancel responses
705        log::debug!(
706            "Order missing quantity fields, using 0 for both (will be reconciled from cache): order_id={:?}, client_order_id={:?}, status={:?}",
707            order.order_id,
708            order.cl_ord_id,
709            order_status,
710        );
711        let zero_qty = Quantity::zero(instrument.size_precision());
712        (zero_qty, zero_qty)
713    } else {
714        anyhow::bail!(
715            "Order missing order_qty and cannot reconstruct (order_id={}, cum_qty={:?}, leaves_qty={:?})",
716            order.order_id,
717            order.cum_qty,
718            order.leaves_qty
719        );
720    };
721    let report_id = UUID4::new();
722    let ts_accepted = order.transact_time.map_or_else(
723        || get_atomic_clock_realtime().get_time_ns(),
724        UnixNanos::from,
725    );
726    let ts_last = order.timestamp.map_or_else(
727        || get_atomic_clock_realtime().get_time_ns(),
728        UnixNanos::from,
729    );
730
731    let mut report = OrderStatusReport::new(
732        account_id,
733        instrument_id,
734        None, // client_order_id - will be set later if present
735        venue_order_id,
736        order_side,
737        order_type,
738        time_in_force,
739        order_status,
740        quantity,
741        filled_qty,
742        ts_accepted,
743        ts_last,
744        ts_init,
745        Some(report_id),
746    );
747
748    if let Some(cl_ord_id) = order.cl_ord_id {
749        report = report.with_client_order_id(ClientOrderId::new(cl_ord_id));
750    }
751
752    if let Some(cl_ord_link_id) = order.cl_ord_link_id {
753        report = report.with_order_list_id(OrderListId::new(cl_ord_link_id));
754    }
755
756    let price_precision = instrument.price_precision();
757
758    if let Some(price) = order.price {
759        report = report.with_price(Price::new(price, price_precision));
760    }
761
762    if let Some(avg_px) = order.avg_px {
763        report = report.with_avg_px(avg_px)?;
764    }
765
766    if let Some(trigger_price) = order.stop_px {
767        report = report
768            .with_trigger_price(Price::new(trigger_price, price_precision))
769            .with_trigger_type(TriggerType::Default);
770    }
771
772    if let Some(exec_instructions) = &order.exec_inst {
773        for inst in exec_instructions {
774            match inst {
775                BitmexExecInstruction::ParticipateDoNotInitiate => {
776                    report = report.with_post_only(true);
777                }
778                BitmexExecInstruction::ReduceOnly => report = report.with_reduce_only(true),
779                BitmexExecInstruction::LastPrice
780                | BitmexExecInstruction::Close
781                | BitmexExecInstruction::MarkPrice
782                | BitmexExecInstruction::IndexPrice
783                | BitmexExecInstruction::AllOrNone
784                | BitmexExecInstruction::Fixed
785                | BitmexExecInstruction::Unknown => {}
786            }
787        }
788    }
789
790    if let Some(contingency_type) = order.contingency_type {
791        report = report.with_contingency_type(contingency_type.into());
792    }
793
794    if matches!(
795        report.contingency_type,
796        ContingencyType::Oco | ContingencyType::Oto | ContingencyType::Ouo
797    ) && report.order_list_id.is_none()
798    {
799        log::debug!(
800            "BitMEX order missing clOrdLinkID for contingent order: order_id={}, client_order_id={:?}, contingency_type={:?}",
801            order.order_id,
802            report.client_order_id,
803            report.contingency_type,
804        );
805    }
806
807    // Extract rejection/cancellation reason
808    if order_status == OrderStatus::Rejected {
809        if let Some(reason) = order.ord_rej_reason.or(order.text) {
810            log::debug!(
811                "Order rejected with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
812                order.order_id,
813                order.cl_ord_id,
814                reason,
815            );
816            report = report.with_cancel_reason(clean_reason(reason.as_ref()));
817        } else {
818            log::debug!(
819                "Order rejected without reason from BitMEX: order_id={:?}, client_order_id={:?}, ord_status={:?}, ord_rej_reason={:?}, text={:?}",
820                order.order_id,
821                order.cl_ord_id,
822                order.ord_status,
823                order.ord_rej_reason,
824                order.text,
825            );
826        }
827    } else if order_status == OrderStatus::Canceled
828        && let Some(reason) = order.ord_rej_reason.or(order.text)
829    {
830        log::trace!(
831            "Order canceled with reason: order_id={:?}, client_order_id={:?}, reason={:?}",
832            order.order_id,
833            order.cl_ord_id,
834            reason,
835        );
836        report = report.with_cancel_reason(clean_reason(reason.as_ref()));
837    }
838
839    // BitMEX does not currently include an explicit expiry timestamp
840    // in the order status response, so `report.expire_time` remains `None`.
841    Ok(report)
842}
843
844/// Parse a BitMEX execution into a Nautilus `FillReport`.
845///
846/// # Errors
847///
848/// Currently this function does not return errors as all fields are handled gracefully,
849/// but returns `Result` for future error handling compatibility.
850///
851/// Parse a BitMEX execution into a Nautilus `FillReport` using instrument scaling.
852///
853/// # Panics
854///
855/// Panics if:
856/// - Execution is missing required fields: `symbol`, `order_id`, `trd_match_id`, `last_qty`, `last_px`, or `transact_time`
857///
858/// # Errors
859///
860/// Returns an error when the execution does not represent a trade or lacks required identifiers.
861pub fn parse_fill_report(
862    exec: BitmexExecution,
863    instrument: &InstrumentAny,
864    ts_init: UnixNanos,
865) -> anyhow::Result<FillReport> {
866    // Skip non-trade executions (funding, settlements, etc.)
867    // Trade executions have exec_type of Trade and must have order_id
868    if !matches!(exec.exec_type, BitmexExecType::Trade) {
869        anyhow::bail!("Skipping non-trade execution: {:?}", exec.exec_type);
870    }
871
872    // Additional check: skip executions without order_id (likely funding/settlement)
873    let order_id = exec.order_id.ok_or_else(|| {
874        anyhow::anyhow!("Skipping execution without order_id: {:?}", exec.exec_type)
875    })?;
876
877    let account_id = AccountId::new(format!("BITMEX-{}", exec.account));
878    let instrument_id = instrument.id();
879    let venue_order_id = VenueOrderId::new(order_id.to_string());
880    // trd_match_id might be missing for some execution types, use exec_id as fallback
881    let trade_id = TradeId::new(
882        exec.trd_match_id
883            .or(Some(exec.exec_id))
884            .ok_or_else(|| anyhow::anyhow!("Fill missing both trd_match_id and exec_id"))?
885            .to_string(),
886    );
887    // Skip executions without side (likely not trades)
888    let Some(side) = exec.side else {
889        anyhow::bail!("Skipping execution without side: {:?}", exec.exec_type);
890    };
891    let order_side: OrderSide = side.into();
892    let last_qty = parse_signed_contracts_quantity(exec.last_qty, instrument);
893    let last_px = Price::new(exec.last_px, instrument.price_precision());
894
895    // Map BitMEX currency to standard currency code
896    let settlement_currency_str = exec.settl_currency.unwrap_or(Ustr::from("XBT")).as_str();
897    let mapped_currency = map_bitmex_currency(settlement_currency_str);
898    let currency = get_currency(&mapped_currency);
899    let commission = Money::new(exec.commission.unwrap_or(0.0), currency);
900    let liquidity_side = parse_liquidity_side(&exec.last_liquidity_ind);
901    let client_order_id = exec.cl_ord_id.map(ClientOrderId::new);
902    let venue_position_id = None; // Not applicable on BitMEX
903    let ts_event = exec.transact_time.map_or_else(
904        || get_atomic_clock_realtime().get_time_ns(),
905        UnixNanos::from,
906    );
907
908    Ok(FillReport::new(
909        account_id,
910        instrument_id,
911        venue_order_id,
912        trade_id,
913        order_side,
914        last_qty,
915        last_px,
916        commission,
917        liquidity_side,
918        client_order_id,
919        venue_position_id,
920        ts_event,
921        ts_init,
922        None,
923    ))
924}
925
926/// Parse a BitMEX position into a Nautilus `PositionStatusReport`.
927///
928/// # Errors
929///
930/// Currently this function does not return errors as all fields are handled gracefully,
931/// but returns `Result` for future error handling compatibility.
932pub fn parse_position_report(
933    position: BitmexPosition,
934    instrument: &InstrumentAny,
935    ts_init: UnixNanos,
936) -> anyhow::Result<PositionStatusReport> {
937    let account_id = AccountId::new(format!("BITMEX-{}", position.account));
938    let instrument_id = instrument.id();
939    let position_side = parse_position_side(position.current_qty).as_specified();
940    let quantity = parse_signed_contracts_quantity(position.current_qty.unwrap_or(0), instrument);
941    let venue_position_id = None; // Not applicable on BitMEX
942    let avg_px_open = position.avg_entry_price.and_then(Decimal::from_f64);
943    let ts_last = parse_optional_datetime_to_unix_nanos(&position.timestamp, "timestamp");
944
945    Ok(PositionStatusReport::new(
946        account_id,
947        instrument_id,
948        position_side,
949        quantity,
950        ts_last,
951        ts_init,
952        None,              // report_id
953        venue_position_id, // venue_position_id
954        avg_px_open,       // avg_px_open
955    ))
956}
957
958/// Returns a currency from the internal map or creates a new crypto currency.
959///
960/// Uses [`Currency::get_or_create_crypto`] to handle unknown currency codes,
961/// which automatically registers newly listed BitMEX assets.
962pub fn get_currency(code: &str) -> Currency {
963    Currency::get_or_create_crypto(code)
964}
965
966#[cfg(test)]
967mod tests {
968    use std::str::FromStr;
969
970    use chrono::{DateTime, Utc};
971    use nautilus_model::{
972        data::{BarSpecification, BarType},
973        enums::{AggregationSource, BarAggregation, LiquiditySide, PositionSide, PriceType},
974        instruments::InstrumentAny,
975    };
976    use rstest::rstest;
977    use rust_decimal::{Decimal, prelude::ToPrimitive};
978    use uuid::Uuid;
979
980    use super::*;
981    use crate::{
982        common::{
983            enums::{
984                BitmexContingencyType, BitmexFairMethod, BitmexInstrumentState,
985                BitmexInstrumentType, BitmexLiquidityIndicator, BitmexMarkMethod,
986                BitmexOrderStatus, BitmexOrderType, BitmexSide, BitmexTickDirection,
987                BitmexTimeInForce,
988            },
989            testing::load_test_json,
990        },
991        http::models::{
992            BitmexExecution, BitmexInstrument, BitmexOrder, BitmexPosition, BitmexTradeBin,
993            BitmexWallet,
994        },
995    };
996
997    #[rstest]
998    fn test_perp_instrument_deserialization() {
999        let json_data = load_test_json("http_get_instrument_xbtusd.json");
1000        let instrument: BitmexInstrument = serde_json::from_str(&json_data).unwrap();
1001
1002        assert_eq!(instrument.symbol, "XBTUSD");
1003        assert_eq!(instrument.root_symbol, "XBT");
1004        assert_eq!(instrument.state, BitmexInstrumentState::Open);
1005        assert!(instrument.is_inverse);
1006        assert_eq!(instrument.maker_fee, Some(0.0005));
1007        assert_eq!(
1008            instrument.timestamp.to_rfc3339(),
1009            "2024-11-24T23:33:19.034+00:00"
1010        );
1011    }
1012
1013    #[rstest]
1014    fn test_parse_orders() {
1015        let json_data = load_test_json("http_get_orders.json");
1016        let orders: Vec<BitmexOrder> = serde_json::from_str(&json_data).unwrap();
1017
1018        assert_eq!(orders.len(), 2);
1019
1020        // Test first order (New)
1021        let order1 = &orders[0];
1022        assert_eq!(order1.symbol, Some(Ustr::from("XBTUSD")));
1023        assert_eq!(order1.side, Some(BitmexSide::Buy));
1024        assert_eq!(order1.order_qty, Some(100));
1025        assert_eq!(order1.price, Some(98000.0));
1026        assert_eq!(order1.ord_status, Some(BitmexOrderStatus::New));
1027        assert_eq!(order1.leaves_qty, Some(100));
1028        assert_eq!(order1.cum_qty, Some(0));
1029
1030        // Test second order (Filled)
1031        let order2 = &orders[1];
1032        assert_eq!(order2.symbol, Some(Ustr::from("XBTUSD")));
1033        assert_eq!(order2.side, Some(BitmexSide::Sell));
1034        assert_eq!(order2.order_qty, Some(200));
1035        assert_eq!(order2.ord_status, Some(BitmexOrderStatus::Filled));
1036        assert_eq!(order2.leaves_qty, Some(0));
1037        assert_eq!(order2.cum_qty, Some(200));
1038        assert_eq!(order2.avg_px, Some(98950.5));
1039    }
1040
1041    #[rstest]
1042    fn test_parse_executions() {
1043        let json_data = load_test_json("http_get_executions.json");
1044        let executions: Vec<BitmexExecution> = serde_json::from_str(&json_data).unwrap();
1045
1046        assert_eq!(executions.len(), 2);
1047
1048        // Test first execution (Maker)
1049        let exec1 = &executions[0];
1050        assert_eq!(exec1.symbol, Some(Ustr::from("XBTUSD")));
1051        assert_eq!(exec1.side, Some(BitmexSide::Sell));
1052        assert_eq!(exec1.last_qty, 100);
1053        assert_eq!(exec1.last_px, 98950.0);
1054        assert_eq!(
1055            exec1.last_liquidity_ind,
1056            Some(BitmexLiquidityIndicator::Maker)
1057        );
1058        assert_eq!(exec1.commission, Some(0.00075));
1059
1060        // Test second execution (Taker)
1061        let exec2 = &executions[1];
1062        assert_eq!(
1063            exec2.last_liquidity_ind,
1064            Some(BitmexLiquidityIndicator::Taker)
1065        );
1066        assert_eq!(exec2.last_px, 98951.0);
1067    }
1068
1069    #[rstest]
1070    fn test_parse_positions() {
1071        let json_data = load_test_json("http_get_positions.json");
1072        let positions: Vec<BitmexPosition> = serde_json::from_str(&json_data).unwrap();
1073
1074        assert_eq!(positions.len(), 1);
1075
1076        let position = &positions[0];
1077        assert_eq!(position.account, 1234567);
1078        assert_eq!(position.symbol, "XBTUSD");
1079        assert_eq!(position.current_qty, Some(100));
1080        assert_eq!(position.avg_entry_price, Some(98390.88));
1081        assert_eq!(position.unrealised_pnl, Some(1350));
1082        assert_eq!(position.realised_pnl, Some(-227));
1083        assert_eq!(position.is_open, Some(true));
1084    }
1085
1086    #[rstest]
1087    fn test_parse_trades() {
1088        let json_data = load_test_json("http_get_trades.json");
1089        let trades: Vec<BitmexTrade> = serde_json::from_str(&json_data).unwrap();
1090
1091        assert_eq!(trades.len(), 3);
1092
1093        // Test first trade
1094        let trade1 = &trades[0];
1095        assert_eq!(trade1.symbol, "XBTUSD");
1096        assert_eq!(trade1.side, Some(BitmexSide::Buy));
1097        assert_eq!(trade1.size, 100);
1098        assert_eq!(trade1.price, 98950.0);
1099
1100        // Test third trade (Sell side)
1101        let trade3 = &trades[2];
1102        assert_eq!(trade3.side, Some(BitmexSide::Sell));
1103        assert_eq!(trade3.size, 50);
1104        assert_eq!(trade3.price, 98949.5);
1105    }
1106
1107    #[rstest]
1108    fn test_parse_wallet() {
1109        let json_data = load_test_json("http_get_wallet.json");
1110        let wallets: Vec<BitmexWallet> = serde_json::from_str(&json_data).unwrap();
1111
1112        assert_eq!(wallets.len(), 1);
1113
1114        let wallet = &wallets[0];
1115        assert_eq!(wallet.account, 1234567);
1116        assert_eq!(wallet.currency, "XBt");
1117        assert_eq!(wallet.amount, Some(1000123456));
1118        assert_eq!(wallet.delta_amount, Some(123456));
1119    }
1120
1121    #[rstest]
1122    fn test_parse_trade_bins() {
1123        let json_data = load_test_json("http_get_trade_bins.json");
1124        let bins: Vec<BitmexTradeBin> = serde_json::from_str(&json_data).unwrap();
1125
1126        assert_eq!(bins.len(), 3);
1127
1128        // Test first bin
1129        let bin1 = &bins[0];
1130        assert_eq!(bin1.symbol, "XBTUSD");
1131        assert_eq!(bin1.open, Some(98900.0));
1132        assert_eq!(bin1.high, Some(98980.5));
1133        assert_eq!(bin1.low, Some(98890.0));
1134        assert_eq!(bin1.close, Some(98950.0));
1135        assert_eq!(bin1.volume, Some(150000));
1136        assert_eq!(bin1.trades, Some(45));
1137
1138        // Test last bin
1139        let bin3 = &bins[2];
1140        assert_eq!(bin3.close, Some(98970.0));
1141        assert_eq!(bin3.volume, Some(78000));
1142    }
1143
1144    #[rstest]
1145    fn test_parse_trade_bin_to_bar() {
1146        let json_data = load_test_json("http_get_trade_bins.json");
1147        let bins: Vec<BitmexTradeBin> = serde_json::from_str(&json_data).unwrap();
1148        let instrument_json = load_test_json("http_get_instrument_xbtusd.json");
1149        let instrument: BitmexInstrument = serde_json::from_str(&instrument_json).unwrap();
1150
1151        let ts_init = UnixNanos::from(1u64);
1152        let instrument_any = match parse_instrument_any(&instrument, ts_init) {
1153            InstrumentParseResult::Ok(inst) => inst,
1154            other => panic!("Expected Ok, got {other:?}"),
1155        };
1156
1157        let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
1158        let bar_type = BarType::new(instrument_any.id(), spec, AggregationSource::External);
1159
1160        let bar = parse_trade_bin(bins[0].clone(), &instrument_any, &bar_type, ts_init).unwrap();
1161
1162        let precision = instrument_any.price_precision();
1163        let expected_open =
1164            Price::from_decimal_dp(Decimal::from_str("98900.0").unwrap(), precision)
1165                .expect("open price");
1166        let expected_close =
1167            Price::from_decimal_dp(Decimal::from_str("98950.0").unwrap(), precision)
1168                .expect("close price");
1169
1170        assert_eq!(bar.bar_type, bar_type);
1171        assert_eq!(bar.open, expected_open);
1172        assert_eq!(bar.close, expected_close);
1173    }
1174
1175    #[rstest]
1176    fn test_parse_trade_bin_extreme_adjustment() {
1177        let instrument_json = load_test_json("http_get_instrument_xbtusd.json");
1178        let instrument: BitmexInstrument = serde_json::from_str(&instrument_json).unwrap();
1179
1180        let ts_init = UnixNanos::from(1u64);
1181        let instrument_any = match parse_instrument_any(&instrument, ts_init) {
1182            InstrumentParseResult::Ok(inst) => inst,
1183            other => panic!("Expected Ok, got {other:?}"),
1184        };
1185
1186        let spec = BarSpecification::new(1, BarAggregation::Minute, PriceType::Last);
1187        let bar_type = BarType::new(instrument_any.id(), spec, AggregationSource::External);
1188
1189        let bin = BitmexTradeBin {
1190            timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1191                .unwrap()
1192                .with_timezone(&Utc),
1193            symbol: Ustr::from("XBTUSD"),
1194            open: Some(50_000.0),
1195            high: Some(49_990.0),
1196            low: Some(50_010.0),
1197            close: Some(50_005.0),
1198            trades: Some(5),
1199            volume: Some(1_000),
1200            vwap: None,
1201            last_size: None,
1202            turnover: None,
1203            home_notional: None,
1204            foreign_notional: None,
1205        };
1206
1207        let bar = parse_trade_bin(bin, &instrument_any, &bar_type, ts_init).unwrap();
1208
1209        let precision = instrument_any.price_precision();
1210        let expected_high =
1211            Price::from_decimal_dp(Decimal::from_str("50010.0").unwrap(), precision)
1212                .expect("high price");
1213        let expected_low = Price::from_decimal_dp(Decimal::from_str("49990.0").unwrap(), precision)
1214            .expect("low price");
1215        let expected_open =
1216            Price::from_decimal_dp(Decimal::from_str("50000.0").unwrap(), precision)
1217                .expect("open price");
1218
1219        assert_eq!(bar.high, expected_high);
1220        assert_eq!(bar.low, expected_low);
1221        assert_eq!(bar.open, expected_open);
1222    }
1223
1224    #[rstest]
1225    fn test_parse_order_status_report() {
1226        let order = BitmexOrder {
1227            account: 123456,
1228            symbol: Some(Ustr::from("XBTUSD")),
1229            order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
1230            cl_ord_id: Some(Ustr::from("client-123")),
1231            cl_ord_link_id: None,
1232            side: Some(BitmexSide::Buy),
1233            ord_type: Some(BitmexOrderType::Limit),
1234            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1235            ord_status: Some(BitmexOrderStatus::New),
1236            order_qty: Some(100),
1237            cum_qty: Some(50),
1238            price: Some(50000.0),
1239            stop_px: Some(49000.0),
1240            display_qty: None,
1241            peg_offset_value: None,
1242            peg_price_type: None,
1243            currency: Some(Ustr::from("USD")),
1244            settl_currency: Some(Ustr::from("XBt")),
1245            exec_inst: Some(vec![
1246                BitmexExecInstruction::ParticipateDoNotInitiate,
1247                BitmexExecInstruction::ReduceOnly,
1248            ]),
1249            contingency_type: Some(BitmexContingencyType::OneCancelsTheOther),
1250            ex_destination: None,
1251            triggered: None,
1252            working_indicator: Some(true),
1253            ord_rej_reason: None,
1254            leaves_qty: Some(50),
1255            avg_px: None,
1256            multi_leg_reporting_type: None,
1257            text: None,
1258            transact_time: Some(
1259                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1260                    .unwrap()
1261                    .with_timezone(&Utc),
1262            ),
1263            timestamp: Some(
1264                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1265                    .unwrap()
1266                    .with_timezone(&Utc),
1267            ),
1268        };
1269
1270        let instrument =
1271            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1272                .unwrap();
1273        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1274
1275        assert_eq!(report.account_id.to_string(), "BITMEX-123456");
1276        assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
1277        assert_eq!(
1278            report.venue_order_id.as_str(),
1279            "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
1280        );
1281        assert_eq!(report.client_order_id.unwrap().as_str(), "client-123");
1282        assert_eq!(report.quantity.as_f64(), 100.0);
1283        assert_eq!(report.filled_qty.as_f64(), 50.0);
1284        assert_eq!(report.price.unwrap().as_f64(), 50000.0);
1285        assert_eq!(report.trigger_price.unwrap().as_f64(), 49000.0);
1286        assert!(report.post_only);
1287        assert!(report.reduce_only);
1288    }
1289
1290    #[rstest]
1291    fn test_parse_order_status_report_minimal() {
1292        let order = BitmexOrder {
1293            account: 0, // Use 0 for test account
1294            symbol: Some(Ustr::from("ETHUSD")),
1295            order_id: Uuid::parse_str("11111111-2222-3333-4444-555555555555").unwrap(),
1296            cl_ord_id: None,
1297            cl_ord_link_id: None,
1298            side: Some(BitmexSide::Sell),
1299            ord_type: Some(BitmexOrderType::Market),
1300            time_in_force: Some(BitmexTimeInForce::ImmediateOrCancel),
1301            ord_status: Some(BitmexOrderStatus::Filled),
1302            order_qty: Some(200),
1303            cum_qty: Some(200),
1304            price: None,
1305            stop_px: None,
1306            display_qty: None,
1307            peg_offset_value: None,
1308            peg_price_type: None,
1309            currency: None,
1310            settl_currency: None,
1311            exec_inst: None,
1312            contingency_type: None,
1313            ex_destination: None,
1314            triggered: None,
1315            working_indicator: Some(false),
1316            ord_rej_reason: None,
1317            leaves_qty: Some(0),
1318            avg_px: None,
1319            multi_leg_reporting_type: None,
1320            text: None,
1321            transact_time: Some(
1322                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1323                    .unwrap()
1324                    .with_timezone(&Utc),
1325            ),
1326            timestamp: Some(
1327                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1328                    .unwrap()
1329                    .with_timezone(&Utc),
1330            ),
1331        };
1332
1333        let mut instrument_def = create_test_perpetual_instrument();
1334        instrument_def.symbol = Ustr::from("ETHUSD");
1335        instrument_def.underlying = Ustr::from("ETH");
1336        instrument_def.quote_currency = Ustr::from("USD");
1337        instrument_def.settl_currency = Some(Ustr::from("USDt"));
1338        let instrument = parse_perpetual_instrument(&instrument_def, UnixNanos::default()).unwrap();
1339        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1340
1341        assert_eq!(report.account_id.to_string(), "BITMEX-0");
1342        assert_eq!(report.instrument_id.to_string(), "ETHUSD.BITMEX");
1343        assert_eq!(
1344            report.venue_order_id.as_str(),
1345            "11111111-2222-3333-4444-555555555555"
1346        );
1347        assert!(report.client_order_id.is_none());
1348        assert_eq!(report.quantity.as_f64(), 200.0);
1349        assert_eq!(report.filled_qty.as_f64(), 200.0);
1350        assert!(report.price.is_none());
1351        assert!(report.trigger_price.is_none());
1352        assert!(!report.post_only);
1353        assert!(!report.reduce_only);
1354    }
1355
1356    #[rstest]
1357    fn test_parse_order_status_report_missing_order_qty_reconstructed() {
1358        let order = BitmexOrder {
1359            account: 789012,
1360            symbol: Some(Ustr::from("XBTUSD")),
1361            order_id: Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffffffffffff").unwrap(),
1362            cl_ord_id: Some(Ustr::from("client-cancel-test")),
1363            cl_ord_link_id: None,
1364            side: Some(BitmexSide::Buy),
1365            ord_type: Some(BitmexOrderType::Limit),
1366            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1367            ord_status: Some(BitmexOrderStatus::Canceled),
1368            order_qty: None,      // Missing - should be reconstructed
1369            cum_qty: Some(75),    // Filled 75
1370            leaves_qty: Some(25), // Remaining 25
1371            price: Some(45000.0),
1372            stop_px: None,
1373            display_qty: None,
1374            peg_offset_value: None,
1375            peg_price_type: None,
1376            currency: Some(Ustr::from("USD")),
1377            settl_currency: Some(Ustr::from("XBt")),
1378            exec_inst: None,
1379            contingency_type: None,
1380            ex_destination: None,
1381            triggered: None,
1382            working_indicator: Some(false),
1383            ord_rej_reason: None,
1384            avg_px: Some(45050.0),
1385            multi_leg_reporting_type: None,
1386            text: None,
1387            transact_time: Some(
1388                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1389                    .unwrap()
1390                    .with_timezone(&Utc),
1391            ),
1392            timestamp: Some(
1393                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1394                    .unwrap()
1395                    .with_timezone(&Utc),
1396            ),
1397        };
1398
1399        let instrument =
1400            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1401                .unwrap();
1402        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1403
1404        // Verify order_qty was reconstructed from cum_qty + leaves_qty
1405        assert_eq!(report.quantity.as_f64(), 100.0); // 75 + 25
1406        assert_eq!(report.filled_qty.as_f64(), 75.0);
1407        assert_eq!(report.order_status, OrderStatus::Canceled);
1408    }
1409
1410    #[rstest]
1411    fn test_parse_order_status_report_uses_provided_order_qty() {
1412        let order = BitmexOrder {
1413            account: 123456,
1414            symbol: Some(Ustr::from("XBTUSD")),
1415            order_id: Uuid::parse_str("bbbbcccc-dddd-eeee-ffff-000000000000").unwrap(),
1416            cl_ord_id: Some(Ustr::from("client-provided-qty")),
1417            cl_ord_link_id: None,
1418            side: Some(BitmexSide::Sell),
1419            ord_type: Some(BitmexOrderType::Limit),
1420            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1421            ord_status: Some(BitmexOrderStatus::PartiallyFilled),
1422            order_qty: Some(150),  // Explicitly provided
1423            cum_qty: Some(50),     // Filled 50
1424            leaves_qty: Some(100), // Remaining 100
1425            price: Some(48000.0),
1426            stop_px: None,
1427            display_qty: None,
1428            peg_offset_value: None,
1429            peg_price_type: None,
1430            currency: Some(Ustr::from("USD")),
1431            settl_currency: Some(Ustr::from("XBt")),
1432            exec_inst: None,
1433            contingency_type: None,
1434            ex_destination: None,
1435            triggered: None,
1436            working_indicator: Some(true),
1437            ord_rej_reason: None,
1438            avg_px: Some(48100.0),
1439            multi_leg_reporting_type: None,
1440            text: None,
1441            transact_time: Some(
1442                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1443                    .unwrap()
1444                    .with_timezone(&Utc),
1445            ),
1446            timestamp: Some(
1447                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1448                    .unwrap()
1449                    .with_timezone(&Utc),
1450            ),
1451        };
1452
1453        let instrument =
1454            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1455                .unwrap();
1456        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1457
1458        // Verify order_qty was used directly (not reconstructed)
1459        assert_eq!(report.quantity.as_f64(), 150.0);
1460        assert_eq!(report.filled_qty.as_f64(), 50.0);
1461        assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
1462    }
1463
1464    #[rstest]
1465    fn test_parse_order_status_report_missing_order_qty_fails() {
1466        let order = BitmexOrder {
1467            account: 789012,
1468            symbol: Some(Ustr::from("XBTUSD")),
1469            order_id: Uuid::parse_str("aaaabbbb-cccc-dddd-eeee-ffffffffffff").unwrap(),
1470            cl_ord_id: Some(Ustr::from("client-fail-test")),
1471            cl_ord_link_id: None,
1472            side: Some(BitmexSide::Buy),
1473            ord_type: Some(BitmexOrderType::Limit),
1474            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1475            ord_status: Some(BitmexOrderStatus::PartiallyFilled),
1476            order_qty: None,   // Missing
1477            cum_qty: Some(75), // Present
1478            leaves_qty: None,  // Missing - cannot reconstruct
1479            price: Some(45000.0),
1480            stop_px: None,
1481            display_qty: None,
1482            peg_offset_value: None,
1483            peg_price_type: None,
1484            currency: Some(Ustr::from("USD")),
1485            settl_currency: Some(Ustr::from("XBt")),
1486            exec_inst: None,
1487            contingency_type: None,
1488            ex_destination: None,
1489            triggered: None,
1490            working_indicator: Some(false),
1491            ord_rej_reason: None,
1492            avg_px: None,
1493            multi_leg_reporting_type: None,
1494            text: None,
1495            transact_time: Some(
1496                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1497                    .unwrap()
1498                    .with_timezone(&Utc),
1499            ),
1500            timestamp: Some(
1501                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1502                    .unwrap()
1503                    .with_timezone(&Utc),
1504            ),
1505        };
1506
1507        let instrument =
1508            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1509                .unwrap();
1510
1511        // Should fail because we cannot reconstruct order_qty
1512        let result = parse_order_status_report(&order, &instrument, UnixNanos::from(1));
1513        assert!(result.is_err());
1514        assert!(
1515            result
1516                .unwrap_err()
1517                .to_string()
1518                .contains("Order missing order_qty and cannot reconstruct")
1519        );
1520    }
1521
1522    #[rstest]
1523    fn test_parse_order_status_report_canceled_missing_all_quantities() {
1524        let order = BitmexOrder {
1525            account: 123456,
1526            symbol: Some(Ustr::from("XBTUSD")),
1527            order_id: Uuid::parse_str("ffff0000-1111-2222-3333-444444444444").unwrap(),
1528            cl_ord_id: Some(Ustr::from("client-cancel-no-qty")),
1529            cl_ord_link_id: None,
1530            side: Some(BitmexSide::Buy),
1531            ord_type: Some(BitmexOrderType::Limit),
1532            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1533            ord_status: Some(BitmexOrderStatus::Canceled),
1534            order_qty: None,  // Missing
1535            cum_qty: None,    // Missing
1536            leaves_qty: None, // Missing
1537            price: Some(50000.0),
1538            stop_px: None,
1539            display_qty: None,
1540            peg_offset_value: None,
1541            peg_price_type: None,
1542            currency: Some(Ustr::from("USD")),
1543            settl_currency: Some(Ustr::from("XBt")),
1544            exec_inst: None,
1545            contingency_type: None,
1546            ex_destination: None,
1547            triggered: None,
1548            working_indicator: Some(false),
1549            ord_rej_reason: None,
1550            avg_px: None,
1551            multi_leg_reporting_type: None,
1552            text: None,
1553            transact_time: Some(
1554                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1555                    .unwrap()
1556                    .with_timezone(&Utc),
1557            ),
1558            timestamp: Some(
1559                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1560                    .unwrap()
1561                    .with_timezone(&Utc),
1562            ),
1563        };
1564
1565        let instrument =
1566            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1567                .unwrap();
1568        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1569
1570        // For canceled orders with missing quantities, parser uses 0 (will be reconciled from cache)
1571        assert_eq!(report.order_status, OrderStatus::Canceled);
1572        assert_eq!(report.quantity.as_f64(), 0.0);
1573        assert_eq!(report.filled_qty.as_f64(), 0.0);
1574    }
1575
1576    #[rstest]
1577    fn test_parse_order_status_report_rejected_with_reason() {
1578        let order = BitmexOrder {
1579            account: 123456,
1580            symbol: Some(Ustr::from("XBTUSD")),
1581            order_id: Uuid::parse_str("ccccdddd-eeee-ffff-0000-111111111111").unwrap(),
1582            cl_ord_id: Some(Ustr::from("client-rejected")),
1583            cl_ord_link_id: None,
1584            side: Some(BitmexSide::Buy),
1585            ord_type: Some(BitmexOrderType::Limit),
1586            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1587            ord_status: Some(BitmexOrderStatus::Rejected),
1588            order_qty: Some(100),
1589            cum_qty: Some(0),
1590            leaves_qty: Some(0),
1591            price: Some(50000.0),
1592            stop_px: None,
1593            display_qty: None,
1594            peg_offset_value: None,
1595            peg_price_type: None,
1596            currency: Some(Ustr::from("USD")),
1597            settl_currency: Some(Ustr::from("XBt")),
1598            exec_inst: None,
1599            contingency_type: None,
1600            ex_destination: None,
1601            triggered: None,
1602            working_indicator: Some(false),
1603            ord_rej_reason: Some(Ustr::from("Insufficient margin")),
1604            avg_px: None,
1605            multi_leg_reporting_type: None,
1606            text: None,
1607            transact_time: Some(
1608                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1609                    .unwrap()
1610                    .with_timezone(&Utc),
1611            ),
1612            timestamp: Some(
1613                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1614                    .unwrap()
1615                    .with_timezone(&Utc),
1616            ),
1617        };
1618
1619        let instrument =
1620            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1621                .unwrap();
1622        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1623
1624        assert_eq!(report.order_status, OrderStatus::Rejected);
1625        assert_eq!(
1626            report.cancel_reason,
1627            Some("Insufficient margin".to_string())
1628        );
1629    }
1630
1631    #[rstest]
1632    fn test_parse_order_status_report_rejected_with_text_fallback() {
1633        let order = BitmexOrder {
1634            account: 123456,
1635            symbol: Some(Ustr::from("XBTUSD")),
1636            order_id: Uuid::parse_str("ddddeeee-ffff-0000-1111-222222222222").unwrap(),
1637            cl_ord_id: Some(Ustr::from("client-rejected-text")),
1638            cl_ord_link_id: None,
1639            side: Some(BitmexSide::Sell),
1640            ord_type: Some(BitmexOrderType::Limit),
1641            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
1642            ord_status: Some(BitmexOrderStatus::Rejected),
1643            order_qty: Some(100),
1644            cum_qty: Some(0),
1645            leaves_qty: Some(0),
1646            price: Some(50000.0),
1647            stop_px: None,
1648            display_qty: None,
1649            peg_offset_value: None,
1650            peg_price_type: None,
1651            currency: Some(Ustr::from("USD")),
1652            settl_currency: Some(Ustr::from("XBt")),
1653            exec_inst: None,
1654            contingency_type: None,
1655            ex_destination: None,
1656            triggered: None,
1657            working_indicator: Some(false),
1658            ord_rej_reason: None,
1659            avg_px: None,
1660            multi_leg_reporting_type: None,
1661            text: Some(Ustr::from("Order would immediately execute")),
1662            transact_time: Some(
1663                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1664                    .unwrap()
1665                    .with_timezone(&Utc),
1666            ),
1667            timestamp: Some(
1668                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1669                    .unwrap()
1670                    .with_timezone(&Utc),
1671            ),
1672        };
1673
1674        let instrument =
1675            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1676                .unwrap();
1677        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1678
1679        assert_eq!(report.order_status, OrderStatus::Rejected);
1680        assert_eq!(
1681            report.cancel_reason,
1682            Some("Order would immediately execute".to_string())
1683        );
1684    }
1685
1686    #[rstest]
1687    fn test_parse_order_status_report_rejected_without_reason() {
1688        let order = BitmexOrder {
1689            account: 123456,
1690            symbol: Some(Ustr::from("XBTUSD")),
1691            order_id: Uuid::parse_str("eeeeffff-0000-1111-2222-333333333333").unwrap(),
1692            cl_ord_id: Some(Ustr::from("client-rejected-no-reason")),
1693            cl_ord_link_id: None,
1694            side: Some(BitmexSide::Buy),
1695            ord_type: Some(BitmexOrderType::Market),
1696            time_in_force: Some(BitmexTimeInForce::ImmediateOrCancel),
1697            ord_status: Some(BitmexOrderStatus::Rejected),
1698            order_qty: Some(50),
1699            cum_qty: Some(0),
1700            leaves_qty: Some(0),
1701            price: None,
1702            stop_px: None,
1703            display_qty: None,
1704            peg_offset_value: None,
1705            peg_price_type: None,
1706            currency: Some(Ustr::from("USD")),
1707            settl_currency: Some(Ustr::from("XBt")),
1708            exec_inst: None,
1709            contingency_type: None,
1710            ex_destination: None,
1711            triggered: None,
1712            working_indicator: Some(false),
1713            ord_rej_reason: None,
1714            avg_px: None,
1715            multi_leg_reporting_type: None,
1716            text: None,
1717            transact_time: Some(
1718                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1719                    .unwrap()
1720                    .with_timezone(&Utc),
1721            ),
1722            timestamp: Some(
1723                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
1724                    .unwrap()
1725                    .with_timezone(&Utc),
1726            ),
1727        };
1728
1729        let instrument =
1730            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1731                .unwrap();
1732        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
1733
1734        assert_eq!(report.order_status, OrderStatus::Rejected);
1735        assert_eq!(report.cancel_reason, None);
1736    }
1737
1738    #[rstest]
1739    fn test_parse_fill_report() {
1740        let exec = BitmexExecution {
1741            exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
1742            account: 654321,
1743            symbol: Some(Ustr::from("XBTUSD")),
1744            order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
1745            cl_ord_id: Some(Ustr::from("client-456")),
1746            side: Some(BitmexSide::Buy),
1747            last_qty: 50,
1748            last_px: 50100.5,
1749            commission: Some(0.00075),
1750            settl_currency: Some(Ustr::from("XBt")),
1751            last_liquidity_ind: Some(BitmexLiquidityIndicator::Taker),
1752            trd_match_id: Some(Uuid::parse_str("99999999-8888-7777-6666-555555555555").unwrap()),
1753            transact_time: Some(
1754                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1755                    .unwrap()
1756                    .with_timezone(&Utc),
1757            ),
1758            cl_ord_link_id: None,
1759            underlying_last_px: None,
1760            last_mkt: None,
1761            order_qty: Some(50),
1762            price: Some(50100.0),
1763            display_qty: None,
1764            stop_px: None,
1765            peg_offset_value: None,
1766            peg_price_type: None,
1767            currency: None,
1768            exec_type: BitmexExecType::Trade,
1769            ord_type: BitmexOrderType::Limit,
1770            time_in_force: BitmexTimeInForce::GoodTillCancel,
1771            exec_inst: None,
1772            contingency_type: None,
1773            ex_destination: None,
1774            ord_status: Some(BitmexOrderStatus::Filled),
1775            triggered: None,
1776            working_indicator: None,
1777            ord_rej_reason: None,
1778            leaves_qty: None,
1779            cum_qty: Some(50),
1780            avg_px: Some(50100.5),
1781            trade_publish_indicator: None,
1782            multi_leg_reporting_type: None,
1783            text: None,
1784            exec_cost: None,
1785            exec_comm: None,
1786            home_notional: None,
1787            foreign_notional: None,
1788            timestamp: None,
1789        };
1790
1791        let instrument =
1792            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1793                .unwrap();
1794
1795        let report = parse_fill_report(exec, &instrument, UnixNanos::from(1)).unwrap();
1796
1797        assert_eq!(report.account_id.to_string(), "BITMEX-654321");
1798        assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
1799        assert_eq!(
1800            report.venue_order_id.as_str(),
1801            "a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6"
1802        );
1803        assert_eq!(
1804            report.trade_id.to_string(),
1805            "99999999-8888-7777-6666-555555555555"
1806        );
1807        assert_eq!(report.client_order_id.unwrap().as_str(), "client-456");
1808        assert_eq!(report.last_qty.as_f64(), 50.0);
1809        assert_eq!(report.last_px.as_f64(), 50100.5);
1810        assert_eq!(report.commission.as_f64(), 0.00075);
1811        assert_eq!(report.commission.currency.code.as_str(), "XBT");
1812        assert_eq!(report.liquidity_side, LiquiditySide::Taker);
1813    }
1814
1815    #[rstest]
1816    fn test_parse_fill_report_with_missing_trd_match_id() {
1817        let exec = BitmexExecution {
1818            exec_id: Uuid::parse_str("f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6").unwrap(),
1819            account: 111111,
1820            symbol: Some(Ustr::from("ETHUSD")),
1821            order_id: Some(Uuid::parse_str("a1a2a3a4-b5b6-c7c8-d9d0-e1e2e3e4e5e6").unwrap()),
1822            cl_ord_id: None,
1823            side: Some(BitmexSide::Sell),
1824            last_qty: 100,
1825            last_px: 3000.0,
1826            commission: None,
1827            settl_currency: None,
1828            last_liquidity_ind: Some(BitmexLiquidityIndicator::Maker),
1829            trd_match_id: None, // Missing, should fall back to exec_id
1830            transact_time: Some(
1831                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1832                    .unwrap()
1833                    .with_timezone(&Utc),
1834            ),
1835            cl_ord_link_id: None,
1836            underlying_last_px: None,
1837            last_mkt: None,
1838            order_qty: Some(100),
1839            price: Some(3000.0),
1840            display_qty: None,
1841            stop_px: None,
1842            peg_offset_value: None,
1843            peg_price_type: None,
1844            currency: None,
1845            exec_type: BitmexExecType::Trade,
1846            ord_type: BitmexOrderType::Market,
1847            time_in_force: BitmexTimeInForce::ImmediateOrCancel,
1848            exec_inst: None,
1849            contingency_type: None,
1850            ex_destination: None,
1851            ord_status: Some(BitmexOrderStatus::Filled),
1852            triggered: None,
1853            working_indicator: None,
1854            ord_rej_reason: None,
1855            leaves_qty: None,
1856            cum_qty: Some(100),
1857            avg_px: Some(3000.0),
1858            trade_publish_indicator: None,
1859            multi_leg_reporting_type: None,
1860            text: None,
1861            exec_cost: None,
1862            exec_comm: None,
1863            home_notional: None,
1864            foreign_notional: None,
1865            timestamp: None,
1866        };
1867
1868        let mut instrument_def = create_test_perpetual_instrument();
1869        instrument_def.symbol = Ustr::from("ETHUSD");
1870        instrument_def.underlying = Ustr::from("ETH");
1871        instrument_def.quote_currency = Ustr::from("USD");
1872        instrument_def.settl_currency = Some(Ustr::from("USDt"));
1873        let instrument = parse_perpetual_instrument(&instrument_def, UnixNanos::default()).unwrap();
1874
1875        let report = parse_fill_report(exec, &instrument, UnixNanos::from(1)).unwrap();
1876
1877        assert_eq!(report.account_id.to_string(), "BITMEX-111111");
1878        assert_eq!(report.instrument_id.to_string(), "ETHUSD.BITMEX");
1879        assert_eq!(
1880            report.trade_id.to_string(),
1881            "f1f2f3f4-e5e6-d7d8-c9c0-b1b2b3b4b5b6"
1882        );
1883        assert!(report.client_order_id.is_none());
1884        assert_eq!(report.commission.as_f64(), 0.0);
1885        assert_eq!(report.commission.currency.code.as_str(), "XBT");
1886        assert_eq!(report.liquidity_side, LiquiditySide::Maker);
1887    }
1888
1889    #[rstest]
1890    fn test_parse_position_report() {
1891        let position = BitmexPosition {
1892            account: 789012,
1893            symbol: Ustr::from("XBTUSD"),
1894            current_qty: Some(1000),
1895            timestamp: Some(
1896                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
1897                    .unwrap()
1898                    .with_timezone(&Utc),
1899            ),
1900            currency: None,
1901            underlying: None,
1902            quote_currency: None,
1903            commission: None,
1904            init_margin_req: None,
1905            maint_margin_req: None,
1906            risk_limit: None,
1907            leverage: None,
1908            cross_margin: None,
1909            deleverage_percentile: None,
1910            rebalanced_pnl: None,
1911            prev_realised_pnl: None,
1912            prev_unrealised_pnl: None,
1913            prev_close_price: None,
1914            opening_timestamp: None,
1915            opening_qty: None,
1916            opening_cost: None,
1917            opening_comm: None,
1918            open_order_buy_qty: None,
1919            open_order_buy_cost: None,
1920            open_order_buy_premium: None,
1921            open_order_sell_qty: None,
1922            open_order_sell_cost: None,
1923            open_order_sell_premium: None,
1924            exec_buy_qty: None,
1925            exec_buy_cost: None,
1926            exec_sell_qty: None,
1927            exec_sell_cost: None,
1928            exec_qty: None,
1929            exec_cost: None,
1930            exec_comm: None,
1931            current_timestamp: None,
1932            current_cost: None,
1933            current_comm: None,
1934            realised_cost: None,
1935            unrealised_cost: None,
1936            gross_open_cost: None,
1937            gross_open_premium: None,
1938            gross_exec_cost: None,
1939            is_open: Some(true),
1940            mark_price: None,
1941            mark_value: None,
1942            risk_value: None,
1943            home_notional: None,
1944            foreign_notional: None,
1945            pos_state: None,
1946            pos_cost: None,
1947            pos_cost2: None,
1948            pos_cross: None,
1949            pos_init: None,
1950            pos_comm: None,
1951            pos_loss: None,
1952            pos_margin: None,
1953            pos_maint: None,
1954            pos_allowance: None,
1955            taxable_margin: None,
1956            init_margin: None,
1957            maint_margin: None,
1958            session_margin: None,
1959            target_excess_margin: None,
1960            var_margin: None,
1961            realised_gross_pnl: None,
1962            realised_tax: None,
1963            realised_pnl: None,
1964            unrealised_gross_pnl: None,
1965            long_bankrupt: None,
1966            short_bankrupt: None,
1967            tax_base: None,
1968            indicative_tax_rate: None,
1969            indicative_tax: None,
1970            unrealised_tax: None,
1971            unrealised_pnl: None,
1972            unrealised_pnl_pcnt: None,
1973            unrealised_roe_pcnt: None,
1974            avg_cost_price: None,
1975            avg_entry_price: None,
1976            break_even_price: None,
1977            margin_call_price: None,
1978            liquidation_price: None,
1979            bankrupt_price: None,
1980            last_price: None,
1981            last_value: None,
1982        };
1983
1984        let instrument =
1985            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
1986                .unwrap();
1987
1988        let report = parse_position_report(position, &instrument, UnixNanos::from(1)).unwrap();
1989
1990        assert_eq!(report.account_id.to_string(), "BITMEX-789012");
1991        assert_eq!(report.instrument_id.to_string(), "XBTUSD.BITMEX");
1992        assert_eq!(report.position_side.as_position_side(), PositionSide::Long);
1993        assert_eq!(report.quantity.as_f64(), 1000.0);
1994    }
1995
1996    #[rstest]
1997    fn test_parse_position_report_short() {
1998        let position = BitmexPosition {
1999            account: 789012,
2000            symbol: Ustr::from("ETHUSD"),
2001            current_qty: Some(-500),
2002            timestamp: Some(
2003                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2004                    .unwrap()
2005                    .with_timezone(&Utc),
2006            ),
2007            currency: None,
2008            underlying: None,
2009            quote_currency: None,
2010            commission: None,
2011            init_margin_req: None,
2012            maint_margin_req: None,
2013            risk_limit: None,
2014            leverage: None,
2015            cross_margin: None,
2016            deleverage_percentile: None,
2017            rebalanced_pnl: None,
2018            prev_realised_pnl: None,
2019            prev_unrealised_pnl: None,
2020            prev_close_price: None,
2021            opening_timestamp: None,
2022            opening_qty: None,
2023            opening_cost: None,
2024            opening_comm: None,
2025            open_order_buy_qty: None,
2026            open_order_buy_cost: None,
2027            open_order_buy_premium: None,
2028            open_order_sell_qty: None,
2029            open_order_sell_cost: None,
2030            open_order_sell_premium: None,
2031            exec_buy_qty: None,
2032            exec_buy_cost: None,
2033            exec_sell_qty: None,
2034            exec_sell_cost: None,
2035            exec_qty: None,
2036            exec_cost: None,
2037            exec_comm: None,
2038            current_timestamp: None,
2039            current_cost: None,
2040            current_comm: None,
2041            realised_cost: None,
2042            unrealised_cost: None,
2043            gross_open_cost: None,
2044            gross_open_premium: None,
2045            gross_exec_cost: None,
2046            is_open: Some(true),
2047            mark_price: None,
2048            mark_value: None,
2049            risk_value: None,
2050            home_notional: None,
2051            foreign_notional: None,
2052            pos_state: None,
2053            pos_cost: None,
2054            pos_cost2: None,
2055            pos_cross: None,
2056            pos_init: None,
2057            pos_comm: None,
2058            pos_loss: None,
2059            pos_margin: None,
2060            pos_maint: None,
2061            pos_allowance: None,
2062            taxable_margin: None,
2063            init_margin: None,
2064            maint_margin: None,
2065            session_margin: None,
2066            target_excess_margin: None,
2067            var_margin: None,
2068            realised_gross_pnl: None,
2069            realised_tax: None,
2070            realised_pnl: None,
2071            unrealised_gross_pnl: None,
2072            long_bankrupt: None,
2073            short_bankrupt: None,
2074            tax_base: None,
2075            indicative_tax_rate: None,
2076            indicative_tax: None,
2077            unrealised_tax: None,
2078            unrealised_pnl: None,
2079            unrealised_pnl_pcnt: None,
2080            unrealised_roe_pcnt: None,
2081            avg_cost_price: None,
2082            avg_entry_price: None,
2083            break_even_price: None,
2084            margin_call_price: None,
2085            liquidation_price: None,
2086            bankrupt_price: None,
2087            last_price: None,
2088            last_value: None,
2089        };
2090
2091        let mut instrument_def = create_test_futures_instrument();
2092        instrument_def.symbol = Ustr::from("ETHUSD");
2093        instrument_def.underlying = Ustr::from("ETH");
2094        instrument_def.quote_currency = Ustr::from("USD");
2095        instrument_def.settl_currency = Some(Ustr::from("USD"));
2096        let instrument = parse_futures_instrument(&instrument_def, UnixNanos::default()).unwrap();
2097
2098        let report = parse_position_report(position, &instrument, UnixNanos::from(1)).unwrap();
2099
2100        assert_eq!(report.position_side.as_position_side(), PositionSide::Short);
2101        assert_eq!(report.quantity.as_f64(), 500.0); // Should be absolute value
2102    }
2103
2104    #[rstest]
2105    fn test_parse_position_report_flat() {
2106        let position = BitmexPosition {
2107            account: 789012,
2108            symbol: Ustr::from("SOLUSD"),
2109            current_qty: Some(0),
2110            timestamp: Some(
2111                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2112                    .unwrap()
2113                    .with_timezone(&Utc),
2114            ),
2115            currency: None,
2116            underlying: None,
2117            quote_currency: None,
2118            commission: None,
2119            init_margin_req: None,
2120            maint_margin_req: None,
2121            risk_limit: None,
2122            leverage: None,
2123            cross_margin: None,
2124            deleverage_percentile: None,
2125            rebalanced_pnl: None,
2126            prev_realised_pnl: None,
2127            prev_unrealised_pnl: None,
2128            prev_close_price: None,
2129            opening_timestamp: None,
2130            opening_qty: None,
2131            opening_cost: None,
2132            opening_comm: None,
2133            open_order_buy_qty: None,
2134            open_order_buy_cost: None,
2135            open_order_buy_premium: None,
2136            open_order_sell_qty: None,
2137            open_order_sell_cost: None,
2138            open_order_sell_premium: None,
2139            exec_buy_qty: None,
2140            exec_buy_cost: None,
2141            exec_sell_qty: None,
2142            exec_sell_cost: None,
2143            exec_qty: None,
2144            exec_cost: None,
2145            exec_comm: None,
2146            current_timestamp: None,
2147            current_cost: None,
2148            current_comm: None,
2149            realised_cost: None,
2150            unrealised_cost: None,
2151            gross_open_cost: None,
2152            gross_open_premium: None,
2153            gross_exec_cost: None,
2154            is_open: Some(true),
2155            mark_price: None,
2156            mark_value: None,
2157            risk_value: None,
2158            home_notional: None,
2159            foreign_notional: None,
2160            pos_state: None,
2161            pos_cost: None,
2162            pos_cost2: None,
2163            pos_cross: None,
2164            pos_init: None,
2165            pos_comm: None,
2166            pos_loss: None,
2167            pos_margin: None,
2168            pos_maint: None,
2169            pos_allowance: None,
2170            taxable_margin: None,
2171            init_margin: None,
2172            maint_margin: None,
2173            session_margin: None,
2174            target_excess_margin: None,
2175            var_margin: None,
2176            realised_gross_pnl: None,
2177            realised_tax: None,
2178            realised_pnl: None,
2179            unrealised_gross_pnl: None,
2180            long_bankrupt: None,
2181            short_bankrupt: None,
2182            tax_base: None,
2183            indicative_tax_rate: None,
2184            indicative_tax: None,
2185            unrealised_tax: None,
2186            unrealised_pnl: None,
2187            unrealised_pnl_pcnt: None,
2188            unrealised_roe_pcnt: None,
2189            avg_cost_price: None,
2190            avg_entry_price: None,
2191            break_even_price: None,
2192            margin_call_price: None,
2193            liquidation_price: None,
2194            bankrupt_price: None,
2195            last_price: None,
2196            last_value: None,
2197        };
2198
2199        let mut instrument_def = create_test_spot_instrument();
2200        instrument_def.symbol = Ustr::from("SOLUSD");
2201        instrument_def.underlying = Ustr::from("SOL");
2202        instrument_def.quote_currency = Ustr::from("USD");
2203        let instrument = parse_spot_instrument(&instrument_def, UnixNanos::default()).unwrap();
2204
2205        let report = parse_position_report(position, &instrument, UnixNanos::from(1)).unwrap();
2206
2207        assert_eq!(report.position_side.as_position_side(), PositionSide::Flat);
2208        assert_eq!(report.quantity.as_f64(), 0.0);
2209    }
2210
2211    #[rstest]
2212    fn test_parse_position_report_spot_scaling() {
2213        let position = BitmexPosition {
2214            account: 789012,
2215            symbol: Ustr::from("SOLUSD"),
2216            current_qty: Some(1000),
2217            timestamp: Some(
2218                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2219                    .unwrap()
2220                    .with_timezone(&Utc),
2221            ),
2222            currency: None,
2223            underlying: None,
2224            quote_currency: None,
2225            commission: None,
2226            init_margin_req: None,
2227            maint_margin_req: None,
2228            risk_limit: None,
2229            leverage: None,
2230            cross_margin: None,
2231            deleverage_percentile: None,
2232            rebalanced_pnl: None,
2233            prev_realised_pnl: None,
2234            prev_unrealised_pnl: None,
2235            prev_close_price: None,
2236            opening_timestamp: None,
2237            opening_qty: None,
2238            opening_cost: None,
2239            opening_comm: None,
2240            open_order_buy_qty: None,
2241            open_order_buy_cost: None,
2242            open_order_buy_premium: None,
2243            open_order_sell_qty: None,
2244            open_order_sell_cost: None,
2245            open_order_sell_premium: None,
2246            exec_buy_qty: None,
2247            exec_buy_cost: None,
2248            exec_sell_qty: None,
2249            exec_sell_cost: None,
2250            exec_qty: None,
2251            exec_cost: None,
2252            exec_comm: None,
2253            current_timestamp: None,
2254            current_cost: None,
2255            current_comm: None,
2256            realised_cost: None,
2257            unrealised_cost: None,
2258            gross_open_cost: None,
2259            gross_open_premium: None,
2260            gross_exec_cost: None,
2261            is_open: Some(true),
2262            mark_price: None,
2263            mark_value: None,
2264            risk_value: None,
2265            home_notional: None,
2266            foreign_notional: None,
2267            pos_state: None,
2268            pos_cost: None,
2269            pos_cost2: None,
2270            pos_cross: None,
2271            pos_init: None,
2272            pos_comm: None,
2273            pos_loss: None,
2274            pos_margin: None,
2275            pos_maint: None,
2276            pos_allowance: None,
2277            taxable_margin: None,
2278            init_margin: None,
2279            maint_margin: None,
2280            session_margin: None,
2281            target_excess_margin: None,
2282            var_margin: None,
2283            realised_gross_pnl: None,
2284            realised_tax: None,
2285            realised_pnl: None,
2286            unrealised_gross_pnl: None,
2287            long_bankrupt: None,
2288            short_bankrupt: None,
2289            tax_base: None,
2290            indicative_tax_rate: None,
2291            indicative_tax: None,
2292            unrealised_tax: None,
2293            unrealised_pnl: None,
2294            unrealised_pnl_pcnt: None,
2295            unrealised_roe_pcnt: None,
2296            avg_cost_price: None,
2297            avg_entry_price: None,
2298            break_even_price: None,
2299            margin_call_price: None,
2300            liquidation_price: None,
2301            bankrupt_price: None,
2302            last_price: None,
2303            last_value: None,
2304        };
2305
2306        let mut instrument_def = create_test_spot_instrument();
2307        instrument_def.symbol = Ustr::from("SOLUSD");
2308        instrument_def.underlying = Ustr::from("SOL");
2309        instrument_def.quote_currency = Ustr::from("USD");
2310        let instrument = parse_spot_instrument(&instrument_def, UnixNanos::default()).unwrap();
2311
2312        let report = parse_position_report(position, &instrument, UnixNanos::from(1)).unwrap();
2313
2314        assert_eq!(report.position_side.as_position_side(), PositionSide::Long);
2315        assert!((report.quantity.as_f64() - 0.1).abs() < 1e-9);
2316    }
2317
2318    fn create_test_spot_instrument() -> BitmexInstrument {
2319        BitmexInstrument {
2320            symbol: Ustr::from("XBTUSD"),
2321            root_symbol: Ustr::from("XBT"),
2322            state: BitmexInstrumentState::Open,
2323            instrument_type: BitmexInstrumentType::Spot,
2324            listing: Some(
2325                DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2326                    .unwrap()
2327                    .with_timezone(&Utc),
2328            ),
2329            front: Some(
2330                DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2331                    .unwrap()
2332                    .with_timezone(&Utc),
2333            ),
2334            expiry: None,
2335            settle: None,
2336            listed_settle: None,
2337            position_currency: Some(Ustr::from("USD")),
2338            underlying: Ustr::from("XBT"),
2339            quote_currency: Ustr::from("USD"),
2340            underlying_symbol: Some(Ustr::from("XBT=")),
2341            reference: Some(Ustr::from("BMEX")),
2342            reference_symbol: Some(Ustr::from(".BXBT")),
2343            lot_size: Some(1000.0),
2344            tick_size: 0.01,
2345            multiplier: 1.0,
2346            settl_currency: Some(Ustr::from("USD")),
2347            is_quanto: false,
2348            is_inverse: false,
2349            maker_fee: Some(-0.00025),
2350            taker_fee: Some(0.00075),
2351            timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
2352                .unwrap()
2353                .with_timezone(&Utc),
2354            // Set other fields to reasonable defaults
2355            max_order_qty: Some(10000000.0),
2356            max_price: Some(1000000.0),
2357            settlement_fee: Some(0.0),
2358            mark_price: Some(50500.0),
2359            last_price: Some(50500.0),
2360            bid_price: Some(50499.5),
2361            ask_price: Some(50500.5),
2362            open_interest: Some(0.0),
2363            open_value: Some(0.0),
2364            total_volume: Some(1000000.0),
2365            volume: Some(50000.0),
2366            volume_24h: Some(75000.0),
2367            total_turnover: Some(150000000.0),
2368            turnover: Some(5000000.0),
2369            turnover_24h: Some(7500000.0),
2370            has_liquidity: Some(true),
2371            // Set remaining fields to None/defaults
2372            calc_interval: None,
2373            publish_interval: None,
2374            publish_time: None,
2375            underlying_to_position_multiplier: Some(10000.0),
2376            underlying_to_settle_multiplier: None,
2377            quote_to_settle_multiplier: Some(1.0),
2378            init_margin: Some(0.1),
2379            maint_margin: Some(0.05),
2380            risk_limit: Some(20000000000.0),
2381            risk_step: Some(10000000000.0),
2382            limit: None,
2383            taxed: Some(true),
2384            deleverage: Some(true),
2385            funding_base_symbol: None,
2386            funding_quote_symbol: None,
2387            funding_premium_symbol: None,
2388            funding_timestamp: None,
2389            funding_interval: None,
2390            funding_rate: None,
2391            indicative_funding_rate: None,
2392            rebalance_timestamp: None,
2393            rebalance_interval: None,
2394            prev_close_price: Some(50000.0),
2395            limit_down_price: None,
2396            limit_up_price: None,
2397            prev_total_turnover: Some(100000000.0),
2398            home_notional_24h: Some(1.5),
2399            foreign_notional_24h: Some(75000.0),
2400            prev_price_24h: Some(49500.0),
2401            vwap: Some(50100.0),
2402            high_price: Some(51000.0),
2403            low_price: Some(49000.0),
2404            last_price_protected: Some(50500.0),
2405            last_tick_direction: Some(BitmexTickDirection::PlusTick),
2406            last_change_pcnt: Some(0.0202),
2407            mid_price: Some(50500.0),
2408            impact_bid_price: Some(50490.0),
2409            impact_mid_price: Some(50495.0),
2410            impact_ask_price: Some(50500.0),
2411            fair_method: None,
2412            fair_basis_rate: None,
2413            fair_basis: None,
2414            fair_price: None,
2415            mark_method: Some(BitmexMarkMethod::LastPrice),
2416            indicative_settle_price: None,
2417            settled_price_adjustment_rate: None,
2418            settled_price: None,
2419            instant_pnl: false,
2420            min_tick: None,
2421            funding_base_rate: None,
2422            funding_quote_rate: None,
2423            capped: None,
2424            opening_timestamp: None,
2425            closing_timestamp: None,
2426            prev_total_volume: None,
2427        }
2428    }
2429
2430    fn create_test_perpetual_instrument() -> BitmexInstrument {
2431        BitmexInstrument {
2432            symbol: Ustr::from("XBTUSD"),
2433            root_symbol: Ustr::from("XBT"),
2434            state: BitmexInstrumentState::Open,
2435            instrument_type: BitmexInstrumentType::PerpetualContract,
2436            listing: Some(
2437                DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2438                    .unwrap()
2439                    .with_timezone(&Utc),
2440            ),
2441            front: Some(
2442                DateTime::parse_from_rfc3339("2016-05-13T12:00:00.000Z")
2443                    .unwrap()
2444                    .with_timezone(&Utc),
2445            ),
2446            expiry: None,
2447            settle: None,
2448            listed_settle: None,
2449            position_currency: Some(Ustr::from("USD")),
2450            underlying: Ustr::from("XBT"),
2451            quote_currency: Ustr::from("USD"),
2452            underlying_symbol: Some(Ustr::from("XBT=")),
2453            reference: Some(Ustr::from("BMEX")),
2454            reference_symbol: Some(Ustr::from(".BXBT")),
2455            lot_size: Some(100.0),
2456            tick_size: 0.5,
2457            multiplier: -100000000.0,
2458            settl_currency: Some(Ustr::from("XBt")),
2459            is_quanto: false,
2460            is_inverse: true,
2461            maker_fee: Some(-0.00025),
2462            taker_fee: Some(0.00075),
2463            timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
2464                .unwrap()
2465                .with_timezone(&Utc),
2466            // Set other fields
2467            max_order_qty: Some(10000000.0),
2468            max_price: Some(1000000.0),
2469            settlement_fee: Some(0.0),
2470            mark_price: Some(50500.01),
2471            last_price: Some(50500.0),
2472            bid_price: Some(50499.5),
2473            ask_price: Some(50500.5),
2474            open_interest: Some(500000000.0),
2475            open_value: Some(990099009900.0),
2476            total_volume: Some(12345678900000.0),
2477            volume: Some(5000000.0),
2478            volume_24h: Some(75000000.0),
2479            total_turnover: Some(150000000000000.0),
2480            turnover: Some(5000000000.0),
2481            turnover_24h: Some(7500000000.0),
2482            has_liquidity: Some(true),
2483            // Perpetual specific fields
2484            funding_base_symbol: Some(Ustr::from(".XBTBON8H")),
2485            funding_quote_symbol: Some(Ustr::from(".USDBON8H")),
2486            funding_premium_symbol: Some(Ustr::from(".XBTUSDPI8H")),
2487            funding_timestamp: Some(
2488                DateTime::parse_from_rfc3339("2024-01-01T08:00:00.000Z")
2489                    .unwrap()
2490                    .with_timezone(&Utc),
2491            ),
2492            funding_interval: Some(
2493                DateTime::parse_from_rfc3339("2000-01-01T08:00:00.000Z")
2494                    .unwrap()
2495                    .with_timezone(&Utc),
2496            ),
2497            funding_rate: Some(0.0001),
2498            indicative_funding_rate: Some(0.0001),
2499            funding_base_rate: Some(0.01),
2500            funding_quote_rate: Some(-0.01),
2501            // Other fields
2502            calc_interval: None,
2503            publish_interval: None,
2504            publish_time: None,
2505            underlying_to_position_multiplier: None,
2506            underlying_to_settle_multiplier: Some(-100000000.0),
2507            quote_to_settle_multiplier: None,
2508            init_margin: Some(0.01),
2509            maint_margin: Some(0.005),
2510            risk_limit: Some(20000000000.0),
2511            risk_step: Some(10000000000.0),
2512            limit: None,
2513            taxed: Some(true),
2514            deleverage: Some(true),
2515            rebalance_timestamp: None,
2516            rebalance_interval: None,
2517            prev_close_price: Some(50000.0),
2518            limit_down_price: None,
2519            limit_up_price: None,
2520            prev_total_turnover: Some(100000000000000.0),
2521            home_notional_24h: Some(1500.0),
2522            foreign_notional_24h: Some(75000000.0),
2523            prev_price_24h: Some(49500.0),
2524            vwap: Some(50100.0),
2525            high_price: Some(51000.0),
2526            low_price: Some(49000.0),
2527            last_price_protected: Some(50500.0),
2528            last_tick_direction: Some(BitmexTickDirection::PlusTick),
2529            last_change_pcnt: Some(0.0202),
2530            mid_price: Some(50500.0),
2531            impact_bid_price: Some(50490.0),
2532            impact_mid_price: Some(50495.0),
2533            impact_ask_price: Some(50500.0),
2534            fair_method: Some(BitmexFairMethod::FundingRate),
2535            fair_basis_rate: Some(0.1095),
2536            fair_basis: Some(0.01),
2537            fair_price: Some(50500.01),
2538            mark_method: Some(BitmexMarkMethod::FairPrice),
2539            indicative_settle_price: Some(50500.0),
2540            settled_price_adjustment_rate: None,
2541            settled_price: None,
2542            instant_pnl: false,
2543            min_tick: None,
2544            capped: None,
2545            opening_timestamp: None,
2546            closing_timestamp: None,
2547            prev_total_volume: None,
2548        }
2549    }
2550
2551    fn create_test_futures_instrument() -> BitmexInstrument {
2552        BitmexInstrument {
2553            symbol: Ustr::from("XBTH25"),
2554            root_symbol: Ustr::from("XBT"),
2555            state: BitmexInstrumentState::Open,
2556            instrument_type: BitmexInstrumentType::Futures,
2557            listing: Some(
2558                DateTime::parse_from_rfc3339("2024-09-27T12:00:00.000Z")
2559                    .unwrap()
2560                    .with_timezone(&Utc),
2561            ),
2562            front: Some(
2563                DateTime::parse_from_rfc3339("2024-12-27T12:00:00.000Z")
2564                    .unwrap()
2565                    .with_timezone(&Utc),
2566            ),
2567            expiry: Some(
2568                DateTime::parse_from_rfc3339("2025-03-28T12:00:00.000Z")
2569                    .unwrap()
2570                    .with_timezone(&Utc),
2571            ),
2572            settle: Some(
2573                DateTime::parse_from_rfc3339("2025-03-28T12:00:00.000Z")
2574                    .unwrap()
2575                    .with_timezone(&Utc),
2576            ),
2577            listed_settle: None,
2578            position_currency: Some(Ustr::from("USD")),
2579            underlying: Ustr::from("XBT"),
2580            quote_currency: Ustr::from("USD"),
2581            underlying_symbol: Some(Ustr::from("XBT=")),
2582            reference: Some(Ustr::from("BMEX")),
2583            reference_symbol: Some(Ustr::from(".BXBT30M")),
2584            lot_size: Some(100.0),
2585            tick_size: 0.5,
2586            multiplier: -100000000.0,
2587            settl_currency: Some(Ustr::from("XBt")),
2588            is_quanto: false,
2589            is_inverse: true,
2590            maker_fee: Some(-0.00025),
2591            taker_fee: Some(0.00075),
2592            settlement_fee: Some(0.0005),
2593            timestamp: DateTime::parse_from_rfc3339("2024-01-01T00:00:00.000Z")
2594                .unwrap()
2595                .with_timezone(&Utc),
2596            // Set other fields
2597            max_order_qty: Some(10000000.0),
2598            max_price: Some(1000000.0),
2599            mark_price: Some(55500.0),
2600            last_price: Some(55500.0),
2601            bid_price: Some(55499.5),
2602            ask_price: Some(55500.5),
2603            open_interest: Some(50000000.0),
2604            open_value: Some(90090090090.0),
2605            total_volume: Some(1000000000.0),
2606            volume: Some(500000.0),
2607            volume_24h: Some(7500000.0),
2608            total_turnover: Some(15000000000000.0),
2609            turnover: Some(500000000.0),
2610            turnover_24h: Some(750000000.0),
2611            has_liquidity: Some(true),
2612            // Futures specific fields
2613            funding_base_symbol: None,
2614            funding_quote_symbol: None,
2615            funding_premium_symbol: None,
2616            funding_timestamp: None,
2617            funding_interval: None,
2618            funding_rate: None,
2619            indicative_funding_rate: None,
2620            funding_base_rate: None,
2621            funding_quote_rate: None,
2622            // Other fields
2623            calc_interval: None,
2624            publish_interval: None,
2625            publish_time: None,
2626            underlying_to_position_multiplier: None,
2627            underlying_to_settle_multiplier: Some(-100000000.0),
2628            quote_to_settle_multiplier: None,
2629            init_margin: Some(0.02),
2630            maint_margin: Some(0.01),
2631            risk_limit: Some(20000000000.0),
2632            risk_step: Some(10000000000.0),
2633            limit: None,
2634            taxed: Some(true),
2635            deleverage: Some(true),
2636            rebalance_timestamp: None,
2637            rebalance_interval: None,
2638            prev_close_price: Some(55000.0),
2639            limit_down_price: None,
2640            limit_up_price: None,
2641            prev_total_turnover: Some(10000000000000.0),
2642            home_notional_24h: Some(150.0),
2643            foreign_notional_24h: Some(7500000.0),
2644            prev_price_24h: Some(54500.0),
2645            vwap: Some(55100.0),
2646            high_price: Some(56000.0),
2647            low_price: Some(54000.0),
2648            last_price_protected: Some(55500.0),
2649            last_tick_direction: Some(BitmexTickDirection::PlusTick),
2650            last_change_pcnt: Some(0.0183),
2651            mid_price: Some(55500.0),
2652            impact_bid_price: Some(55490.0),
2653            impact_mid_price: Some(55495.0),
2654            impact_ask_price: Some(55500.0),
2655            fair_method: Some(BitmexFairMethod::ImpactMidPrice),
2656            fair_basis_rate: Some(1.8264),
2657            fair_basis: Some(1000.0),
2658            fair_price: Some(55500.0),
2659            mark_method: Some(BitmexMarkMethod::FairPrice),
2660            indicative_settle_price: Some(55500.0),
2661            settled_price_adjustment_rate: None,
2662            settled_price: None,
2663            instant_pnl: false,
2664            min_tick: None,
2665            capped: None,
2666            opening_timestamp: None,
2667            closing_timestamp: None,
2668            prev_total_volume: None,
2669        }
2670    }
2671
2672    #[rstest]
2673    fn test_parse_spot_instrument() {
2674        let instrument = create_test_spot_instrument();
2675        let ts_init = UnixNanos::default();
2676        let result = parse_spot_instrument(&instrument, ts_init).unwrap();
2677
2678        // Check it's a CurrencyPair variant
2679        match result {
2680            InstrumentAny::CurrencyPair(spot) => {
2681                assert_eq!(spot.id.symbol.as_str(), "XBTUSD");
2682                assert_eq!(spot.id.venue.as_str(), "BITMEX");
2683                assert_eq!(spot.raw_symbol.as_str(), "XBTUSD");
2684                assert_eq!(spot.price_precision, 2);
2685                assert_eq!(spot.size_precision, 4);
2686                assert_eq!(spot.price_increment.as_f64(), 0.01);
2687                assert!((spot.size_increment.as_f64() - 0.0001).abs() < 1e-9);
2688                assert!((spot.lot_size.unwrap().as_f64() - 0.1).abs() < 1e-9);
2689                assert_eq!(spot.maker_fee.to_f64().unwrap(), -0.00025);
2690                assert_eq!(spot.taker_fee.to_f64().unwrap(), 0.00075);
2691            }
2692            _ => panic!("Expected CurrencyPair variant"),
2693        }
2694    }
2695
2696    #[rstest]
2697    fn test_parse_perpetual_instrument() {
2698        let instrument = create_test_perpetual_instrument();
2699        let ts_init = UnixNanos::default();
2700        let result = parse_perpetual_instrument(&instrument, ts_init).unwrap();
2701
2702        // Check it's a CryptoPerpetual variant
2703        match result {
2704            InstrumentAny::CryptoPerpetual(perp) => {
2705                assert_eq!(perp.id.symbol.as_str(), "XBTUSD");
2706                assert_eq!(perp.id.venue.as_str(), "BITMEX");
2707                assert_eq!(perp.raw_symbol.as_str(), "XBTUSD");
2708                assert_eq!(perp.price_precision, 1);
2709                assert_eq!(perp.size_precision, 0);
2710                assert_eq!(perp.price_increment.as_f64(), 0.5);
2711                assert_eq!(perp.size_increment.as_f64(), 1.0);
2712                assert_eq!(perp.maker_fee.to_f64().unwrap(), -0.00025);
2713                assert_eq!(perp.taker_fee.to_f64().unwrap(), 0.00075);
2714                assert!(perp.is_inverse);
2715            }
2716            _ => panic!("Expected CryptoPerpetual variant"),
2717        }
2718    }
2719
2720    #[rstest]
2721    fn test_parse_futures_instrument() {
2722        let instrument = create_test_futures_instrument();
2723        let ts_init = UnixNanos::default();
2724        let result = parse_futures_instrument(&instrument, ts_init).unwrap();
2725
2726        // Check it's a CryptoFuture variant
2727        match result {
2728            InstrumentAny::CryptoFuture(instrument) => {
2729                assert_eq!(instrument.id.symbol.as_str(), "XBTH25");
2730                assert_eq!(instrument.id.venue.as_str(), "BITMEX");
2731                assert_eq!(instrument.raw_symbol.as_str(), "XBTH25");
2732                assert_eq!(instrument.underlying.code.as_str(), "XBT");
2733                assert_eq!(instrument.price_precision, 1);
2734                assert_eq!(instrument.size_precision, 0);
2735                assert_eq!(instrument.price_increment.as_f64(), 0.5);
2736                assert_eq!(instrument.size_increment.as_f64(), 1.0);
2737                assert_eq!(instrument.maker_fee.to_f64().unwrap(), -0.00025);
2738                assert_eq!(instrument.taker_fee.to_f64().unwrap(), 0.00075);
2739                assert!(instrument.is_inverse);
2740                // Check expiration timestamp instead of expiry_date
2741                // The futures contract expires on 2025-03-28
2742                assert!(instrument.expiration_ns.as_u64() > 0);
2743            }
2744            _ => panic!("Expected CryptoFuture variant"),
2745        }
2746    }
2747
2748    #[rstest]
2749    fn test_parse_order_status_report_missing_ord_status_infers_filled() {
2750        let order = BitmexOrder {
2751            account: 123456,
2752            symbol: Some(Ustr::from("XBTUSD")),
2753            order_id: Uuid::parse_str("a1b2c3d4-e5f6-7890-abcd-ef1234567890").unwrap(),
2754            cl_ord_id: Some(Ustr::from("client-filled")),
2755            cl_ord_link_id: None,
2756            side: Some(BitmexSide::Buy),
2757            ord_type: Some(BitmexOrderType::Limit),
2758            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
2759            ord_status: None, // Missing - should infer Filled
2760            order_qty: Some(100),
2761            cum_qty: Some(100), // Fully filled
2762            price: Some(50000.0),
2763            stop_px: None,
2764            display_qty: None,
2765            peg_offset_value: None,
2766            peg_price_type: None,
2767            currency: Some(Ustr::from("USD")),
2768            settl_currency: Some(Ustr::from("XBt")),
2769            exec_inst: None,
2770            contingency_type: None,
2771            ex_destination: None,
2772            triggered: None,
2773            working_indicator: Some(false),
2774            ord_rej_reason: None,
2775            leaves_qty: Some(0), // No remaining quantity
2776            avg_px: Some(50050.0),
2777            multi_leg_reporting_type: None,
2778            text: None,
2779            transact_time: Some(
2780                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2781                    .unwrap()
2782                    .with_timezone(&Utc),
2783            ),
2784            timestamp: Some(
2785                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
2786                    .unwrap()
2787                    .with_timezone(&Utc),
2788            ),
2789        };
2790
2791        let instrument =
2792            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2793                .unwrap();
2794        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
2795
2796        assert_eq!(report.order_status, OrderStatus::Filled);
2797        assert_eq!(report.account_id.to_string(), "BITMEX-123456");
2798        assert_eq!(report.filled_qty.as_f64(), 100.0);
2799    }
2800
2801    #[rstest]
2802    fn test_parse_order_status_report_missing_ord_status_infers_canceled() {
2803        let order = BitmexOrder {
2804            account: 123456,
2805            symbol: Some(Ustr::from("XBTUSD")),
2806            order_id: Uuid::parse_str("b2c3d4e5-f6a7-8901-bcde-f12345678901").unwrap(),
2807            cl_ord_id: Some(Ustr::from("client-canceled")),
2808            cl_ord_link_id: None,
2809            side: Some(BitmexSide::Sell),
2810            ord_type: Some(BitmexOrderType::Limit),
2811            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
2812            ord_status: None, // Missing - should infer Canceled
2813            order_qty: Some(200),
2814            cum_qty: Some(0), // Nothing filled
2815            price: Some(60000.0),
2816            stop_px: None,
2817            display_qty: None,
2818            peg_offset_value: None,
2819            peg_price_type: None,
2820            currency: Some(Ustr::from("USD")),
2821            settl_currency: Some(Ustr::from("XBt")),
2822            exec_inst: None,
2823            contingency_type: None,
2824            ex_destination: None,
2825            triggered: None,
2826            working_indicator: Some(false),
2827            ord_rej_reason: None,
2828            leaves_qty: Some(0), // No remaining quantity
2829            avg_px: None,
2830            multi_leg_reporting_type: None,
2831            text: Some(Ustr::from("Canceled: Already filled")),
2832            transact_time: Some(
2833                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2834                    .unwrap()
2835                    .with_timezone(&Utc),
2836            ),
2837            timestamp: Some(
2838                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
2839                    .unwrap()
2840                    .with_timezone(&Utc),
2841            ),
2842        };
2843
2844        let instrument =
2845            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2846                .unwrap();
2847        let report = parse_order_status_report(&order, &instrument, UnixNanos::from(1)).unwrap();
2848
2849        assert_eq!(report.order_status, OrderStatus::Canceled);
2850        assert_eq!(report.account_id.to_string(), "BITMEX-123456");
2851        assert_eq!(report.filled_qty.as_f64(), 0.0);
2852        // Verify text/reason is still captured
2853        assert_eq!(
2854            report.cancel_reason.as_ref().unwrap(),
2855            "Canceled: Already filled"
2856        );
2857    }
2858
2859    #[rstest]
2860    fn test_parse_order_status_report_missing_ord_status_with_leaves_qty_fails() {
2861        let order = BitmexOrder {
2862            account: 123456,
2863            symbol: Some(Ustr::from("XBTUSD")),
2864            order_id: Uuid::parse_str("c3d4e5f6-a7b8-9012-cdef-123456789012").unwrap(),
2865            cl_ord_id: Some(Ustr::from("client-partial")),
2866            cl_ord_link_id: None,
2867            side: Some(BitmexSide::Buy),
2868            ord_type: Some(BitmexOrderType::Limit),
2869            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
2870            ord_status: None, // Missing
2871            order_qty: Some(100),
2872            cum_qty: Some(50),
2873            price: Some(50000.0),
2874            stop_px: None,
2875            display_qty: None,
2876            peg_offset_value: None,
2877            peg_price_type: None,
2878            currency: Some(Ustr::from("USD")),
2879            settl_currency: Some(Ustr::from("XBt")),
2880            exec_inst: None,
2881            contingency_type: None,
2882            ex_destination: None,
2883            triggered: None,
2884            working_indicator: Some(true),
2885            ord_rej_reason: None,
2886            leaves_qty: Some(50), // Still has remaining qty - can't infer status
2887            avg_px: None,
2888            multi_leg_reporting_type: None,
2889            text: None,
2890            transact_time: Some(
2891                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2892                    .unwrap()
2893                    .with_timezone(&Utc),
2894            ),
2895            timestamp: Some(
2896                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
2897                    .unwrap()
2898                    .with_timezone(&Utc),
2899            ),
2900        };
2901
2902        let instrument =
2903            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2904                .unwrap();
2905        let result = parse_order_status_report(&order, &instrument, UnixNanos::from(1));
2906
2907        assert!(result.is_err());
2908        let err_msg = result.unwrap_err().to_string();
2909        assert!(err_msg.contains("missing ord_status"));
2910        assert!(err_msg.contains("cannot infer"));
2911    }
2912
2913    #[rstest]
2914    fn test_parse_order_status_report_missing_ord_status_no_quantities_fails() {
2915        let order = BitmexOrder {
2916            account: 123456,
2917            symbol: Some(Ustr::from("XBTUSD")),
2918            order_id: Uuid::parse_str("d4e5f6a7-b8c9-0123-def0-123456789013").unwrap(),
2919            cl_ord_id: Some(Ustr::from("client-unknown")),
2920            cl_ord_link_id: None,
2921            side: Some(BitmexSide::Buy),
2922            ord_type: Some(BitmexOrderType::Limit),
2923            time_in_force: Some(BitmexTimeInForce::GoodTillCancel),
2924            ord_status: None, // Missing
2925            order_qty: Some(100),
2926            cum_qty: None, // Missing
2927            price: Some(50000.0),
2928            stop_px: None,
2929            display_qty: None,
2930            peg_offset_value: None,
2931            peg_price_type: None,
2932            currency: Some(Ustr::from("USD")),
2933            settl_currency: Some(Ustr::from("XBt")),
2934            exec_inst: None,
2935            contingency_type: None,
2936            ex_destination: None,
2937            triggered: None,
2938            working_indicator: Some(true),
2939            ord_rej_reason: None,
2940            leaves_qty: None, // Missing
2941            avg_px: None,
2942            multi_leg_reporting_type: None,
2943            text: None,
2944            transact_time: Some(
2945                DateTime::parse_from_rfc3339("2024-01-01T00:00:00Z")
2946                    .unwrap()
2947                    .with_timezone(&Utc),
2948            ),
2949            timestamp: Some(
2950                DateTime::parse_from_rfc3339("2024-01-01T00:00:01Z")
2951                    .unwrap()
2952                    .with_timezone(&Utc),
2953            ),
2954        };
2955
2956        let instrument =
2957            parse_perpetual_instrument(&create_test_perpetual_instrument(), UnixNanos::default())
2958                .unwrap();
2959        let result = parse_order_status_report(&order, &instrument, UnixNanos::from(1));
2960
2961        assert!(result.is_err());
2962        let err_msg = result.unwrap_err().to_string();
2963        assert!(err_msg.contains("missing ord_status"));
2964        assert!(err_msg.contains("cannot infer"));
2965    }
2966}