@hackape/tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
335 lines (274 loc) • 10.4 kB
text/typescript
import { BookChange, DerivativeTicker, Exchange, FilterForExchange, Liquidation, Trade } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'
// https://docs.bitfinex.com/v2/docs/ws-general
export class BitfinexTradesMapper implements Mapper<'bitfinex' | 'bitfinex-derivatives', Trade> {
private readonly _channelIdToSymbolMap: Map<number, string> = new Map()
constructor(private readonly _exchange: Exchange) {}
canHandle(message: BitfinexMessage) {
// non sub messages are provided as arrays
if (Array.isArray(message)) {
// first test if message itself provides channel name and if so if it's trades
const channelName = message[message.length - 2]
if (typeof channelName === 'string') {
return channelName === 'trades'
}
// otherwise use channel to id mapping
return this._channelIdToSymbolMap.get(message[0]) !== undefined
}
// store mapping between channel id and symbols
if (message.event === 'subscribed') {
const isTradeChannel = message.channel === 'trades'
if (isTradeChannel) {
this._channelIdToSymbolMap.set(message.chanId, message.pair)
}
}
return false
}
getFilters(symbols?: string[]) {
return [
{
channel: 'trades',
symbols
} as const
]
}
*map(message: BitfinexTrades, localTimestamp: Date) {
const symbolFromMessage = message[message.length - 1]
const symbol = typeof symbolFromMessage === 'string' ? symbolFromMessage : this._channelIdToSymbolMap.get(message[0])
// ignore if we don't have matching symbol
if (symbol === undefined) {
return
}
// ignore heartbeats
if (message[1] === 'hb') {
return
}
// ignore snapshots
if (message[1] !== 'te') {
return
}
const [id, timestamp, amount, price] = message[2]
const trade: Trade = {
type: 'trade',
symbol,
exchange: this._exchange,
id: String(id),
price,
amount: Math.abs(amount),
side: amount < 0 ? 'sell' : 'buy',
timestamp: new Date(timestamp),
localTimestamp: localTimestamp
}
yield trade
}
}
export class BitfinexBookChangeMapper implements Mapper<'bitfinex' | 'bitfinex-derivatives', BookChange> {
private readonly _channelIdToSymbolMap: Map<number, string> = new Map()
constructor(private readonly _exchange: Exchange) {}
canHandle(message: BitfinexMessage) {
// non sub messages are provided as arrays
if (Array.isArray(message)) {
// first test if message itself provides channel name and if so if it's a book
const channelName = message[message.length - 2]
if (typeof channelName === 'string') {
return channelName === 'book'
}
// otherwise use channel to id mapping
return this._channelIdToSymbolMap.get(message[0]) !== undefined
}
// store mapping between channel id and symbols
if (message.event === 'subscribed') {
const isBookP0Channel = message.channel === 'book' && message.prec === 'P0'
if (isBookP0Channel) {
this._channelIdToSymbolMap.set(message.chanId, message.pair)
}
}
return false
}
getFilters(symbols?: string[]) {
return [
{
channel: 'book',
symbols
} as const
]
}
*map(message: BitfinexBooks, localTimestamp: Date) {
const symbolFromMessage = message[message.length - 1]
const symbol = typeof symbolFromMessage === 'string' ? symbolFromMessage : this._channelIdToSymbolMap.get(message[0])
// ignore if we don't have matching symbol
if (symbol === undefined) {
return
}
// ignore heartbeats
if (message[1] === 'hb') {
return
}
const isSnapshot = Array.isArray(message[1][0])
const bookLevels = (isSnapshot ? message[1] : [message[1]]) as BitfinexBookLevel[]
const asks = bookLevels.filter((level) => level[2] < 0)
const bids = bookLevels.filter((level) => level[2] > 0)
const bookChange: BookChange = {
type: 'book_change',
symbol,
exchange: this._exchange,
isSnapshot,
bids: bids.map(this._mapBookLevel),
asks: asks.map(this._mapBookLevel),
timestamp: new Date(message[3]),
localTimestamp: localTimestamp
}
yield bookChange
}
private _mapBookLevel(level: BitfinexBookLevel) {
const [price, count, bitfinexAmount] = level
const amount = count === 0 ? 0 : Math.abs(bitfinexAmount)
return { price, amount }
}
}
export class BitfinexDerivativeTickerMapper implements Mapper<'bitfinex-derivatives', DerivativeTicker> {
private readonly _channelIdToSymbolMap: Map<number, string> = new Map()
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
canHandle(message: BitfinexMessage) {
// non sub messages are provided as arrays
if (Array.isArray(message)) {
// first test if message itself provides channel name and if so if it's a status
const channelName = message[message.length - 2]
if (typeof channelName === 'string') {
return channelName === 'status'
}
// otherwise use channel to id mapping
return this._channelIdToSymbolMap.get(message[0]) !== undefined
}
// store mapping between channel id and symbols
if (message.event === 'subscribed') {
const isDerivStatusChannel = message.channel === 'status' && message.key && message.key.startsWith('deriv:')
if (isDerivStatusChannel) {
this._channelIdToSymbolMap.set(message.chanId, message.key!.replace('deriv:t', ''))
}
}
return false
}
getFilters(symbols?: string[]) {
return [
{
channel: 'status',
symbols
} as const
]
}
*map(message: BitfinexStatusMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
const symbolFromMessage = message[message.length - 1]
const symbol = typeof symbolFromMessage === 'string' ? symbolFromMessage : this._channelIdToSymbolMap.get(message[0])
// ignore if we don't have matching symbol
if (symbol === undefined) {
return
}
// ignore heartbeats
if (message[1] === 'hb') {
return
}
const statusInfo = message[1]
// https://docs.bitfinex.com/v2/reference#ws-public-status
const fundingRate = statusInfo[11]
const indexPrice = statusInfo[3]
const lastPrice = statusInfo[2]
const markPrice = statusInfo[14]
const openInterest = statusInfo[17]
const nextFundingTimestamp = statusInfo[7]
const predictedFundingRate = statusInfo[8]
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'bitfinex-derivatives')
pendingTickerInfo.updateFundingRate(fundingRate)
pendingTickerInfo.updateFundingTimestamp(nextFundingTimestamp !== undefined ? new Date(nextFundingTimestamp) : undefined)
pendingTickerInfo.updatePredictedFundingRate(predictedFundingRate)
pendingTickerInfo.updateIndexPrice(indexPrice)
pendingTickerInfo.updateLastPrice(lastPrice)
pendingTickerInfo.updateMarkPrice(markPrice)
pendingTickerInfo.updateOpenInterest(openInterest)
pendingTickerInfo.updateTimestamp(new Date(message[3]))
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
export class BitfinexLiquidationsMapper implements Mapper<'bitfinex-derivatives', Liquidation> {
private _liquidationsChannelId: number | undefined = undefined
constructor(private readonly _exchange: Exchange) {}
canHandle(message: BitfinexMessage) {
// non sub messages are provided as arrays
if (Array.isArray(message)) {
// first test if message itself provides channel name and if so if it's liquidations
const channelName = message[message.length - 2]
if (typeof channelName === 'string') {
return channelName === 'liquidations'
}
// otherwise use channel id
return this._liquidationsChannelId === message[0]
}
// store liquidation channel id
if (message.event === 'subscribed') {
const isLiquidationsChannel = message.channel === 'status' && message.key === 'liq:global'
if (isLiquidationsChannel) {
this._liquidationsChannelId = message.chanId
}
}
return false
}
getFilters() {
// liquidations channel is global, not per symbol
return [
{
channel: 'liquidations'
} as const
]
}
*map(message: BitfinexLiquidation, localTimestamp: Date) {
// ignore heartbeats
if (message[1] === 'hb') {
return
}
// see https://docs.bitfinex.com/reference#ws-public-status
for (let bitfinexLiquidation of message[1]) {
const isInitialLiquidationTrigger = bitfinexLiquidation[8] === 0
// process only initial liquidation triggers not subsequent 'matches', assumption here is that
// there's only single initial liquidation trigger but there can be multiple matches for single liquidation
if (isInitialLiquidationTrigger) {
const id = String(bitfinexLiquidation[1])
const timestamp = new Date(bitfinexLiquidation[2])
const symbol = bitfinexLiquidation[4].replace('t', '')
const price = bitfinexLiquidation[6]
const amount = bitfinexLiquidation[5]
const liquidation: Liquidation = {
type: 'liquidation',
symbol,
exchange: this._exchange,
id,
price,
amount: Math.abs(amount),
side: amount < 0 ? 'buy' : 'sell',
timestamp,
localTimestamp: localTimestamp
}
yield liquidation
}
}
}
}
type BitfinexMessage =
| {
event: 'subscribed'
channel: FilterForExchange['bitfinex-derivatives']['channel']
chanId: number
pair: string
prec: string
key?: string
}
| Array<any>
type BitfinexHeartbeat = [number, 'hb']
type BitfinexTrades = [number, 'te' | any[], [number, number, number, number]] | BitfinexHeartbeat
type BitfinexBookLevel = [number, number, number]
type BitfinexBooks = [number, BitfinexBookLevel | BitfinexBookLevel[], number, number] | BitfinexHeartbeat
type BitfinexStatusMessage = [number, (number | undefined)[], number, number] | BitfinexHeartbeat
type BitfinexLiquidation =
| [number, ['pos', number, number, null, string, number, number, null, number, number, null, number][]]
| BitfinexHeartbeat