ndaxrs 0.1.0

Rust client library for the NDAX cryptocurrency exchange API
Documentation
// Copyright (C) 2026 ndaxrs Art Morozov
// SPDX-License-Identifier: GPL-3.0-only

//! Instrument and product types for the NDAX API.

use serde::{Deserialize, Serialize};

/// Request payload for the `GetInstruments` API endpoint.
/// Returns all available trading instruments (pairs).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetInstrumentsRequest {
  /// The OMS (Order Management System) ID.
  #[serde(rename = "OMSId")]
  pub oms_id: i64,
}

/// Request payload for the `GetInstrument` API endpoint.
/// Returns a single instrument by ID.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetInstrumentRequest {
  /// The OMS (Order Management System) ID.
  #[serde(rename = "OMSId")]
  pub oms_id:        i64,
  /// The unique instrument identifier.
  #[serde(rename = "InstrumentId")]
  pub instrument_id: i64,
}

/// A trading instrument (pair) from the NDAX API.
///
/// NDAX uses PascalCase for JSON field names (e.g., "OMSId", "InstrumentId").
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Instrument {
  /// The OMS (Order Management System) ID.
  #[serde(rename = "OMSId", alias = "omsId")]
  pub oms_id:                   i64,
  /// The unique instrument identifier.
  #[serde(rename = "InstrumentId", alias = "instrumentId")]
  pub instrument_id:            i64,
  /// The symbol (e.g., "BTCCAD").
  #[serde(rename = "Symbol", alias = "symbol")]
  pub symbol:                   String,
  /// The ID of the first product (base asset).
  #[serde(rename = "Product1", alias = "product1")]
  pub product1:                 i64,
  /// The symbol of the first product (e.g., "BTC").
  #[serde(rename = "Product1Symbol", alias = "product1Symbol")]
  pub product1_symbol:          String,
  /// The ID of the second product (quote asset).
  #[serde(rename = "Product2", alias = "product2")]
  pub product2:                 i64,
  /// The symbol of the second product (e.g., "CAD").
  #[serde(rename = "Product2Symbol", alias = "product2Symbol")]
  pub product2_symbol:          String,
  /// The type of instrument (e.g., "Standard").
  #[serde(rename = "InstrumentType", alias = "instrumentType")]
  pub instrument_type:          String,
  /// The venue instrument ID.
  #[serde(rename = "VenueInstrumentId", alias = "venueInstrumentId")]
  pub venue_instrument_id:      i64,
  /// The venue ID.
  #[serde(rename = "VenueId", alias = "venueId")]
  pub venue_id:                 i64,
  /// Sort order index.
  #[serde(rename = "SortIndex", alias = "sortIndex")]
  pub sort_index:               i64,
  /// Current session status (e.g., "Running").
  #[serde(rename = "SessionStatus", alias = "sessionStatus")]
  pub session_status:           String,
  /// Previous session status.
  #[serde(rename = "PreviousSessionStatus", alias = "previousSessionStatus")]
  pub previous_session_status:  String,
  /// Date/time of session status change.
  #[serde(rename = "SessionStatusDateTime", alias = "sessionStatusDateTime")]
  pub session_status_date_time: String,
  /// Whether self-trade prevention is enabled.
  #[serde(rename = "SelfTradePrevention", alias = "selfTradePrevention")]
  pub self_trade_prevention:    bool,
  /// Minimum quantity increment for orders.
  #[serde(rename = "QuantityIncrement", alias = "quantityIncrement")]
  pub quantity_increment:       f64,
  /// Minimum price increment for orders.
  #[serde(rename = "PriceIncrement", alias = "priceIncrement")]
  pub price_increment:          f64,
}

impl Instrument {
  /// Returns the trading pair in standard format (e.g., "BTC/CAD").
  pub fn trading_pair(&self) -> String {
    format!("{}/{}", self.product1_symbol, self.product2_symbol)
  }

  /// Returns the base symbol (the first product, what you're buying/selling).
  pub fn base_symbol(&self) -> &str { &self.product1_symbol }

  /// Returns the quote symbol (the second product, what you're pricing in).
  pub fn quote_symbol(&self) -> &str { &self.product2_symbol }
}

/// Request payload for the `GetProducts` API endpoint.
/// Returns all available products (assets).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetProductsRequest {
  /// The OMS (Order Management System) ID.
  #[serde(rename = "OMSId")]
  pub oms_id: i64,
}

/// Request payload for the `GetProduct` API endpoint.
/// Returns a single product by ID.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetProductRequest {
  /// The OMS (Order Management System) ID.
  #[serde(rename = "OMSId")]
  pub oms_id:     i64,
  /// The unique product identifier.
  #[serde(rename = "ProductId")]
  pub product_id: i64,
}

/// A product (asset) from the NDAX API.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Product {
  /// The OMS (Order Management System) ID.
  pub oms_id:            i64,
  /// The unique product identifier.
  pub product_id:        i64,
  /// The product symbol/name (e.g., "BTC").
  pub product:           String,
  /// The full name of the product (e.g., "Bitcoin").
  pub product_full_name: String,
  /// The type of product (e.g., "CryptoCurrency", "NationalCurrency").
  pub product_type:      String,
  /// Number of decimal places for the product.
  pub decimal_places:    i32,
  /// Minimum tick size for the product.
  pub tick_size:         f64,
  /// Whether the product is exempt from fees.
  pub no_fees:           bool,
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn test_instruments_deserialize() {
    let json = r#"[{"omsId":1,"instrumentId":1,"symbol":"BTCCAD","product1":1,"product1Symbol":"BTC","product2":2,"product2Symbol":"CAD","instrumentType":"Standard","venueInstrumentId":1,"venueId":1,"sortIndex":0,"sessionStatus":"Running","previousSessionStatus":"Running","sessionStatusDateTime":"2024-01-01T00:00:00Z","selfTradePrevention":true,"quantityIncrement":0.00001,"priceIncrement":0.01}]"#;

    let instruments: Vec<Instrument> = serde_json::from_str(json).unwrap();

    assert_eq!(instruments.len(), 1);
    let instrument = &instruments[0];
    assert_eq!(instrument.oms_id, 1);
    assert_eq!(instrument.instrument_id, 1);
    assert_eq!(instrument.symbol, "BTCCAD");
    assert_eq!(instrument.product1, 1);
    assert_eq!(instrument.product1_symbol, "BTC");
    assert_eq!(instrument.product2, 2);
    assert_eq!(instrument.product2_symbol, "CAD");
    assert_eq!(instrument.instrument_type, "Standard");
    assert_eq!(instrument.venue_instrument_id, 1);
    assert_eq!(instrument.venue_id, 1);
    assert_eq!(instrument.sort_index, 0);
    assert_eq!(instrument.session_status, "Running");
    assert_eq!(instrument.previous_session_status, "Running");
    assert!(instrument.self_trade_prevention);
    assert!((instrument.quantity_increment - 0.00001).abs() < f64::EPSILON);
    assert!((instrument.price_increment - 0.01).abs() < f64::EPSILON);
  }

  #[test]
  fn test_products_deserialize() {
    let json = r#"[{"omsId":1,"productId":1,"product":"BTC","productFullName":"Bitcoin","productType":"CryptoCurrency","decimalPlaces":8,"tickSize":0.00000001,"noFees":false},{"omsId":1,"productId":2,"product":"CAD","productFullName":"Canadian Dollar","productType":"NationalCurrency","decimalPlaces":2,"tickSize":0.01,"noFees":false}]"#;

    let products: Vec<Product> = serde_json::from_str(json).unwrap();

    assert_eq!(products.len(), 2);

    let btc = &products[0];
    assert_eq!(btc.oms_id, 1);
    assert_eq!(btc.product_id, 1);
    assert_eq!(btc.product, "BTC");
    assert_eq!(btc.product_full_name, "Bitcoin");
    assert_eq!(btc.product_type, "CryptoCurrency");
    assert_eq!(btc.decimal_places, 8);
    assert!((btc.tick_size - 0.00000001).abs() < f64::EPSILON);
    assert!(!btc.no_fees);

    let cad = &products[1];
    assert_eq!(cad.oms_id, 1);
    assert_eq!(cad.product_id, 2);
    assert_eq!(cad.product, "CAD");
    assert_eq!(cad.product_full_name, "Canadian Dollar");
    assert_eq!(cad.product_type, "NationalCurrency");
    assert_eq!(cad.decimal_places, 2);
    assert!((cad.tick_size - 0.01).abs() < f64::EPSILON);
    assert!(!cad.no_fees);
  }

  #[test]
  fn test_trading_pair_format() {
    let instrument = Instrument {
      oms_id:                   1,
      instrument_id:            1,
      symbol:                   "BTCCAD".to_string(),
      product1:                 1,
      product1_symbol:          "BTC".to_string(),
      product2:                 2,
      product2_symbol:          "CAD".to_string(),
      instrument_type:          "Standard".to_string(),
      venue_instrument_id:      1,
      venue_id:                 1,
      sort_index:               0,
      session_status:           "Running".to_string(),
      previous_session_status:  "Running".to_string(),
      session_status_date_time: "2024-01-01T00:00:00Z".to_string(),
      self_trade_prevention:    true,
      quantity_increment:       0.00001,
      price_increment:          0.01,
    };

    assert_eq!(instrument.trading_pair(), "BTC/CAD");
  }

  #[test]
  fn test_base_quote_symbols() {
    let instrument = Instrument {
      oms_id:                   1,
      instrument_id:            1,
      symbol:                   "ETHCAD".to_string(),
      product1:                 3,
      product1_symbol:          "ETH".to_string(),
      product2:                 2,
      product2_symbol:          "CAD".to_string(),
      instrument_type:          "Standard".to_string(),
      venue_instrument_id:      2,
      venue_id:                 1,
      sort_index:               1,
      session_status:           "Running".to_string(),
      previous_session_status:  "Running".to_string(),
      session_status_date_time: "2024-01-01T00:00:00Z".to_string(),
      self_trade_prevention:    true,
      quantity_increment:       0.0001,
      price_increment:          0.01,
    };

    assert_eq!(instrument.base_symbol(), "ETH");
    assert_eq!(instrument.quote_symbol(), "CAD");
  }

  #[test]
  fn test_get_instruments_request_serialize() {
    let request = GetInstrumentsRequest {
      oms_id: 1
    };
    let json = serde_json::to_string(&request).unwrap();

    assert!(json.contains("\"OMSId\""));
    assert!(json.contains(":1"));
  }

  #[test]
  fn test_get_instrument_request_serialize() {
    let request = GetInstrumentRequest {
      oms_id:        1,
      instrument_id: 5,
    };
    let json = serde_json::to_string(&request).unwrap();

    assert!(json.contains("\"OMSId\""));
    assert!(json.contains("\"InstrumentId\""));
  }

  #[test]
  fn test_get_products_request_serialize() {
    let request = GetProductsRequest {
      oms_id: 1
    };
    let json = serde_json::to_string(&request).unwrap();

    assert!(json.contains("\"OMSId\""));
  }

  #[test]
  fn test_get_product_request_serialize() {
    let request = GetProductRequest {
      oms_id:     1,
      product_id: 3,
    };
    let json = serde_json::to_string(&request).unwrap();

    assert!(json.contains("\"OMSId\""));
    assert!(json.contains("\"ProductId\""));
  }
}