ccxws
Version:
Websocket client for 37 cryptocurrency exchanges
279 lines (248 loc) • 8.92 kB
text/typescript
/* eslint-disable @typescript-eslint/member-ordering */
/* eslint-disable @typescript-eslint/no-unsafe-return */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import { BasicClient } from "../BasicClient";
import { ClientOptions } from "../ClientOptions";
import { Level2Point } from "../Level2Point";
import { Level2Snapshot } from "../Level2Snapshots";
import { NotImplementedFn } from "../NotImplementedFn";
import { Ticker } from "../Ticker";
import { Trade } from "../Trade";
export class UpbitClient extends BasicClient {
public debouceTimeoutHandles: Map<string, NodeJS.Timeout>;
public debounceWait: number;
constructor({ wssPath = "wss://api.upbit.com/websocket/v1", watcherMs }: ClientOptions = {}) {
super(wssPath, "Upbit", undefined, watcherMs);
this.hasTickers = true;
this.hasTrades = true;
this.hasLevel2Snapshots = true;
this.debouceTimeoutHandles = new Map();
this.debounceWait = 200;
}
protected _sendSubTicker() {
this._debounce("sub-ticker", () => {
if (!this._wss) return;
const codes = Array.from(this._tickerSubs.keys());
this._wss.send(
JSON.stringify([{ ticket: "tickers" }, { type: "ticker", codes: codes }]),
);
});
}
protected _sendUnsubTicker() {
this._debounce("unsub-ticker", () => {
if (!this._wss) return;
const codes = Array.from(this._tickerSubs.keys());
this._wss.send(
JSON.stringify([{ ticket: "tickers" }, { type: "ticker", codes: codes }]),
);
});
}
protected _sendSubTrades() {
this._debounce("sub-trades", () => {
if (!this._wss) return;
const codes = Array.from(this._tradeSubs.keys());
this._wss.send(JSON.stringify([{ ticket: "trades" }, { type: "trade", codes: codes }]));
});
}
protected _sendUnsubTrades() {
this._debounce("unsub-trades", () => {
if (!this._wss) return;
const codes = Array.from(this._tradeSubs.keys());
this._wss.send(JSON.stringify([{ ticket: "trades" }, { type: "trade", codes: codes }]));
});
}
protected _sendSubLevel2Snapshots() {
this._debounce("sub-l2snapshots", () => {
if (!this._wss) return;
const codes = Array.from(this._level2SnapshotSubs.keys());
this._wss.send(
JSON.stringify([{ ticket: "quotation" }, { type: "orderbook", codes: codes }]),
);
});
}
protected _sendUnsubLevel2Snapshots() {
this._debounce("unsub-l2snapshots", () => {
if (!this._wss) return;
const codes = Array.from(this._level2SnapshotSubs.keys());
this._wss.send(
JSON.stringify([{ ticket: "quotation" }, { type: "orderbook", codes: codes }]),
);
});
}
protected _sendSubCandles = NotImplementedFn;
protected _sendUnsubCandles = NotImplementedFn;
protected _sendSubLevel2Updates = NotImplementedFn;
protected _sendUnsubLevel2Updates = NotImplementedFn;
protected _sendSubLevel3Snapshots = NotImplementedFn;
protected _sendUnsubLevel3Snapshots = NotImplementedFn;
protected _sendSubLevel3Updates = NotImplementedFn;
protected _sendUnsubLevel3Updates = NotImplementedFn;
protected _debounce(type: string, fn: () => void) {
clearTimeout(this.debouceTimeoutHandles.get(type));
this.debouceTimeoutHandles.set(type, setTimeout(fn, this.debounceWait));
}
protected _onMessage(raw: Buffer) {
const rawStr = raw.toString("utf8");
const msgs: any = JSON.parse(rawStr.replace(/:(\d+\.{0,1}\d+)(,|\})/g, ':"$1"$2'));
this._processsMessage(msgs);
}
protected _processsMessage(msg: any) {
// console.log(msg);
// trades
if (msg.type === "trade") {
const market = this._tradeSubs.get(msg.code);
if (!market) return;
const trade = this._constructTradesFromMessage(msg, market);
this.emit("trade", trade, market);
return;
}
// tickers
if (msg.type === "ticker") {
const market = this._tickerSubs.get(msg.code);
if (!market) return;
const ticker = this._constructTicker(msg, market);
this.emit("ticker", ticker, market);
return;
}
// l2 updates
if (msg.type === "orderbook") {
const market = this._level2SnapshotSubs.get(msg.code);
if (!market) return;
const snapshot = this._constructLevel2Snapshot(msg, market);
this.emit("l2snapshot", snapshot, market);
return;
}
}
protected _constructTicker(msg, market) {
/*
{ type: 'ticker',
code: 'KRW-BTC',
opening_price: '3980000.00000000',
high_price: '4017000.00000000',
low_price: '3967000.00000000',
trade_price: '3982000.0',
prev_closing_price: '3981000.00000000',
acc_trade_price: '10534309614.237530000',
change: 'RISE',
change_price: '1000.00000000',
signed_change_price: '1000.00000000',
change_rate: '0.0002511932',
signed_change_rate: '0.0002511932',
ask_bid: 'ASK',
trade_volume: '0.01562134',
acc_trade_volume: '2641.03667958',
trade_date: '20190213',
trade_time: '132020',
trade_timestamp: '1550064020000',
acc_ask_volume: '1232.55205784',
acc_bid_volume: '1408.48462174',
highest_52_week_price: '14149000.00000000',
highest_52_week_date: '2018-02-20',
lowest_52_week_price: '3562000.00000000',
lowest_52_week_date: '2018-12-15',
trade_status: null,
market_state: 'ACTIVE',
market_state_for_ios: null,
is_trading_suspended: false,
delisting_date: null,
market_warning: 'NONE',
timestamp: '1550064020393',
acc_trade_price_24h: null,
acc_trade_volume_24h: null,
stream_type: 'SNAPSHOT' }
*/
const {
opening_price,
trade_price,
acc_trade_volume,
change_rate,
change_price,
low_price,
high_price,
timestamp,
} = msg;
return new Ticker({
exchange: this.name,
base: market.base,
quote: market.quote,
timestamp: parseInt(timestamp),
last: trade_price,
open: opening_price,
high: high_price,
low: low_price,
volume: acc_trade_volume,
quoteVolume: (acc_trade_volume * trade_price).toFixed(8),
change: change_price,
changePercent: change_rate,
});
}
protected _constructTradesFromMessage(datum, market) {
/*
{
"type":"trade",
"code":"KRW-BTC",
"timestamp":1549443263262,
"trade_date":"2019-02-06",
"trade_time":"08:54:23",
"trade_timestamp":1549443263000,
"trade_price":3794000.0,
"trade_volume":0.00833522,
"ask_bid":"BID",
"prev_closing_price":3835000.00000000,
"change":"FALL","change_price":41000.00000000,
"sequential_id":1549443263000001,
"stream_type":"REALTIME"}
*/
const { trade_timestamp, trade_price, trade_volume, ask_bid, sequential_id } = datum;
const side = ask_bid === "BID" ? "buy" : "sell";
return new Trade({
exchange: this.name,
base: market.base,
quote: market.quote,
tradeId: sequential_id,
side: side,
unix: parseInt(trade_timestamp),
price: trade_price,
amount: trade_volume,
});
}
protected _constructLevel2Snapshot(msg, market) {
/*
{ type: 'orderbook',
code: 'KRW-BTT',
timestamp: 1549465903782,
total_ask_size: 1550925205.4196181,
total_bid_size: 2900599205.9702206,
orderbook_units:
[ { ask_price: 1.04,
bid_price: 1.03,
ask_size: 185206052.57158336,
bid_size: 354443748.7514278 },
...
{ ask_price: 1.13,
bid_price: 0.94,
ask_size: 198013382.3803366,
bid_size: 267304509.61145836 } ],
stream_type: 'SNAPSHOT' }
*/
const asks = msg.orderbook_units.map(p => new Level2Point(s(p.ask_price), s(p.ask_size)));
const bids = msg.orderbook_units.map(p => new Level2Point(s(p.bid_price), s(p.bid_size)));
return new Level2Snapshot({
exchange: this.name,
base: market.base,
quote: market.quote,
timestampMs: parseInt(msg.timestamp),
asks,
bids,
});
}
}
function s(v: any) {
if (typeof v === "number") {
return v.toFixed(8);
} else {
return v;
}
}