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#[cfg(test)]
493mod tests {
494    use rstest::rstest;
495
496    use super::*;
497    use crate::common::{enums::CoinbaseIntxTradingState, testing::load_test_json};
498
499    #[rstest]
500    fn test_parse_asset_model() {
501        let json_data = load_test_json("http_get_assets_BTC.json");
502        let parsed: CoinbaseIntxAsset = serde_json::from_str(&json_data).unwrap();
503
504        assert_eq!(parsed.asset_id, "118059611751202816");
505        assert_eq!(parsed.asset_uuid, "5b71fc48-3dd3-540c-809b-f8c94d0e68b5");
506        assert_eq!(parsed.asset_name, "BTC");
507        assert_eq!(parsed.status, CoinbaseIntxAssetStatus::Active);
508        assert_eq!(parsed.collateral_weight, 0.9);
509        assert!(parsed.supported_networks_enabled);
510        assert_eq!(parsed.min_borrow_qty, Some("0".to_string()));
511        assert_eq!(parsed.max_borrow_qty, Some("0".to_string()));
512        assert_eq!(parsed.loan_collateral_requirement_multiplier, 0.0);
513        assert_eq!(parsed.account_collateral_limit, Some("0".to_string()));
514        assert!(!parsed.ecosystem_collateral_limit_breached);
515    }
516
517    #[rstest]
518    fn test_parse_spot_model() {
519        let json_data = load_test_json("http_get_instruments_BTC-USDC.json");
520        let parsed: CoinbaseIntxInstrument = serde_json::from_str(&json_data).unwrap();
521
522        assert_eq!(parsed.instrument_id, "252572044003115008");
523        assert_eq!(
524            parsed.instrument_uuid,
525            "cf8dee38-6d4e-4658-a5ff-70c19201c485"
526        );
527        assert_eq!(parsed.symbol, "BTC-USDC");
528        assert_eq!(parsed.instrument_type, CoinbaseIntxInstrumentType::Spot);
529        assert_eq!(parsed.mode, "STANDARD");
530        assert_eq!(parsed.base_asset_id, "118059611751202816");
531        assert_eq!(
532            parsed.base_asset_uuid,
533            "5b71fc48-3dd3-540c-809b-f8c94d0e68b5"
534        );
535        assert_eq!(parsed.base_asset_name, "BTC");
536        assert_eq!(parsed.quote_asset_id, "1");
537        assert_eq!(
538            parsed.quote_asset_uuid,
539            "2b92315d-eab7-5bef-84fa-089a131333f5"
540        );
541        assert_eq!(parsed.quote_asset_name, "USDC");
542        assert_eq!(parsed.base_increment, "0.00001");
543        assert_eq!(parsed.quote_increment, "0.01");
544        assert_eq!(parsed.price_band_percent, 0.02);
545        assert_eq!(parsed.market_order_percent, 0.0075);
546        assert_eq!(parsed.qty_24hr, "0");
547        assert_eq!(parsed.notional_24hr, "0");
548        assert_eq!(parsed.avg_daily_qty, "1241.5042833333332");
549        assert_eq!(parsed.avg_daily_notional, "125201028.9956107");
550        assert_eq!(parsed.avg_30day_notional, "3756030869.868321");
551        assert_eq!(parsed.avg_30day_qty, "37245.1285");
552        assert_eq!(parsed.previous_day_qty, "0");
553        assert_eq!(parsed.open_interest, "0");
554        assert_eq!(parsed.position_limit_qty, "0");
555        assert_eq!(parsed.position_limit_adq_pct, 0.0);
556        assert_eq!(parsed.position_notional_limit.as_ref().unwrap(), "5000000");
557        assert_eq!(
558            parsed.open_interest_notional_limit.as_ref().unwrap(),
559            "26000000"
560        );
561        assert_eq!(parsed.replacement_cost, "0");
562        assert_eq!(parsed.base_imf, 1.0);
563        assert_eq!(parsed.min_notional_value, "10");
564        assert_eq!(parsed.funding_interval, "0");
565        assert_eq!(parsed.trading_state, CoinbaseIntxTradingState::Trading);
566        assert_eq!(parsed.default_imf.unwrap(), 1.0);
567        assert_eq!(parsed.base_asset_multiplier, "1.0");
568        assert_eq!(parsed.underlying_type, CoinbaseIntxInstrumentType::Spot);
569
570        // Quote assertions
571        assert_eq!(parsed.quote.best_bid_size.as_ref().unwrap(), "0");
572        assert_eq!(parsed.quote.best_ask_size.as_ref().unwrap(), "0");
573        assert_eq!(parsed.quote.trade_price, Some("101761.64".to_string()));
574        assert_eq!(parsed.quote.trade_qty, Some("3".to_string()));
575        assert_eq!(parsed.quote.index_price.as_ref().unwrap(), "97728.02");
576        assert_eq!(parsed.quote.mark_price, "101761.64");
577        assert_eq!(parsed.quote.settlement_price, "101761.64");
578        assert_eq!(parsed.quote.limit_up.as_ref().unwrap(), "102614.41");
579        assert_eq!(parsed.quote.limit_down.as_ref().unwrap(), "92841.61");
580        assert_eq!(
581            parsed.quote.timestamp.to_rfc3339(),
582            "2025-02-05T06:40:23.040+00:00"
583        );
584    }
585
586    #[rstest]
587    fn test_parse_perp_model() {
588        let json_data = load_test_json("http_get_instruments_BTC-PERP.json");
589        let parsed: CoinbaseIntxInstrument = serde_json::from_str(&json_data).unwrap();
590
591        assert_eq!(parsed.instrument_id, "149264167780483072");
592        assert_eq!(
593            parsed.instrument_uuid,
594            "b3469e0b-222c-4f8a-9f68-1f9e44d7e5e0"
595        );
596        assert_eq!(parsed.symbol, "BTC-PERP");
597        assert_eq!(parsed.instrument_type, CoinbaseIntxInstrumentType::Perp);
598        assert_eq!(parsed.mode, "STANDARD");
599        assert_eq!(parsed.base_asset_id, "118059611751202816");
600        assert_eq!(
601            parsed.base_asset_uuid,
602            "5b71fc48-3dd3-540c-809b-f8c94d0e68b5"
603        );
604        assert_eq!(parsed.base_asset_name, "BTC");
605        assert_eq!(parsed.quote_asset_id, "1");
606        assert_eq!(
607            parsed.quote_asset_uuid,
608            "2b92315d-eab7-5bef-84fa-089a131333f5"
609        );
610        assert_eq!(parsed.quote_asset_name, "USDC");
611        assert_eq!(parsed.base_increment, "0.0001");
612        assert_eq!(parsed.quote_increment, "0.1");
613        assert_eq!(parsed.price_band_percent, 0.05);
614        assert_eq!(parsed.market_order_percent, 0.01);
615        assert_eq!(parsed.qty_24hr, "0.0051");
616        assert_eq!(parsed.notional_24hr, "499.3577");
617        assert_eq!(parsed.avg_daily_qty, "2362.797683333333");
618        assert_eq!(parsed.avg_daily_notional, "237951057.95349997");
619        assert_eq!(parsed.avg_30day_notional, "7138531738.605");
620        assert_eq!(parsed.avg_30day_qty, "70883.9305");
621        assert_eq!(parsed.previous_day_qty, "0.0116");
622        assert_eq!(parsed.open_interest, "899.6503");
623        assert_eq!(parsed.position_limit_qty, "2362.7977");
624        assert_eq!(parsed.position_limit_adq_pct, 1.0);
625        assert_eq!(
626            parsed.position_notional_limit.as_ref().unwrap(),
627            "120000000"
628        );
629        assert_eq!(
630            parsed.open_interest_notional_limit.as_ref().unwrap(),
631            "300000000"
632        );
633        assert_eq!(parsed.replacement_cost, "0.19");
634        assert_eq!(parsed.base_imf, 0.1);
635        assert_eq!(parsed.min_notional_value, "10");
636        assert_eq!(parsed.funding_interval, "3600000000000");
637        assert_eq!(parsed.trading_state, CoinbaseIntxTradingState::Trading);
638        assert_eq!(parsed.default_imf.unwrap(), 0.2);
639        assert_eq!(parsed.base_asset_multiplier, "1.0");
640        assert_eq!(parsed.underlying_type, CoinbaseIntxInstrumentType::Spot);
641
642        assert_eq!(parsed.quote.best_bid_price.as_ref().unwrap(), "96785.5");
643        assert_eq!(parsed.quote.best_bid_size.as_ref().unwrap(), "0.0005");
644        assert_eq!(parsed.quote.best_ask_size.as_ref().unwrap(), "0");
645        assert_eq!(parsed.quote.trade_price, Some("97908.8".to_string()));
646        assert_eq!(parsed.quote.trade_qty, Some("0.0005".to_string()));
647        assert_eq!(parsed.quote.index_price.as_ref().unwrap(), "97743.1");
648        assert_eq!(parsed.quote.mark_price, "97908.8");
649        assert_eq!(parsed.quote.settlement_price, "97908.8");
650        assert_eq!(parsed.quote.limit_up.as_ref().unwrap(), "107517.3");
651        assert_eq!(parsed.quote.limit_down.as_ref().unwrap(), "87968.7");
652        assert_eq!(
653            parsed.quote.predicted_funding.as_ref().unwrap(),
654            "-0.000044"
655        );
656        assert_eq!(
657            parsed.quote.timestamp.to_rfc3339(),
658            "2025-02-05T06:40:42.399+00:00"
659        );
660    }
661
662    #[rstest]
663    fn test_parse_fee_rate_tiers() {
664        let json_data = load_test_json("http_get_fee-rate-tiers.json");
665        let parsed: Vec<CoinbaseIntxFeeTier> = serde_json::from_str(&json_data).unwrap();
666
667        assert_eq!(parsed.len(), 2);
668
669        let first = &parsed[0];
670        assert_eq!(first.fee_tier_type, CoinbaseIntxFeeTierType::Regular);
671        assert_eq!(first.instrument_type, "PERPETUAL_FUTURE");
672        assert_eq!(first.fee_tier_id, "1");
673        assert_eq!(first.fee_tier_name, "Public Tier 6");
674        assert_eq!(
675            first.maker_fee_rate,
676            "0.00020000000000000000958434720477185919662588275969028472900390625"
677        );
678        assert_eq!(
679            first.taker_fee_rate,
680            "0.0004000000000000000191686944095437183932517655193805694580078125"
681        );
682        assert_eq!(first.min_balance, "0");
683        assert_eq!(first.min_volume, "0");
684        assert!(!first.require_balance_and_volume);
685
686        let second = &parsed[1];
687        assert_eq!(second.fee_tier_type, CoinbaseIntxFeeTierType::Regular);
688        assert_eq!(second.instrument_type, "PERPETUAL_FUTURE");
689        assert_eq!(second.fee_tier_id, "2");
690        assert_eq!(second.fee_tier_name, "Public Tier 5");
691        assert_eq!(
692            second.maker_fee_rate,
693            "0.00016000000000000001308848862624500952733797021210193634033203125"
694        );
695        assert_eq!(
696            second.taker_fee_rate,
697            "0.0004000000000000000191686944095437183932517655193805694580078125"
698        );
699        assert_eq!(second.min_balance, "50000");
700        assert_eq!(second.min_volume, "1000000");
701        assert!(second.require_balance_and_volume);
702    }
703
704    #[rstest]
705    fn test_parse_order() {
706        let json_data = load_test_json("http_post_orders.json");
707        let parsed: CoinbaseIntxOrder = serde_json::from_str(&json_data).unwrap();
708
709        assert_eq!(parsed.order_id, "2v2ckc1g-1-0");
710        assert_eq!(
711            parsed.client_order_id,
712            "f346ca69-11b4-4e1b-ae47-85971290c771"
713        );
714        assert_eq!(parsed.side, CoinbaseIntxSide::Sell);
715        assert_eq!(parsed.instrument_id, "114jqqhr-0-0");
716        assert_eq!(
717            parsed.instrument_uuid,
718            Uuid::parse_str("e9360798-6a10-45d6-af05-67c30eb91e2d").unwrap()
719        );
720        assert_eq!(parsed.symbol, "ETH-PERP");
721        assert_eq!(parsed.portfolio_id, "3mnk39ap-1-21");
722        assert_eq!(
723            parsed.portfolio_uuid,
724            Uuid::parse_str("cc0958ad-0c7d-4445-a812-1370fe46d0d4").unwrap()
725        );
726        assert_eq!(parsed.order_type, CoinbaseIntxOrderType::Limit);
727        assert_eq!(parsed.price, Some("3000".to_string()));
728        assert_eq!(parsed.stop_price, None);
729        assert_eq!(parsed.stop_limit_price, None);
730        assert_eq!(parsed.size, "0.01");
731        assert_eq!(parsed.tif, CoinbaseIntxTimeInForce::Gtc);
732        assert_eq!(parsed.expire_time, None);
733        assert_eq!(parsed.stp_mode, CoinbaseIntxSTPMode::Both);
734        assert_eq!(parsed.event_type, CoinbaseIntxOrderEventType::New);
735        assert_eq!(parsed.event_time, None);
736        assert_eq!(parsed.submit_time, None);
737        assert_eq!(parsed.order_status, CoinbaseIntxOrderStatus::Working);
738        assert_eq!(parsed.leaves_qty, "0.01");
739        assert_eq!(parsed.exec_qty, "0");
740        assert_eq!(parsed.avg_price, Some("0".to_string()));
741        assert_eq!(parsed.fee, Some("0".to_string()));
742        assert!(!parsed.post_only);
743        assert!(!parsed.close_only);
744        assert_eq!(parsed.algo_strategy, None);
745        assert_eq!(parsed.text, None);
746    }
747
748    #[rstest]
749    fn test_parse_position() {
750        let json_data = load_test_json("http_get_portfolios_positions_ETH-PERP.json");
751        let parsed: CoinbaseIntxPosition = serde_json::from_str(&json_data).unwrap();
752
753        assert_eq!(parsed.id, "2vev82mx-1-57");
754        assert_eq!(
755            parsed.uuid,
756            Uuid::parse_str("cb1df22f-05c7-8000-8000-7102a7804039").unwrap()
757        );
758        assert_eq!(parsed.symbol, "ETH-PERP");
759        assert_eq!(parsed.instrument_id, "114jqqhr-0-0");
760        assert_eq!(
761            parsed.instrument_uuid,
762            Uuid::parse_str("e9360798-6a10-45d6-af05-67c30eb91e2d").unwrap()
763        );
764        assert_eq!(parsed.vwap, "2747.71");
765        assert_eq!(parsed.net_size, "0.01");
766        assert_eq!(parsed.buy_order_size, "0");
767        assert_eq!(parsed.sell_order_size, "0");
768        assert_eq!(parsed.im_contribution, "0.2");
769        assert_eq!(parsed.unrealized_pnl, "0.0341");
770        assert_eq!(parsed.mark_price, "2751.12");
771        assert_eq!(parsed.entry_vwap, "2749.61");
772    }
773}