ccxws
Version:
Websocket client for 37 cryptocurrency exchanges
252 lines (227 loc) • 8.28 kB
text/typescript
/* eslint-disable prefer-const */
/* eslint-disable @typescript-eslint/no-misused-promises */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
import semaphore from "semaphore";
import { BasicClient } from "../BasicClient";
import { ClientOptions } from "../ClientOptions";
import { Level2Point } from "../Level2Point";
import { Level2Update } from "../Level2Update";
import { Market } from "../Market";
import { NotImplementedFn } from "../NotImplementedFn";
import * as https from "../Https";
import { wait } from "../Util";
import { Level2Snapshot } from "../Level2Snapshots";
import { Trade } from "../Trade";
import moment from "moment";
import { Ticker } from "../Ticker";
export class BitflyerClient extends BasicClient {
public requestSnapshot: boolean;
public REST_REQUEST_DELAY_MS: number;
protected _restSem: semaphore.Semaphore;
protected _sendSubCandles = NotImplementedFn;
protected _sendUnsubCandles = NotImplementedFn;
protected _sendSubLevel2Snapshots = NotImplementedFn;
protected _sendUnsubLevel2Snapshots = NotImplementedFn;
protected _sendSubLevel3Snapshots = NotImplementedFn;
protected _sendUnsubLevel3Snapshots = NotImplementedFn;
protected _sendSubLevel3Updates = NotImplementedFn;
protected _sendUnsubLevel3Updates = NotImplementedFn;
constructor({
wssPath = "wss://ws.lightstream.bitflyer.com/json-rpc",
watcherMs,
}: ClientOptions = {}) {
super(wssPath, "BitFlyer", undefined, watcherMs);
this.hasTickers = true;
this.hasTrades = true;
this.hasLevel2Updates = true;
this.requestSnapshot = true;
this._restSem = semaphore(1);
this.REST_REQUEST_DELAY_MS = 250;
}
protected _sendSubTicker(remote_id) {
this._wss.send(
JSON.stringify({
method: "subscribe",
params: {
channel: `lightning_ticker_${remote_id}`,
},
}),
);
}
protected _sendUnsubTicker(remote_id) {
this._wss.send(
JSON.stringify({
method: "unsubscribe",
params: {
channel: `lightning_ticker_${remote_id}`,
},
}),
);
}
protected _sendSubTrades(remote_id) {
this._wss.send(
JSON.stringify({
method: "subscribe",
params: {
channel: `lightning_executions_${remote_id}`,
},
}),
);
}
protected _sendSubLevel2Updates(remote_id) {
// this method is trigger on connections events... so safe to send snapshot request here
if (this.requestSnapshot)
this._requestLevel2Snapshot(this._level2UpdateSubs.get(remote_id));
this._wss.send(
JSON.stringify({
method: "subscribe",
params: {
channel: `lightning_board_${remote_id}`,
},
}),
);
}
protected _sendUnsubTrades(remote_id) {
this._wss.send(
JSON.stringify({
method: "unsubscribe",
params: {
channel: `lightning_executions_${remote_id}`,
},
}),
);
}
protected _sendUnsubLevel2Updates(remote_id) {
this._wss.send(
JSON.stringify({
method: "unsubscribe",
params: {
channel: `lightning_board_${remote_id}`,
},
}),
);
}
protected _onMessage(data) {
const parsed = JSON.parse(data);
if (!parsed.params || !parsed.params.channel || !parsed.params.message) return;
const { channel, message } = parsed.params;
if (channel.startsWith("lightning_ticker_")) {
const remote_id = channel.substr("lightning_ticker_".length);
const market = this._tickerSubs.get(remote_id);
if (!market) return;
const ticker = this._createTicker(message, market);
this.emit("ticker", ticker, market);
return;
}
// trades
if (channel.startsWith("lightning_executions_")) {
const remote_id = channel.substr("lightning_executions_".length);
const market = this._tradeSubs.get(remote_id);
if (!market) return;
for (const datum of message) {
const trade = this._createTrades(datum, market);
this.emit("trade", trade, market);
}
}
// orderbook
if (channel.startsWith("lightning_board_")) {
const remote_id = channel.substr("lightning_board_".length);
const market = this._level2UpdateSubs.get(remote_id);
if (!market) return;
const update = this._createLevel2Update(message, market);
this.emit("l2update", update, market);
}
}
protected _createTicker(data, market) {
const {
timestamp,
best_bid,
best_ask,
best_bid_size,
best_ask_size,
ltp,
volume,
volume_by_product,
} = data;
return new Ticker({
exchange: "bitFlyer",
base: market.base,
quote: market.quote,
timestamp: moment.utc(timestamp).valueOf(),
last: ltp.toFixed(8),
volume: volume.toFixed(8),
quoteVolume: volume_by_product.toFixed(8),
bid: best_bid.toFixed(8),
bidVolume: best_bid_size.toFixed(8),
ask: best_ask.toFixed(8),
askVolume: best_ask_size.toFixed(8),
});
}
protected _createTrades(datum, market) {
let {
size,
side,
exec_date,
price,
id,
buy_child_order_acceptance_id,
sell_child_order_acceptance_id,
} = datum;
side = side.toLowerCase();
const unix = moment(exec_date).valueOf();
return new Trade({
exchange: "bitFlyer",
base: market.base,
quote: market.quote,
tradeId: id.toFixed(),
unix,
side: side.toLowerCase(),
price: price.toFixed(8),
amount: size.toFixed(8),
buyOrderId: buy_child_order_acceptance_id,
sellOrderId: sell_child_order_acceptance_id,
});
}
protected _createLevel2Update(msg: any, market: Market) {
const asks = msg.asks.map(p => new Level2Point(p.price.toFixed(8), p.size.toFixed(8)));
const bids = msg.bids.map(p => new Level2Point(p.price.toFixed(8), p.size.toFixed(8)));
return new Level2Update({
exchange: "bitFlyer",
base: market.base,
quote: market.quote,
asks,
bids,
});
}
protected _requestLevel2Snapshot(market: Market) {
this._restSem.take(async () => {
try {
const remote_id = market.id;
const uri = `https://api.bitflyer.com/v1/board?product_code=${remote_id}`;
const raw = (await https.get(uri)) as any;
const asks = raw.asks.map(
p => new Level2Point(p.price.toFixed(8), p.size.toFixed(8)),
);
const bids = raw.bids.map(
p => new Level2Point(p.price.toFixed(8), p.size.toFixed(8)),
);
const snapshot = new Level2Snapshot({
exchange: "bitFlyer",
base: market.base,
quote: market.quote,
asks,
bids,
});
this.emit("l2snapshot", snapshot, market);
} catch (ex) {
this._onError(ex);
this._requestLevel2Snapshot(market);
} finally {
await wait(this.REST_REQUEST_DELAY_MS);
this._restSem.leave();
}
});
}
}