JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 77
  • Score
    100M100P100Q91873F
  • License MIT

Universal TypeScript client library for Binance and Bybit with unified API, type safety, and WebSocket support

Package Exports

  • @solncebro/exchange-engine

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.12.1

  • Fixed Binance hedge-mode order params: reduceOnly is omitted when positionSide is provided.
  • Fixed Bybit linear hedge-mode order params: positionSide is correctly mapped to positionIdx.
  • Added regression tests for Binance Futures, Bybit Linear, and Bybit Spot order parameter mapping.

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-engine

Quick 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

  1. Batch requests — use Promise.all() for multiple operations

    const [tickers, position, balance] = await Promise.all([
      client.fetchTickers(),
      client.fetchPosition('BTCUSDT'),
      client.fetchBalances(),
    ]);
  2. Reuse trade symbols — call loadTradeSymbols() once at startup

    const tradeSymbols = await client.loadTradeSymbols();
    const symbols = [...tradeSymbols.keys()];
  3. 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
    });
  4. 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:

  1. HTTP Client → add method to BinanceFuturesHttpClient or BybitHttpClient
  2. Normalizer → add raw type + normalization function
  3. Interface → add method to ExchangeClient
  4. 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