coindcx-client
Production-grade Ruby SDK for the CoinDCX exchange. Full REST + real-time WebSocket coverage with built-in safety defaults for trading systems.
Why this gem?
| Feature | coindcx-client |
|---|---|
| All official REST namespaces | ✓ public, spot, margin, futures, funding, transfers, user |
| Idempotency enforcement | ✓ client_order_id required on all create paths |
| Per-endpoint rate limiting | ✓ 60+ buckets with token-bucket algorithm |
| Circuit breaker | ✓ fail-fast on repeated order failures |
| Exponential-backoff retries | ✓ respects Retry-After headers |
| WebSocket with auto-reconnect | ✓ subscription replay, heartbeat liveness |
| Structured error hierarchy | ✓ 10+ classes, all with retryable flag |
| Typed models | ✓ Order, Trade, Market, Position, Instrument, Balance, Candle |
| Validated keyword APIs | ✓ required params enforced at call site |
Quick start (5 minutes)
1. Add to Gemfile
gem 'coindcx-client', '~> 1.0'2. Configure
require 'coindcx'
CoinDCX.configure do |config|
config.api_key = ENV.fetch('COINDCX_API_KEY')
config.api_secret = ENV.fetch('COINDCX_API_SECRET')
config.logger = Logger.new($stdout)
end3. Call a public endpoint
client = CoinDCX.client
client.public.market_data.list_tickers.first4. Place an order with idempotency
client.spot.orders.create(
side: 'buy',
order_type: 'limit_order',
market: 'SNTBTC',
price_per_unit: '0.03244',
total_quantity: 400,
client_order_id: CoinDCX.generate_client_order_id # always persist this first
)5. Subscribe to live prices
ws = client.ws
channel = CoinDCX::WS::PublicChannels.price_stats(pair: 'B-BTC_USDT')
ws.connect
ws.subscribe_public(channel_name: channel, event_name: 'price-change') do |payload|
puts "#{payload['s']}: #{payload['p']}"
end
sleep # keep the process aliveFull API reference
Public market data
pub = client.public.market_data
pub.list_tickers # → [Market, ...]
pub.list_markets # → ["SNTBTC", ...]
pub.list_market_details # → [Market, ...]
pub.list_trades(pair: 'B-BTC_USDT', limit: 50) # → [Trade, ...]
pub.fetch_order_book(pair: 'B-BTC_USDT') # → { "bids" => ..., "asks" => ... }
pub.list_candles(
pair: 'B-BTC_USDT',
interval: '1h', # 1m 5m 15m 30m 1h 2h 4h 6h 8h 1d 3d 1w 1M
start_time: 1_700_000_000_000,
end_time: 1_700_100_000_000,
limit: 500
) # → [Candle, ...]User account
user = client.user.accounts
user.list_balances # → [Balance, ...]
user.fetch_info # → { "coindcx_id" => ..., "email" => ... }Spot orders
All methods require client_order_id for create operations. The transport rejects calls without it.
spot = client.spot.orders
# Create
spot.create(
side: 'buy', order_type: 'limit_order', market: 'SNTBTC',
price_per_unit: '0.03244', total_quantity: 400,
client_order_id: CoinDCX.generate_client_order_id
) # → Order
# Create multiple (INR markets only; exchange restriction)
spot.create_many(orders: [...]) # → [Order, ...]
# Query
spot.fetch_status(id: 'order-id') # → Order
spot.fetch_status(client_order_id: 'my-ref') # → Order
spot.fetch_statuses(ids: ['id-1', 'id-2']) # → [Order, ...]
spot.fetch_statuses(client_order_ids: [...]) # → [Order, ...]
# Active orders — market: is required by the exchange
spot.list_active(market: 'SNTBTC') # → [Order, ...]
spot.list_active(market: 'SNTBTC', side: 'buy')
spot.count_active(market: 'SNTBTC') # → { "count" => 3 }
# Trade history — all filters optional
spot.list_trade_history # → [Trade, ...]
spot.list_trade_history(
limit: 200,
sort: 'desc', # 'asc' | 'desc'
symbol: 'SNTBTC',
from_timestamp: 1_700_000_000_000,
to_timestamp: 1_700_100_000_000
)
# Cancel
spot.cancel(id: 'order-id') # → Order
spot.cancel(client_order_id: 'my-ref') # → Order
spot.cancel_many(ids: ['id-1', 'id-2']) # → [Order, ...]
spot.cancel_all(market: 'SNTBTC') # cancel all open; market: required
spot.cancel_all(market: 'SNTBTC', side: 'buy')
# Edit price (limit orders only)
spot.edit_price(id: 'order-id', price_per_unit: '0.035') # → OrderMargin orders
margin = client.margin.orders
margin.create(
side: 'buy', order_type: 'limit_order', market: 'SNTBTC',
quantity: 400, price_per_unit: '0.03244',
client_order_id: CoinDCX.generate_client_order_id
) # → Order
margin.list # → [Order, ...]
margin.fetch(id: 'order-id') # → Order
margin.cancel(id: 'order-id')
margin.cancel(client_order_id: 'my-ref')
margin.exit_order(id: 'order-id')
margin.edit_target(id: 'order-id', target_price: '0.04')
margin.edit_stop_loss(id: 'order-id', sl_price: '0.03')
margin.edit_trailing_stop_loss(id: 'order-id', sl_price: '0.03')
margin.edit_target_order_price(id: 'order-id', price_per_unit: '0.039')
margin.add_margin(id: 'order-id', amount: 100)
margin.remove_margin(id: 'order-id', amount: 50)Futures
Market data (public)
fmd = client.futures.market_data
fmd.list_active_instruments(margin_currency_short_names: ['USDT']) # → [Hash, ...]
fmd.fetch_instrument(pair: 'B-BTC_USDT', margin_currency_short_name: 'USDT') # → Instrument
fmd.list_trades(pair: 'B-BTC_USDT') # → [Trade, ...]
fmd.fetch_order_book(instrument: 'BTCUSDT', depth: 20) # depth: 10 | 20 | 50
fmd.list_candlesticks(pair: 'B-BTC_USDT', from: ts, to: ts, resolution: '1')
fmd.current_prices # → Hash
fmd.stats(pair: 'B-BTC_USDT') # → Hash
fmd.conversions # → HashOrders
fo = client.futures.orders
fo.list(status: 'open', margin_currency_short_name: ['USDT']) # → [Order, ...]
fo.list_trades(page: 1, size: 50) # → [Trade, ...]
fo.create(order: {
side: 'buy', pair: 'B-BTC_USDT', size: 1,
order_type: 'limit_order', price: '50000',
client_order_id: CoinDCX.generate_client_order_id
}) # → Order
fo.cancel(id: 'order-id')
fo.cancel(client_order_id: 'my-ref')
fo.edit(id: 'order-id', price: '51000') # → OrderPositions
fp = client.futures.positions
fp.list # → [Position, ...]
fp.update_leverage(id: 'pos-id', leverage: 10)
fp.add_margin(id: 'pos-id', amount: 100)
fp.remove_margin(id: 'pos-id', amount: 50)
fp.cancel_all_open_orders(id: 'pos-id')
fp.cancel_all_open_orders_for_position(id: 'pos-id')
fp.exit_position(id: 'pos-id')
fp.create_take_profit_stop_loss(id: 'pos-id', target_price: '55000', sl_price: '48000')
fp.list_transactions # → [Hash, ...]
fp.fetch_cross_margin_details # → Hash
fp.update_margin_type(id: 'pos-id', margin_type: 'isolated')Wallets
fw = client.futures.wallets
fw.transfer(type: 'in', amount: 100, currency: 'USDT')
fw.fetch_details # → Hash
fw.list_transactions(page: 1, size: 50) # → [Hash, ...]Lending / Funding
funding = client.funding.orders
funding.list # → [Hash, ...]
funding.lend(currency_short_name: 'USDT', amount: '100', duration: 7)
funding.settle(id: 'funding-order-id')Wallet transfers
wallets = client.transfers.wallets
# Move between spot and futures wallets
wallets.transfer(
source_wallet_type: 'spot',
destination_wallet_type: 'futures',
currency_short_name: 'USDT',
amount: 100
)
# Sub-account transfer (API keys created after 2024-08-12 only)
wallets.sub_account_transfer(
from_account_id: 'acct-a',
to_account_id: 'acct-b',
currency_short_name: 'USDT',
amount: 50
)WebSocket
ws = client.ws
ws.connect
# Public streams
channel = CoinDCX::WS::PublicChannels.price_stats(pair: 'B-BTC_USDT')
ws.subscribe_public(channel_name: channel, event_name: CoinDCX::WS::PublicChannels::PRICE_CHANGE_EVENT) do |payload|
puts payload
end
# Order book (depth: 10, 20, or 50)
ob = CoinDCX::WS::PublicChannels.order_book('B-BTC_USDT', depth: 20)
ws.subscribe_public(channel_name: ob, event_name: CoinDCX::WS::PublicChannels::DEPTH_SNAPSHOT_EVENT) { |snap| ... }
ws.subscribe_public(channel_name: ob, event_name: CoinDCX::WS::PublicChannels::DEPTH_UPDATE_EVENT) { |delta| ... }
# Candlesticks
candle_ch = CoinDCX::WS::PublicChannels.candlestick('B-BTC_USDT', '1m')
ws.subscribe_public(channel_name: candle_ch, event_name: CoinDCX::WS::PublicChannels::CANDLESTICK_EVENT) { |c| ... }
# New trades
trades_ch = CoinDCX::WS::PublicChannels.new_trade('B-BTC_USDT')
ws.subscribe_public(channel_name: trades_ch, event_name: CoinDCX::WS::PublicChannels::NEW_TRADE_EVENT) { |t| ... }
# Private streams (requires api_key + api_secret)
ws.subscribe_private(event_name: CoinDCX::WS::PrivateChannels::ORDER_UPDATE_EVENT) { |e| ... }
ws.subscribe_private(event_name: CoinDCX::WS::PrivateChannels::TRADE_UPDATE_EVENT) { |e| ... }
ws.subscribe_private(event_name: CoinDCX::WS::PrivateChannels::BALANCE_UPDATE_EVENT) { |e| ... }
# Futures private streams
ws.subscribe_private(event_name: CoinDCX::WS::PrivateChannels::DF_ORDER_UPDATE_EVENT) { |e| ... }
ws.subscribe_private(event_name: CoinDCX::WS::PrivateChannels::DF_POSITION_UPDATE_EVENT) { |e| ... }
# Futures public streams
fi_ch = CoinDCX::WS::PublicChannels.futures_candlestick('BTCUSDT', '1m')
ws.subscribe_public(channel_name: fi_ch, event_name: CoinDCX::WS::PublicChannels::CANDLESTICK_EVENT) { |c| ... }
# Liveness check
ws.alive? # → true / false
ws.disconnectWebSocket guarantees:
- Auto-reconnect with exponential backoff (configurable attempts + interval)
- Private auth re-sent on every reconnect
- All subscriptions replayed after reconnect
- Heartbeat liveness timeout raises
SocketHeartbeatTimeoutError
Error handling
Every error class exposes .status, .category, .code, .request_context, .retryable.
begin
client.spot.orders.create(...)
rescue CoinDCX::Errors::ValidationError => e
# client_order_id missing, bad side/market/qty
rescue CoinDCX::Errors::AuthError => e
# 401 — bad key/secret
rescue CoinDCX::Errors::RetryableRateLimitError => e
# 429 with Retry-After — SDK already backed off; this means budget exhausted
rescue CoinDCX::Errors::RateLimitError => e
# 429 without Retry-After
rescue CoinDCX::Errors::CircuitOpenError => e
# repeated create failures — circuit is open; pause and investigate
rescue CoinDCX::Errors::RemoteValidationError => e
# 400/422 — exchange rejected payload
rescue CoinDCX::Errors::UpstreamServerError => e
# 5xx from exchange
rescue CoinDCX::Errors::TransportError => e
# network-level failure
endSocket errors:
rescue CoinDCX::Errors::SocketConnectionError
rescue CoinDCX::Errors::SocketAuthenticationError
rescue CoinDCX::Errors::SocketHeartbeatTimeoutErrorConfiguration reference
CoinDCX.configure do |c|
# Credentials (required for private endpoints)
c.api_key = ENV.fetch('COINDCX_API_KEY')
c.api_secret = ENV.fetch('COINDCX_API_SECRET')
# HTTP
c.open_timeout = 5 # seconds
c.read_timeout = 30 # seconds
# Retry
c.max_retries = 2
c.retry_base_interval = 0.25 # seconds (exponential backoff base)
# Retry budgets per request class
c.market_data_retry_budget = 2 # public GETs
c.private_read_retry_budget = 1 # authenticated reads
c.idempotent_order_retry_budget = 1 # create with client_order_id
# Circuit breaker (order create paths)
c.circuit_breaker_threshold = 3 # failures before opening
c.circuit_breaker_cooldown = 30.0 # seconds
# WebSocket
c.socket_base_url = 'wss://stream.coindcx.com'
c.socket_reconnect_attempts = 5
c.socket_reconnect_interval = 1.0 # seconds
c.socket_heartbeat_interval = 10.0 # seconds
c.socket_liveness_timeout = 60.0 # seconds (raises SocketHeartbeatTimeoutError)
# Logging
c.logger = Logger.new($stdout)
endTrading safety rules
-
Persist
client_order_idbefore API submission. On timeout, reconcile using your persisted key viafetch_status. -
Never reuse a
client_order_idfor a different order. Generate withCoinDCX.generate_client_order_id. - WebSocket delivery is at-least-once across reconnects. Dedupe order/trade events in your app layer.
- After reconnects or restarts, reconcile missed events via REST snapshots before resuming.
- Map each error class to explicit policy — retry, abort, or operator alert. Avoid string-matching error bodies.
Documentation
- Docs index
- Core usage
- Configuration reference
- Use-case playbook
- Rails integration
- Standalone trading bot
Requirements
- Ruby >= 3.2
faraday ~> 2.14socket.io-client-simple ~> 1.2
License
MIT