ccxt-csharp

v2026.09.24

CCXT cryptocurrency exchange library for C# and .NET developers. Covers both REST API (standard) and WebSocket API (real-time). Helps install CCXT, connect to exchanges, fetch market data, place orders, stream live tickers/orderbooks, handle authentication, and manage errors in .NET projects. Use when working with crypto exchanges in C# applications, trading systems, or financial software. Supports .NET Standard 2.0+.

GitHub
安装命令
npx skhub add ccxt/ccxt-csharp
Markdown
SKILL.md

CCXT for C#

A comprehensive guide to using CCXT in C# and .NET projects for cryptocurrency exchange integration.

Installation

Via NuGet Package Manager

dotnet add package CCXT.NET

Or via Visual Studio:

  1. Right-click project → Manage NuGet Packages
  2. Search for "CCXT.NET"
  3. Click Install

Requirements

  • .NET Standard 2.0 or higher
  • .NET Core 2.0+ / .NET 5+ / .NET Framework 4.6.1+

Quick Start

REST API

using ccxt;

var exchange = new Binance();
await exchange.LoadMarkets();
var ticker = await exchange.FetchTicker("BTC/USDT");
Console.WriteLine(ticker);

WebSocket API - Real-time Updates

using ccxt.pro;

var exchange = new Binance();
while (true)
{
    var ticker = await exchange.WatchTicker("BTC/USDT");
    Console.WriteLine(ticker.Last);  // Live updates!
}
await exchange.Close();

REST vs WebSocket

FeatureREST APIWebSocket API
Use forOne-time queries, placing ordersReal-time monitoring, live price feeds
Importusing ccxt;using ccxt.pro;
MethodsFetch* (FetchTicker, FetchOrderBook)Watch* (WatchTicker, WatchOrderBook)
SpeedSlower (HTTP request/response)Faster (persistent connection)
Rate limitsStrict (1-2 req/sec)More lenient (continuous stream)
Best forTrading, account managementPrice monitoring, arbitrage detection

Method naming: C# uses PascalCase - FetchTicker not fetchTicker, WatchTicker not watchTicker

Creating Exchange Instance

REST API

using ccxt;

// Public API (no authentication)
var exchange = new Binance
{
    EnableRateLimit = true  // Recommended!
};

// Private API (with authentication)
var exchange = new Binance
{
    ApiKey = "YOUR_API_KEY",
    Secret = "YOUR_SECRET",
    EnableRateLimit = true
};

WebSocket API

using ccxt.pro;

// Public WebSocket
var exchange = new Binance();

// Private WebSocket (with authentication)
var exchange = new Binance
{
    ApiKey = "YOUR_API_KEY",
    Secret = "YOUR_SECRET"
};

// Always close when done
await exchange.Close();

Common REST Operations

Loading Markets

// Load all available trading pairs
await exchange.LoadMarkets();

// Access market information
var btcMarket = exchange.Market("BTC/USDT");
Console.WriteLine(btcMarket.Limits.Amount.Min);  // Minimum order amount

Fetching Ticker

// Single ticker
var ticker = await exchange.FetchTicker("BTC/USDT");
Console.WriteLine(ticker.Last);      // Last price
Console.WriteLine(ticker.Bid);       // Best bid
Console.WriteLine(ticker.Ask);       // Best ask
Console.WriteLine(ticker.Volume);    // 24h volume

// Multiple tickers (if supported)
var tickers = await exchange.FetchTickers(new[] { "BTC/USDT", "ETH/USDT" });

Fetching Order Book

// Full orderbook
var orderbook = await exchange.FetchOrderBook("BTC/USDT");
Console.WriteLine(orderbook.Bids[0]);  // [price, amount]
Console.WriteLine(orderbook.Asks[0]);  // [price, amount]

// Limited depth
var orderbook = await exchange.FetchOrderBook("BTC/USDT", 5);  // Top 5 levels

Creating Orders

Limit Order

// Buy limit order
var order = await exchange.CreateLimitBuyOrder("BTC/USDT", 0.01, 50000);
Console.WriteLine(order.Id);

// Sell limit order
var order = await exchange.CreateLimitSellOrder("BTC/USDT", 0.01, 60000);

// Generic limit order
var order = await exchange.CreateOrder("BTC/USDT", "limit", "buy", 0.01, 50000);

Market Order

// Buy market order
var order = await exchange.CreateMarketBuyOrder("BTC/USDT", 0.01);

// Sell market order
var order = await exchange.CreateMarketSellOrder("BTC/USDT", 0.01);

// Generic market order
var order = await exchange.CreateOrder("BTC/USDT", "market", "sell", 0.01);

Fetching Balance

var balance = await exchange.FetchBalance();
Console.WriteLine(balance["BTC"].Free);   // Available balance
Console.WriteLine(balance["BTC"].Used);   // Balance in orders
Console.WriteLine(balance["BTC"].Total);  // Total balance

Fetching Orders

// Open orders
var openOrders = await exchange.FetchOpenOrders("BTC/USDT");

// Closed orders
var closedOrders = await exchange.FetchClosedOrders("BTC/USDT");

// All orders (open + closed)
var allOrders = await exchange.FetchOrders("BTC/USDT");

// Single order by ID
var order = await exchange.FetchOrder(orderId, "BTC/USDT");

Fetching Trades

// Recent public trades
var trades = await exchange.FetchTrades("BTC/USDT", limit: 10);

// Your trades (requires authentication)
var myTrades = await exchange.FetchMyTrades("BTC/USDT");

Canceling Orders

// Cancel single order
await exchange.CancelOrder(orderId, "BTC/USDT");

// Cancel all orders for a symbol
await exchange.CancelAllOrders("BTC/USDT");

WebSocket Operations (Real-time)

Watching Ticker (Live Price Updates)

using ccxt.pro;

var exchange = new Binance();
while (true)
{
    var ticker = await exchange.WatchTicker("BTC/USDT");
    Console.WriteLine($"Last: {ticker.Last}");
}
await exchange.Close();

Watching Order Book (Live Depth Updates)

var exchange = new Binance();
while (true)
{
    var orderbook = await exchange.WatchOrderBook("BTC/USDT");
    Console.WriteLine($"Best bid: {orderbook.Bids[0][0]}");
    Console.WriteLine($"Best ask: {orderbook.Asks[0][0]}");
}
await exchange.Close();

Watching Trades (Live Trade Stream)

var exchange = new Binance();
while (true)
{
    var trades = await exchange.WatchTrades("BTC/USDT");
    foreach (var trade in trades)
    {
        Console.WriteLine($"{trade.Price} {trade.Amount} {trade.Side}");
    }
}
await exchange.Close();

Watching Your Orders (Live Order Updates)

var exchange = new Binance
{
    ApiKey = "YOUR_API_KEY",
    Secret = "YOUR_SECRET"
};

while (true)
{
    var orders = await exchange.WatchOrders("BTC/USDT");
    foreach (var order in orders)
    {
        Console.WriteLine($"{order.Id} {order.Status} {order.Filled}");
    }
}
await exchange.Close();

Watching Balance (Live Balance Updates)

var exchange = new Binance
{
    ApiKey = "YOUR_API_KEY",
    Secret = "YOUR_SECRET"
};

while (true)
{
    var balance = await exchange.WatchBalance();
    Console.WriteLine($"BTC: {balance["BTC"].Total}");
}
await exchange.Close();

Watching Multiple Symbols

var exchange = new Binance();
var symbols = new[] { "BTC/USDT", "ETH/USDT", "SOL/USDT" };

while (true)
{
    var tickers = await exchange.WatchTickers(symbols);
    foreach (var kvp in tickers)
    {
        Console.WriteLine($"{kvp.Key}: {kvp.Value.Last}");
    }
}
await exchange.Close();

Complete Method Reference

Market Data Methods

Tickers & Prices

  • fetchTicker(symbol) - Fetch ticker for one symbol
  • fetchTickers([symbols]) - Fetch multiple tickers at once
  • fetchBidsAsks([symbols]) - Fetch best bid/ask for multiple symbols
  • fetchLastPrices([symbols]) - Fetch last prices
  • fetchMarkPrices([symbols]) - Fetch mark prices (derivatives)

Order Books

  • fetchOrderBook(symbol, limit) - Fetch order book
  • fetchOrderBooks([symbols]) - Fetch multiple order books
  • fetchL2OrderBook(symbol) - Fetch level 2 order book
  • fetchL3OrderBook(symbol) - Fetch level 3 order book (if supported)

Trades

  • fetchTrades(symbol, since, limit) - Fetch public trades
  • fetchMyTrades(symbol, since, limit) - Fetch your trades (auth required)
  • fetchOrderTrades(orderId, symbol) - Fetch trades for specific order

OHLCV (Candlesticks)

  • fetchOHLCV(symbol, timeframe, since, limit) - Fetch candlestick data
  • fetchIndexOHLCV(symbol, timeframe) - Fetch index price OHLCV
  • fetchMarkOHLCV(symbol, timeframe) - Fetch mark price OHLCV
  • fetchPremiumIndexOHLCV(symbol, timeframe) - Fetch premium index OHLCV

Account & Balance

  • fetchBalance() - Fetch account balance (auth required)
  • fetchAccounts() - Fetch sub-accounts
  • fetchLedger(code, since, limit) - Fetch ledger history
  • fetchLedgerEntry(id, code) - Fetch specific ledger entry
  • fetchTransactions(code, since, limit) - Fetch transactions
  • fetchDeposits(code, since, limit) - Fetch deposit history
  • fetchWithdrawals(code, since, limit) - Fetch withdrawal history
  • fetchDepositsWithdrawals(code, since, limit) - Fetch both deposits and withdrawals

Trading Methods

Creating Orders

  • createOrder(symbol, type, side, amount, price, params) - Create order (generic)
  • createLimitOrder(symbol, side, amount, price) - Create limit order
  • createMarketOrder(symbol, side, amount) - Create market order
  • createLimitBuyOrder(symbol, amount, price) - Buy limit order
  • createLimitSellOrder(symbol, amount, price) - Sell limit order
  • createMarketBuyOrder(symbol, amount) - Buy market order
  • createMarketSellOrder(symbol, amount) - Sell market order
  • createMarketBuyOrderWithCost(symbol, cost) - Buy with specific cost
  • createStopLimitOrder(symbol, side, amount, price, stopPrice) - Stop-limit order
  • createStopMarketOrder(symbol, side, amount, stopPrice) - Stop-market order
  • createStopLossOrder(symbol, side, amount, stopPrice) - Stop-loss order
  • createTakeProfitOrder(symbol, side, amount, takeProfitPrice) - Take-profit order
  • createTrailingAmountOrder(symbol, side, amount, trailingAmount) - Trailing stop
  • createTrailingPercentOrder(symbol, side, amount, trailingPercent) - Trailing stop %
  • createTriggerOrder(symbol, side, amount, triggerPrice) - Trigger order
  • createPostOnlyOrder(symbol, side, amount, price) - Post-only order
  • createReduceOnlyOrder(symbol, side, amount, price) - Reduce-only order
  • createOrders([orders]) - Create multiple orders at once
  • createOrderWithTakeProfitAndStopLoss(symbol, type, side, amount, price, tpPrice, slPrice) - OCO order

Managing Orders

  • fetchOrder(orderId, symbol) - Fetch single order
  • fetchOrders(symbol, since, limit) - Fetch all orders
  • fetchOpenOrders(symbol, since, limit) - Fetch open orders
  • fetchClosedOrders(symbol, since, limit) - Fetch closed orders
  • fetchCanceledOrders(symbol, since, limit) - Fetch canceled orders
  • fetchOpenOrder(orderId, symbol) - Fetch specific open order
  • fetchOrdersByStatus(status, symbol) - Fetch orders by status
  • cancelOrder(orderId, symbol) - Cancel single order
  • cancelOrders([orderIds], symbol) - Cancel multiple orders
  • cancelAllOrders(symbol) - Cancel all orders for symbol
  • editOrder(orderId, symbol, type, side, amount, price) - Modify order

Margin & Leverage

  • fetchBorrowRate(code) - Fetch borrow rate for margin
  • fetchBorrowRates([codes]) - Fetch multiple borrow rates
  • fetchBorrowRateHistory(code, since, limit) - Historical borrow rates
  • fetchCrossBorrowRate(code) - Cross margin borrow rate
  • fetchIsolatedBorrowRate(symbol, code) - Isolated margin borrow rate
  • borrowMargin(code, amount, symbol) - Borrow margin
  • repayMargin(code, amount, symbol) - Repay margin
  • fetchLeverage(symbol) - Fetch leverage
  • setLeverage(leverage, symbol) - Set leverage
  • fetchLeverageTiers(symbols) - Fetch leverage tiers
  • fetchMarketLeverageTiers(symbol) - Leverage tiers for market
  • setMarginMode(marginMode, symbol) - Set margin mode (cross/isolated)
  • fetchMarginMode(symbol) - Fetch margin mode

Derivatives & Futures

Positions

  • fetchPosition(symbol) - Fetch single position
  • fetchPositions([symbols]) - Fetch all positions
  • fetchPositionsForSymbol(symbol) - Fetch positions for symbol
  • fetchPositionHistory(symbol, since, limit) - Position history
  • fetchPositionsHistory(symbols, since, limit) - Multiple position history
  • fetchPositionMode(symbol) - Fetch position mode (one-way/hedge)
  • setPositionMode(hedged, symbol) - Set position mode
  • closePosition(symbol, side) - Close position
  • closeAllPositions() - Close all positions

Funding & Settlement

  • fetchFundingRate(symbol) - Current funding rate
  • fetchFundingRates([symbols]) - Multiple funding rates
  • fetchFundingRateHistory(symbol, since, limit) - Funding rate history
  • fetchFundingHistory(symbol, since, limit) - Your funding payments
  • fetchFundingInterval(symbol) - Funding interval
  • fetchSettlementHistory(symbol, since, limit) - Settlement history
  • fetchMySettlementHistory(symbol, since, limit) - Your settlement history

Open Interest & Liquidations

  • fetchOpenInterest(symbol) - Open interest for symbol
  • fetchOpenInterests([symbols]) - Multiple open interests
  • fetchOpenInterestHistory(symbol, timeframe, since, limit) - OI history
  • fetchLiquidations(symbol, since, limit) - Public liquidations
  • fetchMyLiquidations(symbol, since, limit) - Your liquidations

Options

  • fetchOption(symbol) - Fetch option info
  • fetchOptionChain(code) - Fetch option chain
  • fetchGreeks(symbol) - Fetch option greeks
  • fetchVolatilityHistory(code, since, limit) - Volatility history
  • fetchUnderlyingAssets() - Fetch underlying assets

Fees & Limits

  • fetchTradingFee(symbol) - Trading fee for symbol
  • fetchTradingFees([symbols]) - Trading fees for multiple symbols
  • fetchTradingLimits([symbols]) - Trading limits
  • fetchTransactionFee(code) - Transaction/withdrawal fee
  • fetchTransactionFees([codes]) - Multiple transaction fees
  • fetchDepositWithdrawFee(code) - Deposit/withdrawal fee
  • fetchDepositWithdrawFees([codes]) - Multiple deposit/withdraw fees

Deposits & Withdrawals

  • fetchDepositAddress(code, params) - Get deposit address
  • fetchDepositAddresses([codes]) - Multiple deposit addresses
  • fetchDepositAddressesByNetwork(code) - Addresses by network
  • createDepositAddress(code, params) - Create new deposit address
  • fetchDeposit(id, code) - Fetch single deposit
  • fetchWithdrawal(id, code) - Fetch single withdrawal
  • fetchWithdrawAddresses(code) - Fetch withdrawal addresses
  • fetchWithdrawalWhitelist(code) - Fetch whitelist
  • withdraw(code, amount, address, tag, params) - Withdraw funds
  • deposit(code, amount, params) - Deposit funds (if supported)

Transfer & Convert

  • transfer(code, amount, fromAccount, toAccount) - Internal transfer
  • fetchTransfer(id, code) - Fetch transfer info
  • fetchTransfers(code, since, limit) - Fetch transfer history
  • fetchConvertCurrencies() - Currencies available for convert
  • fetchConvertQuote(fromCode, toCode, amount) - Get conversion quote
  • createConvertTrade(fromCode, toCode, amount) - Execute conversion
  • fetchConvertTrade(id) - Fetch convert trade
  • fetchConvertTradeHistory(code, since, limit) - Convert history

Market Info

  • fetchMarkets() - Fetch all markets
  • fetchCurrencies() - Fetch all currencies
  • fetchTime() - Fetch exchange server time
  • fetchStatus() - Fetch exchange status
  • fetchBorrowInterest(code, symbol, since, limit) - Borrow interest paid
  • fetchLongShortRatio(symbol, timeframe, since, limit) - Long/short ratio
  • fetchLongShortRatioHistory(symbol, timeframe, since, limit) - L/S ratio history

WebSocket Methods (ccxt.pro)

All REST methods have WebSocket equivalents with watch* prefix:

Real-time Market Data

  • watchTicker(symbol) - Watch single ticker
  • watchTickers([symbols]) - Watch multiple tickers
  • watchOrderBook(symbol) - Watch order book updates
  • watchOrderBookForSymbols([symbols]) - Watch multiple order books
  • watchTrades(symbol) - Watch public trades
  • watchOHLCV(symbol, timeframe) - Watch candlestick updates
  • watchBidsAsks([symbols]) - Watch best bid/ask

Real-time Account Data (Auth Required)

  • watchBalance() - Watch balance updates
  • watchOrders(symbol) - Watch your order updates
  • watchMyTrades(symbol) - Watch your trade updates
  • watchPositions([symbols]) - Watch position updates
  • watchPositionsForSymbol(symbol) - Watch positions for symbol

Authentication Required

Methods marked with 🔒 require API credentials:

  • All create* methods (creating orders, addresses)
  • All cancel* methods (canceling orders)
  • All edit* methods (modifying orders)
  • All fetchMy* methods (your trades, orders)
  • fetchBalance, fetchLedger, fetchAccounts
  • withdraw, transfer, deposit
  • Margin/leverage methods
  • Position methods
  • watchBalance, watchOrders, watchMyTrades, watchPositions

Checking Method Availability

Not all exchanges support all methods. Check before using:

// Check if method is supported
if (exchange.has['fetchOHLCV']) {
    const candles = await exchange.fetchOHLCV('BTC/USDT', '1h')
}

// Check multiple capabilities
console.log(exchange.has)
// {
//   fetchTicker: true,
//   fetchOHLCV: true,
//   fetchMyTrades: true,
//   fetchPositions: false,
//   ...
// }

Method Naming Convention

  • fetch* - REST API methods (HTTP requests)
  • watch* - WebSocket methods (real-time streams)
  • create* - Create new resources (orders, addresses)
  • cancel* - Cancel existing resources
  • edit* - Modify existing resources
  • set* - Configure settings (leverage, margin mode)
  • *Ws suffix - WebSocket variant (some exchanges)

Proxy Configuration

CCXT supports HTTP, HTTPS, and SOCKS proxies for both REST and WebSocket connections.

Setting Proxy

// HTTP Proxy
exchange.httpProxy = 'http://your-proxy-host:port'

// HTTPS Proxy  
exchange.httpsProxy = 'https://your-proxy-host:port'

// SOCKS Proxy
exchange.socksProxy = 'socks://your-proxy-host:port'

// Proxy with authentication
exchange.httpProxy = 'http://user:pass@proxy-host:port'

Proxy for WebSocket

WebSocket connections also respect proxy settings:

exchange.httpsProxy = 'https://proxy:8080'
// WebSocket connections will use this proxy

Testing Proxy Connection

exchange.httpProxy = 'http://localhost:8080'
try {
    await exchange.fetchTicker('BTC/USDT')
    console.log('Proxy working!')
} catch (error) {
    console.error('Proxy connection failed:', error)
}

WebSocket-Specific Methods

Some exchanges provide WebSocket variants of REST methods for faster order placement and management. These use the *Ws suffix:

Trading via WebSocket

Creating Orders:

  • createOrderWs - Create order via WebSocket (faster than REST)
  • createLimitOrderWs - Create limit order via WebSocket
  • createMarketOrderWs - Create market order via WebSocket
  • createLimitBuyOrderWs - Buy limit order via WebSocket
  • createLimitSellOrderWs - Sell limit order via WebSocket
  • createMarketBuyOrderWs - Buy market order via WebSocket
  • createMarketSellOrderWs - Sell market order via WebSocket
  • createStopLimitOrderWs - Stop-limit order via WebSocket
  • createStopMarketOrderWs - Stop-market order via WebSocket
  • createStopLossOrderWs - Stop-loss order via WebSocket
  • createTakeProfitOrderWs - Take-profit order via WebSocket
  • createTrailingAmountOrderWs - Trailing stop via WebSocket
  • createTrailingPercentOrderWs - Trailing stop % via WebSocket
  • createPostOnlyOrderWs - Post-only order via WebSocket
  • createReduceOnlyOrderWs - Reduce-only order via WebSocket

Managing Orders:

  • editOrderWs - Edit order via WebSocket
  • cancelOrderWs - Cancel order via WebSocket (faster than REST)
  • cancelOrdersWs - Cancel multiple orders via WebSocket
  • cancelAllOrdersWs - Cancel all orders via WebSocket

Fetching Data:

  • fetchOrderWs - Fetch order via WebSocket
  • fetchOrdersWs - Fetch orders via WebSocket
  • fetchOpenOrdersWs - Fetch open orders via WebSocket
  • fetchClosedOrdersWs - Fetch closed orders via WebSocket
  • fetchMyTradesWs - Fetch your trades via WebSocket
  • fetchBalanceWs - Fetch balance via WebSocket
  • fetchPositionWs - Fetch position via WebSocket
  • fetchPositionsWs - Fetch positions via WebSocket
  • fetchPositionsForSymbolWs - Fetch positions for symbol via WebSocket
  • fetchTradingFeesWs - Fetch trading fees via WebSocket

When to Use WebSocket Methods

Use *Ws methods when:

  • You need faster order placement (lower latency)
  • You're already connected via WebSocket
  • You want to reduce REST API rate limit usage
  • Trading strategies require sub-100ms latency

Use REST methods when:

  • You need guaranteed execution confirmation
  • You're making one-off requests
  • The exchange doesn't support the WebSocket variant
  • You need detailed error responses

Example: Order Placement Comparison

REST API (slower, more reliable):

const order = await exchange.createOrder('BTC/USDT', 'limit', 'buy', 0.01, 50000)

WebSocket API (faster, lower latency):

const order = await exchange.createOrderWs('BTC/USDT', 'limit', 'buy', 0.01, 50000)

Checking WebSocket Method Availability

Not all exchanges support WebSocket trading methods:

if (exchange.has['createOrderWs']) {
    // Exchange supports WebSocket order creation
    const order = await exchange.createOrderWs('BTC/USDT', 'limit', 'buy', 0.01, 50000)
} else {
    // Fall back to REST
    const order = await exchange.createOrder('BTC/USDT', 'limit', 'buy', 0.01, 50000)
}

Authentication

Setting API Keys

using System;

// During instantiation (recommended)
var exchange = new Binance
{
    ApiKey = Environment.GetEnvironmentVariable("BINANCE_API_KEY"),
    Secret = Environment.GetEnvironmentVariable("BINANCE_SECRET"),
    EnableRateLimit = true
};

// After instantiation
exchange.ApiKey = Environment.GetEnvironmentVariable("BINANCE_API_KEY");
exchange.Secret = Environment.GetEnvironmentVariable("BINANCE_SECRET");

Testing Authentication

try
{
    var balance = await exchange.FetchBalance();
    Console.WriteLine("Authentication successful!");
}
catch (AuthenticationError)
{
    Console.WriteLine("Invalid API credentials");
}

Error Handling

Exception Hierarchy

BaseError
├─ NetworkError (recoverable - retry)
│  ├─ RequestTimeout
│  ├─ ExchangeNotAvailable
│  ├─ RateLimitExceeded
│  └─ DDoSProtection
└─ ExchangeError (non-recoverable - don't retry)
   ├─ AuthenticationError
   ├─ InsufficientFunds
   ├─ InvalidOrder
   └─ NotSupported

Basic Error Handling

using ccxt;

try
{
    var ticker = await exchange.FetchTicker("BTC/USDT");
}
catch (NetworkError ex)
{
    Console.WriteLine($"Network error - retry: {ex.Message}");
}
catch (ExchangeError ex)
{
    Console.WriteLine($"Exchange error - do not retry: {ex.Message}");
}
catch (Exception ex)
{
    Console.WriteLine($"Unknown error: {ex.Message}");
}

Specific Exception Handling

try
{
    var order = await exchange.CreateOrder("BTC/USDT", "limit", "buy", 0.01, 50000);
}
catch (InsufficientFunds)
{
    Console.WriteLine("Not enough balance");
}
catch (InvalidOrder)
{
    Console.WriteLine("Invalid order parameters");
}
catch (RateLimitExceeded)
{
    Console.WriteLine("Rate limit hit - wait before retrying");
    await Task.Delay(1000);  // Wait 1 second
}
catch (AuthenticationError)
{
    Console.WriteLine("Check your API credentials");
}

Retry Logic for Network Errors

async Task<Ticker> FetchWithRetry(int maxRetries = 3)
{
    for (int i = 0; i < maxRetries; i++)
    {
        try
        {
            return await exchange.FetchTicker("BTC/USDT");
        }
        catch (NetworkError)
        {
            if (i < maxRetries - 1)
            {
                Console.WriteLine($"Retry {i + 1}/{maxRetries}");
                await Task.Delay(1000 * (i + 1));  // Exponential backoff
            }
            else
            {
                throw;
            }
        }
    }
    return null;
}

Rate Limiting

Built-in Rate Limiter (Recommended)

var exchange = new Binance
{
    EnableRateLimit = true  // Automatically throttles requests
};

Manual Delays

await exchange.FetchTicker("BTC/USDT");
await Task.Delay((int)exchange.RateLimit);  // Wait between requests
await exchange.FetchTicker("ETH/USDT");

Checking Rate Limit

Console.WriteLine(exchange.RateLimit);  // Milliseconds between requests

Common Pitfalls

Wrong Method Casing

// Wrong - lowercase (JavaScript style)
var ticker = await exchange.fetchTicker("BTC/USDT");  // ERROR!

// Correct - PascalCase (C# style)
var ticker = await exchange.FetchTicker("BTC/USDT");

Not Awaiting Async Methods

// Wrong - missing await
var ticker = exchange.FetchTicker("BTC/USDT");  // Returns Task, not Ticker!
Console.WriteLine(ticker.Last);  // ERROR!

// Correct
var ticker = await exchange.FetchTicker("BTC/USDT");
Console.WriteLine(ticker.Last);  // Works!

Using REST for Real-time Monitoring

// Wrong - wastes rate limits
while (true)
{
    var ticker = await exchange.FetchTicker("BTC/USDT");  // REST
    Console.WriteLine(ticker.Last);
    await Task.Delay(1000);
}

// Correct - use WebSocket
using ccxt.pro;
var exchange = new Binance();
while (true)
{
    var ticker = await exchange.WatchTicker("BTC/USDT");  // WebSocket
    Console.WriteLine(ticker.Last);
}

Not Closing WebSocket Connections

// Wrong - memory leak
var exchange = new ccxt.pro.Binance();
var ticker = await exchange.WatchTicker("BTC/USDT");
// Forgot to close!

// Correct
var exchange = new ccxt.pro.Binance();
try
{
    while (true)
    {
        var ticker = await exchange.WatchTicker("BTC/USDT");
        Console.WriteLine(ticker.Last);
    }
}
finally
{
    await exchange.Close();
}

Incorrect Symbol Format

// Wrong symbol formats
"BTCUSDT"    // Wrong - no separator
"BTC-USDT"   // Wrong - dash separator
"btc/usdt"   // Wrong - lowercase

// Correct symbol format
"BTC/USDT"   // Unified CCXT format

Troubleshooting

Common Issues

1. "Package CCXT.NET not found"

  • Solution: Run dotnet add package CCXT.NET

2. "RateLimitExceeded"

  • Solution: Set EnableRateLimit = true

3. "AuthenticationError"

  • Solution: Check API key and secret
  • Verify API key permissions on exchange
  • Check system clock is synced

4. "InvalidNonce"

  • Solution: Sync system clock
  • Use only one exchange instance per API key

5. "InsufficientFunds"

  • Solution: Check available balance (balance["BTC"].Free)
  • Account for trading fees

6. "ExchangeNotAvailable"

  • Solution: Check exchange status/maintenance
  • Retry after a delay

Debugging

// Enable verbose logging
exchange.Verbose = true;

// Check exchange capabilities
Console.WriteLine(exchange.Has);
// {
//   FetchTicker = true,
//   FetchOrderBook = true,
//   CreateOrder = true,
//   ...
// }

// Check market information
var market = exchange.Markets["BTC/USDT"];

// Check last request/response
Console.WriteLine(exchange.LastHttpResponse);
Console.WriteLine(exchange.LastJsonResponse);

Prediction Markets

CCXT supports prediction-market exchanges (Polymarket, Kalshi, Limitless, Myriad, Hyperliquid) under a dedicated ccxt.prediction namespace. They use the same unified API, but prices are quoted 0–1 (USDC per outcome share) and the tradeable unit is an outcome (e.g. a market's YES/NO token), not a regular market symbol.

using ccxt;

var exchange = new ccxt.prediction.polymarket();
await exchange.loadMarkets();
// discover events -> markets -> outcomes (each outcome has: outcome (handle),
// outcomeId, market, label); also fetchEvents / fetchEvent are available
var events = await exchange.fetchEvents(new Dictionary<string, object>() {{ "query", "Trump" }});
// an outcome handle looks like 'TRUMP_OUT_PRESIDENT_2027:YES'
string handle = "TRUMP_OUT_PRESIDENT_2027:YES";
var ticker = await exchange.fetchTicker(handle);
var book = await exchange.fetchOrderBook(handle);
// limit buy 5 YES shares @ 0.40 USDC (price is 0..1 per share)
var order = await exchange.createOrder(handle, "limit", "buy", 5, 0.40);
await exchange.cancelOrder(exchange.safeString(order, "id"), handle);
  • Price/trade methods (fetchTicker, fetchOrderBook, fetchOHLCV, fetchTrades, createOrder, cancelOrder, …) take an outcome handle or outcomeId (the outcome / outcomes parameter), not symbol.
  • Check support with exchange.has["prediction"]; discover markets via fetchEvents / fetchEvent (or loadMarkets).

Order Router

Two things, either usable without the other. A client for the CCXT order-router service — a separate process holding live books across many venues, which answers "what is the cheapest way to turn asset A into asset B right now?", including bridges (SOL -> USDT -> BTC when no SOL/BTC market exists). And an execution engine for plans you build yourself, which needs no router service and no API key. It is not an exchange: it does not derive from Exchange, has no unified methods, and is constructed directly.

using dict = System.Collections.Generic.Dictionary<string, object>;

var router = new ccxt.OrderRouter(new dict());

// exactly one of amountIn / amountOut
var route = await router.FetchRoute("USDT", "BTC", new dict() { { "amountIn", 1000 } });

// Execute takes the route directly: it builds the plan, loads each venue's markets and
// runs the safety check itself, refusing to place anything on a blocking violation
var venues = new Dictionary<string, Exchange>() { { "binance", binance }, { "kraken", kraken } };
var report = await router.Execute(route, venues, new dict() {
    { "strategy", "sequential" },
    { "usdRates", new dict() { { "USDT", 1 } } },
});
// want to see or change the plan first? the steps in between are public and PURE (no I/O):
// BuildExecutionPlan(route, new dict()) then CheckExecutionPlanSafety(plan, markets, new dict())

execute defaults to dry_run, and anything other than an explicit live flag forces dry_run regardless of the strategy requested — a call that looks live but forgot the flag places nothing.

The service, and what it costs

https://docs.ccxt.com/router/api. Every endpoint is public: there is no API key, no signup and no login. The service rate-limits by client IP address instead.

The client still accepts an apiKey and still sends it as x-api-key when you pass one, so a deployment that fronts the service with its own authentication keeps working. With no key the header is omitted entirely rather than sent empty.

The full contract is published as OpenAPI 3.1 at https://docs.ccxt.com/router/openapi.yaml. curl -O https://docs.ccxt.com/router/openapi.yaml and point codegen at it, import it into Postman/Insomnia, or diff it between deploys. It is the authority on every field this client reads; where the two disagree, the spec is right. Rendered prose version: /router/docs and /router/docs/api.

Free to use for now, up to the published rate limit — not a permanent commitment, so expect a paid tier eventually. Your existing key is how that would be billed; nothing in the client changes. Read the limit off the response headers (x-ratelimit-limit, x-ratelimit-remaining, x-ratelimit-reset) rather than hardcoding a number. A 429 raises RateLimitExceeded with the retry interval folded into the message.

A router that has restarted is alive long before it can price anything. Asked to route in that window it refuses with 503 cache_cold, and the client raises ExchangeNotAvailable — a retry, distinct from the ExchangeError that means something is actually wrong.

Holdings are POSTed, never put in a URL. FetchRoute normally sends a GET, but when you pass balances the client switches to POST /route and puts every parameter in the body: the service scrubs holdings from its own logs, but a reverse proxy, an ALB, a CDN, browser history and a Referer all see the full request line, and no in-process redaction reaches them. FetchRouteWithBalances does this for you.

Two flags the client verifies for you, because one silently lost in transit looks identical to one never sent. balances: the service ignores them entirely if it predates the feature and answers byte-identically, so FetchRoute throws unless the router echoes balancesApplied (or balanceEntryCount, which is how an empty wallet is confirmed) — pass requireBalancesApplied: false to opt out. requireFullFill: the one flag that fails open, so the client stamps what you asked for and the safety check makes partial_fill blocking when you asked for a full fill and did not get one.

An empty value is not an omitted one. Omit bridges and you get the default bridge set; send bridges= and you have asked for no bridging at all. Same for exchanges= (no venues) and balances= (you hold nothing). The client forwards an empty value rather than dropping it.

requestId is sent as the x-request-id header, so your log and the router's decision log can be joined; the service mints one when absent.

Asking the service about itself

MethodEndpointKey?Answers
FetchHealth()/healthnois the process alive — 200 from the first millisecond of boot
FetchReadiness()/readynocan it route yet: book counts, and how many are fresh
FetchVersion()/versionyeswhich commit is deployed
FetchSymbols()/symbolsyesthe symbols it holds a book for
FetchExchangesStatus()/exchanges/statusyesper-venue connection health
FetchCachedOrderBook(exchangeId, symbol)/orderbook/{exchange}/{symbol}yesthe exact book a route was ranked on

Gate deploys on readiness, not health — /health is 200 before a single websocket has connected. FetchReadiness() does not raise when the answer is no: the service replies 503 carrying the same body it returns on 200, and you need those counts to know why.

var readiness = await router.FetchReadiness();
if ((string)readiness["status"] != "ready")
{
    Console.WriteLine($"{readiness["freshCount"]} of {readiness["bookCount"]} books are fresh");
}

/metrics (Prometheus) has no client method — it answers text/plain and this class parses every response as JSON.

Watching a route. WatchRoute holds a WebSocket open and calls your hook with each RouteResult as the books move; return 'stop' to close cleanly and get the last route back. Every frame is stamped exactly as FetchRoute stamps its answer, so it can go straight into the plan builder. Three endpoint rules differ from FetchRoute: balances and balanceMode are refused (a socket outlives the holdings it was opened with — refused client-side, before anything opens), includeQuotes defaults to false, and refusals arrive as close codes rather than statuses — 1008 raises BadRequest and 1013 raises ExchangeNotAvailable, the same classes the REST path uses. A hook that throws stops the stream and reaches you, unlike execute's step hook.

Watching a run, and stopping it

execute is not opaque. options.onStep is called after each step completes AND after its reconciliation — never mid-order — and its return value decides whether the route continues.

var report = await router.Execute(plan, venues, new Dictionary<string, object> {
    { "strategy", "sequential" },
    { "retryFailedSteps", 2 },        // only a DEFINITIVELY REJECTED step is retried
    { "onStep", (Func<IDictionary<string, object>, string>)(ev =>
        (string)ev["status"] == "partial" ? "halt" : "") },   // "halt" stops the route
});

The event is a plain dictionary: planId, stepIndex, hopIndex, legIndex, exchangeId, symbol, side, status, requestedAmount, filledAmount, outAsset, outAmount, orderId, clientOrderId, errorCode, attempt, reconciliation, ordersPlaced, halted, haltReason, stepsTotal, stepsRemaining.

  • It can only narrow. 'halt' stops the route and sets haltReason to halted_by_on_step; nothing it returns resumes a route the reconciliation already halted.
  • It is called on the halt paths too, with an empty reconciliation, so it always learns how the route ended.
  • Do no network I/O in it — it sits between orders on the money path.
  • A hook that throws is recorded in report['errors'] as on_step_hook_failed and the run continues; losing the report would destroy the only account of orders already live.

For decisions that need I/O, slice the plan and call execute per hop with its own idempotencyKey instead.

options.retryFailedSteps (default 0, retryDelayMs default 1000) re-places a step the venue definitively rejected. An outcome_unknown step is never retried at any setting: it may already be a live position, and re-placing it is the double-fill this class exists to prevent. The winning attempt is reported as attempt. The router sets no client order id of its own — venues disagree on length and charset, so whatever you pass in orderParams travels untouched.

Executing your own plans

execute takes a plan, not a route, and never checks where the plan came from — so your own strategy can supply its own trades and still get the notional cap, halt-and-reconcile between hops, resting-order cleanup and the unwind plan. A plan that has been through JSON or a database, or a hand-rebuilt tail of a halted route, is equally valid.

A step is one order on one venue. Required: exchangeId, symbol, side, amount, base, quote. Optional: stepIndex (defaults to position), hopIndex/legIndex (steps sharing a hopIndex are one hop — what parallel_within_hop parallelises), expectedPrice, limitPrice, notionalQuote.

var plan = new Dictionary<string, object> {
    { "requestId", "my-strategy-0001" },        // identity; a live run refuses without one
    { "calculatedAt", exchange.Milliseconds() },
    { "steps", new List<object> {
        new Dictionary<string, object> {
            { "exchangeId", "binance" }, { "symbol", "BTC/USDT" }, { "side", "buy" },
            { "amount", 0.01 }, { "base", "BTC" }, { "quote", "USDT" },
            { "hopIndex", 0 }, { "expectedPrice", 64000 },
        },
    } },
};
var report = await router.Execute(plan, venues, new Dictionary<string, object> {
    { "strategy", "sequential" }, { "maxNotionalUsd", 25 },
});

A live execute requires an identity and refuses without one: the identity is remembered in-process so a second execute of the same plan is refused before any venue is contacted. Supply it as the plan's requestId or as the options' idempotencyKey. execute never sets a clientOrderId of its own: each exchange's CreateOrder keeps sending whatever identifier it generates internally, and a clientOrderId you put in the options' orderParams travels untouched (to every step alike).

Make it stable and tied to the intent (a strategy name plus the signal's timestamp). A fresh value per call — a wall-clock timestamp and friends — turns the guard off while looking like it is on. To re-run deliberately, pass allowReexecution. The guard does not survive a restart.

checkExecutionPlanSafety is worth running on a hand-written plan first: it checks every step against that venue's real market rules — minimum amount, minimum cost, precision — which is where a hand-picked amount usually goes wrong.

Strategies: dry_run (default), sequential, parallel_within_hop (concurrent across venues, serialised within a venue), limit_protected (rests a limit order and cancels it at orderTimeoutMs, polling every pollIntervalMs), atomic_ish (requires the route pre-funded), best_effort (single-hop, never halts).

Every report carries planAgeMs — how old the plan's prices were when execute was called (-1 when the route carried no calculatedAt, which means unknown, not fresh). Pass options.maxPlanAgeMs to refuse a live execution of a plan older than that; there is no default limit, and under an active limit a plan whose age cannot be determined is refused too.

There is no notional cap by default — trade cents or trade thousands. maxNotionalUsd is an opt-in guardrail: pass it to the constructor, or per call in the options, and it is honoured exactly at whatever value you choose, in either direction; omit it (or pass 0) and no notional check runs. Only a negative value is refused.

A market order cannot be placed under a cap: the cap is checked against the plan's limit price and a market order carries no price at all, so asking for allowMarketOrders together with a cap is refused rather than silently unbounded.

When a cap IS set it is enforced immediately before every order — not just at plan time, because a reconciliation may have resized the plan since — and a step that cannot be valued in USD blocks, so supply a USD rate for every quote asset in the plan. With no cap set there is nothing to evaluate and USD rates are not required either.

In the report, status: 'outcome_unknown' means the request may or may not have reached the venue — execution halts rather than reconciling, because reconciling would read the fill as 0 and report "nothing filled", asserting the one thing nobody knows. Check the open orders and the venue before retrying. placementAttempted is false until an order was actually dispatched.

buildExecutionPlan refuses a route that does not run from the asset you offered to the asset you wanted, or whose hops do not connect — the answer is checked against the client's own record of the question, so a compromised or buggy router response cannot steer orders into another market.

Full reference: Order Router in the CCXT Manual.

Learn More

发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

.claude/skills/ccxt-csharp

默认分支

master

最新提交

1259174

Tree SHA

663c755