@hackape/tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
223 lines (189 loc) • 6.67 kB
text/typescript
import { BookChange, DerivativeTicker, Exchange, Trade } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'
// https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/websocket.md
export class BybitTradesMapper implements Mapper<'bybit', Trade> {
private readonly _seenSymbols = new Set<string>()
constructor(private readonly _exchange: Exchange) {}
canHandle(message: BybitDataMessage) {
if (message.topic === undefined) {
return false
}
return message.topic.startsWith('trade.')
}
getFilters(symbols?: string[]) {
return [
{
channel: 'trade',
symbols
} as const
]
}
*map(message: BybitTradeDataMessage, localTimestamp: Date): IterableIterator<Trade> {
for (const trade of message.data) {
const symbol = trade.symbol
const isLinear = symbol.endsWith('USDT')
// for some reason bybit publishes 'stale' trades for it's linear contracts (trades that already been published before disconnect)
if (isLinear && this._seenSymbols.has(symbol) === false) {
this._seenSymbols.add(symbol)
break
}
const timestamp = trade.trade_time_ms !== undefined ? new Date(Number(trade.trade_time_ms)) : new Date(trade.timestamp)
yield {
type: 'trade',
symbol: trade.symbol,
exchange: this._exchange,
id: trade.trade_id,
price: Number(trade.price),
amount: trade.size,
side: trade.side == 'Buy' ? 'buy' : trade.side === 'Sell' ? 'sell' : 'unknown',
timestamp,
localTimestamp
}
}
}
}
export class BybitBookChangeMapper implements Mapper<'bybit', BookChange> {
constructor(protected readonly _exchange: Exchange, private readonly _canUseBook200Channel: boolean) {}
canHandle(message: BybitDataMessage) {
if (message.topic === undefined) {
return false
}
if (this._canUseBook200Channel) {
return message.topic.startsWith('orderBook_200.')
} else {
return message.topic.startsWith('orderBookL2_25.')
}
}
getFilters(symbols?: string[]) {
if (this._canUseBook200Channel) {
return [
{
channel: 'orderBook_200',
symbols
} as const
]
}
return [
{
channel: 'orderBookL2_25',
symbols
} as const
]
}
*map(message: BybitBookSnapshotDataMessage | BybitBookSnapshotUpdateMessage, localTimestamp: Date) {
const topicArray = message.topic.split('.')
const symbol = topicArray[topicArray.length - 1]
const data =
message.type === 'snapshot'
? 'order_book' in message.data
? message.data.order_book
: message.data
: [...message.data.delete, ...message.data.update, ...message.data.insert]
const timestampBybit = Number(message.timestamp_e6)
const timestamp = new Date(timestampBybit / 1000)
timestamp.μs = timestampBybit % 1000
yield {
type: 'book_change',
symbol,
exchange: this._exchange,
isSnapshot: message.type === 'snapshot',
bids: data.filter((d) => d.side === 'Buy').map(this._mapBookLevel),
asks: data.filter((d) => d.side === 'Sell').map(this._mapBookLevel),
timestamp,
localTimestamp
} as const
}
private _mapBookLevel(level: BybitBookLevel) {
return { price: Number(level.price), amount: level.size !== undefined ? level.size : 0 }
}
}
export class BybitDerivativeTickerMapper implements Mapper<'bybit', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
canHandle(message: BybitDataMessage) {
if (message.topic === undefined) {
return false
}
return message.topic.startsWith('instrument_info.')
}
getFilters(symbols?: string[]) {
return [
{
channel: 'instrument_info',
symbols
} as const
]
}
*map(message: BybitInstrumentDataMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
const instrumentInfo = 'symbol' in message.data ? message.data : message.data.update[0]
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(instrumentInfo.symbol, 'bybit')
pendingTickerInfo.updateFundingRate(instrumentInfo.funding_rate_e6 !== undefined ? instrumentInfo.funding_rate_e6 / 1000000 : undefined)
pendingTickerInfo.updatePredictedFundingRate(
instrumentInfo.predicted_funding_rate_e6 !== undefined ? instrumentInfo.predicted_funding_rate_e6 / 1000000 : undefined
)
pendingTickerInfo.updateFundingTimestamp(
instrumentInfo.next_funding_time !== undefined ? new Date(instrumentInfo.next_funding_time) : undefined
)
pendingTickerInfo.updateIndexPrice(instrumentInfo.index_price_e4 !== undefined ? instrumentInfo.index_price_e4 / 10000 : undefined)
pendingTickerInfo.updateMarkPrice(instrumentInfo.mark_price_e4 !== undefined ? instrumentInfo.mark_price_e4 / 10000 : undefined)
pendingTickerInfo.updateOpenInterest(
instrumentInfo.open_interest_e8 !== undefined ? instrumentInfo.open_interest_e8 / 100000000 : instrumentInfo.open_interest
)
pendingTickerInfo.updateLastPrice(instrumentInfo.last_price_e4 !== undefined ? instrumentInfo.last_price_e4 / 10000 : undefined)
pendingTickerInfo.updateTimestamp(new Date(instrumentInfo.updated_at))
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
type BybitDataMessage = {
topic: string
}
type BybitTradeDataMessage = BybitDataMessage & {
data: {
timestamp: string
trade_time_ms?: number | string
symbol: string
side: 'Buy' | 'Sell'
size: number
price: number | string
trade_id: string
}[]
}
type BybitBookLevel = {
price: string
side: 'Buy' | 'Sell'
size?: number
}
type BybitBookSnapshotDataMessage = BybitDataMessage & {
type: 'snapshot'
data: BybitBookLevel[] | { order_book: BybitBookLevel[] }
timestamp_e6: number | string
}
type BybitBookSnapshotUpdateMessage = BybitDataMessage & {
type: 'delta'
data: {
delete: BybitBookLevel[]
update: BybitBookLevel[]
insert: BybitBookLevel[]
}
timestamp_e6: number | string
}
type BybitInstrumentUpdate = {
symbol: string
mark_price_e4?: number
index_price_e4?: number
open_interest?: number
open_interest_e8?: number
funding_rate_e6?: number
predicted_funding_rate_e6?: number
next_funding_time?: string
last_price_e4?: number
updated_at: string
}
type BybitInstrumentDataMessage = BybitDataMessage & {
data:
| BybitInstrumentUpdate
| {
update: [BybitInstrumentUpdate]
}
}