tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
188 lines (159 loc) • 5.89 kB
text/typescript
import crypto from 'crypto'
import { wait } from '../handy'
import { Filter } from '../types'
import { MultiConnectionRealTimeFeedBase, RealTimeFeedBase } from './realtimefeed'
export class OkexRealTimeFeed extends MultiConnectionRealTimeFeedBase {
protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
const nonBusinessFilters = filters.filter((f) => f.channel !== 'trades-all')
if (nonBusinessFilters.length > 0) {
yield new OkexSingleRealTimeFeed('wss://ws.okx.com:8443/ws/v5/public', exchange, nonBusinessFilters, timeoutIntervalMS, onError)
}
const businessFilters = filters.filter((f) => f.channel === 'trades-all')
if (businessFilters.length > 0) {
yield new OkexSingleRealTimeFeed('wss://ws.okx.com:8443/ws/v5/business', exchange, businessFilters, timeoutIntervalMS, onError)
}
}
}
class OkexSingleRealTimeFeed extends RealTimeFeedBase {
constructor(
protected wssURL: string,
exchange: string,
filters: Filter<string>[],
timeoutIntervalMS: number | undefined,
onError?: (error: Error) => void
) {
super(exchange, filters, timeoutIntervalMS, onError)
}
private _hasCredentials = process.env.OKX_API_KEY !== undefined
private secondsSinceEpoch() {
return Math.floor(Date.now() / 1000)
}
protected async onConnected() {
if (this._hasCredentials) {
const timestamp = this.secondsSinceEpoch().toString()
const sign = crypto.createHmac('sha256', process.env.OKX_API_SECRET_KEY!).update(`${timestamp}GET/users/self/verify`).digest('base64')
this.send({
op: 'login',
args: [
{
apiKey: process.env.OKX_API_KEY,
passphrase: process.env.OKX_API_PASSPHRASE,
timestamp,
sign
}
]
})
await wait(50)
}
}
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
const args = filters
.filter((f) => f.channel != 'liquidations')
.map((filter) => {
if (!filter.symbols || filter.symbols.length === 0) {
throw new Error(`${this._exchange} RealTimeFeed requires explicitly specified symbols when subscribing to live feed`)
}
return filter.symbols.map((symbol) => {
if (filter.channel === 'liquidation-orders') {
return {
channel: filter.channel,
instType: symbol.endsWith('SWAP') ? 'SWAP' : 'FUTURES'
}
}
return {
channel: filter.channel,
instId: symbol
}
})
})
.flatMap((s) => s)
return [
{
op: 'subscribe',
args: [...new Set(args)]
}
]
}
protected messageIsError(message: any): boolean {
return message.event === 'error'
}
protected isIgnoredError(message: any) {
if (message.msg.includes('channel') && message.msg.includes(`doesn't exist`)) {
return true
}
return false
}
}
export class OKCoinRealTimeFeed extends OkexSingleRealTimeFeed {
constructor(exchange: string, filters: Filter<string>[], timeoutIntervalMS: number | undefined, onError?: (error: Error) => void) {
super('wss://real.okcoin.com:8443/ws/v5/public', exchange, filters, timeoutIntervalMS, onError)
}
}
export class OkexOptionsRealTimeFeed extends MultiConnectionRealTimeFeedBase {
protected *_getRealTimeFeeds(exchange: string, filters: Filter<string>[], timeoutIntervalMS?: number, onError?: (error: Error) => void) {
const nonBusinessFilters = filters.filter((f) => f.channel !== 'trades-all')
if (nonBusinessFilters.length > 0) {
yield new OkexOptionsSingleRealTimeFeed(
'wss://ws.okx.com:8443/ws/v5/public',
exchange,
nonBusinessFilters,
timeoutIntervalMS,
onError
)
}
const businessFilters = filters.filter((f) => f.channel === 'trades-all')
if (businessFilters.length > 0) {
yield new OkexOptionsSingleRealTimeFeed('wss://ws.okx.com:8443/ws/v5/business', exchange, businessFilters, timeoutIntervalMS, onError)
}
}
}
class OkexOptionsSingleRealTimeFeed extends OkexSingleRealTimeFeed {
constructor(
protected wssURL: string,
exchange: string,
filters: Filter<string>[],
timeoutIntervalMS: number | undefined,
onError?: (error: Error) => void
) {
super(wssURL, exchange, filters, timeoutIntervalMS, onError)
}
private _defaultIndexes = ['BTC-USD', 'ETH-USD']
private _channelRequiresIndexNotSymbol(channel: string) {
if (channel === 'index-tickers' || channel === 'opt-summary') {
return true
}
return false
}
protected mapToSubscribeMessages(filters: Filter<string>[]): any[] {
const args = filters
.map((filter) => {
let symbols = filter.symbols || []
const channelRequiresIndexNotSymbol = this._channelRequiresIndexNotSymbol(filter.channel)
if (symbols.length === 0 && channelRequiresIndexNotSymbol) {
symbols = this._defaultIndexes
}
if (symbols.length === 0) {
throw new Error(`${this._exchange} RealTimeFeed requires explicitly specified symbols when subscribing to live feed`)
}
return symbols.map((symbol) => {
let finalSymbol = symbol
if (channelRequiresIndexNotSymbol) {
const symbolParts = symbol.split('-')
finalSymbol = `${symbolParts[0]}-${symbolParts[1]}`
}
return {
channel: filter.channel,
instId: filter.channel !== 'opt-summary' ? finalSymbol : undefined,
instFamily: filter.channel === 'opt-summary' ? finalSymbol : undefined
}
})
})
.flatMap((s) => s)
return [
{
op: 'subscribe',
args: [...new Set(args)]
}
]
}
}