tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
1,183 lines (990 loc) • 35.2 kB
text/typescript
import { asNumberIfValid, upperCaseSymbols } from '../handy'
import { BookChange, BookTicker, DerivativeTicker, Exchange, Liquidation, OptionSummary, Trade } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'
// V5 Okex API mappers
// https://www.okex.com/docs-v5/en/#websocket-api-public-channel-trades-channel
export class OkexV5TradesMapper implements Mapper<OKEX_EXCHANGES, Trade> {
constructor(private readonly _exchange: Exchange, private readonly _useTradesAll: boolean) {}
canHandle(message: any) {
if (message.event !== undefined || message.arg === undefined) {
return false
}
return message.arg.channel === 'trades' || message.arg.channel === 'trades-all'
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
if (this._useTradesAll) {
return [
{
channel: `trades-all` as const,
symbols
}
]
}
return [
{
channel: `trades` as const,
symbols
}
]
}
*map(okexTradesMessage: OkexV5TradeMessage | OkexV5TradesAllMessage, localTimestamp: Date): IterableIterator<Trade> {
for (const okexTrade of okexTradesMessage.data) {
yield {
type: 'trade',
symbol: okexTrade.instId,
exchange: this._exchange,
id: okexTrade.tradeId,
price: Number(okexTrade.px),
amount: Number(okexTrade.sz),
side: okexTrade.side === 'buy' ? 'buy' : 'sell',
timestamp: new Date(Number(okexTrade.ts)),
localTimestamp: localTimestamp
}
}
}
}
const mapV5BookLevel = (level: OkexV5BookLevel) => {
const price = Number(level[0])
const amount = Number(level[1])
return { price, amount }
}
export class OkexV5BookChangeMapper implements Mapper<OKEX_EXCHANGES, BookChange> {
private _channelName: string
constructor(private readonly _exchange: Exchange, usePublicBooksChannel: boolean) {
this._channelName = this._getBooksChannelName(usePublicBooksChannel)
}
canHandle(message: any) {
if (message.event !== undefined || message.arg === undefined) {
return false
}
return message.arg.channel === this._channelName
}
private _hasCredentials = process.env.OKX_API_KEY !== undefined
private _hasVip5Access = process.env.OKX_API_VIP_5 !== undefined
private _hasColoAccess = process.env.OKX_API_COLO !== undefined
private _getBooksChannelName(usePublicBooksChannel: boolean) {
if (usePublicBooksChannel === false) {
// historical data always uses books-l2-tbt
return 'books-l2-tbt'
}
if (this._hasCredentials && this._hasVip5Access) {
return 'books-l2-tbt'
}
if (this._hasColoAccess) {
return 'books-l2-tbt'
}
if (this._hasCredentials) {
return 'books50-l2-tbt'
}
return 'books'
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: this._channelName as any,
symbols
}
]
}
*map(okexDepthDataMessage: OkexV5BookMessage, localTimestamp: Date): IterableIterator<BookChange> {
for (const message of okexDepthDataMessage.data) {
if (okexDepthDataMessage.action === 'update' && message.bids.length === 0 && message.asks.length === 0) {
continue
}
const timestamp = new Date(Number(message.ts))
if (timestamp.valueOf() === 0) {
continue
}
yield {
type: 'book_change',
symbol: okexDepthDataMessage.arg.instId,
exchange: this._exchange,
isSnapshot: okexDepthDataMessage.action === 'snapshot',
bids: message.bids.map(mapV5BookLevel),
asks: message.asks.map(mapV5BookLevel),
timestamp,
localTimestamp: localTimestamp
}
}
}
}
export class OkexV5BookTickerMapper implements Mapper<OKEX_EXCHANGES, BookTicker> {
constructor(private readonly _exchange: Exchange, private readonly _useTbtTickerChannel: boolean) {}
canHandle(message: any) {
if (message.event !== undefined || message.arg === undefined) {
return false
}
if (this._useTbtTickerChannel) {
return message.arg.channel === 'bbo-tbt'
}
return message.arg.channel === 'tickers'
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
if (this._useTbtTickerChannel) {
return [
{
channel: `bbo-tbt` as const,
symbols
}
]
}
return [
{
channel: `tickers` as const,
symbols
}
]
}
map(message: OkexV5TickerMessage | OkexBBOTbtData, localTimestamp: Date): IterableIterator<BookTicker> {
if (message.arg.channel === 'bbo-tbt') {
return this._mapFromTbtTicker(message as OkexBBOTbtData, localTimestamp)
} else {
return this._mapFromTicker(message as OkexV5TickerMessage, localTimestamp)
}
}
private *_mapFromTbtTicker(message: OkexBBOTbtData, localTimestamp: Date): IterableIterator<BookTicker> {
if (!message.data) {
return
}
for (const tbtTicker of message.data) {
const bestAsk = tbtTicker.asks !== undefined && tbtTicker.asks[0] ? mapBookLevel(tbtTicker.asks[0]) : undefined
const bestBid = tbtTicker.bids !== undefined && tbtTicker.bids[0] ? mapBookLevel(tbtTicker.bids[0]) : undefined
const ticker: BookTicker = {
type: 'book_ticker',
symbol: message.arg.instId,
exchange: this._exchange,
askAmount: bestAsk?.amount,
askPrice: bestAsk?.price,
bidPrice: bestBid?.price,
bidAmount: bestBid?.amount,
timestamp: new Date(Number(tbtTicker.ts)),
localTimestamp: localTimestamp
}
yield ticker
}
}
private *_mapFromTicker(message: OkexV5TickerMessage, localTimestamp: Date): IterableIterator<BookTicker> {
for (const okexTicker of message.data) {
const ticker: BookTicker = {
type: 'book_ticker',
symbol: okexTicker.instId,
exchange: this._exchange,
askAmount: asNumberIfValid(okexTicker.askSz),
askPrice: asNumberIfValid(okexTicker.askPx),
bidPrice: asNumberIfValid(okexTicker.bidPx),
bidAmount: asNumberIfValid(okexTicker.bidSz),
timestamp: new Date(Number(okexTicker.ts)),
localTimestamp: localTimestamp
}
yield ticker
}
}
}
export class OkexV5DerivativeTickerMapper implements Mapper<'okex-futures' | 'okex-swap', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
private readonly _indexPrices = new Map<string, number>()
private _futuresChannels = ['tickers', 'open-interest', 'mark-price', 'index-tickers'] as const
private _swapChannels = ['tickers', 'open-interest', 'mark-price', 'index-tickers', 'funding-rate'] as const
constructor(private readonly _exchange: Exchange) {}
canHandle(message: any) {
const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels
if (message.event !== undefined || message.arg === undefined) {
return false
}
return channels.includes(message.arg.channel)
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels
return channels.map((channel) => {
if (channel === 'index-tickers') {
const indexes =
symbols !== undefined
? symbols.map((s) => {
const symbolParts = s.split('-')
const quotePart = symbolParts[1] === 'USDC' ? 'USD' : symbolParts[1]
return `${symbolParts[0]}-${quotePart}`
})
: undefined
return {
channel,
symbols: indexes
}
}
return {
channel,
symbols
}
})
}
*map(
message: OkexV5TickerMessage | OkexV5OpenInterestMessage | OkexV5MarkPriceMessage | OkexV5IndexTickerMessage | OkexV5FundingRateMessage,
localTimestamp: Date
): IterableIterator<DerivativeTicker> {
if (message.arg.channel === 'index-tickers') {
for (const dataMessage of message.data) {
const indexTickerMessage = dataMessage as OkexV5IndexTickerMessage['data'][0]
const lastIndexPrice = Number(indexTickerMessage.idxPx)
if (lastIndexPrice > 0) {
this._indexPrices.set(indexTickerMessage.instId, lastIndexPrice)
}
}
return
}
for (const dataMessage of message.data) {
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(dataMessage.instId, this._exchange)
const symbolParts = dataMessage.instId.split('-')
const quotePart = symbolParts[1] === 'USDC' ? 'USD' : symbolParts[1]
const indexSymbol = `${symbolParts[0]}-${quotePart}`
const indexPrice = this._indexPrices.get(indexSymbol)
if (indexPrice !== undefined) {
pendingTickerInfo.updateIndexPrice(indexPrice)
}
if (message.arg.channel === 'mark-price') {
const markPriceMessage = dataMessage as OkexV5MarkPriceMessage['data'][0]
const markPrice = Number(markPriceMessage.markPx)
if (markPrice > 0) {
pendingTickerInfo.updateMarkPrice(markPrice)
pendingTickerInfo.updateTimestamp(new Date(Number(markPriceMessage.ts)))
}
}
if (message.arg.channel === 'open-interest') {
const openInterestMessage = dataMessage as OkexV5OpenInterestMessage['data'][0]
const openInterest = Number(openInterestMessage.oi)
if (openInterest > 0) {
pendingTickerInfo.updateOpenInterest(openInterest)
pendingTickerInfo.updateTimestamp(new Date(Number(openInterestMessage.ts)))
}
}
if (message.arg.channel === 'funding-rate') {
const fundingRateMessage = dataMessage as OkexV5FundingRateMessage['data'][0]
if (fundingRateMessage.fundingRate !== undefined) {
pendingTickerInfo.updateFundingRate(Number(fundingRateMessage.fundingRate))
}
if (fundingRateMessage.fundingTime !== undefined) {
pendingTickerInfo.updateFundingTimestamp(new Date(Number(fundingRateMessage.fundingTime)))
}
if (fundingRateMessage.nextFundingRate !== undefined && fundingRateMessage.nextFundingRate !== '') {
pendingTickerInfo.updatePredictedFundingRate(Number(fundingRateMessage.nextFundingRate))
}
}
if (message.arg.channel === 'tickers') {
const tickerMessage = dataMessage as OkexV5TickerMessage['data'][0]
const lastPrice = Number(tickerMessage.last)
if (lastPrice > 0) {
pendingTickerInfo.updateLastPrice(lastPrice)
pendingTickerInfo.updateTimestamp(new Date(Number(tickerMessage.ts)))
}
}
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
}
export class OkexV5LiquidationsMapper implements Mapper<OKEX_EXCHANGES, Liquidation> {
private _isFirstMessage = true
constructor(private readonly _exchange: Exchange) {}
canHandle(message: any) {
if (message.event !== undefined || message.arg === undefined) {
return false
}
return message.arg.channel === 'liquidations' || message.arg.channel === 'liquidation-orders'
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: 'liquidations',
symbols
} as any,
{
channel: 'liquidation-orders',
symbols
} as any
]
}
*map(
okexLiquidationMessage: OkexV5LiquidationMessage | OkexV5LiquidationOrderMessage,
localTimestamp: Date
): IterableIterator<Liquidation> {
if (okexLiquidationMessage.arg.channel === 'liquidation-orders') {
if (this._isFirstMessage) {
this._isFirstMessage = false
return
}
for (const okexLiquidation of (okexLiquidationMessage as OkexV5LiquidationOrderMessage).data) {
for (const detail of okexLiquidation.details) {
const liquidation: Liquidation = {
type: 'liquidation',
symbol: okexLiquidation.instId,
exchange: this._exchange,
id: undefined,
price: Number(detail.bkPx),
amount: Number(detail.sz),
side: detail.side === 'buy' ? 'buy' : 'sell',
timestamp: new Date(Number(detail.ts)),
localTimestamp: localTimestamp
}
yield liquidation
}
}
} else {
for (const okexLiquidation of (okexLiquidationMessage as OkexV5LiquidationMessage).data) {
const liquidation: Liquidation = {
type: 'liquidation',
symbol: okexLiquidationMessage.arg.instId,
exchange: this._exchange,
id: undefined,
price: Number(okexLiquidation.bkPx),
amount: Number(okexLiquidation.sz),
side: okexLiquidation.side === 'buy' ? 'buy' : 'sell',
timestamp: new Date(Number(okexLiquidation.ts)),
localTimestamp: localTimestamp
}
yield liquidation
}
}
}
}
export class OkexV5OptionSummaryMapper implements Mapper<'okex-options', OptionSummary> {
private readonly _indexPrices = new Map<string, number>()
private readonly _openInterests = new Map<string, number>()
private readonly _markPrices = new Map<string, number>()
private readonly _tickers = new Map<string, OkexV5TickerMessage['data'][0]>()
private readonly expiration_regex = /(\d{2})(\d{2})(\d{2})/
canHandle(message: any) {
if (message.event !== undefined || message.arg === undefined) {
return false
}
return (
message.arg.channel === 'opt-summary' ||
message.arg.channel === 'index-tickers' ||
message.arg.channel === 'tickers' ||
message.arg.channel === 'open-interest' ||
message.arg.channel === 'mark-price'
)
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
const indexes =
symbols !== undefined
? symbols.map((s) => {
const symbolParts = s.split('-')
return `${symbolParts[0]}-${symbolParts[1]}`
})
: undefined
return [
{
channel: `opt-summary`,
symbols: [] as string[]
} as const,
{
channel: `index-tickers`,
symbols: indexes
} as const,
{
channel: `tickers`,
symbols: symbols
} as const,
{
channel: `open-interest`,
symbols: symbols
} as const,
{
channel: `mark-price`,
symbols: symbols
} as const
]
}
*map(
message: OkexV5SummaryMessage | OkexV5IndexTickerMessage | OkexV5TickerMessage | OkexV5OpenInterestMessage | OkexV5MarkPriceMessage,
localTimestamp: Date
): IterableIterator<OptionSummary> | undefined {
if (message.arg.channel === 'index-tickers') {
for (const dataMessage of message.data) {
const indexTickerMessage = dataMessage as OkexV5IndexTickerMessage['data'][0]
const lastIndexPrice = asNumberIfValid(indexTickerMessage.idxPx)
if (lastIndexPrice !== undefined) {
this._indexPrices.set(indexTickerMessage.instId, lastIndexPrice)
}
}
return
}
if (message.arg.channel === 'open-interest') {
for (const dataMessage of message.data) {
const openInterestMessage = dataMessage as OkexV5OpenInterestMessage['data'][0]
const openInterestValue = asNumberIfValid(openInterestMessage.oi)
if (openInterestValue !== undefined) {
this._openInterests.set(openInterestMessage.instId, openInterestValue)
}
}
return
}
if (message.arg.channel === 'mark-price') {
for (const dataMessage of message.data) {
const markPriceMessage = dataMessage as OkexV5MarkPriceMessage['data'][0]
const markPrice = asNumberIfValid(markPriceMessage.markPx)
if (markPrice !== undefined) {
this._markPrices.set(markPriceMessage.instId, markPrice)
}
}
return
}
if (message.arg.channel === 'tickers') {
for (const dataMessage of message.data) {
const tickerMessage = dataMessage as OkexV5TickerMessage['data'][0]
this._tickers.set(tickerMessage.instId, tickerMessage)
}
return
}
if (message.arg.channel === 'opt-summary') {
for (const dataMessage of message.data) {
const summary = dataMessage as OkexV5SummaryMessage['data'][0]
const symbolParts = summary.instId.split('-')
const isPut = symbolParts[4] === 'P'
const strikePrice = Number(symbolParts[3])
var dateArray = this.expiration_regex.exec(symbolParts[2])!
const expirationDate = new Date(Date.UTC(+('20' + dateArray[1]), +dateArray[2] - 1, +dateArray[3], 8, 0, 0, 0))
const lastUnderlyingPrice = this._indexPrices.get(summary.uly)
const lastOpenInterest = this._openInterests.get(summary.instId)
const lastMarkPrice = this._markPrices.get(summary.instId)
const lastTickerInfo = this._tickers.get(summary.instId)
const optionSummary: OptionSummary = {
type: 'option_summary',
symbol: summary.instId,
exchange: 'okex-options',
optionType: isPut ? 'put' : 'call',
strikePrice,
expirationDate,
bestBidPrice: lastTickerInfo !== undefined ? asNumberIfValid(lastTickerInfo.bidPx) : undefined,
bestBidAmount: lastTickerInfo !== undefined ? asNumberIfValid(lastTickerInfo.bidSz) : undefined,
bestBidIV: asNumberIfValid(summary.bidVol),
bestAskPrice: lastTickerInfo !== undefined ? asNumberIfValid(lastTickerInfo.askPx) : undefined,
bestAskAmount: lastTickerInfo !== undefined ? asNumberIfValid(lastTickerInfo.askSz) : undefined,
bestAskIV: asNumberIfValid(summary.askVol),
lastPrice: lastTickerInfo !== undefined ? asNumberIfValid(lastTickerInfo.last) : undefined,
openInterest: lastOpenInterest,
markPrice: lastMarkPrice,
markIV: asNumberIfValid(summary.markVol),
delta: asNumberIfValid(summary.delta),
gamma: asNumberIfValid(summary.gamma),
vega: asNumberIfValid(summary.vega),
theta: asNumberIfValid(summary.theta),
rho: undefined,
underlyingPrice: lastUnderlyingPrice,
underlyingIndex: summary.uly,
timestamp: new Date(Number(summary.ts)),
localTimestamp: localTimestamp
}
yield optionSummary
}
}
}
}
type OkexV5TradeMessage = {
arg: { channel: 'trades'; instId: 'CRV-USDT' }
data: [{ instId: 'CRV-USDT'; tradeId: '21300150'; px: '3.973'; sz: '13.491146'; side: 'buy'; ts: '1639999319938' }]
}
type OkexV5TradesAllMessage = {
arg: { channel: 'trades-all'; instId: string }
data: [{ instId: 'WAXP-USDT'; tradeId: '2251300'; px: '0.05566'; sz: '838.714488'; side: 'sell'; ts: '1697760000083' }]
}
type OkexV5BookLevel = [string, string, string, string]
type OkexV5BookMessage =
| {
arg: { channel: 'books-l2-tbt'; instId: string }
action: 'snapshot'
data: [
{
asks: OkexV5BookLevel[]
bids: OkexV5BookLevel[]
ts: string
}
]
}
| {
arg: { channel: 'books-l2-tbt'; instId: string }
action: 'update'
data: [{ asks: OkexV5BookLevel[]; bids: OkexV5BookLevel[]; ts: string }]
}
type OkexV5TickerMessage = {
arg: { channel: 'tickers'; instId: string }
data: [
{
instType: 'SPOT'
instId: 'ACT-USDT'
last: '0.00718'
lastSz: '8052.117146'
askPx: '0.0072'
askSz: '54969.407534'
bidPx: '0.00713'
bidSz: '4092.326'
open24h: '0.00717'
high24h: '0.00722'
low24h: '0.00696'
sodUtc0: '0.00714'
sodUtc8: '0.00721'
volCcy24h: '278377.765301'
vol24h: '39168761.49997'
ts: '1639999318686'
}
]
}
type OkexV5OpenInterestMessage = {
arg: { channel: 'open-interest'; instId: string }
data: [{ instId: 'FIL-USDT-220325'; instType: 'FUTURES'; oi: '236870'; oiCcy: '23687'; ts: '1640131202886' }]
}
type OkexV5MarkPriceMessage = {
arg: { channel: 'mark-price'; instId: string }
data: [{ instId: 'FIL-USDT-220325'; instType: 'FUTURES'; markPx: '36.232'; ts: '1640131204676' }]
}
type OkexV5IndexTickerMessage = {
arg: { channel: 'index-tickers'; instId: string }
data: [
{
instId: 'FIL-USDT'
idxPx: '35.583'
open24h: '34.558'
high24h: '35.862'
low24h: '34.529'
sodUtc0: '35.309'
sodUtc8: '34.83'
ts: '1640140200581'
}
]
}
type OkexV5FundingRateMessage = {
arg: { channel: 'funding-rate'; instId: string }
data: [{ fundingRate: '0.00048105' | undefined; fundingTime: '1640131200000'; instId: string; instType: 'SWAP'; nextFundingRate: '' }]
}
type OkexV5LiquidationMessage = {
arg: { channel: 'liquidations'; instId: 'BTC-USDT-211231'; generated: true }
data: [{ bkLoss: '0'; bkPx: '49674.2'; ccy: ''; posSide: 'short'; side: 'buy'; sz: '40'; ts: '1640140211925' }]
}
type OkexV5LiquidationOrderMessage = {
arg: { channel: 'liquidation-orders'; instType: 'FUTURES' }
data: [
{
details: [{ bkLoss: '0'; bkPx: '0.55205'; ccy: ''; posSide: 'short'; side: 'buy'; sz: '39'; ts: '1680173247614' }]
instFamily: 'XRP-USD'
instId: 'XRP-USD-230929'
instType: 'FUTURES'
uly: 'XRP-USD'
}
]
}
type OkexV5SummaryMessage = {
arg: { channel: 'opt-summary'; uly: 'ETH-USD' }
data: [
{
instType: 'OPTION'
instId: 'ETH-USD-211222-4000-C'
uly: 'ETH-USD'
delta: '0.1975745164'
gamma: '4.7290833601'
vega: '0.0002005415'
theta: '-0.004262964'
lever: '162.472613953'
markVol: '0.7794507758'
bidVol: '0.7421960156'
askVol: '0.8203208593'
realVol: ''
deltaBS: '0.2038286081'
gammaBS: '0.0013437829'
thetaBS: '-16.4798150221'
vegaBS: '0.7647227087'
ts: '1640001659301'
}
]
}
//---
//V3 Okex API mappers
// https://www.okex.com/docs/en/#ws_swap-README
export class OkexTradesMapper implements Mapper<OKEX_EXCHANGES, Trade> {
constructor(private readonly _exchange: Exchange, private readonly _market: OKEX_MARKETS) {}
canHandle(message: OkexDataMessage) {
return message.table === `${this._market}/trade`
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: `${this._market}/trade` as const,
symbols
}
]
}
*map(okexTradesMessage: OKexTradesDataMessage, localTimestamp: Date): IterableIterator<Trade> {
for (const okexTrade of okexTradesMessage.data) {
const symbol = okexTrade.instrument_id
yield {
type: 'trade',
symbol,
exchange: this._exchange,
id: typeof okexTrade.trade_id === 'string' ? okexTrade.trade_id : undefined,
price: Number(okexTrade.price),
amount: okexTrade.qty !== undefined ? Number(okexTrade.qty) : Number(okexTrade.size),
side: okexTrade.side,
timestamp: new Date(okexTrade.timestamp),
localTimestamp: localTimestamp
}
}
}
}
const mapBookLevel = (level: OkexBookLevel) => {
const price = Number(level[0])
const amount = Number(level[1])
return { price, amount }
}
export class OkexBookChangeMapper implements Mapper<OKEX_EXCHANGES, BookChange> {
constructor(
private readonly _exchange: Exchange,
private readonly _market: OKEX_MARKETS,
private readonly _canUseTickByTickChannel: boolean
) {}
canHandle(message: OkexDataMessage) {
const channelSuffix = this._canUseTickByTickChannel ? 'depth_l2_tbt' : 'depth'
return message.table === `${this._market}/${channelSuffix}`
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
if (this._canUseTickByTickChannel) {
return [
{
channel: `${this._market}/depth_l2_tbt` as const,
symbols
}
]
}
// subscribe to both book channels and in canHandle decide which one to use
// as one can subscribe to date range period that overlaps both when only depth channel has been available
// and when both were available (both depth and depth_l2_tbt)
return [
{
channel: `${this._market}/depth_l2_tbt`,
symbols
} as const,
{
channel: `${this._market}/depth`,
symbols
} as const
]
}
*map(okexDepthDataMessage: OkexDepthDataMessage, localTimestamp: Date): IterableIterator<BookChange> {
for (const message of okexDepthDataMessage.data) {
if (message.bids.length === 0 && message.asks.length === 0 && okexDepthDataMessage.action !== 'partial') {
continue
}
const timestamp = new Date(message.timestamp)
if (timestamp.valueOf() === 0) {
continue
}
yield {
type: 'book_change',
symbol: message.instrument_id,
exchange: this._exchange,
isSnapshot: okexDepthDataMessage.action === 'partial',
bids: message.bids.map(mapBookLevel),
asks: message.asks.map(mapBookLevel),
timestamp,
localTimestamp: localTimestamp
}
}
}
}
export class OkexDerivativeTickerMapper implements Mapper<'okex-futures' | 'okex-swap', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
private _futuresChannels = ['futures/ticker', 'futures/mark_price']
private _swapChannels = ['swap/ticker', 'swap/mark_price', 'swap/funding_rate']
constructor(private readonly _exchange: Exchange) {}
canHandle(message: OkexDataMessage) {
const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels
return channels.includes(message.table)
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
const channels = this._exchange === 'okex-futures' ? this._futuresChannels : this._swapChannels
return channels.map((channel) => {
return {
channel,
symbols
} as any
})
}
*map(
message: OkexTickersMessage | OkexFundingRateMessage | OkexMarkPriceMessage,
localTimestamp: Date
): IterableIterator<DerivativeTicker> {
for (const okexMessage of message.data) {
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(okexMessage.instrument_id, this._exchange)
if ('funding_rate' in okexMessage) {
pendingTickerInfo.updateFundingRate(Number(okexMessage.funding_rate))
pendingTickerInfo.updateFundingTimestamp(new Date(okexMessage.funding_time))
if (okexMessage.estimated_rate !== undefined) {
pendingTickerInfo.updatePredictedFundingRate(Number(okexMessage.estimated_rate))
}
}
if ('mark_price' in okexMessage) {
pendingTickerInfo.updateMarkPrice(Number(okexMessage.mark_price))
}
if ('open_interest' in okexMessage) {
const openInterest = Number(okexMessage.open_interest)
if (openInterest > 0) {
pendingTickerInfo.updateOpenInterest(Number(okexMessage.open_interest))
}
}
if ('last' in okexMessage) {
pendingTickerInfo.updateLastPrice(Number(okexMessage.last))
}
if (okexMessage.timestamp !== undefined) {
pendingTickerInfo.updateTimestamp(new Date(okexMessage.timestamp))
}
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
}
export class OkexOptionSummaryMapper implements Mapper<'okex-options', OptionSummary> {
private readonly _indexPrices = new Map<string, number>()
private readonly expiration_regex = /(\d{2})(\d{2})(\d{2})/
canHandle(message: OkexDataMessage) {
return message.table === 'index/ticker' || message.table === 'option/summary'
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
const indexes =
symbols !== undefined
? symbols.map((s) => {
const symbolParts = s.split('-')
return `${symbolParts[0]}-${symbolParts[1]}`
})
: undefined
return [
{
channel: `option/summary`,
symbols
} as const,
{
channel: `index/ticker`,
symbols: indexes
} as const
]
}
*map(message: OkexOptionSummaryData | OkexIndexData, localTimestamp: Date): IterableIterator<OptionSummary> | undefined {
if (message.table === 'index/ticker') {
for (const index of message.data) {
const lastIndexPrice = Number(index.last)
if (lastIndexPrice > 0) {
this._indexPrices.set(index.instrument_id, lastIndexPrice)
}
}
return
}
for (const summary of message.data) {
const symbolParts = summary.instrument_id.split('-')
const isPut = symbolParts[4] === 'P'
const strikePrice = Number(symbolParts[3])
var dateArray = this.expiration_regex.exec(symbolParts[2])!
const expirationDate = new Date(Date.UTC(+('20' + dateArray[1]), +dateArray[2] - 1, +dateArray[3], 8, 0, 0, 0))
const lastUnderlyingPrice = this._indexPrices.get(summary.underlying)
const optionSummary: OptionSummary = {
type: 'option_summary',
symbol: summary.instrument_id,
exchange: 'okex-options',
optionType: isPut ? 'put' : 'call',
strikePrice,
expirationDate,
bestBidPrice: asNumberIfValid(summary.best_bid),
bestBidAmount: asNumberIfValid(summary.best_bid_size),
bestBidIV: asNumberIfValid(summary.bid_vol),
bestAskPrice: asNumberIfValid(summary.best_ask),
bestAskAmount: asNumberIfValid(summary.best_ask_size),
bestAskIV: asNumberIfValid(summary.ask_vol),
lastPrice: asNumberIfValid(summary.last),
openInterest: asNumberIfValid(summary.open_interest),
markPrice: asNumberIfValid(summary.mark_price),
markIV: asNumberIfValid(summary.mark_vol),
delta: asNumberIfValid(summary.delta),
gamma: asNumberIfValid(summary.gamma),
vega: asNumberIfValid(summary.vega),
theta: asNumberIfValid(summary.theta),
rho: undefined,
underlyingPrice: lastUnderlyingPrice,
underlyingIndex: summary.underlying,
timestamp: new Date(summary.timestamp),
localTimestamp: localTimestamp
}
yield optionSummary
}
}
}
export class OkexLiquidationsMapper implements Mapper<OKEX_EXCHANGES, Liquidation> {
constructor(private readonly _exchange: Exchange, private readonly _market: OKEX_MARKETS) {}
canHandle(message: OkexDataMessage) {
return message.table === `${this._market}/liquidation`
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: `${this._market}/liquidation`,
symbols
} as any
]
}
*map(okexLiquidationDataMessage: OkexLiqudationDataMessage, localTimestamp: Date): IterableIterator<Liquidation> {
for (const okexLiquidation of okexLiquidationDataMessage.data) {
const liquidation: Liquidation = {
type: 'liquidation',
symbol: okexLiquidation.instrument_id,
exchange: this._exchange,
id: undefined,
price: Number(okexLiquidation.price),
amount: Number(okexLiquidation.size),
side: okexLiquidation.type === '3' ? 'sell' : 'buy',
timestamp: new Date(okexLiquidation.created_at),
localTimestamp: localTimestamp
}
yield liquidation
}
}
}
export class OkexBookTickerMapper implements Mapper<OKEX_EXCHANGES, BookTicker> {
constructor(private readonly _exchange: Exchange, private readonly _market: OKEX_MARKETS) {}
canHandle(message: OkexDataMessage) {
return message.table === `${this._market}/ticker`
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: `${this._market}/ticker`,
symbols
} as any
]
}
*map(message: OkexTickersMessage, localTimestamp: Date): IterableIterator<BookTicker> {
for (const okexTicker of message.data) {
const ticker: BookTicker = {
type: 'book_ticker',
symbol: okexTicker.instrument_id,
exchange: this._exchange,
askAmount: asNumberIfValid(okexTicker.best_ask_size),
askPrice: asNumberIfValid(okexTicker.best_ask),
bidPrice: asNumberIfValid(okexTicker.best_bid),
bidAmount: asNumberIfValid(okexTicker.best_bid_size),
timestamp: new Date(okexTicker.timestamp),
localTimestamp: localTimestamp
}
yield ticker
}
}
}
type OkexDataMessage = {
table: string
}
type OKexTradesDataMessage = {
data: {
side: 'buy' | 'sell'
trade_id: string | number
price: string | number
qty?: string | number
size?: string | number
instrument_id: string
timestamp: string
}[]
}
type OkexLiqudationDataMessage = {
data: {
loss: string
size: string
price: string
created_at: string
type: string
instrument_id: string
}[]
}
type OkexTickersMessage = {
data: {
last: string | number
best_bid: string | number
best_ask: string | number
open_interest: string | undefined
instrument_id: string
timestamp: string
best_bid_size: string | undefined
best_ask_size: string | undefined
}[]
}
type OkexFundingRateMessage = {
data: {
funding_rate: string
funding_time: string
estimated_rate?: string
instrument_id: string
timestamp: undefined
}[]
}
type OkexMarkPriceMessage = {
data: {
instrument_id: string
mark_price: string
timestamp: string
}[]
}
type OkexDepthDataMessage = {
action: 'partial' | 'update'
data: {
instrument_id: string
asks: OkexBookLevel[]
bids: OkexBookLevel[]
timestamp: string
}[]
}
type OkexBookLevel = [number | string, number | string, number | string, number | string]
type OKEX_EXCHANGES = 'okex' | 'okcoin' | 'okex-futures' | 'okex-swap' | 'okex-options'
type OKEX_MARKETS = 'spot' | 'swap' | 'futures' | 'option'
type OkexIndexData = {
table: 'index/ticker'
data: [
{
last: number
instrument_id: string
}
]
}
type OkexOptionSummaryData = {
table: 'option/summary'
data: [
{
instrument_id: string
underlying: string
best_ask: string
best_bid: string
best_ask_size: string
best_bid_size: string
change_rate: string
delta: string
gamma: string
bid_vol: string
ask_vol: string
mark_vol: string
last: string
leverage: string
mark_price: string
theta: string
vega: string
open_interest: string
timestamp: string
}
]
}
type OkexBBOTbtData = {
arg: { channel: 'bbo-tbt'; instId: 'WAVES-USDT-SWAP' }
data: [{ asks: [['16.083', '65', '0', '1']]; bids: [['16.082', '143', '0', '4']]; ts: '1651750920000' }]
}