ccxws
Version:
Websocket client for 37 cryptocurrency exchanges
433 lines (385 loc) • 12.4 kB
text/typescript
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import * as crypto from "crypto";
import { BasicClient } from "../BasicClient";
import { BasicMultiClient } from "../BasicMultiClient";
import { Candle } from "../Candle";
import { CandlePeriod } from "../CandlePeriod";
import { IClient } from "../IClient";
import { Level2Point } from "../Level2Point";
import { Level2Snapshot } from "../Level2Snapshots";
import { Market } from "../Market";
import { NotImplementedFn } from "../NotImplementedFn";
import { Ticker } from "../Ticker";
import { Trade } from "../Trade";
import { Watcher } from "../Watcher";
function createSignature(timestamp: number, apiKey: string, apiSecret: string) {
const hmac = crypto.createHmac("sha256", apiSecret);
hmac.update(timestamp.toString() + apiKey);
return hmac.digest("hex");
}
function createAuthToken(apiKey: string, apiSecret: string) {
const timestamp = Math.floor(Date.now() / 1000);
return {
key: apiKey,
signature: createSignature(timestamp, apiKey, apiSecret),
timestamp,
};
}
const multiplier = {
ADA: 1e6,
ATOM: 1e6,
BAT: 1e6,
GAS: 1e8,
NEO: 1e6,
ONT: 1e6,
ONG: 1e6,
MATIC: 1e6,
LINK: 1e6,
XTZ: 1e6,
BCH: 1e8,
BTC: 1e8,
BTG: 1e8,
BTT: 1e6,
DASH: 1e8,
ETH: 1e6,
GUSD: 1e2,
LTC: 1e8,
MHC: 1e6,
OMG: 1e6,
TRX: 1e6,
XLM: 1e7,
XRP: 1e6,
ZEC: 1e8,
};
function formatAmount(amount: string, symbol: string) {
return (parseInt(amount) / multiplier[symbol]).toFixed(8);
}
export type CexClientOptions = {
apiKey: string;
apiSecret: string;
};
export class CexClient extends BasicMultiClient {
public name: string;
public options: CexClientOptions;
public candlePeriod: CandlePeriod;
/**
* Creates a new CEX.io client using the supplied credentials
*/
constructor(options: CexClientOptions) {
super();
this._clients = new Map();
this.name = "CEX_MULTI";
this.options = options;
this.hasTickers = true;
this.hasTrades = true;
this.hasCandles = true;
this.hasLevel2Snapshots = true;
this.candlePeriod = CandlePeriod._1m;
}
protected _createBasicClient(clientArgs: { market: Market }): IClient {
return new SingleCexClient({
...this.options,
market: clientArgs.market,
parent: this,
});
}
}
export class SingleCexClient extends BasicClient {
public auth: { apiKey: string; apiSecret: string };
public market: Market;
public hasTickers: boolean;
public hasTrades: boolean;
public hasCandles: boolean;
public hasLevel2Snapshots: boolean;
public authorized: boolean;
public parent: CexClient;
protected _sendSubLevel2Updates = NotImplementedFn;
protected _sendUnsubLevel2Updates = NotImplementedFn;
protected _sendSubLevel3Snapshots = NotImplementedFn;
protected _sendUnsubLevel3Snapshots = NotImplementedFn;
protected _sendSubLevel3Updates = NotImplementedFn;
protected _sendUnsubLevel3Updates = NotImplementedFn;
constructor({
wssPath = "wss://ws.cex.io/ws",
watcherMs = 900 * 1000,
apiKey,
apiSecret,
market,
parent,
}: {
wssPath?: string;
watcherMs?: number;
apiKey: string;
apiSecret: string;
market: Market;
parent: CexClient;
}) {
super(wssPath, "CEX", undefined, watcherMs);
this._watcher = new Watcher(this, watcherMs);
this.auth = { apiKey, apiSecret };
this.market = market;
this.hasTickers = true;
this.hasTrades = true;
this.hasCandles = true;
this.hasLevel2Snapshots = true;
this.authorized = false;
this.parent = parent;
}
public get candlePeriod(): CandlePeriod {
return this.parent.candlePeriod;
}
/**
* This method is fired anytime the socket is opened, whether
* the first time, or any subsequent reconnects.
* Since this is an authenticated feed, we first send an authenticate
* request, and the normal subscriptions happen after authentication has
* completed in the _onAuthorized method.
*/
protected _onConnected() {
this._sendAuthorizeRequest();
}
/**
* Trigger after an authorization packet has been successfully received.
* This code triggers the usual _onConnected code afterwards.
*/
protected _onAuthorized() {
this.authorized = true;
this.emit("authorized");
super._onConnected();
}
protected _sendAuthorizeRequest() {
this._wss.send(
JSON.stringify({
e: "auth",
auth: createAuthToken(this.auth.apiKey, this.auth.apiSecret),
}),
);
}
protected _sendPong() {
if (this._wss) {
this._wss.send(JSON.stringify({ e: "pong" }));
}
}
protected _sendSubTicker() {
if (!this.authorized) return;
this._wss.send(
JSON.stringify({
e: "subscribe",
rooms: ["tickers"],
}),
);
}
protected _sendUnsubTicker() {
//
}
protected _sendSubTrades(remote_id: string) {
if (!this.authorized) return;
this._wss.send(
JSON.stringify({
e: "subscribe",
rooms: [`pair-${remote_id.replace("/", "-")}`],
}),
);
}
protected _sendUnsubTrades() {
//
}
protected _sendSubCandles(remote_id: string) {
if (!this.authorized) return;
this._wss.send(
JSON.stringify({
e: "init-ohlcv",
i: candlePeriod(this.candlePeriod),
rooms: [`pair-${remote_id.replace("/", "-")}`],
}),
);
}
protected _sendUnsubCandles() {
//
}
protected _sendSubLevel2Snapshots(remote_id: string) {
if (!this.authorized) return;
this._wss.send(
JSON.stringify({
e: "subscribe",
rooms: [`pair-${remote_id.replace("/", "-")}`],
}),
);
}
protected _sendUnsubLevel2Snapshots() {
//
}
protected _onMessage(raw: any) {
const message = JSON.parse(raw);
const { e, data } = message;
if (e === "ping") {
this._sendPong();
return;
}
if (e === "subscribe") {
if (message.error) {
throw new Error(`CEX error: ${JSON.stringify(message)}`);
}
}
if (e === "auth") {
if (data.ok === "ok") {
this._onAuthorized();
} else {
throw new Error("Authentication error");
}
return;
}
if (e === "tick") {
// {"e":"tick","data":{"symbol1":"BTC","symbol2":"USD","price":"4244.4","open24":"4248.4","volume":"935.58669239"}}
const marketId = `${data.symbol1}/${data.symbol2}`;
const market = this._tickerSubs.get(marketId);
if (!market) return;
const ticker = this._constructTicker(data, market);
this.emit("ticker", ticker, market);
return;
}
if (e === "md") {
const marketId = data.pair.replace(":", "/");
const market = this._level2SnapshotSubs.get(marketId);
if (!market) return;
const result = this._constructevel2Snapshot(data, market);
this.emit("l2snapshot", result, market);
return;
}
if (e === "history") {
const marketId = this.market.id;
const market = this._tradeSubs.get(marketId);
if (!market) return;
// sell/buy:timestamp_ms:amount:price:transaction_id
for (const rawTrade of data.reverse()) {
const tradeData = rawTrade.split(":");
const trade = this._constructTrade(tradeData, market);
this.emit("trade", trade, market);
}
return;
}
if (e === "history-update") {
const marketId = this.market.id;
const market = this._tradeSubs.get(marketId);
if (this._tradeSubs.has(marketId)) {
for (const rawTrade of data) {
const trade = this._constructTrade(rawTrade, market);
this.emit("trade", trade, market);
}
return;
}
}
// ohlcv{period} - why the F*** are there three styles of candles???
if (e === `ohlcv${candlePeriod(this.candlePeriod)}`) {
const marketId = message.data.pair.replace(":", "/");
const market = this._candleSubs.get(marketId);
if (!market) return;
const candle = this._constructCandle(message.data);
this.emit("candle", candle, market);
return;
}
}
protected _constructTicker(data: any, market: Market) {
// {"e":"tick","data":{"symbol1":"BTC","symbol2":"USD","price":"4244.4","open24":"4248.4","volume":"935.58669239"}}
const { open24, price, volume } = data;
const change = parseFloat(price) - parseFloat(open24);
const changePercent =
open24 !== 0
? ((parseFloat(price) - parseFloat(open24)) / parseFloat(open24)) * 100
: 0;
return new Ticker({
exchange: this.name,
base: market.base,
quote: market.quote,
timestamp: Date.now(),
last: price,
open: open24,
volume: volume,
change: change.toFixed(8),
changePercent: changePercent.toFixed(8),
});
}
protected _constructevel2Snapshot(msg: any, market: Market) {
const asks = msg.sell.map(
p => new Level2Point(p[0].toFixed(8), formatAmount(p[1], market.base)),
);
const bids = msg.buy.map(
p => new Level2Point(p[0].toFixed(8), formatAmount(p[1], market.base)),
);
return new Level2Snapshot({
exchange: this.name,
base: market.base,
quote: market.quote,
sequenceId: msg.id,
asks,
bids,
});
}
protected _constructTrade(data: any, market: Market) {
//["buy","1543967891439","4110282","3928.1","9437977"]
//format: sell/buy, timestamp_ms, amount, price, transaction_id
const [side, timestamp_ms, amount, price, tradeId] = data;
return new Trade({
exchange: this.name,
base: market.base,
quote: market.quote,
tradeId: tradeId,
unix: parseInt(timestamp_ms),
side: side,
price: price,
amount: formatAmount(amount, market.base),
rawAmount: amount,
});
}
/**
{
e: 'ohlcv1m',
data: {
pair: 'BTC:USD',
time: '1597261140',
o: '11566.8',
h: '11566.8',
l: '11566.8',
c: '11566.8',
v: 664142,
d: 664142
}
}
*/
protected _constructCandle(data) {
const ms = Number(data.time) * 1000;
return new Candle(ms, data.o, data.h, data.l, data.c, data.v.toFixed(8));
}
}
function candlePeriod(period) {
switch (period) {
case CandlePeriod._1m:
return "1m";
case CandlePeriod._3m:
return "3m";
case CandlePeriod._5m:
return "5m";
case CandlePeriod._15m:
return "15m";
case CandlePeriod._30m:
return "30m";
case CandlePeriod._1h:
return "1h";
case CandlePeriod._2h:
return "2h";
case CandlePeriod._4h:
return "4h";
case CandlePeriod._6h:
return "6h";
case CandlePeriod._12h:
return "12h";
case CandlePeriod._1d:
return "1d";
case CandlePeriod._3d:
return "3d";
case CandlePeriod._1w:
return "1w";
}
}