Skip to main content

nautilus_derive/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//! HTTP response parsing utilities for the Derive execution client.
17
18use anyhow::Context;
19use nautilus_core::{Params, UUID4, UnixNanos, datetime::NANOSECONDS_IN_MILLISECOND};
20use nautilus_model::{
21    enums::{LiquiditySide, OrderType, PositionSide},
22    identifiers::{AccountId, ClientOrderId, InstrumentId, Symbol, TradeId, VenueOrderId},
23    reports::{FillReport, OrderStatusReport, PositionStatusReport},
24    types::{AccountBalance, Currency, MarginBalance, Money, Price, Quantity},
25};
26use rust_decimal::Decimal;
27use serde_json::Value;
28
29use crate::{
30    common::{
31        consts::DERIVE_VENUE,
32        enums::{
33            DeriveLiquidityRole, DeriveOrderSide, DeriveOrderStatus, DeriveOrderType,
34            DeriveTimeInForce, DeriveTriggerType, DeriveTxStatus,
35        },
36        parse::{
37            derive_order_side_to_nautilus, derive_order_type_to_nautilus_for_order,
38            derive_rejection_due_post_only, derive_status_to_nautilus, derive_tif_to_nautilus,
39            derive_trigger_price_type_to_nautilus,
40        },
41    },
42    http::models::{DeriveOrder, DerivePosition, DeriveSubaccount, DeriveTrade},
43};
44
45/// Builds an [`OrderStatusReport`] from a Derive order record.
46///
47/// `client_order_id` is sourced from the `label` field on the order when the
48/// label is non-empty; callers that need a specific client_order_id should
49/// override via `with_client_order_id` after this call.
50/// Trailing zero padding is removed without changing the value.
51///
52/// # Errors
53///
54/// Returns an error when any decimal field cannot be converted to a Nautilus
55/// `Price` or `Quantity`.
56pub fn parse_derive_order_to_report(
57    order: &DeriveOrder,
58    account_id: AccountId,
59    ts_init: UnixNanos,
60) -> anyhow::Result<OrderStatusReport> {
61    parse_derive_order_to_report_with_precision(order, account_id, None, None, ts_init)
62}
63
64pub(crate) fn parse_derive_order_to_report_with_precision(
65    order: &DeriveOrder,
66    account_id: AccountId,
67    price_precision: Option<u8>,
68    size_precision: Option<u8>,
69    ts_init: UnixNanos,
70) -> anyhow::Result<OrderStatusReport> {
71    let instrument_id =
72        InstrumentId::new(Symbol::new(order.instrument_name.as_str()), *DERIVE_VENUE);
73    let venue_order_id = VenueOrderId::new(order.order_id.as_str());
74    let order_side = derive_order_side_to_nautilus(order.direction);
75    let order_type = derive_order_type_to_nautilus_for_report(order);
76    let post_only = matches!(order.time_in_force, DeriveTimeInForce::PostOnly);
77    let time_in_force = derive_tif_to_nautilus(order.time_in_force);
78    let order_status =
79        derive_status_to_nautilus(order.order_status, order.filled_amount, order.amount);
80    let quantity = quantity_from_decimal(order.amount, size_precision, "amount")?;
81    let filled_qty = quantity_from_decimal(order.filled_amount, size_precision, "filled_amount")?;
82
83    let ts_accepted = ms_to_nanos(order.creation_timestamp);
84    let ts_last = ms_to_nanos(order.last_update_timestamp);
85
86    let mut report = OrderStatusReport::new(
87        account_id,
88        instrument_id,
89        None,
90        venue_order_id,
91        order_side.into(),
92        order_type,
93        time_in_force,
94        order_status,
95        quantity,
96        filled_qty,
97        ts_accepted,
98        ts_last,
99        ts_init,
100        Some(UUID4::new()),
101    );
102
103    if !order.label.as_str().is_empty() {
104        let client_order_id = ClientOrderId::new(order.label.as_str());
105        report = report.with_client_order_id(client_order_id);
106    }
107
108    if order.limit_price > Decimal::ZERO
109        && order_type_has_limit_price(order_type)
110        && let Ok(price) = price_from_decimal(order.limit_price, price_precision, "limit_price")
111    {
112        report = report.with_price(price);
113    }
114
115    if let Some(trigger_price) = order.trigger_price
116        && trigger_price > Decimal::ZERO
117        && let Ok(price) = price_from_decimal(trigger_price, price_precision, "trigger_price")
118    {
119        report = report.with_trigger_price(price);
120    }
121
122    if let Some(trigger_price_type) = order.trigger_price_type {
123        report =
124            report.with_trigger_type(derive_trigger_price_type_to_nautilus(trigger_price_type));
125    }
126
127    if order.average_price > Decimal::ZERO {
128        report.avg_px = Some(order.average_price);
129    }
130    report.post_only = post_only;
131    let trigger_reject_message = order
132        .trigger_reject_message
133        .as_deref()
134        .filter(|message| !message.is_empty())
135        .map(str::to_string);
136    let cancel_reason = trigger_reject_message
137        .clone()
138        .unwrap_or_else(|| order.cancel_reason.to_string());
139    if order.order_status == DeriveOrderStatus::Cancelled
140        || (order.order_status == DeriveOrderStatus::Rejected
141            && (trigger_reject_message.is_some()
142                || derive_rejection_due_post_only(None, &cancel_reason)))
143    {
144        report.cancel_reason = Some(cancel_reason);
145    }
146    Ok(report)
147}
148
149fn order_type_has_limit_price(order_type: OrderType) -> bool {
150    matches!(
151        order_type,
152        OrderType::Limit | OrderType::StopLimit | OrderType::LimitIfTouched
153    )
154}
155
156fn derive_order_type_to_nautilus_for_report(order: &DeriveOrder) -> OrderType {
157    let order_type = derive_order_type_to_nautilus_for_order(order.order_type, order.trigger_type);
158    if order_type != OrderType::LimitIfTouched {
159        return order_type;
160    }
161
162    match (order.order_type, order.trigger_type, order.trigger_price) {
163        (DeriveOrderType::Limit, Some(DeriveTriggerType::Takeprofit), Some(trigger_price))
164            if !limit_if_touched_prices_are_valid(
165                order.direction,
166                order.limit_price,
167                trigger_price,
168            ) =>
169        {
170            OrderType::StopLimit
171        }
172        _ => order_type,
173    }
174}
175
176fn limit_if_touched_prices_are_valid(
177    direction: DeriveOrderSide,
178    limit_price: Decimal,
179    trigger_price: Decimal,
180) -> bool {
181    match direction {
182        DeriveOrderSide::Buy => trigger_price <= limit_price,
183        DeriveOrderSide::Sell => trigger_price >= limit_price,
184    }
185}
186
187/// Builds a [`FillReport`] from a Derive trade record.
188///
189/// Quote-currency commission is reported in the same currency as the
190/// instrument's settlement (USDC for perps and options). `client_order_id`
191/// is sourced from the trade `label` when populated.
192/// Trailing zero padding is removed without changing the value.
193///
194/// # Errors
195///
196/// Returns an error when any decimal field cannot be converted to a Nautilus
197/// `Price`, `Quantity`, or `Money`.
198pub fn parse_derive_trade_to_fill_report(
199    trade: &DeriveTrade,
200    account_id: AccountId,
201    fee_currency: Currency,
202    ts_init: UnixNanos,
203) -> anyhow::Result<Option<FillReport>> {
204    parse_derive_trade_to_fill_report_with_precision(
205        trade,
206        account_id,
207        fee_currency,
208        None,
209        None,
210        ts_init,
211    )
212}
213
214pub(crate) fn parse_derive_trade_to_fill_report_with_precision(
215    trade: &DeriveTrade,
216    account_id: AccountId,
217    fee_currency: Currency,
218    price_precision: Option<u8>,
219    size_precision: Option<u8>,
220    ts_init: UnixNanos,
221) -> anyhow::Result<Option<FillReport>> {
222    // The venue ships pending settlements with an empty trade_id and tx_hash;
223    // those rows would otherwise collapse identity-aware deduplication, so we
224    // skip them and let a later poll observe the settled trade.
225    if trade.trade_id.is_empty() || trade.tx_status == DeriveTxStatus::Reverted {
226        return Ok(None);
227    }
228
229    let instrument_id =
230        InstrumentId::new(Symbol::new(trade.instrument_name.as_str()), *DERIVE_VENUE);
231    let venue_order_id = VenueOrderId::new(trade.order_id.as_str());
232    let trade_id = TradeId::new(trade.trade_id.as_str());
233    let order_side = derive_order_side_to_nautilus(trade.direction);
234    let last_qty = quantity_from_decimal(trade.trade_amount, size_precision, "trade_amount")?;
235    let last_px = price_from_decimal(trade.trade_price, price_precision, "trade_price")?;
236    let commission = commission_from_decimal(trade.trade_fee, fee_currency)?;
237    let liquidity_side = match trade.liquidity_role {
238        DeriveLiquidityRole::Maker => LiquiditySide::Maker,
239        DeriveLiquidityRole::Taker => LiquiditySide::Taker,
240        DeriveLiquidityRole::Unknown => LiquiditySide::NoLiquiditySide,
241    };
242
243    let client_order_id = if trade.label.as_str().is_empty() {
244        None
245    } else {
246        Some(ClientOrderId::new(trade.label.as_str()))
247    };
248
249    let ts_event = ms_to_nanos(trade.timestamp);
250
251    Ok(Some(FillReport::new(
252        account_id,
253        instrument_id,
254        venue_order_id,
255        trade_id,
256        order_side,
257        last_qty,
258        last_px,
259        commission,
260        liquidity_side,
261        client_order_id,
262        None,
263        ts_event,
264        ts_init,
265        Some(UUID4::new()),
266    )))
267}
268
269/// Builds a [`PositionStatusReport`] from a Derive position record.
270///
271/// Trailing zero padding is removed without changing the value.
272///
273/// # Errors
274///
275/// Returns an error when the position amount cannot be converted to a
276/// Nautilus `Quantity`.
277pub fn parse_derive_position_to_report(
278    position: &DerivePosition,
279    account_id: AccountId,
280    ts_init: UnixNanos,
281) -> anyhow::Result<PositionStatusReport> {
282    parse_derive_position_to_report_with_precision(position, account_id, None, ts_init)
283}
284
285pub(crate) fn parse_derive_position_to_report_with_precision(
286    position: &DerivePosition,
287    account_id: AccountId,
288    size_precision: Option<u8>,
289    ts_init: UnixNanos,
290) -> anyhow::Result<PositionStatusReport> {
291    let instrument_id = InstrumentId::new(
292        Symbol::new(position.instrument_name.as_str()),
293        *DERIVE_VENUE,
294    );
295    let signed_amount = position.amount;
296    let side = if signed_amount > Decimal::ZERO {
297        PositionSide::Long
298    } else if signed_amount < Decimal::ZERO {
299        PositionSide::Short
300    } else {
301        PositionSide::Flat
302    };
303    let abs_amount = signed_amount.abs();
304    let quantity = quantity_from_decimal(abs_amount, size_precision, "position.amount")?;
305
306    Ok(PositionStatusReport::new(
307        account_id,
308        instrument_id,
309        side,
310        quantity,
311        ts_init,
312        ts_init,
313        Some(UUID4::new()),
314        None,
315        Some(position.average_price),
316    ))
317}
318
319/// Derives [`AccountBalance`], [`MarginBalance`], and supplemental info rows
320/// from a [`DeriveSubaccount`] snapshot.
321///
322/// Each collateral row becomes one [`AccountBalance`] in the collateral's own
323/// units with `total = amount` and `locked = 0`: the venue holds margin at the
324/// subaccount level and reports no per-collateral reservation, while
325/// `collaterals[].initial_margin` is USD credit contributed, not locked funds.
326///
327/// Portfolio requirements collapse into a single account-wide [`MarginBalance`]
328/// in the subaccount currency: `initial = positions_initial_margin +
329/// open_orders_margin` and `maintenance = positions_maintenance_margin`. The
330/// subaccount's `initial_margin`/`maintenance_margin` are signed net health
331/// values, not requirements, so they travel in the returned [`Params`] as
332/// `net_initial_margin`/`net_maintenance_margin` alongside the requirement
333/// split and the liquidation flag.
334///
335/// # Errors
336///
337/// Returns an error when a decimal field cannot be represented at the
338/// currency precision used by [`Money`].
339pub fn parse_derive_subaccount_to_balances(
340    subaccount: &DeriveSubaccount,
341) -> anyhow::Result<(Vec<AccountBalance>, Vec<MarginBalance>, Params)> {
342    let mut balances = Vec::with_capacity(subaccount.collaterals.len());
343    for collateral in &subaccount.collaterals {
344        let currency = Currency::get_or_create_crypto(collateral.asset_name.as_str());
345        let balance =
346            AccountBalance::from_total_and_locked(collateral.amount, Decimal::ZERO, currency)
347                .map_err(|e| {
348                    anyhow::anyhow!(
349                        "failed to build collateral balance for {} (total={}): {e}",
350                        collateral.asset_name,
351                        collateral.amount,
352                    )
353                })?;
354        balances.push(balance);
355    }
356
357    let currency = Currency::get_or_create_crypto(subaccount.currency.as_str());
358    let initial_dec = subaccount.positions_initial_margin + subaccount.open_orders_margin;
359    let maintenance_dec = subaccount.positions_maintenance_margin;
360    let initial = Money::from_decimal(initial_dec, currency).with_context(|| {
361        format!(
362            "initial margin requirement {initial_dec} cannot be represented at {currency} precision",
363        )
364    })?;
365    let maintenance =
366        Money::from_decimal(maintenance_dec, currency).with_context(|| {
367            format!(
368                "maintenance margin requirement {maintenance_dec} cannot be represented at {currency} precision",
369            )
370        })?;
371    let margins = vec![MarginBalance::new(initial, maintenance, None)];
372
373    let mut info = Params::new();
374    info.insert(
375        "net_initial_margin".to_string(),
376        Value::String(subaccount.initial_margin.to_string()),
377    );
378    info.insert(
379        "net_maintenance_margin".to_string(),
380        Value::String(subaccount.maintenance_margin.to_string()),
381    );
382    info.insert(
383        "positions_initial_margin".to_string(),
384        Value::String(subaccount.positions_initial_margin.to_string()),
385    );
386    info.insert(
387        "positions_maintenance_margin".to_string(),
388        Value::String(subaccount.positions_maintenance_margin.to_string()),
389    );
390    info.insert(
391        "open_orders_margin".to_string(),
392        Value::String(subaccount.open_orders_margin.to_string()),
393    );
394    info.insert(
395        "is_under_liquidation".to_string(),
396        Value::Bool(subaccount.is_under_liquidation),
397    );
398
399    Ok((balances, margins, info))
400}
401
402fn price_from_decimal(value: Decimal, precision: Option<u8>, field: &str) -> anyhow::Result<Price> {
403    match precision {
404        Some(precision) => Price::from_decimal_dp(value, precision),
405        None => Price::from_decimal(value.normalize()),
406    }
407    .with_context(|| format!("invalid Derive {field}"))
408}
409
410fn quantity_from_decimal(
411    value: Decimal,
412    precision: Option<u8>,
413    field: &str,
414) -> anyhow::Result<Quantity> {
415    match precision {
416        Some(precision) => Quantity::from_decimal_dp(value, precision),
417        None => Quantity::from_decimal(value.normalize()),
418    }
419    .with_context(|| format!("invalid Derive {field}"))
420}
421
422fn commission_from_decimal(value: Decimal, currency: Currency) -> anyhow::Result<Money> {
423    Money::from_decimal(value, currency)
424        .with_context(|| format!("trade_fee {value} cannot be represented at {currency} precision"))
425}
426
427fn ms_to_nanos(value: i64) -> UnixNanos {
428    let clamped = u64::try_from(value.max(0)).unwrap_or(0);
429    UnixNanos::from(clamped.saturating_mul(NANOSECONDS_IN_MILLISECOND))
430}
431
432#[cfg(test)]
433mod tests {
434    use nautilus_model::enums::{OrderSide, OrderStatus, OrderType, TimeInForce, TriggerType};
435    use rstest::rstest;
436    use rust_decimal_macros::dec;
437
438    use super::*;
439    use crate::{
440        common::{
441            enums::{
442                DeriveAssetType, DeriveInstrumentType, DeriveLiquidityRole, DeriveMarginType,
443                DeriveOrderCancelReason, DeriveOrderSide, DeriveOrderStatus, DeriveOrderType,
444                DeriveTimeInForce, DeriveTriggerPriceType, DeriveTriggerType, DeriveTxStatus,
445            },
446            parse::{
447                derive_status_to_nautilus, order_side_to_derive, order_type_to_derive,
448                time_in_force_to_derive,
449            },
450        },
451        http::models::DeriveCollateral,
452    };
453
454    fn sample_order() -> DeriveOrder {
455        DeriveOrder {
456            amount: dec!(10),
457            average_price: dec!(3500),
458            cancel_reason: DeriveOrderCancelReason::Empty,
459            creation_timestamp: 1_700_000_000_000,
460            direction: DeriveOrderSide::Buy,
461            filled_amount: dec!(4),
462            instrument_name: "ETH-PERP".into(),
463            is_transfer: false,
464            label: "STRATEGY-1-O-1".into(),
465            last_update_timestamp: 1_700_000_001_000,
466            limit_price: dec!(3500),
467            max_fee: dec!(1),
468            mmp: false,
469            nonce: 1,
470            order_fee: dec!(0),
471            order_id: "ord-1".to_string(),
472            order_status: DeriveOrderStatus::Open,
473            order_type: DeriveOrderType::Limit,
474            quote_id: None,
475            replaced_order_id: None,
476            signature: "0x00".to_string(),
477            signature_expiry_sec: 1_700_000_999,
478            signer: "0xsigner".into(),
479            subaccount_id: 30769,
480            time_in_force: DeriveTimeInForce::Gtc,
481            trigger_price: None,
482            trigger_price_type: None,
483            trigger_reject_message: None,
484            trigger_type: None,
485        }
486    }
487
488    fn sample_trade() -> DeriveTrade {
489        DeriveTrade {
490            direction: DeriveOrderSide::Sell,
491            index_price: dec!(3500),
492            instrument_name: "ETH-PERP".into(),
493            is_transfer: false,
494            label: "STRATEGY-1-O-2".into(),
495            liquidity_role: DeriveLiquidityRole::Taker,
496            mark_price: dec!(3500),
497            order_id: "ord-2".to_string(),
498            quote_id: None,
499            realized_pnl: dec!(0),
500            subaccount_id: 30769,
501            timestamp: 1_700_000_002_000,
502            trade_amount: dec!(2),
503            trade_fee: dec!(0.5),
504            trade_id: "tr-1".to_string(),
505            trade_price: dec!(3505),
506            tx_hash: Some("0xabc".to_string()),
507            tx_status: DeriveTxStatus::Settled,
508            wallet: Some("0xwallet".into()),
509        }
510    }
511
512    #[rstest]
513    fn test_order_side_round_trip() {
514        assert_eq!(order_side_to_derive(OrderSide::Buy), DeriveOrderSide::Buy,);
515        assert_eq!(order_side_to_derive(OrderSide::Sell), DeriveOrderSide::Sell,);
516    }
517
518    #[rstest]
519    fn test_order_type_rejects_unsupported() {
520        assert_eq!(
521            order_type_to_derive(OrderType::Limit).unwrap(),
522            DeriveOrderType::Limit,
523        );
524        assert_eq!(
525            order_type_to_derive(OrderType::Market).unwrap(),
526            DeriveOrderType::Market,
527        );
528        assert!(order_type_to_derive(OrderType::StopMarket).is_err());
529    }
530
531    #[rstest]
532    #[case(TimeInForce::Gtc, false, DeriveTimeInForce::Gtc)]
533    #[case(TimeInForce::Gtc, true, DeriveTimeInForce::PostOnly)]
534    #[case(TimeInForce::Ioc, false, DeriveTimeInForce::Ioc)]
535    #[case(TimeInForce::Fok, false, DeriveTimeInForce::Fok)]
536    fn test_time_in_force_maps_supported_values(
537        #[case] tif: TimeInForce,
538        #[case] post_only: bool,
539        #[case] expected: DeriveTimeInForce,
540    ) {
541        assert_eq!(time_in_force_to_derive(tif, post_only).unwrap(), expected);
542    }
543
544    #[rstest]
545    #[case(TimeInForce::Ioc)]
546    #[case(TimeInForce::Fok)]
547    fn test_time_in_force_rejects_post_only_immediate_values(#[case] tif: TimeInForce) {
548        let err = time_in_force_to_derive(tif, true)
549            .expect_err("post-only immediate TIF must be rejected");
550
551        assert!(
552            err.to_string()
553                .contains("post-only Derive orders only support GTC"),
554            "unexpected error: {err}",
555        );
556    }
557
558    #[rstest]
559    #[case(TimeInForce::Gtd, false)]
560    #[case(TimeInForce::Gtd, true)]
561    #[case(TimeInForce::Day, false)]
562    #[case(TimeInForce::Day, true)]
563    #[case(TimeInForce::AtTheOpen, false)]
564    #[case(TimeInForce::AtTheOpen, true)]
565    #[case(TimeInForce::AtTheClose, false)]
566    #[case(TimeInForce::AtTheClose, true)]
567    fn test_time_in_force_rejects_unsupported(#[case] tif: TimeInForce, #[case] post_only: bool) {
568        let err = time_in_force_to_derive(tif, post_only).expect_err("must reject unsupported TIF");
569
570        assert!(
571            err.to_string().contains("unsupported time in force"),
572            "unexpected error: {err}",
573        );
574    }
575
576    #[rstest]
577    fn test_derive_status_partial_fill_classification() {
578        assert_eq!(
579            derive_status_to_nautilus(DeriveOrderStatus::Open, dec!(0), dec!(10)),
580            OrderStatus::Accepted,
581        );
582        assert_eq!(
583            derive_status_to_nautilus(DeriveOrderStatus::Open, dec!(4), dec!(10)),
584            OrderStatus::PartiallyFilled,
585        );
586        assert_eq!(
587            derive_status_to_nautilus(DeriveOrderStatus::Filled, dec!(10), dec!(10)),
588            OrderStatus::Filled,
589        );
590        assert_eq!(
591            derive_status_to_nautilus(DeriveOrderStatus::Cancelled, dec!(0), dec!(10)),
592            OrderStatus::Canceled,
593        );
594    }
595
596    #[rstest]
597    fn test_parse_order_report_assigns_partial_fill_status() {
598        let account_id = AccountId::new("DERIVE-001");
599        let report =
600            parse_derive_order_to_report(&sample_order(), account_id, UnixNanos::from(1)).unwrap();
601        assert_eq!(report.order_status, OrderStatus::PartiallyFilled);
602        assert_eq!(report.quantity, Quantity::from("10"));
603        assert_eq!(report.filled_qty, Quantity::from("4"));
604        assert_eq!(report.client_order_id.unwrap().as_str(), "STRATEGY-1-O-1");
605        assert_eq!(report.venue_order_id.as_str(), "ord-1");
606    }
607
608    #[rstest]
609    fn test_parse_order_report_normalizes_without_instrument_precision() {
610        let mut order = sample_order();
611        order.amount = Decimal::from_str_exact("0.100000000000000000").unwrap();
612        order.filled_amount = Decimal::from_str_exact("0.000000000000000000").unwrap();
613        order.limit_price = Decimal::from_str_exact("0.100000000000000000").unwrap();
614        order.average_price = Decimal::ZERO;
615        order.order_status = DeriveOrderStatus::Cancelled;
616        let account_id = AccountId::new("DERIVE-001");
617
618        let report = parse_derive_order_to_report(&order, account_id, UnixNanos::from(1)).unwrap();
619
620        assert_eq!(report.quantity, Quantity::from("0.1"));
621        assert_eq!(report.filled_qty, Quantity::from("0"));
622        assert_eq!(report.price, Some(Price::from("0.1")));
623    }
624
625    #[rstest]
626    fn test_parse_order_report_uses_instrument_precision() {
627        let mut order = sample_order();
628        order.amount = Decimal::from_str_exact("25.000").unwrap();
629        order.filled_amount = Decimal::from_str_exact("5.000").unwrap();
630        order.limit_price = Decimal::from_str_exact("25.000").unwrap();
631
632        let report = parse_derive_order_to_report_with_precision(
633            &order,
634            AccountId::new("DERIVE-001"),
635            Some(2),
636            Some(2),
637            UnixNanos::from(1),
638        )
639        .unwrap();
640
641        assert_eq!(report.quantity, Quantity::from("25.00"));
642        assert_eq!(report.quantity.precision, 2);
643        assert_eq!(report.filled_qty, Quantity::from("5.00"));
644        assert_eq!(report.filled_qty.precision, 2);
645        assert_eq!(report.price, Some(Price::from("25.00")));
646        assert_eq!(report.price.unwrap().precision, 2);
647    }
648
649    #[rstest]
650    fn test_parse_order_report_maps_untriggered_stop_market() {
651        let mut order = sample_order();
652        order.average_price = Decimal::ZERO;
653        order.filled_amount = Decimal::ZERO;
654        order.limit_price = dec!(3400);
655        order.order_status = DeriveOrderStatus::Untriggered;
656        order.order_type = DeriveOrderType::Market;
657        order.trigger_price = Some(dec!(3450));
658        order.trigger_price_type = Some(DeriveTriggerPriceType::Mark);
659        order.trigger_type = Some(DeriveTriggerType::Stoploss);
660        let account_id = AccountId::new("DERIVE-001");
661
662        let report = parse_derive_order_to_report(&order, account_id, UnixNanos::from(1)).unwrap();
663
664        assert_eq!(report.order_type, OrderType::StopMarket);
665        assert_eq!(report.order_status, OrderStatus::Accepted);
666        assert_eq!(report.price, None);
667        assert_eq!(report.trigger_price, Some(Price::from("3450")));
668        assert_eq!(report.trigger_type, Some(TriggerType::MarkPrice));
669    }
670
671    #[rstest]
672    #[case(DeriveOrderSide::Buy, dec!(3700), dec!(3600))]
673    #[case(DeriveOrderSide::Buy, dec!(3700), dec!(3700))]
674    #[case(DeriveOrderSide::Sell, dec!(3700), dec!(3800))]
675    #[case(DeriveOrderSide::Sell, dec!(3700), dec!(3700))]
676    fn test_parse_order_report_maps_limit_if_touched_trigger(
677        #[case] direction: DeriveOrderSide,
678        #[case] limit_price: Decimal,
679        #[case] trigger_price: Decimal,
680    ) {
681        let mut order = sample_order();
682        order.average_price = Decimal::ZERO;
683        order.direction = direction;
684        order.filled_amount = Decimal::ZERO;
685        order.limit_price = limit_price;
686        order.order_status = DeriveOrderStatus::Untriggered;
687        order.order_type = DeriveOrderType::Limit;
688        order.trigger_price = Some(trigger_price);
689        order.trigger_price_type = Some(DeriveTriggerPriceType::Index);
690        order.trigger_type = Some(DeriveTriggerType::Takeprofit);
691        let account_id = AccountId::new("DERIVE-001");
692
693        let report = parse_derive_order_to_report(&order, account_id, UnixNanos::from(1)).unwrap();
694
695        assert_eq!(report.order_type, OrderType::LimitIfTouched);
696        assert_eq!(
697            report.price,
698            Some(Price::from_decimal(limit_price.normalize()).unwrap())
699        );
700        assert_eq!(
701            report.trigger_price,
702            Some(Price::from_decimal(trigger_price.normalize()).unwrap())
703        );
704        assert_eq!(report.trigger_type, Some(TriggerType::IndexPrice));
705    }
706
707    #[rstest]
708    #[case(DeriveOrderSide::Buy, dec!(3700), dec!(3800))]
709    #[case(DeriveOrderSide::Sell, dec!(3700), dec!(3600))]
710    fn test_parse_order_report_maps_take_profit_limit_with_stop_shape(
711        #[case] direction: DeriveOrderSide,
712        #[case] limit_price: Decimal,
713        #[case] trigger_price: Decimal,
714    ) {
715        let mut order = sample_order();
716        order.average_price = Decimal::ZERO;
717        order.direction = direction;
718        order.filled_amount = Decimal::ZERO;
719        order.limit_price = limit_price;
720        order.order_status = DeriveOrderStatus::Untriggered;
721        order.order_type = DeriveOrderType::Limit;
722        order.trigger_price = Some(trigger_price);
723        order.trigger_price_type = Some(DeriveTriggerPriceType::Index);
724        order.trigger_type = Some(DeriveTriggerType::Takeprofit);
725        let account_id = AccountId::new("DERIVE-001");
726
727        let report = parse_derive_order_to_report(&order, account_id, UnixNanos::from(1)).unwrap();
728
729        assert_eq!(report.order_type, OrderType::StopLimit);
730        assert_eq!(
731            report.price,
732            Some(Price::from_decimal(limit_price.normalize()).unwrap())
733        );
734        assert_eq!(
735            report.trigger_price,
736            Some(Price::from_decimal(trigger_price.normalize()).unwrap())
737        );
738        assert_eq!(report.trigger_type, Some(TriggerType::IndexPrice));
739    }
740
741    #[rstest]
742    fn test_parse_rejected_post_only_report_keeps_cross_market_reason() {
743        let mut order = sample_order();
744        order.cancel_reason = DeriveOrderCancelReason::PostOnlyCrossMarket;
745        order.order_status = DeriveOrderStatus::Rejected;
746        order.time_in_force = DeriveTimeInForce::PostOnly;
747        let account_id = AccountId::new("DERIVE-001");
748
749        let report = parse_derive_order_to_report(&order, account_id, UnixNanos::from(1)).unwrap();
750
751        assert_eq!(report.order_status, OrderStatus::Rejected);
752        assert!(report.post_only);
753        assert_eq!(
754            report.cancel_reason.as_deref(),
755            Some("Post only order cannot cross the market")
756        );
757    }
758
759    #[rstest]
760    fn test_parse_rejected_trigger_report_uses_trigger_message() {
761        let mut order = sample_order();
762        order.cancel_reason = DeriveOrderCancelReason::TriggerFailed;
763        order.order_status = DeriveOrderStatus::Rejected;
764        order.trigger_reject_message = Some("trigger price moved through limit".to_string());
765        let account_id = AccountId::new("DERIVE-001");
766
767        let report = parse_derive_order_to_report(&order, account_id, UnixNanos::from(1)).unwrap();
768
769        assert_eq!(report.order_status, OrderStatus::Rejected);
770        assert_eq!(
771            report.cancel_reason.as_deref(),
772            Some("trigger price moved through limit")
773        );
774    }
775
776    #[rstest]
777    fn test_parse_trade_report_emits_taker_fill() {
778        let account_id = AccountId::new("DERIVE-001");
779        let usdc = Currency::USDC();
780        let report = parse_derive_trade_to_fill_report(
781            &sample_trade(),
782            account_id,
783            usdc,
784            UnixNanos::from(2),
785        )
786        .unwrap()
787        .unwrap();
788        assert_eq!(report.order_side, OrderSide::Sell);
789        assert_eq!(report.last_qty, Quantity::from("2"));
790        assert_eq!(report.last_px, Price::from("3505"));
791        assert_eq!(report.liquidity_side, LiquiditySide::Taker);
792        assert_eq!(report.commission.as_decimal(), dec!(0.5));
793    }
794
795    #[rstest]
796    fn test_parse_trade_report_uses_instrument_precision() {
797        let mut trade = sample_trade();
798        trade.trade_amount = Decimal::from_str_exact("25.000").unwrap();
799        trade.trade_price = Decimal::from_str_exact("25.000").unwrap();
800
801        let report = parse_derive_trade_to_fill_report_with_precision(
802            &trade,
803            AccountId::new("DERIVE-001"),
804            Currency::USDC(),
805            Some(2),
806            Some(3),
807            UnixNanos::from(2),
808        )
809        .unwrap()
810        .unwrap();
811
812        assert_eq!(report.last_px, Price::from("25.00"));
813        assert_eq!(report.last_px.precision, 2);
814        assert_eq!(report.last_qty, Quantity::from("25.000"));
815        assert_eq!(report.last_qty.precision, 3);
816    }
817
818    #[rstest]
819    #[case(DeriveLiquidityRole::Taker, LiquiditySide::Taker)]
820    #[case(DeriveLiquidityRole::Maker, LiquiditySide::Maker)]
821    fn test_parse_trade_report_preserves_exact_decimal_commission(
822        #[case] liquidity_role: DeriveLiquidityRole,
823        #[case] expected_liquidity_side: LiquiditySide,
824    ) {
825        let mut trade = sample_trade();
826        trade.trade_fee = dec!(0.12345678);
827        trade.liquidity_role = liquidity_role;
828        let account_id = AccountId::new("DERIVE-001");
829        let usdc = Currency::USDC();
830        let report =
831            parse_derive_trade_to_fill_report(&trade, account_id, usdc, UnixNanos::from(2))
832                .unwrap()
833                .expect("exact USDC-precision fee must emit the fill");
834        assert_eq!(report.commission.as_decimal(), dec!(0.12345678));
835        assert_eq!(report.commission.currency, usdc);
836        assert_eq!(report.liquidity_side, expected_liquidity_side);
837    }
838
839    #[rstest]
840    #[case(dec!(0.000000025), dec!(0.00000002))]
841    #[case(dec!(0.000000015), dec!(0.00000002))]
842    fn test_parse_trade_report_rounds_half_unit_commission_from_decimal(
843        #[case] trade_fee: Decimal,
844        #[case] expected: Decimal,
845    ) {
846        // These half-unit values are where the old f64 path diverged
847        let mut trade = sample_trade();
848        trade.trade_fee = trade_fee;
849        let account_id = AccountId::new("DERIVE-001");
850        let report = parse_derive_trade_to_fill_report(
851            &trade,
852            account_id,
853            Currency::USDC(),
854            UnixNanos::from(2),
855        )
856        .unwrap()
857        .expect("sub-precision fee must still emit the fill");
858        assert_eq!(report.commission.as_decimal(), expected);
859    }
860
861    #[rstest]
862    fn test_parse_trade_report_errors_on_out_of_range_commission() {
863        let mut trade = sample_trade();
864        trade.trade_fee = Decimal::MAX;
865        let account_id = AccountId::new("DERIVE-001");
866        let err = parse_derive_trade_to_fill_report(
867            &trade,
868            account_id,
869            Currency::USDC(),
870            UnixNanos::from(2),
871        )
872        .expect_err("out-of-range fee must error instead of panicking");
873        assert!(
874            err.to_string().contains("trade_fee"),
875            "unexpected error: {err}",
876        );
877    }
878
879    #[rstest]
880    fn test_parse_trade_report_skips_reverted_settlement() {
881        let mut trade = sample_trade();
882        trade.tx_status = DeriveTxStatus::Reverted;
883        let account_id = AccountId::new("DERIVE-001");
884        let usdc = Currency::USDC();
885        let report =
886            parse_derive_trade_to_fill_report(&trade, account_id, usdc, UnixNanos::from(2))
887                .unwrap();
888        assert!(report.is_none());
889    }
890
891    #[rstest]
892    fn test_parse_trade_report_degrades_unknown_liquidity_role() {
893        let mut trade = sample_trade();
894        trade.liquidity_role = DeriveLiquidityRole::Unknown;
895        let account_id = AccountId::new("DERIVE-001");
896        let usdc = Currency::USDC();
897
898        let report =
899            parse_derive_trade_to_fill_report(&trade, account_id, usdc, UnixNanos::from(2))
900                .unwrap()
901                .expect("unknown liquidity role must still emit the fill");
902
903        assert_eq!(report.liquidity_side, LiquiditySide::NoLiquiditySide);
904    }
905
906    #[rstest]
907    fn test_parse_position_long_short_flat() {
908        let account_id = AccountId::new("DERIVE-001");
909
910        let mut long_pos = sample_position();
911        long_pos.amount = dec!(3);
912        let report =
913            parse_derive_position_to_report(&long_pos, account_id, UnixNanos::from(3)).unwrap();
914        assert_eq!(report.position_side, PositionSide::Long);
915        assert_eq!(report.quantity, Quantity::from("3"));
916
917        let mut short_pos = sample_position();
918        short_pos.amount = dec!(-2);
919        let report =
920            parse_derive_position_to_report(&short_pos, account_id, UnixNanos::from(3)).unwrap();
921        assert_eq!(report.position_side, PositionSide::Short);
922        assert_eq!(report.quantity, Quantity::from("2"));
923
924        let mut flat_pos = sample_position();
925        flat_pos.amount = dec!(0);
926        let report =
927            parse_derive_position_to_report(&flat_pos, account_id, UnixNanos::from(3)).unwrap();
928        assert_eq!(report.position_side, PositionSide::Flat);
929    }
930
931    #[rstest]
932    fn test_parse_position_report_uses_instrument_precision() {
933        let mut position = sample_position();
934        position.amount = Decimal::from_str_exact("25.000").unwrap();
935
936        let report = parse_derive_position_to_report_with_precision(
937            &position,
938            AccountId::new("DERIVE-001"),
939            Some(3),
940            UnixNanos::from(3),
941        )
942        .unwrap();
943
944        assert_eq!(report.quantity, Quantity::from("25.000"));
945        assert_eq!(report.quantity.precision, 3);
946    }
947
948    fn sample_position() -> DerivePosition {
949        DerivePosition {
950            amount: dec!(0),
951            average_price: dec!(3500),
952            creation_timestamp: 0,
953            cumulative_funding: dec!(0),
954            delta: dec!(0),
955            gamma: dec!(0),
956            index_price: dec!(3500),
957            initial_margin: dec!(0),
958            instrument_name: "ETH-PERP".into(),
959            instrument_type: DeriveInstrumentType::Perp,
960            leverage: None,
961            liquidation_price: None,
962            maintenance_margin: dec!(0),
963            mark_price: dec!(3500),
964            mark_value: dec!(0),
965            net_settlements: dec!(0),
966            open_orders_margin: dec!(0),
967            pending_funding: dec!(0),
968            realized_pnl: dec!(0),
969            theta: dec!(0),
970            unrealized_pnl: dec!(0),
971            vega: dec!(0),
972        }
973    }
974
975    #[rstest]
976    fn test_parse_subaccount_emits_balances_margins_and_info() {
977        let subaccount = sample_subaccount();
978        let (balances, margins, info) = parse_derive_subaccount_to_balances(&subaccount).unwrap();
979        assert_eq!(balances.len(), 1);
980        assert_eq!(balances[0].total.as_decimal(), dec!(1000));
981        assert_eq!(balances[0].locked.as_decimal(), dec!(0));
982        assert_eq!(balances[0].free.as_decimal(), dec!(1000));
983        assert_eq!(margins.len(), 1);
984        assert_eq!(margins[0].initial.as_decimal(), dec!(0));
985        assert_eq!(margins[0].maintenance.as_decimal(), dec!(0));
986        assert_eq!(
987            info.get("net_initial_margin"),
988            Some(&serde_json::json!("100")),
989        );
990        assert_eq!(
991            info.get("net_maintenance_margin"),
992            Some(&serde_json::json!("50")),
993        );
994        assert_eq!(
995            info.get("is_under_liquidation"),
996            Some(&serde_json::json!(false)),
997        );
998    }
999
1000    #[rstest]
1001    fn test_parse_subaccount_preserves_multi_collateral_units() {
1002        // 2.5 ETH collateral reporting $1000 of credit stays 2.5 ETH total
1003        // with nothing locked: credit is not a reservation on the collateral,
1004        // so no mark-price conversion applies
1005        let mut subaccount = sample_subaccount();
1006        subaccount.collaterals = vec![
1007            DeriveCollateral {
1008                amount: dec!(2.5),
1009                asset_name: "ETH".into(),
1010                asset_type: DeriveAssetType::Erc20,
1011                cumulative_interest: dec!(0),
1012                currency: "ETH".into(),
1013                initial_margin: dec!(1000),
1014                maintenance_margin: dec!(500),
1015                mark_price: dec!(3500),
1016                mark_value: dec!(8750),
1017                pending_interest: dec!(0),
1018            },
1019            DeriveCollateral {
1020                amount: dec!(1000),
1021                asset_name: "USDC".into(),
1022                asset_type: DeriveAssetType::Erc20,
1023                cumulative_interest: dec!(0),
1024                currency: "USDC".into(),
1025                initial_margin: dec!(1000),
1026                maintenance_margin: dec!(1000),
1027                mark_price: dec!(1),
1028                mark_value: dec!(1000),
1029                pending_interest: dec!(0),
1030            },
1031        ];
1032
1033        let (balances, _, _) = parse_derive_subaccount_to_balances(&subaccount).unwrap();
1034        assert_eq!(balances.len(), 2);
1035        assert_eq!(balances[0].total.as_decimal(), dec!(2.5));
1036        assert_eq!(balances[0].locked.as_decimal(), dec!(0));
1037        assert_eq!(balances[0].free.as_decimal(), dec!(2.5));
1038        assert_eq!(balances[1].total.as_decimal(), dec!(1000));
1039        assert_eq!(balances[1].locked.as_decimal(), dec!(0));
1040        assert_eq!(balances[1].free.as_decimal(), dec!(1000));
1041    }
1042
1043    #[rstest]
1044    fn test_parse_subaccount_aggregates_requirements_and_keeps_health_in_info() {
1045        // Requirements aggregate position IM with open-order margin; the
1046        // signed net health values must not appear as requirements
1047        let mut subaccount = sample_subaccount();
1048        subaccount.positions_initial_margin = dec!(350);
1049        subaccount.positions_maintenance_margin = dec!(175);
1050        subaccount.open_orders_margin = dec!(40);
1051        subaccount.initial_margin = dec!(610);
1052        subaccount.maintenance_margin = dec!(825);
1053
1054        let (balances, margins, info) = parse_derive_subaccount_to_balances(&subaccount).unwrap();
1055        assert_eq!(balances[0].locked.as_decimal(), dec!(0));
1056        assert_eq!(margins.len(), 1);
1057        assert_eq!(margins[0].initial.as_decimal(), dec!(390));
1058        assert_eq!(margins[0].maintenance.as_decimal(), dec!(175));
1059        assert_eq!(
1060            info.get("positions_initial_margin"),
1061            Some(&serde_json::json!("350")),
1062        );
1063        assert_eq!(
1064            info.get("positions_maintenance_margin"),
1065            Some(&serde_json::json!("175")),
1066        );
1067        assert_eq!(
1068            info.get("open_orders_margin"),
1069            Some(&serde_json::json!("40")),
1070        );
1071        assert_eq!(
1072            info.get("net_initial_margin"),
1073            Some(&serde_json::json!("610")),
1074        );
1075        assert_eq!(
1076            info.get("net_maintenance_margin"),
1077            Some(&serde_json::json!("825")),
1078        );
1079    }
1080
1081    #[rstest]
1082    fn test_parse_subaccount_funded_positionless_fixture_reports_no_locked() {
1083        let (balances, margins, info) =
1084            parse_subaccount_fixture("common/http_subaccount_usdc.json");
1085        assert_eq!(balances.len(), 1);
1086        assert_eq!(balances[0].total.as_decimal(), dec!(1000));
1087        assert_eq!(balances[0].locked.as_decimal(), dec!(0));
1088        assert_eq!(balances[0].free.as_decimal(), dec!(1000));
1089        assert_eq!(margins.len(), 1);
1090        assert_eq!(margins[0].initial.as_decimal(), dec!(0));
1091        assert_eq!(margins[0].maintenance.as_decimal(), dec!(0));
1092        // Net health equals the collateral credit with no requirements,
1093        // matching observed mainnet responses for funded positionless accounts
1094        assert_eq!(
1095            info.get("net_initial_margin"),
1096            Some(&serde_json::json!("1000")),
1097        );
1098        assert_eq!(
1099            info.get("net_maintenance_margin"),
1100            Some(&serde_json::json!("1000")),
1101        );
1102    }
1103
1104    #[rstest]
1105    fn test_parse_subaccount_positions_margin_fixture_maps_requirements() {
1106        let (balances, margins, info) =
1107            parse_subaccount_fixture("common/http_subaccount_positions_margin.json");
1108        assert_eq!(balances[0].locked.as_decimal(), dec!(0));
1109        assert_eq!(margins[0].initial.as_decimal(), dec!(350));
1110        assert_eq!(margins[0].maintenance.as_decimal(), dec!(175));
1111        assert_eq!(
1112            info.get("net_initial_margin"),
1113            Some(&serde_json::json!("650")),
1114        );
1115        assert_eq!(
1116            info.get("net_maintenance_margin"),
1117            Some(&serde_json::json!("825")),
1118        );
1119    }
1120
1121    #[rstest]
1122    fn test_parse_subaccount_open_orders_margin_fixture_maps_reservation() {
1123        let (_, margins, info) =
1124            parse_subaccount_fixture("common/http_subaccount_open_orders_margin.json");
1125        assert_eq!(margins[0].initial.as_decimal(), dec!(40));
1126        assert_eq!(margins[0].maintenance.as_decimal(), dec!(0));
1127        assert_eq!(
1128            info.get("open_orders_margin"),
1129            Some(&serde_json::json!("40")),
1130        );
1131    }
1132
1133    #[rstest]
1134    fn test_parse_subaccount_negative_health_fixture_preserves_signs() {
1135        let (balances, margins, info) =
1136            parse_subaccount_fixture("common/http_subaccount_negative_health.json");
1137        assert_eq!(balances[0].locked.as_decimal(), dec!(0));
1138        assert_eq!(margins[0].initial.as_decimal(), dec!(390));
1139        assert_eq!(margins[0].maintenance.as_decimal(), dec!(175));
1140        assert_eq!(
1141            info.get("net_initial_margin"),
1142            Some(&serde_json::json!("-50")),
1143        );
1144        assert_eq!(
1145            info.get("net_maintenance_margin"),
1146            Some(&serde_json::json!("-20")),
1147        );
1148        assert_eq!(
1149            info.get("is_under_liquidation"),
1150            Some(&serde_json::json!(true)),
1151        );
1152    }
1153
1154    #[rstest]
1155    fn test_parse_subaccount_with_no_collateral_emits_margins_only() {
1156        let mut subaccount = sample_subaccount();
1157        subaccount.collaterals = vec![];
1158        subaccount.positions_initial_margin = dec!(350);
1159        subaccount.positions_maintenance_margin = dec!(175);
1160
1161        let (balances, margins, _) = parse_derive_subaccount_to_balances(&subaccount).unwrap();
1162        assert!(balances.is_empty());
1163        assert_eq!(margins.len(), 1);
1164        assert_eq!(margins[0].initial.as_decimal(), dec!(350));
1165        assert_eq!(margins[0].maintenance.as_decimal(), dec!(175));
1166    }
1167
1168    #[rstest]
1169    fn test_parse_subaccount_errors_on_unrepresentable_amount() {
1170        let mut subaccount = sample_subaccount();
1171        subaccount.collaterals[0].amount = Decimal::MAX;
1172
1173        let err = parse_derive_subaccount_to_balances(&subaccount)
1174            .expect_err("out-of-range collateral amount must error instead of panicking");
1175        assert!(
1176            err.to_string().contains("collateral balance"),
1177            "unexpected error: {err}",
1178        );
1179    }
1180
1181    fn parse_subaccount_fixture(
1182        filename: &str,
1183    ) -> (Vec<AccountBalance>, Vec<MarginBalance>, Params) {
1184        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
1185            .join("test_data")
1186            .join(filename);
1187        let content =
1188            std::fs::read_to_string(&path).unwrap_or_else(|_| panic!("failed to read {filename}"));
1189        let subaccount: DeriveSubaccount = serde_json::from_str(&content)
1190            .unwrap_or_else(|e| panic!("failed to parse {filename}: {e}"));
1191        parse_derive_subaccount_to_balances(&subaccount).expect("subaccount maps")
1192    }
1193
1194    fn sample_subaccount() -> DeriveSubaccount {
1195        DeriveSubaccount {
1196            collaterals: vec![DeriveCollateral {
1197                amount: dec!(1000),
1198                asset_name: "USDC".into(),
1199                asset_type: DeriveAssetType::Erc20,
1200                cumulative_interest: dec!(0),
1201                currency: "USDC".into(),
1202                initial_margin: dec!(100),
1203                maintenance_margin: dec!(50),
1204                mark_price: dec!(1),
1205                mark_value: dec!(1000),
1206                pending_interest: dec!(0),
1207            }],
1208            collaterals_initial_margin: dec!(100),
1209            collaterals_maintenance_margin: dec!(50),
1210            collaterals_value: dec!(1000),
1211            currency: "USDC".into(),
1212            initial_margin: dec!(100),
1213            is_under_liquidation: false,
1214            label: None,
1215            maintenance_margin: dec!(50),
1216            margin_type: DeriveMarginType::Sm,
1217            open_orders: vec![],
1218            open_orders_margin: dec!(0),
1219            positions: vec![],
1220            positions_initial_margin: dec!(0),
1221            positions_maintenance_margin: dec!(0),
1222            positions_value: dec!(0),
1223            subaccount_id: 30769,
1224            subaccount_value: dec!(1000),
1225        }
1226    }
1227}