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

//! Market data message types for the NDAX API.
//!
//! This module contains types for Level2 (order book) data from NDAX.

use rust_decimal::Decimal;
use serde::{Deserialize, Serialize};

use super::ActionType;
use crate::{Error, Result};

/// A single Level2 order book entry from NDAX.
///
/// NDAX sends Level2 data as JSON arrays with this format:
/// `[MDUpdateId, AccountId, ActionDateTime, ActionType, LastTradePrice,
/// OrderId, Price, InstrumentId, Quantity, Side]`
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Level2Entry {
  /// Market data update identifier.
  pub md_update_id:     u64,
  /// Account ID (usually 0 for public data).
  pub account_id:       u64,
  /// Action date/time as string.
  pub action_date_time: String,
  /// Action type: 0=New, 1=Update, 2=Delete.
  pub action_type:      ActionType,
  /// Last trade price at this level.
  pub last_trade_price: Decimal,
  /// Order ID.
  pub order_id:         u64,
  /// Price at this level.
  pub price:            Decimal,
  /// Instrument ID (product pair code).
  pub instrument_id:    u64,
  /// Quantity at this price level.
  pub quantity:         Decimal,
  /// Side: 0=Buy (bid), 1=Sell (ask).
  pub side:             u8,
}

impl Level2Entry {
  /// Parse a Level2Entry from an NDAX array format.
  ///
  /// The array format is:
  /// `[MDUpdateId, AccountId, ActionDateTime, ActionType, LastTradePrice,
  /// OrderId, Price, InstrumentId, Quantity, Side]`
  ///
  /// # Arguments
  ///
  /// * `arr` - A JSON array value from NDAX
  ///
  /// # Returns
  ///
  /// A parsed Level2Entry, or an error if the format is invalid.
  ///
  /// # Example
  ///
  /// ```
  /// use ndaxrs::messages::Level2Entry;
  ///
  /// let json = serde_json::json!([
  ///   1234567,
  ///   0,
  ///   "2024-01-15T10:00:00Z",
  ///   0,
  ///   50000.00,
  ///   98765,
  ///   50000.00,
  ///   1,
  ///   0.001,
  ///   0
  /// ]);
  /// let entry = Level2Entry::from_array(&json).unwrap();
  /// assert_eq!(entry.md_update_id, 1234567);
  /// assert_eq!(entry.side, 0); // Buy side
  /// ```
  pub fn from_array(value: &serde_json::Value) -> Result<Self> {
    let arr = value.as_array().ok_or_else(|| {
      Error::NdaxError(0, "Level2 entry must be an array".to_string())
    })?;

    if arr.len() < 10 {
      return Err(Error::NdaxError(
        0,
        format!(
          "Level2 entry array must have at least 10 elements, got {}",
          arr.len()
        ),
      ));
    }

    Ok(Self {
      md_update_id:     arr[0].as_u64().unwrap_or(0),
      account_id:       arr[1].as_u64().unwrap_or(0),
      action_date_time: arr[2].as_str().unwrap_or("").to_string(),
      action_type:      parse_action_type(&arr[3]),
      last_trade_price: parse_decimal(&arr[4]),
      order_id:         arr[5].as_u64().unwrap_or(0),
      price:            parse_decimal(&arr[6]),
      instrument_id:    arr[7].as_u64().unwrap_or(0),
      quantity:         parse_decimal(&arr[8]),
      side:             arr[9].as_u64().unwrap_or(0) as u8,
    })
  }

  /// Check if this is a buy (bid) side entry.
  pub fn is_bid(&self) -> bool { self.side == 0 }

  /// Check if this is a sell (ask) side entry.
  pub fn is_ask(&self) -> bool { self.side == 1 }

  /// Check if this entry is a delete action.
  pub fn is_delete(&self) -> bool { self.action_type == ActionType::Delete }
}

/// Parse a JSON value as a Decimal.
fn parse_decimal(value: &serde_json::Value) -> Decimal {
  if let Some(s) = value.as_str() {
    s.parse().unwrap_or_default()
  } else if let Some(f) = value.as_f64() {
    Decimal::try_from(f).unwrap_or_default()
  } else if let Some(i) = value.as_i64() {
    Decimal::from(i)
  } else {
    Decimal::default()
  }
}

/// Parse a JSON value as an ActionType.
fn parse_action_type(value: &serde_json::Value) -> ActionType {
  match value.as_u64().unwrap_or(0) {
    0 => ActionType::New,
    1 => ActionType::Update,
    2 => ActionType::Delete,
    _ => ActionType::New,
  }
}

#[cfg(test)]
mod tests {
  use rust_decimal_macros::dec;
  use serde_json::json;

  use super::*;

  #[test]
  fn test_level2_entry_from_array() {
    let json = json!([
      1234567,
      0,
      "2024-01-15T10:00:00Z",
      0,
      50000.00,
      98765,
      50000.00,
      1,
      0.001,
      0
    ]);
    let entry = Level2Entry::from_array(&json).unwrap();

    assert_eq!(entry.md_update_id, 1234567);
    assert_eq!(entry.account_id, 0);
    assert_eq!(entry.action_date_time, "2024-01-15T10:00:00Z");
    assert_eq!(entry.action_type, ActionType::New);
    assert_eq!(entry.last_trade_price, dec!(50000.00));
    assert_eq!(entry.order_id, 98765);
    assert_eq!(entry.price, dec!(50000.00));
    assert_eq!(entry.instrument_id, 1);
    assert_eq!(entry.quantity, dec!(0.001));
    assert_eq!(entry.side, 0);
    assert!(entry.is_bid());
    assert!(!entry.is_ask());
  }

  #[test]
  fn test_level2_entry_sell_side() {
    let json = json!([
      999,
      0,
      "2024-01-15T10:00:00Z",
      1,
      51000.00,
      11111,
      51000.00,
      1,
      0.5,
      1
    ]);
    let entry = Level2Entry::from_array(&json).unwrap();

    assert_eq!(entry.side, 1);
    assert!(!entry.is_bid());
    assert!(entry.is_ask());
    assert_eq!(entry.action_type, ActionType::Update);
  }

  #[test]
  fn test_level2_entry_delete_action() {
    let json = json!([
      888,
      0,
      "2024-01-15T10:00:00Z",
      2,
      0,
      22222,
      49000.00,
      1,
      0,
      0
    ]);
    let entry = Level2Entry::from_array(&json).unwrap();

    assert!(entry.is_delete());
    assert_eq!(entry.action_type, ActionType::Delete);
  }

  #[test]
  fn test_level2_entry_invalid_not_array() {
    let json = json!({"key": "value"});
    let result = Level2Entry::from_array(&json);
    assert!(result.is_err());
  }

  #[test]
  fn test_level2_entry_invalid_too_short() {
    let json = json!([1, 2, 3]);
    let result = Level2Entry::from_array(&json);
    assert!(result.is_err());
  }

  #[test]
  fn test_level2_entry_string_decimals() {
    let json = json!([
      1234567,
      0,
      "2024-01-15T10:00:00Z",
      0,
      "50000.12345",
      98765,
      "49999.99",
      1,
      "0.00123456",
      0
    ]);
    let entry = Level2Entry::from_array(&json).unwrap();

    assert_eq!(entry.last_trade_price, dec!(50000.12345));
    assert_eq!(entry.price, dec!(49999.99));
    assert_eq!(entry.quantity, dec!(0.00123456));
  }
}