tardis-machine
Version:
Locally runnable server with built-in data caching, providing both tick-level historical and consolidated real-time cryptocurrency market data via HTTP and WebSocket APIs
719 lines (626 loc) • 17.1 kB
text/typescript
import { Exchange, Filter } from 'tardis-dev'
// https://www.bitmex.com/app/wsAPI
const bitmexMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op === 'subscribe'
},
map: (message: any) => {
const args = typeof message.args === 'string' ? [message.args] : message.args
return args.map((arg: string) => {
const channelSymbols = arg.split(':')
if (channelSymbols.length == 1) {
return {
channel: channelSymbols[0]
}
}
return {
channel: channelSymbols[0],
symbols: [channelSymbols[1]]
}
})
}
}
// https://docs.pro.coinbase.com/#protocol-overview
const coinbaseMaper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.type === 'subscribe'
},
map: (message: any) => {
const topLevelSymbols = message.product_ids
const finalChannels: Filter<any>[] = []
const channelMappings = {
full: ['received', 'open', 'done', 'match', 'change', 'full_snapshot'],
level2: ['snapshot', 'l2update'],
matches: ['match', 'last_match'],
ticker: ['ticker']
}
message.channels.forEach((channel: any) => {
const channelName = typeof channel == 'string' ? channel : channel.name
const symbols = typeof channel == 'string' ? topLevelSymbols : channel.product_ids
const mappedChannels = (channelMappings as any)[channelName]
mappedChannels.forEach((channel: string) => {
finalChannels.push({
channel,
symbols
})
})
})
return finalChannels
}
}
// https://docs.deribit.com/v2/#subscription-management
const deribitMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method === 'public/subscribe'
},
map: (message: any) => {
return message.params.channels.map((channel: string) => {
const lastSeparator = channel.lastIndexOf('.')
const firstSeparator = channel.indexOf('.')
return {
channel: channel.slice(0, firstSeparator),
// handle both
// "deribit_price_ranking.btc_usd" and "book.ETH-PERPETUAL.100.1.100ms" cases
// we need to extract channel name and symbols out of such strings
symbols: [channel.slice(firstSeparator + 1, lastSeparator == firstSeparator ? undefined : lastSeparator)]
}
})
}
}
// https://www.cryptofacilities.com/resources/hc/en-us/sections/360000120914-Websocket-API-Public
const cryptofacilitiesMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.event == 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.feed,
symbols: message.product_ids
}
]
}
}
// https://www.bitstamp.net/websocket/v2/
const bitstampMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.event == 'bts:subscribe'
},
map: (message: any) => {
const separator = message.data.channel.lastIndexOf('_')
return [
{
channel: message.data.channel.slice(0, separator),
symbols: [message.data.channel.slice(separator + 1)]
}
]
}
}
// https://www.okex.com/docs/en/#spot_ws-sub
const okexMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op == 'subscribe'
},
map: (message: any) => {
return message.args.map((arg: string) => {
const separator = arg.indexOf(':')
return {
channel: arg.slice(0, separator),
symbols: [arg.slice(separator + 1)]
}
})
}
}
// https://docs.ftx.com/#request-format
const ftxMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op === 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.channel,
symbols: [message.market]
}
]
}
}
// https://www.kraken.com/features/websocket-api#message-subscribe
const krakenMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.event === 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.subscription.name,
symbols: message.pair
}
]
}
}
// https://lightning.bitflyer.com/docs?lang=en#json-rpc-2.0-over-websocket
const bitflyerMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method === 'subscribe'
},
map: (message: any) => {
const availableChannels = ['lightning_board_snapshot', 'lightning_board', 'lightning_ticker', 'lightning_executions']
const inputChannel = message.params.channel as string
const channel = availableChannels.find((c) => inputChannel.startsWith(c))!
const symbol = inputChannel.slice(channel.length + 1)
return [
{
channel,
symbols: [symbol]
}
]
}
}
// https://docs.gemini.com/websocket-api/#market-data-version-2
const geminiMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.type === 'subscribe'
},
map: (message: any) => {
const finalChannels: Filter<any>[] = []
const channelMappings = {
l2: ['trade', 'l2_updates', 'auction_open', 'auction_indicative', 'auction_result']
}
message.subscriptions.forEach((sub: any) => {
const matchingChannels = (channelMappings as any)[sub.name]
matchingChannels.forEach((channel: string) => {
finalChannels.push({
channel,
symbols: sub.symbols
})
})
})
return finalChannels
}
}
// https://binance-docs.github.io/apidocs/futures/en/#live-subscribing-unsubscribing-to-streams
const binanceMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method === 'SUBSCRIBE'
},
map: (message: any) => {
return (message.params as string[]).map((param) => {
const lastSeparator = param.lastIndexOf('@')
const firstSeparator = param.indexOf('@')
return {
channel: param.slice(firstSeparator + 1, lastSeparator == firstSeparator ? undefined : lastSeparator),
symbols: [param.slice(0, firstSeparator)]
}
})
}
}
// https://docs.binance.org/api-reference/dex-api/ws-connection.html
const binanceDEXMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method === 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.topic,
symbols: message.symbols
}
]
}
}
// https://huobiapi.github.io/docs/spot/v1/en/#websocket-market-data
const huobiMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.sub !== undefined
},
map: (message: any) => {
const pieces = message.sub.split('.')
return [
{
channel: pieces[2],
symbols: [pieces[1]]
}
]
}
}
// https://github.com/bybit-exchange/bybit-official-api-docs/blob/master/en/websocket.md
const bybitMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op === 'subscribe'
},
map: (message: any) => {
return (message.args as string[]).map((arg) => {
const pieces = arg.split('.')
return {
channel: pieces[0],
symbols: [pieces[pieces.length - 1]]
}
})
}
}
const bybitSpotMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.event === 'sub'
},
map: (message: any) => {
return [
{
channel: message.topic,
symbols: [message.symbol]
}
]
}
}
const blockchainComMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.action === 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.channel,
symbols: [message.symbol]
}
]
}
}
// https://api.hitbtc.com/#subscribe-to-trades
const hitBtcMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method !== undefined
},
map: (message: any) => {
const channelMappings = {
subscribeTrades: ['snapshotTrades', 'updateTrades'],
subscribeOrderbook: ['snapshotOrderbook', 'updateOrderbook']
} as any
return channelMappings[message.method].map((channel: string) => {
return {
channel,
symbols: [message.params.symbol]
}
})
}
}
const bitfinexMapper: SubscriptionMapper = {
canHandle: () => true,
map: () => []
}
const coinflexMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op === 'subscribe'
},
map: (message: any) => {
return message.args.map((arg: string) => {
const split = arg.split(':')
return {
channel: split[0],
symbols: [split[1]]
}
})
}
}
const phemexMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method !== undefined
},
map: (message: any) => {
const channelsMapping = {
'orderbook.subscribe': 'book',
'trade.subscribe': 'trades',
'market24h.subscribe': 'market24h'
} as any
return [
{
channel: channelsMapping[message.method],
symbols: message.params
}
]
}
}
const deltaMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.type === 'subscribe'
},
map: (message: any) => {
return message.payload.channels.map((channel: any) => {
return {
channel: channel.name,
symbols:
channel.symbols !== undefined && channel.name === 'mark_price' ? channel.symbols.map((s: any) => `MARK:${s}`) : channel.symbols
}
})
}
}
const gateIOMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method !== undefined && message.method.endsWith('.subscribe')
},
map: (message: any) => {
return [
{
channel: message.method.split('.')[0],
symbols: message.params.map((s: any) => {
if (typeof s === 'string') {
return s
}
return s[0] as string
})
}
]
}
}
const gateIOFuturesMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.event === 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.channel.split('.')[1],
symbols: message.payload.map((s: any) => {
if (typeof s === 'string') {
return s
}
return s[0] as string
})
}
]
}
}
const poloniexMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.command === 'subscribe'
},
map: (message: any) => {
return [
{
channel: 'price_aggregated_book',
symbols: [message.channel]
}
]
}
}
const ascendexMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op === 'sub' || message.op === 'req'
},
map: (message: any) => {
const channel = message.action || message.ch.split(':')[0]
const symbol = (message.args && message.args.symbol) || message.ch.split(':')[1]
return [
{
channel,
symbols: symbol ? [symbol] : []
}
]
}
}
const dydxMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.type === 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.channel,
symbols: message.id ? [message.id] : []
}
]
}
}
const upbitMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return Array.isArray(message)
},
map: (message: any) => {
return message
.filter((m: any) => {
return m.type !== undefined
})
.map((m: any) => {
return {
channel: m.type,
symbols: m.codes
}
})
}
}
const serumMaper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op === 'subscribe'
},
map: (message: any) => {
const finalChannels: Filter<any>[] = []
const channelMappings = {
trades: ['recent_trades', 'trade'],
level1: ['quote'],
level2: ['l2snapshot', 'l2update'],
level3: ['l3snapshot', 'open', 'fill', 'change', 'done']
}
const symbols = message.markets
const mappedChannels = (channelMappings as any)[message.channel]
mappedChannels.forEach((channel: string) => {
finalChannels.push({
channel,
symbols
})
})
return finalChannels
}
}
const cryptoComMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method === 'subscribe'
},
map: (message: any) => {
return message.params.channels.map((channel: string) => {
const parts = channel.split('.')
return {
channel: parts[1],
symbols: [parts[0]]
}
})
}
}
const kucoinMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.type === 'subscribe'
},
map: (message: any) => {
// "topic": "/market/ticker:BTC-USDT,ETH-USDT",
const parts = message.topic.split(':') as string[]
const symbols = parts[1].split(',')
return [
{
channel: parts[0].substring(1),
symbols
}
]
}
}
const bitnomialMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.type === 'subscribe'
},
map: (message: any) => {
const topLevelSymbols = message.product_codes
const finalChannels: Filter<any>[] = []
const channelMappings = {
book: ['book', 'levels'],
trade: ['trade'],
block: ['block']
}
message.channels.forEach((channel: any) => {
const channelName = typeof channel == 'string' ? channel : channel.name
const symbols = typeof channel == 'string' ? topLevelSymbols : channel.product_codes
const mappedChannels = (channelMappings as any)[channelName]
mappedChannels.forEach((channel: string) => {
finalChannels.push({
channel,
symbols
})
})
})
return finalChannels
}
}
const wooxMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.event === 'subscribe'
},
map: (message: any) => {
const [symbol, channel] = message.topic.split('@')
return [
{
channel,
symbols: symbol
}
]
}
}
const bitgetMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.op === 'subscribe'
},
map: (message: any) => {
return message.args.map((arg: any) => {
return {
channel: arg.channel,
symbols: [arg.instId]
}
})
}
}
const coinbaseInternationalMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.type === 'SUBSCRIBE'
},
map: (message: any) => {
return message.channels.map((channel: string) => {
return {
channel,
symbols: message.product_ids
}
})
}
}
const hyperliquidMapper: SubscriptionMapper = {
canHandle: (message: any) => {
return message.method === 'subscribe'
},
map: (message: any) => {
return [
{
channel: message.type,
symbols: [message.coin]
}
]
}
}
export const subscriptionsMappers: { [key in Exchange]: SubscriptionMapper } = {
bitmex: bitmexMapper,
coinbase: coinbaseMaper,
deribit: deribitMapper,
cryptofacilities: cryptofacilitiesMapper,
bitstamp: bitstampMapper,
okex: okexMapper,
'okex-futures': okexMapper,
'okex-swap': okexMapper,
'okex-options': okexMapper,
ftx: ftxMapper,
'ftx-us': ftxMapper,
kraken: krakenMapper,
bitflyer: bitflyerMapper,
gemini: geminiMapper,
binance: binanceMapper,
'binance-futures': binanceMapper,
'binance-delivery': binanceMapper,
'binance-jersey': binanceMapper,
'binance-us': binanceMapper,
'binance-dex': binanceDEXMapper,
huobi: huobiMapper,
'huobi-dm': huobiMapper,
'huobi-dm-swap': huobiMapper,
'huobi-dm-linear-swap': huobiMapper,
bybit: bybitMapper,
bitfinex: bitfinexMapper,
'bitfinex-derivatives': bitfinexMapper,
okcoin: okexMapper,
hitbtc: hitBtcMapper,
coinflex: coinflexMapper,
phemex: phemexMapper,
delta: deltaMapper,
'gate-io': gateIOMapper,
'gate-io-futures': gateIOFuturesMapper,
poloniex: poloniexMapper,
ascendex: ascendexMapper,
dydx: dydxMapper,
'dydx-v4': dydxMapper,
'huobi-dm-options': huobiMapper,
'binance-options': binanceMapper,
upbit: upbitMapper,
serum: serumMaper,
'star-atlas': serumMaper,
mango: serumMaper,
'bybit-spot': bybitSpotMapper,
'crypto-com': cryptoComMapper,
'crypto-com-derivatives': cryptoComMapper,
kucoin: kucoinMapper,
bitnomial: bitnomialMapper,
'woo-x': wooxMapper,
'blockchain-com': blockchainComMapper,
'bybit-options': bybitMapper,
'binance-european-options': binanceMapper,
'okex-spreads': okexMapper,
'kucoin-futures': kucoinMapper,
bitget: bitgetMapper,
'bitget-futures': bitgetMapper,
'coinbase-international': coinbaseInternationalMapper,
hyperliquid: hyperliquidMapper
}
export type SubscriptionMapper = {
canHandle: (message: object) => boolean
map: (message: object) => Filter<string>[]
}