@solncebro/exchange-engine
v0.13.0
Published
Universal TypeScript client library for Binance and Bybit with unified API, type safety, and WebSocket support
Downloads
1,130
Maintainers
Readme
@solncebro/exchange-engine
Universal TypeScript client library for cryptocurrency trading on Binance and Bybit with unified API, WebSocket support, and native type safety.
Latest Release
Current version: 0.13.0
- New
OrderTypeEnumvalues:StopLimit,TakeProfitLimitfor atomic STOP_LOSS_LIMIT / TAKE_PROFIT_LIMIT on Binance Spot and Bybit Spot. - New enums:
TriggerByEnum(MarkPrice|LastPrice|IndexPrice),OrderFilterEnum(Order|tpslOrder|StopOrder),MarketUnitEnum(baseCoin|quoteCoin). CreateOrderWebSocketArgsextended withtriggerBy,closeOnTrigger,orderFilter,marketUnit,trailingDelta,quoteOrderQty.- Spot/futures order param mappings split: separate
BINANCE_FUTURES_ORDER_TYPE_REVERSE/BINANCE_SPOT_ORDER_TYPE_REVERSE. On spot,closePosition/workingType/positionSide/reduceOnlyand BybittriggerDirection/triggerBy/closeOnTriggerare no longer set. BybitLinear.fetchPositionMode()implemented viaGET /v5/position/list?category=linear&settleCoin=USDT(Hedge if any positionIdx ∈ {1,2}; OneWay if all 0;undefinedif no positions).- BREAKING:
ExchangeClient.fetchPositionMode()now returnsPromise<PositionModeEnum | undefined>. awaitWebSocketConnectionsReady()onExchangeClient; Binance Futures public stream uses/market/wswith batchedSUBSCRIBE, stale reconnect, and optionalmessageCount/lastMessageTimestamponWebSocketConnectionInfo.- New utility
formatWebSocketConnectionsReport. - Bybit instruments with
status === 'PreLaunch'are normalized as active (isActive: true).
Full release notes: CHANGELOG.md
Features
- 🔀 Single API for multiple exchanges — same code works with Binance or Bybit
- 🎯 Type-safe unified types — all responses normalized to consistent types (Kline, Ticker, Position, etc.)
- 📊 REST & WebSocket support — fetch historical data and subscribe to real-time streams
- 🔄 Automatic reconnection — resilient WebSocket connections with exponential backoff
- 📝 Comprehensive logging — built-in structured logging via custom logger interface
- 🚀 Zero dependencies — only axios and websocket-engine
Installation
yarn add @solncebro/exchange-engineQuick Start
import { Exchange } from '@solncebro/exchange-engine';
import { pinoLogger } from './logger'; // your logger instance
// Create exchange instance (works identically for 'binance' or 'bybit')
const exchange = new Exchange('binance', {
config: { apiKey: process.env.API_KEY, secret: process.env.API_SECRET },
logger: pinoLogger,
onNotify: (msg) => telegramBot.send(msg), // optional notifications
});
// Load trade symbols (markets)
await exchange.futures.loadTradeSymbols();
// Fetch historical klines
const klines = await exchange.futures.fetchKlines('BTCUSDT', '1h', { limit: 100 });
console.log(klines[0]); // { openTimestamp, openPrice, highPrice, lowPrice, closePrice, volume, ... }
// Get current tickers
const tickers = await exchange.futures.fetchTickers();
// Subscribe to real-time klines
exchange.futures.subscribeKlines({
symbol: 'BTCUSDT',
interval: '1m',
handler: (kline) => {
console.log(`[${kline.openTimestamp}] ${kline.closePrice}`);
},
});
// Create an order via WebSocket
const order = await exchange.futures.createOrderWebSocket({
symbol: 'BTCUSDT',
type: 'market',
side: 'buy',
amount: 0.01,
price: 0, // ignored for market orders
});
// Fetch position info
const position = await exchange.futures.fetchPosition('BTCUSDT');
console.log(`Leverage: ${position.leverage}, Contracts: ${position.contracts}`);
// Set leverage
await exchange.futures.setLeverage(10, 'BTCUSDT');
// Close connection
await exchange.close();API Reference
Exchange (Main Entry Point)
const exchange = new Exchange('binance' | 'bybit', {
config: { apiKey: string; secret: string; recvWindow?: number };
logger: ExchangeLogger;
onNotify?: (message: string) => void | Promise<void>;
});
// Access exchange clients
exchange.futures // BinanceFutures | BybitLinear
exchange.spot // BinanceSpot | BybitSpot
// Cleanup
await exchange.close();ExchangeClient Interface
All four classes (BinanceFutures, BinanceSpot, BybitLinear, BybitSpot) implement this interface:
Market Data (REST)
// Load and cache trade symbols (markets)
await client.loadTradeSymbols(): Promise<TradeSymbolBySymbol>;
// Fetch current ticker prices
await client.fetchTickers(): Promise<TickerBySymbol>;
// Fetch historical candlestick data
await client.fetchKlines(
symbol: string,
interval: KlineInterval,
options?: FetchPageWithLimitArgs
): Promise<Kline[]>;
// Get account balance
await client.fetchBalances(): Promise<AccountBalances>;Trading (REST + WebSocket)
// Create order via WebSocket
await client.createOrderWebSocket({
symbol: string;
type: 'market' | 'limit';
side: 'buy' | 'sell';
amount: number;
price: number;
}): Promise<Order>;
// Cancel an order
await client.cancelOrder(symbol: string, orderId: string): Promise<Order>;
// Fetch order history
await client.fetchOrderHistory(symbol: string, options?: FetchPageWithLimitArgs): Promise<Order[]>;
// Fetch open orders
await client.fetchOpenOrders(symbol?: string): Promise<Order[]>;Futures-Specific
// Fetch position details
await client.fetchPosition(symbol: string): Promise<Position>;
// Set leverage (Binance: 2-125x, Bybit: 1-99.5x)
await client.setLeverage(leverage: number, symbol: string): Promise<void>;
// Set margin mode
await client.setMarginMode(marginMode: 'isolated' | 'cross', symbol: string): Promise<void>;Real-Time Data (WebSocket)
// Subscribe to kline updates
client.subscribeKlines({
symbol: string;
interval: KlineInterval;
handler: (kline: Kline) => void;
}): void;
// Unsubscribe
client.unsubscribeKlines({ symbol, interval, handler }): void;Precision
// Format amount to exchange precision
const formatted = client.amountToPrecision('BTCUSDT', 0.12345);
// Format price to exchange precision
const formatted = client.priceToPrecision('BTCUSDT', 65432.1);Unified Types
All types are normalized across exchanges. No raw exchange formats leak out.
// Candlestick
interface Kline {
openTimestamp: number;
openPrice: number;
highPrice: number;
lowPrice: number;
closePrice: number;
volume: number;
closeTimestamp: number;
quoteAssetVolume: number;
numberOfTrades: number;
isClosed?: boolean;
}
// Current price
interface Ticker {
symbol: string;
lastPrice: number;
priceChangePercent: number; // 24h change %
timestamp: number;
}
// Trade symbol (market metadata)
interface TradeSymbol {
symbol: string;
baseAsset: string;
quoteAsset: string;
settle: string;
isActive: boolean;
type: 'spot' | 'swap' | 'future';
isLinear: boolean;
contractSize: number;
contractType: string;
filter: TradeSymbolFilter;
}
// Open position (futures)
interface Position {
symbol: string;
side: 'long' | 'short' | 'both';
contracts: number;
entryPrice: number;
markPrice: number;
unrealizedPnl: number;
leverage: number;
marginMode: 'isolated' | 'cross';
liquidationPrice: number;
info: Record<string, unknown>; // raw exchange data
}
// Placed order
interface Order {
id: string;
clientOrderId: string;
symbol: string;
side: 'Buy' | 'Sell';
type: 'Market' | 'Limit' | 'StopMarket' | 'TakeProfit' | 'TrailingStop';
amount: number;
price: number;
filledAmount: number;
status: 'open' | 'closed' | 'canceled' | 'rejected';
timestamp: number;
}
// Account balance
interface Balance {
asset: string;
free: number;
locked: number;
total: number;
}
// Account balances
interface AccountBalances {
balanceByAsset: BalanceByAsset; // Map<string, Balance>
totalWalletBalance: number;
totalAvailableBalance: number;
}Logger Interface
Provide any logger that implements this interface:
interface ExchangeLogger {
debug(message: string): void;
info(message: string): void;
warn(message: string): void;
error(message: string): void;
fatal(message: string): void;
}Example with Pino
import pino from 'pino';
const logger = pino({
level: 'info',
transport: {
target: 'pino-pretty',
options: { colorize: true },
},
});
const exchange = new Exchange('binance', {
config: { apiKey, secret },
logger, // pino instance is compatible
});Exchange Differences
API Keys & Permissions
- Binance: Read, Trade, Withdraw permissions (for different features)
- Bybit: Single API key handles all
Order Placement
- Binance:
createOrderWebSocket()prefers WebSocket with REST fallback - Bybit:
createOrderWebSocket()uses dedicated trade WebSocket stream
Position Modes
- Binance: Supports Hedge Mode (separate long/short) and One-Way Mode
- Bybit: Always supports both buy and sell sides simultaneously
Funding Rates
- Binance: 8 times per day at fixed UTC times
- Bybit: Hourly funding
These differences are transparent — the same code works for both.
Performance Tips
Batch requests — use
Promise.all()for multiple operationsconst [tickers, position, balance] = await Promise.all([ client.fetchTickers(), client.fetchPosition('BTCUSDT'), client.fetchBalances(), ]);Reuse trade symbols — call
loadTradeSymbols()once at startupconst tradeSymbols = await client.loadTradeSymbols(); const symbols = [...tradeSymbols.keys()];Limit historical data — fetch only needed range
const klines = await client.fetchKlines('BTCUSDT', '1h', { limit: 100, startTime: Date.now() - 100 * 60 * 60 * 1000, // last 100 hours });Subscribe instead of polling — WebSocket is more efficient
// Instead of: setInterval(() => fetchTickers(), 5000); // Use: client.subscribeKlines({ symbol, interval, handler });
Error Handling
Exchange-specific errors are thrown as ExchangeError with structured code and exchange fields:
Binance futures returns no-op validation responses for unchanged settings. Codes -4059 (No need to change position side.) and -4046 (No need to change margin type.) are handled as successful no-op operations in setPositionMode() and setMarginMode().
import { ExchangeError } from '@solncebro/exchange-engine';
try {
await exchange.futures.setLeverage(100, 'BTCUSDT');
} catch (error) {
if (error instanceof ExchangeError) {
console.error(`[${error.exchange}] Error ${error.code}: ${error.message}`);
}
}Extending the Library
Adding new endpoints follows a standard pattern:
- HTTP Client → add method to
BinanceFuturesHttpClientorBybitHttpClient - Normalizer → add raw type + normalization function
- Interface → add method to
ExchangeClient - Implementation → implement in all 4 exchange classes
Example: adding fetchOpenInterest(symbol)
// 1. In BinanceFuturesHttpClient
private async fetchOpenInterestRaw(symbol: string): Promise<BinanceRawOpenInterest> {
return this.get('/fapi/v1/openInterest', { symbol });
}
// 2. In binanceNormalizer.ts
export function normalizeOpenInterest(raw: BinanceRawOpenInterest): OpenInterest {
return { symbol: raw.symbol, openInterest: parseFloat(raw.openInterest) };
}
// 3. In ExchangeClient interface
fetchOpenInterest(symbol: string): Promise<OpenInterest>;
// 4. In BinanceFutures
async fetchOpenInterest(symbol: string): Promise<OpenInterest> {
const raw = await this.httpClient.fetchOpenInterestRaw(symbol);
return normalizeOpenInterest(raw);
}License
MIT
Support
- GitHub Issues: solncebro/exchange-engine
- Documentation: See inline JSDoc comments
- Examples: Check
examples/directory
