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

//! Trading Example
//!
//! This example demonstrates how to use the ndaxrs crate for authenticated
//! trading operations including placing and canceling orders.
//!
//! # Required Environment Variables
//!
//! Set these environment variables before running:
//! - `NDAX_API_KEY`: Your NDAX API key
//! - `NDAX_API_SECRET`: Your NDAX API secret
//! - `NDAX_USER_ID`: Your NDAX user ID
//! - `NDAX_ACCOUNT_ID`: Your NDAX account ID (for trading)
//!
//! Or create a `.env` file with these values.
//!
//! # WARNING
//!
//! This example places a real order on NDAX.
//! It uses a price far from market ($1 for BTC) so it won't execute.
//! The order is canceled immediately after placement.
//! Use with caution and verify your account has appropriate permissions.
//!
//! Run with: cargo run --example trading

use std::{thread, time::Duration};

use ndaxrs::{
  messages::orders::{create_limit_order, CancelOrderRequest},
  ws::{NdaxWsAPI, NdaxWsConfig, PrivateConfig},
  NdaxCredentials,
  Side,
};
use rust_decimal_macros::dec;

fn main() {
  // Step 1: Initialize logging
  // Set RUST_LOG=info or RUST_LOG=debug for more detail.
  env_logger::init();
  log::info!("Starting NDAX trading example");

  // Step 2: Load credentials from environment variables
  // This looks for NDAX_API_KEY, NDAX_API_SECRET, and NDAX_USER_ID.
  let credentials = match NdaxCredentials::from_env() {
    Ok(c) => {
      log::info!("Loaded credentials for user: {}", c.user_id);
      c
    }
    Err(e) => {
      eprintln!("Error: Failed to load credentials: {}", e);
      eprintln!();
      eprintln!("Please set the following environment variables:");
      eprintln!("  NDAX_API_KEY     - Your NDAX API key");
      eprintln!("  NDAX_API_SECRET  - Your NDAX API secret");
      eprintln!("  NDAX_USER_ID     - Your NDAX user ID");
      eprintln!();
      eprintln!("Or create a .env file with these values.");
      return;
    }
  };

  // Step 3: Get account ID from environment
  // The account ID is needed for trading and position queries.
  let account_id: u64 = match std::env::var("NDAX_ACCOUNT_ID") {
    Ok(id) =>
      match id.parse() {
        Ok(n) => n,
        Err(_) => {
          eprintln!("Error: NDAX_ACCOUNT_ID must be a valid number");
          return;
        }
      },
    Err(_) => {
      eprintln!("Error: NDAX_ACCOUNT_ID environment variable not set");
      eprintln!("This is required for trading operations.");
      return;
    }
  };

  log::info!("Using account ID: {}", account_id);

  // Step 4: Create configuration with credentials
  // We enable account events subscription to receive order updates.
  let config = NdaxWsConfig::builder()
    .credentials(credentials)
    .private(PrivateConfig::new(account_id).with_account_events())
    .build();

  log::info!("Connecting to NDAX with authentication...");

  // Step 5: Connect to the NDAX WebSocket
  // This establishes the connection and authenticates automatically.
  let api = match NdaxWsAPI::new(config) {
    Ok(api) => {
      if api.is_authenticated() {
        log::info!("Successfully connected and authenticated to NDAX");
      } else {
        eprintln!("Error: Connected but authentication failed");
        return;
      }
      api
    }
    Err(e) => {
      eprintln!("Error: Failed to connect to NDAX: {}", e);
      eprintln!("Check your credentials and network connectivity.");
      return;
    }
  };

  // Step 6: Get and print account positions (balances)
  log::info!("Fetching account positions...");
  match api.get_account_positions(account_id) {
    Ok(positions) => {
      println!("\n=== Account Positions ===");
      if positions.is_empty() {
        println!("  No positions found");
      } else {
        for pos in &positions {
          let available = pos.amount - pos.hold;
          println!(
            "  {}: {} (available: {}, hold: {})",
            pos.product_symbol, pos.amount, available, pos.hold
          );
        }
      }
      println!();
    }
    Err(e) => {
      eprintln!("Warning: Failed to get positions: {}", e);
    }
  }

  // Step 7: Get instruments to find BTC/CAD
  log::info!("Fetching available instruments...");
  let btc_cad_id: u64 = match api.get_instruments() {
    Ok(instruments) => {
      let btc_cad = instruments
        .iter()
        .find(|i| i.product1_symbol == "BTC" && i.product2_symbol == "CAD");

      match btc_cad {
        Some(inst) => {
          log::info!("Found BTC/CAD: instrument_id={}", inst.instrument_id);
          inst.instrument_id as u64
        }
        None => {
          log::info!("BTC/CAD not found, using default instrument_id=1");
          1
        }
      }
    }
    Err(e) => {
      log::warn!("Failed to get instruments: {}, using default", e);
      1
    }
  };

  // Step 8: Place a small limit buy order far from market
  // Using $1 CAD for BTC ensures this won't execute.
  println!("=== Placing Test Order ===");
  println!("  Instrument: BTC/CAD (id={})", btc_cad_id);
  println!("  Side: Buy");
  println!("  Quantity: 0.0001 BTC");
  println!("  Price: $1.00 CAD (far below market - won't execute)");
  println!();

  let order = create_limit_order(
    btc_cad_id,
    account_id,
    Side::Buy,
    dec!(0.0001), // Small quantity
    dec!(1.00),   // Far below market price
  );

  let order_id = match api.send_order(&order) {
    Ok(response) => {
      println!("Order Response:");
      println!("  Status: {}", response.status);
      if let Some(ref err) = response.errormsg {
        if !err.is_empty() {
          println!("  Message: {}", err);
        }
      }
      if let Some(id) = response.order_id {
        println!("  Order ID: {}", id);
        Some(id)
      } else {
        println!("  Order ID: (not assigned)");
        None
      }
    }
    Err(e) => {
      eprintln!("Error placing order: {}", e);
      None
    }
  };
  println!();

  // Step 9: Wait briefly for order acknowledgment
  // In production, you would use the account events subscription.
  if order_id.is_some() {
    log::info!("Waiting for order to be acknowledged...");
    thread::sleep(Duration::from_secs(2));
  }

  // Step 10: Cancel the order
  if let Some(id) = order_id {
    println!("=== Canceling Order ===");
    println!("  Order ID: {}", id);

    let cancel_request = CancelOrderRequest {
      oms_id: 1,
      account_id,
      order_id: Some(id),
      client_order_id: None,
    };

    match api.cancel_order(&cancel_request) {
      Ok(response) => {
        println!("Cancel Response:");
        println!("  Result: {}", response.result);
        if let Some(ref err) = response.errormsg {
          if !err.is_empty() {
            println!("  Message: {}", err);
          }
        }
      }
      Err(e) => {
        eprintln!("Error canceling order: {}", e);
      }
    }
    println!();

    // Step 11: Check final order status
    log::info!("Checking open orders...");
    thread::sleep(Duration::from_secs(1));

    match api.get_open_orders(account_id) {
      Ok(orders) => {
        println!("=== Open Orders ===");
        let our_order = orders.iter().find(|o| o.order_id == id);
        if our_order.is_some() {
          println!("  Warning: Order {} is still open!", id);
        } else {
          println!("  Order {} successfully canceled (not in open orders)", id);
        }
        if !orders.is_empty() {
          println!("  Total open orders: {}", orders.len());
        }
      }
      Err(e) => {
        eprintln!("Warning: Failed to check open orders: {}", e);
      }
    }
    println!();
  }

  // Step 12: Gracefully disconnect
  log::info!("Closing WebSocket connection...");
  api.close();
  log::info!("Disconnected. Trading example complete.");
}