nautilus_coinbase_intx/http/
models.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 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
16use chrono::{DateTime, Utc};
17use serde::{Deserialize, Deserializer, Serialize};
18use ustr::Ustr;
19use uuid::Uuid;
20
21use crate::common::enums::{
22    CoinbaseIntxAlgoStrategy, CoinbaseIntxAssetStatus, CoinbaseIntxFeeTierType,
23    CoinbaseIntxInstrumentType, CoinbaseIntxOrderEventType, CoinbaseIntxOrderStatus,
24    CoinbaseIntxOrderType, CoinbaseIntxSTPMode, CoinbaseIntxSide, CoinbaseIntxTimeInForce,
25    CoinbaseIntxTradingState,
26};
27
28fn empty_string_as_none<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
29where
30    D: Deserializer<'de>,
31{
32    let s = String::deserialize(deserializer)?;
33    if s.is_empty() { Ok(None) } else { Ok(Some(s)) }
34}
35
36fn deserialize_optional_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
37where
38    D: Deserializer<'de>,
39{
40    let s: Option<String> = Option::deserialize(deserializer)?;
41    match s {
42        Some(ref s) if s.is_empty() => Ok(None),
43        Some(s) => DateTime::parse_from_rfc3339(&s)
44            .map(|dt| Some(dt.with_timezone(&Utc)))
45            .map_err(serde::de::Error::custom),
46        None => Ok(None),
47    }
48}
49
50/// Represents a Coinbase International asset.
51#[derive(Clone, Debug, Serialize, Deserialize)]
52pub struct CoinbaseIntxAsset {
53    /// Asset ID.
54    pub asset_id: String,
55    /// Asset UUID.
56    pub asset_uuid: String,
57    /// Asset name/symbol (e.g., "BTC").
58    pub asset_name: String,
59    /// Asset status (e.g., "ACTIVE").
60    pub status: CoinbaseIntxAssetStatus,
61    /// Weight used for collateral calculations.
62    pub collateral_weight: f64,
63    /// Whether supported networks are enabled.
64    pub supported_networks_enabled: bool,
65    /// Minimum borrow quantity allowed.
66    pub min_borrow_qty: Option<String>,
67    /// Maximum borrow quantity allowed.
68    pub max_borrow_qty: Option<String>,
69    /// Collateral requirement multiplier for loans.
70    pub loan_collateral_requirement_multiplier: f64,
71    /// Collateral limit per account.
72    pub account_collateral_limit: Option<String>,
73    /// Whether ecosystem collateral limit is breached.
74    pub ecosystem_collateral_limit_breached: bool,
75}
76
77/// Represents a Coinbase International instrument.
78#[derive(Clone, Debug, Serialize, Deserialize)]
79pub struct CoinbaseIntxInstrument {
80    /// Instrument ID.
81    pub instrument_id: String,
82    /// Instrument UUID.
83    pub instrument_uuid: String,
84    /// Trading symbol.
85    pub symbol: Ustr,
86    /// Instrument type (e.g., "PERP"). Renamed from `type` because it is reserved in Rust.
87    #[serde(rename = "type")]
88    pub instrument_type: CoinbaseIntxInstrumentType,
89    /// Mode (e.g., "STANDARD").
90    pub mode: String,
91    /// Base asset ID.
92    pub base_asset_id: String,
93    /// Base asset UUID.
94    pub base_asset_uuid: String,
95    /// Base asset name (e.g., "ETH", "BTC").
96    pub base_asset_name: String,
97    /// Quote asset ID.
98    pub quote_asset_id: String,
99    /// Quote asset UUID.
100    pub quote_asset_uuid: String,
101    /// Quote asset name (e.g., "USDC").
102    pub quote_asset_name: String,
103    /// Minimum increment for the base asset.
104    pub base_increment: String,
105    /// Minimum increment for the quote asset.
106    pub quote_increment: String,
107    /// Price band percent.
108    pub price_band_percent: f64,
109    /// Market order percent.
110    pub market_order_percent: f64,
111    /// 24-hour traded quantity.
112    pub qty_24hr: String,
113    /// 24-hour notional value.
114    pub notional_24hr: String,
115    /// Average daily quantity.
116    pub avg_daily_qty: String,
117    /// Average daily notional value.
118    pub avg_daily_notional: String,
119    /// Average 30‑day notional value.
120    pub avg_30day_notional: String,
121    /// Average 30‑day quantity.
122    pub avg_30day_qty: String,
123    /// Previous day's traded quantity.
124    pub previous_day_qty: String,
125    /// Open interest.
126    pub open_interest: String,
127    /// Position limit quantity.
128    pub position_limit_qty: String,
129    /// Position limit acquisition percent.
130    pub position_limit_adq_pct: f64,
131    /// Position notional limit.
132    pub position_notional_limit: Option<String>,
133    /// Open interest notional limit.
134    pub open_interest_notional_limit: Option<String>,
135    /// Replacement cost.
136    pub replacement_cost: String,
137    /// Base initial margin factor.
138    pub base_imf: f64,
139    /// Minimum notional value.
140    pub min_notional_value: String,
141    /// Funding interval.
142    pub funding_interval: String,
143    /// Trading state.
144    pub trading_state: CoinbaseIntxTradingState,
145    /// Quote details.
146    pub quote: CoinbaseIntxInstrumentQuote,
147    /// Default initial margin factor.
148    pub default_imf: Option<f64>,
149    /// Base asset multiplier.
150    pub base_asset_multiplier: String,
151    /// Underlying type (e.g., "SPOT", "PERP").
152    pub underlying_type: CoinbaseIntxInstrumentType,
153}
154
155/// Represents a Coinbase International instrument quote.
156#[derive(Clone, Debug, Serialize, Deserialize)]
157pub struct CoinbaseIntxInstrumentQuote {
158    /// Best bid price.
159    #[serde(default)]
160    pub best_bid_price: Option<String>,
161    /// Best bid size.
162    #[serde(default)]
163    pub best_bid_size: Option<String>,
164    /// Best ask price.
165    #[serde(default)]
166    pub best_ask_price: Option<String>,
167    /// Best ask size.
168    #[serde(default)]
169    pub best_ask_size: Option<String>,
170    /// Last traded price.
171    #[serde(default)]
172    pub trade_price: Option<String>,
173    /// Last traded quantity.
174    #[serde(default)]
175    pub trade_qty: Option<String>,
176    /// Index price.
177    pub index_price: Option<String>,
178    /// Mark price.
179    pub mark_price: String,
180    /// Settlement price.
181    pub settlement_price: String,
182    /// Upper price limit.
183    pub limit_up: Option<String>,
184    /// Lower price limit.
185    pub limit_down: Option<String>,
186    /// Predicted funding rate (optional; only provided for PERP instruments).
187    #[serde(default)]
188    pub predicted_funding: Option<String>,
189    /// Timestamp of the quote.
190    pub timestamp: DateTime<Utc>,
191}
192
193/// Represents a Coinbase International fee tier.
194#[derive(Clone, Debug, Serialize, Deserialize)]
195pub struct CoinbaseIntxFeeTier {
196    /// Type of fee tier (e.g., "REGULAR", "LIQUIDITY_PROGRAM")
197    pub fee_tier_type: CoinbaseIntxFeeTierType,
198    /// Type of instrument this fee tier applies to.
199    pub instrument_type: String, // Not the same as CoinbaseInstrumentType
200    /// Unique identifier for the fee tier.
201    pub fee_tier_id: String,
202    /// Human readable name for the fee tier.
203    pub fee_tier_name: String,
204    /// Maker fee rate as a decimal string.
205    pub maker_fee_rate: String,
206    /// Taker fee rate as a decimal string.
207    pub taker_fee_rate: String,
208    /// Minimum balance required for this tier.
209    pub min_balance: String,
210    /// Minimum volume required for this tier.
211    pub min_volume: String,
212    /// Whether both balance and volume requirements must be met.
213    pub require_balance_and_volume: bool,
214}
215
216/// Represents Coinbase International portfolio fee rates.
217#[derive(Clone, Debug, Serialize, Deserialize)]
218pub struct CoinbaseIntxPortfolioFeeRates {
219    /// Type of instrument this fee rate applies to (e.g., "SPOT", "PERPETUAL_FUTURE")
220    pub instrument_type: String, // Not the same as CoinbaseInstrumentType.
221    /// Unique identifier for the fee tier.
222    pub fee_tier_id: String,
223    /// Human readable name for the fee tier.
224    pub fee_tier_name: String,
225    /// Fee rate applied when making liquidity, as a decimal string.
226    pub maker_fee_rate: String,
227    /// Fee rate applied when taking liquidity, as a decimal string.
228    pub taker_fee_rate: String,
229    /// Whether this is a VIP fee tier.
230    pub is_vip_tier: bool,
231    /// Whether these rates are overridden from the standard tier rates.
232    pub is_override: bool,
233    /// Trading volume over the last 30 days as a decimal string.
234    #[serde(default)]
235    pub trailing_30day_volume: Option<String>,
236    /// USDC balance over the last 24 hours as a decimal string.
237    pub trailing_24hr_usdc_balance: String,
238}
239
240/// A portfolio summary on Coinbase International.
241#[derive(Clone, Debug, Serialize, Deserialize)]
242pub struct CoinbaseIntxPortfolio {
243    /// Unique identifier for the portfolio.
244    pub portfolio_id: String,
245    /// UUID for the portfolio.
246    pub portfolio_uuid: Uuid,
247    /// Human readable name for the portfolio.
248    pub name: String,
249    /// User UUID for brokers that attribute a single user per portfolio.
250    pub user_uuid: Uuid,
251    /// Fee rate charged for order making liquidity.
252    pub maker_fee_rate: String,
253    /// Fee rate charged for orders taking liquidity.
254    pub taker_fee_rate: String,
255    /// Whether the portfolio has been locked from trading.
256    pub trading_lock: bool,
257    /// Whether or not the portfolio can borrow.
258    pub borrow_disabled: bool,
259    /// Whether the portfolio is setup to take liquidation assignments.
260    pub is_lsp: bool,
261    /// Whether the portfolio is the account default portfolio.
262    pub is_default: bool,
263    /// Whether cross collateral is enabled for the portfolio.
264    pub cross_collateral_enabled: bool,
265    /// Whether pre-launch trading is enabled for the portfolio.
266    pub pre_launch_trading_enabled: bool,
267}
268
269#[derive(Clone, Debug, Serialize, Deserialize)]
270pub struct CoinbaseIntxPortfolioDetails {
271    pub summary: CoinbaseIntxPortfolioSummary,
272    pub balances: Vec<CoinbaseIntxBalance>,
273    pub positions: Vec<CoinbaseIntxPosition>,
274}
275
276#[derive(Clone, Debug, Serialize, Deserialize)]
277pub struct CoinbaseIntxPortfolioSummary {
278    pub collateral: String,
279    pub unrealized_pnl: String,
280    pub unrealized_pnl_percent: String,
281    pub position_notional: String,
282    pub balance: String,
283    pub buying_power: String,
284    pub portfolio_initial_margin: f64,
285    pub portfolio_maintenance_margin: f64,
286    pub in_liquidation: bool,
287}
288
289#[derive(Clone, Debug, Serialize, Deserialize)]
290pub struct CoinbaseIntxBalance {
291    pub asset_id: String,
292    pub asset_name: String,
293    pub quantity: String,
294    pub hold: String,
295    pub collateral_value: String,
296    pub max_withdraw_amount: String,
297}
298
299/// Response for listing orders.
300#[derive(Clone, Debug, Serialize, Deserialize)]
301pub struct CoinbaseIntxOrderList {
302    /// Pagination information.
303    pub pagination: OrderListPagination,
304    /// List of orders matching the query.
305    pub results: Vec<CoinbaseIntxOrder>,
306}
307
308/// Pagination information for list orders response.
309#[derive(Clone, Debug, Serialize, Deserialize)]
310pub struct OrderListPagination {
311    /// The datetime from which results were searched.
312    pub ref_datetime: Option<DateTime<Utc>>,
313    /// Number of results returned.
314    pub result_limit: u32,
315    /// Number of results skipped.
316    pub result_offset: u32,
317}
318
319#[derive(Clone, Debug, Serialize, Deserialize)]
320pub struct CoinbaseIntxOrder {
321    /// Unique identifier assigned by the exchange.
322    pub order_id: Ustr,
323    /// Unique identifier assigned by the client.
324    pub client_order_id: Ustr,
325    /// Side of the transaction (BUY/SELL).
326    pub side: CoinbaseIntxSide,
327    /// Unique identifier of the instrument.
328    pub instrument_id: Ustr,
329    /// UUID of the instrument.
330    pub instrument_uuid: Uuid,
331    /// Trading symbol (e.g., "BTC-PERP").
332    pub symbol: Ustr,
333    /// Portfolio identifier.
334    pub portfolio_id: Ustr,
335    /// Portfolio UUID.
336    pub portfolio_uuid: Uuid,
337    /// Order type (LIMIT, MARKET, etc.).
338    #[serde(rename = "type")]
339    pub order_type: CoinbaseIntxOrderType,
340    /// Price limit in quote asset units (for limit and stop limit orders).
341    pub price: Option<String>,
342    /// Market price that activates a stop order.
343    pub stop_price: Option<String>,
344    /// Limit price for TP/SL stop leg orders.
345    pub stop_limit_price: Option<String>,
346    /// Amount in base asset units.
347    pub size: String,
348    /// Time in force for the order.
349    pub tif: CoinbaseIntxTimeInForce,
350    /// Expiration time for GTT orders.
351    pub expire_time: Option<DateTime<Utc>>,
352    /// Self-trade prevention mode.
353    pub stp_mode: CoinbaseIntxSTPMode,
354    /// Most recent event type for the order.
355    pub event_type: CoinbaseIntxOrderEventType,
356    /// Time of the most recent event.
357    pub event_time: Option<DateTime<Utc>>,
358    /// Time the order was submitted.
359    pub submit_time: Option<DateTime<Utc>>,
360    /// Current order status.
361    pub order_status: CoinbaseIntxOrderStatus,
362    /// Remaining open quantity.
363    pub leaves_qty: String,
364    /// Executed quantity.
365    pub exec_qty: String,
366    /// Average execution price.
367    pub avg_price: Option<String>,
368    /// Exchange fee for trades.
369    pub fee: Option<String>,
370    /// Whether order was post-only.
371    pub post_only: bool,
372    /// Whether order was close-only.
373    pub close_only: bool,
374    /// Algorithmic trading strategy.
375    pub algo_strategy: Option<CoinbaseIntxAlgoStrategy>,
376    /// Cancellation reason or other message.
377    pub text: Option<String>,
378}
379
380/// Response for listing fills.
381#[derive(Clone, Debug, Serialize, Deserialize)]
382pub struct CoinbaseIntxFillList {
383    /// Pagination information.
384    pub pagination: OrderListPagination,
385    /// List of fills matching the query.
386    pub results: Vec<CoinbaseIntxFill>,
387}
388
389/// A fill in a Coinbase International portfolio.
390#[derive(Clone, Debug, Serialize, Deserialize)]
391pub struct CoinbaseIntxFill {
392    /// Unique identifier for the portfolio.
393    pub portfolio_id: Ustr,
394    /// UUID for the portfolio.
395    pub portfolio_uuid: Uuid,
396    /// Human readable name for the portfolio.
397    pub portfolio_name: String,
398    /// Unique identifier for the fill.
399    pub fill_id: String,
400    /// UUID for the fill.
401    pub fill_uuid: Uuid,
402    /// Execution identifier.
403    pub exec_id: String,
404    /// Unique identifier for the order.
405    pub order_id: Ustr,
406    /// UUID for the order.
407    pub order_uuid: Uuid,
408    /// Unique identifier for the instrument.
409    pub instrument_id: Ustr,
410    /// UUID for the instrument.
411    pub instrument_uuid: Uuid,
412    /// Trading symbol (e.g., "BTC-PERP").
413    pub symbol: Ustr,
414    /// Unique identifier for the match.
415    pub match_id: String,
416    /// UUID for the match.
417    pub match_uuid: Uuid,
418    /// Price at which the fill executed.
419    pub fill_price: String,
420    /// Quantity filled in this execution.
421    pub fill_qty: String,
422    /// Client-assigned identifier.
423    pub client_id: String,
424    /// Client-assigned order identifier.
425    pub client_order_id: Ustr,
426    /// Original order quantity.
427    pub order_qty: String,
428    /// Original limit price of the order.
429    pub limit_price: String,
430    /// Total quantity filled for the order.
431    pub total_filled: String,
432    /// Volume-weighted average price of all fills for the order.
433    pub filled_vwap: String,
434    /// Expiration time for GTT orders.
435    #[serde(deserialize_with = "deserialize_optional_datetime")]
436    pub expire_time: Option<DateTime<Utc>>,
437    /// Market price that activates a stop order.
438    #[serde(default)]
439    #[serde(deserialize_with = "empty_string_as_none")]
440    pub stop_price: Option<String>,
441    /// Side of the transaction (BUY/SELL).
442    pub side: CoinbaseIntxSide,
443    /// Time in force for the order.
444    pub tif: CoinbaseIntxTimeInForce,
445    /// Self-trade prevention mode.
446    pub stp_mode: CoinbaseIntxSTPMode,
447    /// Order flags as a string.
448    pub flags: String,
449    /// Fee charged for the trade.
450    pub fee: String,
451    /// Asset in which the fee was charged.
452    pub fee_asset: String,
453    /// Current order status.
454    pub order_status: CoinbaseIntxOrderStatus,
455    /// Time of the fill event.
456    pub event_time: DateTime<Utc>,
457    /// Source of the fill.
458    pub source: String,
459}
460
461/// A position in a Coinbase portfolio.
462#[derive(Clone, Debug, Serialize, Deserialize)]
463pub struct CoinbaseIntxPosition {
464    /// Unique identifier for the position.
465    pub id: String,
466    /// UUID for the position.
467    pub uuid: Uuid,
468    /// Trading symbol (e.g., "ETH-PERP").
469    pub symbol: Ustr,
470    /// Instrument ID.
471    pub instrument_id: Ustr,
472    /// Instrument UUID.
473    pub instrument_uuid: Uuid,
474    /// Volume Weighted Average Price.
475    pub vwap: String,
476    /// Net size of the position.
477    pub net_size: String,
478    /// Size of buy orders.
479    pub buy_order_size: String,
480    /// Size of sell orders.
481    pub sell_order_size: String,
482    /// Initial Margin contribution.
483    pub im_contribution: String,
484    /// Unrealized Profit and Loss.
485    pub unrealized_pnl: String,
486    /// Mark price.
487    pub mark_price: String,
488    /// Entry VWAP.
489    pub entry_vwap: String,
490}
491
492////////////////////////////////////////////////////////////////////////////////
493// Tests
494////////////////////////////////////////////////////////////////////////////////
495
496#[cfg(test)]
497mod tests {
498    use rstest::rstest;
499
500    use super::*;
501    use crate::common::{enums::CoinbaseIntxTradingState, testing::load_test_json};
502
503    #[rstest]
504    fn test_parse_asset_model() {
505        let json_data = load_test_json("http_get_assets_BTC.json");
506        let parsed: CoinbaseIntxAsset = serde_json::from_str(&json_data).unwrap();
507
508        assert_eq!(parsed.asset_id, "118059611751202816");
509        assert_eq!(parsed.asset_uuid, "5b71fc48-3dd3-540c-809b-f8c94d0e68b5");
510        assert_eq!(parsed.asset_name, "BTC");
511        assert_eq!(parsed.status, CoinbaseIntxAssetStatus::Active);
512        assert_eq!(parsed.collateral_weight, 0.9);
513        assert_eq!(parsed.supported_networks_enabled, true);
514        assert_eq!(parsed.min_borrow_qty, Some("0".to_string()));
515        assert_eq!(parsed.max_borrow_qty, Some("0".to_string()));
516        assert_eq!(parsed.loan_collateral_requirement_multiplier, 0.0);
517        assert_eq!(parsed.account_collateral_limit, Some("0".to_string()));
518        assert_eq!(parsed.ecosystem_collateral_limit_breached, false);
519    }
520
521    #[rstest]
522    fn test_parse_spot_model() {
523        let json_data = load_test_json("http_get_instruments_BTC-USDC.json");
524        let parsed: CoinbaseIntxInstrument = serde_json::from_str(&json_data).unwrap();
525
526        assert_eq!(parsed.instrument_id, "252572044003115008");
527        assert_eq!(
528            parsed.instrument_uuid,
529            "cf8dee38-6d4e-4658-a5ff-70c19201c485"
530        );
531        assert_eq!(parsed.symbol, "BTC-USDC");
532        assert_eq!(parsed.instrument_type, CoinbaseIntxInstrumentType::Spot);
533        assert_eq!(parsed.mode, "STANDARD");
534        assert_eq!(parsed.base_asset_id, "118059611751202816");
535        assert_eq!(
536            parsed.base_asset_uuid,
537            "5b71fc48-3dd3-540c-809b-f8c94d0e68b5"
538        );
539        assert_eq!(parsed.base_asset_name, "BTC");
540        assert_eq!(parsed.quote_asset_id, "1");
541        assert_eq!(
542            parsed.quote_asset_uuid,
543            "2b92315d-eab7-5bef-84fa-089a131333f5"
544        );
545        assert_eq!(parsed.quote_asset_name, "USDC");
546        assert_eq!(parsed.base_increment, "0.00001");
547        assert_eq!(parsed.quote_increment, "0.01");
548        assert_eq!(parsed.price_band_percent, 0.02);
549        assert_eq!(parsed.market_order_percent, 0.0075);
550        assert_eq!(parsed.qty_24hr, "0");
551        assert_eq!(parsed.notional_24hr, "0");
552        assert_eq!(parsed.avg_daily_qty, "1241.5042833333332");
553        assert_eq!(parsed.avg_daily_notional, "125201028.9956107");
554        assert_eq!(parsed.avg_30day_notional, "3756030869.868321");
555        assert_eq!(parsed.avg_30day_qty, "37245.1285");
556        assert_eq!(parsed.previous_day_qty, "0");
557        assert_eq!(parsed.open_interest, "0");
558        assert_eq!(parsed.position_limit_qty, "0");
559        assert_eq!(parsed.position_limit_adq_pct, 0.0);
560        assert_eq!(parsed.position_notional_limit.as_ref().unwrap(), "5000000");
561        assert_eq!(
562            parsed.open_interest_notional_limit.as_ref().unwrap(),
563            "26000000"
564        );
565        assert_eq!(parsed.replacement_cost, "0");
566        assert_eq!(parsed.base_imf, 1.0);
567        assert_eq!(parsed.min_notional_value, "10");
568        assert_eq!(parsed.funding_interval, "0");
569        assert_eq!(parsed.trading_state, CoinbaseIntxTradingState::Trading);
570        assert_eq!(parsed.default_imf.unwrap(), 1.0);
571        assert_eq!(parsed.base_asset_multiplier, "1.0");
572        assert_eq!(parsed.underlying_type, CoinbaseIntxInstrumentType::Spot);
573
574        // Quote assertions
575        assert_eq!(parsed.quote.best_bid_size.as_ref().unwrap(), "0");
576        assert_eq!(parsed.quote.best_ask_size.as_ref().unwrap(), "0");
577        assert_eq!(parsed.quote.trade_price, Some("101761.64".to_string()));
578        assert_eq!(parsed.quote.trade_qty, Some("3".to_string()));
579        assert_eq!(parsed.quote.index_price.as_ref().unwrap(), "97728.02");
580        assert_eq!(parsed.quote.mark_price, "101761.64");
581        assert_eq!(parsed.quote.settlement_price, "101761.64");
582        assert_eq!(parsed.quote.limit_up.as_ref().unwrap(), "102614.41");
583        assert_eq!(parsed.quote.limit_down.as_ref().unwrap(), "92841.61");
584        assert_eq!(
585            parsed.quote.timestamp.to_rfc3339(),
586            "2025-02-05T06:40:23.040+00:00"
587        );
588    }
589
590    #[rstest]
591    fn test_parse_perp_model() {
592        let json_data = load_test_json("http_get_instruments_BTC-PERP.json");
593        let parsed: CoinbaseIntxInstrument = serde_json::from_str(&json_data).unwrap();
594
595        assert_eq!(parsed.instrument_id, "149264167780483072");
596        assert_eq!(
597            parsed.instrument_uuid,
598            "b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0"
599        );
600        assert_eq!(parsed.symbol, "BTC-PERP");
601        assert_eq!(parsed.instrument_type, CoinbaseIntxInstrumentType::Perp);
602        assert_eq!(parsed.mode, "STANDARD");
603        assert_eq!(parsed.base_asset_id, "118059611751202816");
604        assert_eq!(
605            parsed.base_asset_uuid,
606            "5b71fc48-3dd3-540c-809b-f8c94d0e68b5"
607        );
608        assert_eq!(parsed.base_asset_name, "BTC");
609        assert_eq!(parsed.quote_asset_id, "1");
610        assert_eq!(
611            parsed.quote_asset_uuid,
612            "2b92315d-eab7-5bef-84fa-089a131333f5"
613        );
614        assert_eq!(parsed.quote_asset_name, "USDC");
615        assert_eq!(parsed.base_increment, "0.0001");
616        assert_eq!(parsed.quote_increment, "0.1");
617        assert_eq!(parsed.price_band_percent, 0.05);
618        assert_eq!(parsed.market_order_percent, 0.01);
619        assert_eq!(parsed.qty_24hr, "0.0051");
620        assert_eq!(parsed.notional_24hr, "499.3577");
621        assert_eq!(parsed.avg_daily_qty, "2362.797683333333");
622        assert_eq!(parsed.avg_daily_notional, "237951057.95349997");
623        assert_eq!(parsed.avg_30day_notional, "7138531738.605");
624        assert_eq!(parsed.avg_30day_qty, "70883.9305");
625        assert_eq!(parsed.previous_day_qty, "0.0116");
626        assert_eq!(parsed.open_interest, "899.6503");
627        assert_eq!(parsed.position_limit_qty, "2362.7977");
628        assert_eq!(parsed.position_limit_adq_pct, 1.0);
629        assert_eq!(
630            parsed.position_notional_limit.as_ref().unwrap(),
631            "120000000"
632        );
633        assert_eq!(
634            parsed.open_interest_notional_limit.as_ref().unwrap(),
635            "300000000"
636        );
637        assert_eq!(parsed.replacement_cost, "0.19");
638        assert_eq!(parsed.base_imf, 0.1);
639        assert_eq!(parsed.min_notional_value, "10");
640        assert_eq!(parsed.funding_interval, "3600000000000");
641        assert_eq!(parsed.trading_state, CoinbaseIntxTradingState::Trading);
642        assert_eq!(parsed.default_imf.unwrap(), 0.2);
643        assert_eq!(parsed.base_asset_multiplier, "1.0");
644        assert_eq!(parsed.underlying_type, CoinbaseIntxInstrumentType::Spot);
645
646        assert_eq!(parsed.quote.best_bid_price.as_ref().unwrap(), "96785.5");
647        assert_eq!(parsed.quote.best_bid_size.as_ref().unwrap(), "0.0005");
648        assert_eq!(parsed.quote.best_ask_size.as_ref().unwrap(), "0");
649        assert_eq!(parsed.quote.trade_price, Some("97908.8".to_string()));
650        assert_eq!(parsed.quote.trade_qty, Some("0.0005".to_string()));
651        assert_eq!(parsed.quote.index_price.as_ref().unwrap(), "97743.1");
652        assert_eq!(parsed.quote.mark_price, "97908.8");
653        assert_eq!(parsed.quote.settlement_price, "97908.8");
654        assert_eq!(parsed.quote.limit_up.as_ref().unwrap(), "107517.3");
655        assert_eq!(parsed.quote.limit_down.as_ref().unwrap(), "87968.7");
656        assert_eq!(
657            parsed.quote.predicted_funding.as_ref().unwrap(),
658            "-0.000044"
659        );
660        assert_eq!(
661            parsed.quote.timestamp.to_rfc3339(),
662            "2025-02-05T06:40:42.399+00:00"
663        );
664    }
665
666    #[rstest]
667    fn test_parse_fee_rate_tiers() {
668        let json_data = load_test_json("http_get_fee-rate-tiers.json");
669        let parsed: Vec<CoinbaseIntxFeeTier> = serde_json::from_str(&json_data).unwrap();
670
671        assert_eq!(parsed.len(), 2);
672
673        let first = &parsed[0];
674        assert_eq!(first.fee_tier_type, CoinbaseIntxFeeTierType::Regular);
675        assert_eq!(first.instrument_type, "PERPETUAL_FUTURE");
676        assert_eq!(first.fee_tier_id, "1");
677        assert_eq!(first.fee_tier_name, "Public Tier 6");
678        assert_eq!(
679            first.maker_fee_rate,
680            "0.00020000000000000000958434720477185919662588275969028472900390625"
681        );
682        assert_eq!(
683            first.taker_fee_rate,
684            "0.0004000000000000000191686944095437183932517655193805694580078125"
685        );
686        assert_eq!(first.min_balance, "0");
687        assert_eq!(first.min_volume, "0");
688        assert!(!first.require_balance_and_volume);
689
690        let second = &parsed[1];
691        assert_eq!(second.fee_tier_type, CoinbaseIntxFeeTierType::Regular);
692        assert_eq!(second.instrument_type, "PERPETUAL_FUTURE");
693        assert_eq!(second.fee_tier_id, "2");
694        assert_eq!(second.fee_tier_name, "Public Tier 5");
695        assert_eq!(
696            second.maker_fee_rate,
697            "0.00016000000000000001308848862624500952733797021210193634033203125"
698        );
699        assert_eq!(
700            second.taker_fee_rate,
701            "0.0004000000000000000191686944095437183932517655193805694580078125"
702        );
703        assert_eq!(second.min_balance, "50000");
704        assert_eq!(second.min_volume, "1000000");
705        assert!(second.require_balance_and_volume);
706    }
707
708    #[rstest]
709    fn test_parse_order() {
710        let json_data = load_test_json("http_post_orders.json");
711        let parsed: CoinbaseIntxOrder = serde_json::from_str(&json_data).unwrap();
712
713        assert_eq!(parsed.order_id, "2v2ckc1g-1-0");
714        assert_eq!(
715            parsed.client_order_id,
716            "f346ca69-11b4-4e1b-ae47-85971290c771"
717        );
718        assert_eq!(parsed.side, CoinbaseIntxSide::Sell);
719        assert_eq!(parsed.instrument_id, "114jqqhr-0-0");
720        assert_eq!(
721            parsed.instrument_uuid,
722            Uuid::parse_str("e9360798-6a10-45d6-af05-67c30eb91e2d").unwrap()
723        );
724        assert_eq!(parsed.symbol, "ETH-PERP");
725        assert_eq!(parsed.portfolio_id, "3mnk39ap-1-21");
726        assert_eq!(
727            parsed.portfolio_uuid,
728            Uuid::parse_str("cc0958ad-0c7d-4445-a812-1370fe46d0d4").unwrap()
729        );
730        assert_eq!(parsed.order_type, CoinbaseIntxOrderType::Limit);
731        assert_eq!(parsed.price, Some("3000".to_string()));
732        assert_eq!(parsed.stop_price, None);
733        assert_eq!(parsed.stop_limit_price, None);
734        assert_eq!(parsed.size, "0.01");
735        assert_eq!(parsed.tif, CoinbaseIntxTimeInForce::Gtc);
736        assert_eq!(parsed.expire_time, None);
737        assert_eq!(parsed.stp_mode, CoinbaseIntxSTPMode::Both);
738        assert_eq!(parsed.event_type, CoinbaseIntxOrderEventType::New);
739        assert_eq!(parsed.event_time, None);
740        assert_eq!(parsed.submit_time, None);
741        assert_eq!(parsed.order_status, CoinbaseIntxOrderStatus::Working);
742        assert_eq!(parsed.leaves_qty, "0.01");
743        assert_eq!(parsed.exec_qty, "0");
744        assert_eq!(parsed.avg_price, Some("0".to_string()));
745        assert_eq!(parsed.fee, Some("0".to_string()));
746        assert_eq!(parsed.post_only, false);
747        assert_eq!(parsed.close_only, false);
748        assert_eq!(parsed.algo_strategy, None);
749        assert_eq!(parsed.text, None);
750    }
751
752    #[rstest]
753    fn test_parse_position() {
754        let json_data = load_test_json("http_get_portfolios_positions_ETH-PERP.json");
755        let parsed: CoinbaseIntxPosition = serde_json::from_str(&json_data).unwrap();
756
757        assert_eq!(parsed.id, "2vev82mx-1-57");
758        assert_eq!(
759            parsed.uuid,
760            Uuid::parse_str("cb1df22f-05c7-8000-8000-7102a7804039").unwrap()
761        );
762        assert_eq!(parsed.symbol, "ETH-PERP");
763        assert_eq!(parsed.instrument_id, "114jqqhr-0-0");
764        assert_eq!(
765            parsed.instrument_uuid,
766            Uuid::parse_str("e9360798-6a10-45d6-af05-67c30eb91e2d").unwrap()
767        );
768        assert_eq!(parsed.vwap, "2747.71");
769        assert_eq!(parsed.net_size, "0.01");
770        assert_eq!(parsed.buy_order_size, "0");
771        assert_eq!(parsed.sell_order_size, "0");
772        assert_eq!(parsed.im_contribution, "0.2");
773        assert_eq!(parsed.unrealized_pnl, "0.0341");
774        assert_eq!(parsed.mark_price, "2751.12");
775        assert_eq!(parsed.entry_vwap, "2749.61");
776    }
777}