Universal TypeScript client library for cryptocurrency trading on Binance and Bybit with unified API, WebSocket support, and native type safety.
- 🔀 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
npm install @solncebro/exchange-engine
# or
yarn add @solncebro/exchange-engineimport { 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 markets
await exchange.futures.loadMarkets();
// Fetch historical klines
const klines = await exchange.futures.fetchKlines('BTCUSDT', '1h', { limit: 100 });
console.log(klines[0]); // { openTimestamp, open, high, low, close, 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.close}`);
},
});
// Create an order
const order = await exchange.futures.createOrderWs({
symbol: 'BTCUSDT',
type: 'market',
side: 'buy',
amount: 0.01,
price: 0, // ignored for market orders
params: {}, // exchange-specific params
});
// 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();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();All four classes (BinanceFutures, BinanceSpot, BybitLinear, BybitSpot) implement this interface:
// Load and cache market information
await client.loadMarkets(reload?: boolean): Promise<MarketBySymbol>;
// Get all markets (already loaded)
const markets = client.markets; // Map<string, Market>
// Fetch current ticker prices
await client.fetchTickers(): Promise<TickerBySymbol>;
// Fetch historical candlestick data
await client.fetchKlines(
symbol: string,
interval: KlineInterval,
options?: FetchKlinesArgs
): Promise<Kline[]>;
// Get account balance
await client.fetchBalance(): Promise<BalanceByAsset>;// Create order via WebSocket (recommended for speed)
await client.createOrderWs({
symbol: string;
type: 'market' | 'limit';
side: 'buy' | 'sell';
amount: number;
price: number;
params?: Record<string, unknown>; // hedgeMode, timeInForce, etc.
}): Promise<Order>;// 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>;// Subscribe to kline updates
client.subscribeKlines({
symbol: string;
interval: KlineInterval;
handler: (kline: Kline) => void;
}): void;
// Unsubscribe
client.unsubscribeKlines({ symbol, interval, handler }): void;// Format amount to exchange precision
const formatted = client.amountToPrecision('BTCUSDT', 0.12345);
// Format price to exchange precision
const formatted = client.priceToPrecision('BTCUSDT', 65432.1);All types are normalized across exchanges. No raw exchange formats leak out.
// Candlestick
interface Kline {
openTimestamp: number;
open: number;
high: number;
low: number;
close: number;
volume: number;
closeTimestamp: number;
quoteVolume: number;
trades: number;
}
// Current price
interface Ticker {
symbol: string;
close: number;
percentage: number; // 24h change %
timestamp: number;
}
// Market metadata
interface Market {
symbol: string;
baseAsset: string;
quoteAsset: string;
settle: string;
active: boolean;
type: 'spot' | 'swap' | 'future';
linear: boolean;
contractSize: number;
filter: MarketFilter;
}
// 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;
symbol: string;
side: 'buy' | 'sell';
type: 'market' | 'limit';
amount: number;
price: number;
status: string;
timestamp: number;
}
// Account balance
interface Balance {
asset: string;
free: number;
locked: number;
total: number;
}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;
}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
});- Binance: Read, Trade, Withdraw permissions (for different features)
- Bybit: Single API key handles all
- Binance:
createOrderWs()uses REST (faster than WS) - Bybit:
createOrderWs()uses dedicated trade WebSocket stream
- Binance: Supports Hedge Mode (separate long/short) and One-Way Mode
- Bybit: Always supports both buy and sell sides simultaneously
- Binance: 8 times per day at fixed UTC times
- Bybit: Hourly funding
These differences are transparent — the same code works for both.
-
Batch requests — use
Promise.all()for multiple operationsconst [tickers, position, balance] = await Promise.all([ client.fetchTickers(), client.fetchPosition('BTCUSDT'), client.fetchBalance(), ]);
-
Reuse markets — call
loadMarkets()once at startupawait client.loadMarkets(); const symbols = [...client.markets.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 });
Exchange-specific errors are thrown as ExchangeError with structured code and exchange fields:
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}`);
}
}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);
}MIT
- GitHub Issues: solncebro/exchange-engine
- Documentation: See inline JSDoc comments
- Examples: Check
examples/directory