tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
435 lines (390 loc) • 11.7 kB
text/typescript
import { upperCaseSymbols } from '../handy'
import { BookChange, Exchange, BookTicker, Trade, DerivativeTicker } from '../types'
import { Mapper, PendingTickerInfoHelper } from './mapper'
export class CryptoComTradesMapper implements Mapper<'crypto-com' | 'crypto-com-derivatives', Trade> {
constructor(private readonly _exchange: Exchange) {}
canHandle(message: CryptoComTradeMessage) {
return message.result !== undefined && message.result.channel === 'trade'
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: 'trade',
symbols
} as const
]
}
*map(message: CryptoComTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
message.result.data.reverse()
for (const item of message.result.data) {
const trade: Trade = {
type: 'trade',
symbol: message.result.instrument_name,
exchange: this._exchange,
id: item.d.toString(),
price: Number(item.p),
amount: Number(item.q),
side: item.s === 'BUY' ? 'buy' : 'sell',
timestamp: new Date(item.t),
localTimestamp
}
yield trade
}
}
}
export class CryptoComBookChangeMapper implements Mapper<'crypto-com' | 'crypto-com-derivatives', BookChange> {
constructor(protected readonly _exchange: Exchange) {}
canHandle(message: CryptoComBookMessage) {
return message.result !== undefined && message.result.channel.startsWith('book')
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: 'book',
symbols
} as const
]
}
*map(message: CryptoComBookMessage, localTimestamp: Date) {
if (message.result.data === undefined || message.result.data[0] === undefined) {
return
}
const bids = (message.result.channel === 'book' ? message.result.data[0].bids : message.result.data[0].update.bids) || []
const asks = (message.result.channel === 'book' ? message.result.data[0].asks : message.result.data[0].update.asks) || []
yield {
type: 'book_change',
symbol: message.result.instrument_name,
exchange: this._exchange,
isSnapshot: message.result.channel === 'book',
bids: bids.map(this._mapBookLevel),
asks: asks.map(this._mapBookLevel),
timestamp: new Date(message.result.data[0].t),
localTimestamp
} as const
}
private _mapBookLevel(level: [number | string, number | string]) {
return { price: Number(level[0]), amount: Number(level[1]) }
}
}
export class CryptoComBookTickerMapper implements Mapper<'crypto-com' | 'crypto-com-derivatives', BookTicker> {
constructor(protected readonly _exchange: Exchange) {}
canHandle(message: CryptoComTickerMessage) {
return message.result !== undefined && message.result.channel === 'ticker'
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
return [
{
channel: 'ticker',
symbols
} as const
]
}
*map(message: CryptoComTickerMessage, localTimestamp: Date) {
for (const item of message.result.data) {
const bookTicker: BookTicker = {
type: 'book_ticker',
symbol: message.result.instrument_name,
exchange: this._exchange,
askAmount: undefined,
askPrice: item.k !== undefined && item.k !== null ? Number(item.k) : undefined,
bidPrice: item.b !== undefined && item.b !== null ? Number(item.b) : undefined,
bidAmount: undefined,
timestamp: new Date(item.t),
localTimestamp: localTimestamp
}
yield bookTicker
}
}
}
export class CryptoComDerivativeTickerMapper implements Mapper<'crypto-com-derivatives', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
private readonly _indexPrices = new Map<string, number>()
constructor(protected readonly exchange: Exchange) {}
canHandle(message: CryptoComDerivativesTickerMessage | CryptoComIndexMessage | CryptoComMarkPriceMessage | CryptoComFundingMessage) {
if (message.result === undefined) {
return false
}
if (message.result.instrument_name === undefined) {
return false
}
// spot symbols
if (message.result.instrument_name.includes('_')) {
return false
}
// options
if (message.result.instrument_name.split('-').length === 3) {
return false
}
return (
message.result.channel === 'ticker' ||
message.result.channel === 'index' ||
message.result.channel === 'mark' ||
message.result.channel === 'funding'
)
}
getFilters(symbols?: string[]) {
symbols = upperCaseSymbols(symbols)
let indexes: string[] = []
if (symbols !== undefined) {
indexes = [...new Set(symbols.map((s) => `${s.split('-')[0]}-INDEX`))]
}
const filters = [
{
channel: 'ticker',
symbols
} as const,
{
channel: 'index',
symbols: indexes
} as const,
{
channel: 'mark',
symbols
} as const,
{
channel: 'funding',
symbols
} as const,
{
channel: 'estimatedfunding',
symbols
} as const
]
return filters
}
*map(
message:
| CryptoComDerivativesTickerMessage
| CryptoComIndexMessage
| CryptoComMarkPriceMessage
| CryptoComFundingMessage
| CryptoComEstFundingMessage,
localTimestamp: Date
): IterableIterator<DerivativeTicker> {
if (message.result.channel === 'index') {
this._indexPrices.set(message.result.instrument_name.split('-')[0], Number(message.result.data[0].v))
return
}
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.result.instrument_name, this.exchange)
const lastIndexPrice = this._indexPrices.get(message.result.instrument_name.split('-')[0])
if (lastIndexPrice !== undefined) {
pendingTickerInfo.updateIndexPrice(lastIndexPrice)
}
if (message.result.channel === 'ticker') {
if (message.result.data[0].a !== null && message.result.data[0].a !== undefined) {
pendingTickerInfo.updateLastPrice(Number(message.result.data[0].a))
}
if (message.result.data[0].oi !== null && message.result.data[0].oi !== undefined) {
pendingTickerInfo.updateOpenInterest(Number(message.result.data[0].oi))
}
}
if (message.result.channel === 'mark') {
if (message.result.data[0].v !== null && message.result.data[0].v !== undefined) {
pendingTickerInfo.updateMarkPrice(Number(message.result.data[0].v))
}
}
if (message.result.channel === 'funding') {
if (message.result.data[0].v !== null && message.result.data[0].v !== undefined) {
pendingTickerInfo.updateFundingRate(Number(message.result.data[0].v))
const nextFundingTimestamp = new Date(message.result.data[0].t)
nextFundingTimestamp.setUTCHours(nextFundingTimestamp.getUTCHours() + 1)
nextFundingTimestamp.setUTCMinutes(0, 0, 0)
pendingTickerInfo.updateFundingTimestamp(nextFundingTimestamp)
}
}
if (message.result.channel === 'estimatedfunding') {
if (message.result.data[0] && message.result.data[0].v !== null && message.result.data[0].v !== undefined) {
pendingTickerInfo.updatePredictedFundingRate(Number(message.result.data[0].v))
}
}
pendingTickerInfo.updateTimestamp(new Date(message.result.data[0].t))
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
type CryptoComTradeMessage =
| {
method: 'subscribe'
result: {
instrument_name: 'ETH_CRO' // instrument_name
subscription: 'trade.ETH_CRO'
channel: 'trade'
data: [
{
p: 162.12 // price
q: 11.085 // quantity
s: 'BUY' // side
d: 1210447366 // trade id
t: 1587523078844 // trade time
dataTime: 0 // please ignore this field
}
]
}
}
| {
id: -1
code: 0
method: 'subscribe'
result: {
channel: 'trade'
subscription: 'trade.BTCUSD-PERP'
instrument_name: 'BTCUSD-PERP'
data: [{ d: '4611686018439397540'; t: 1653992578435; p: '31603.5'; q: '0.1000'; s: 'BUY'; i: 'BTCUSD-PERP' }]
}
}
type CryptoComBookMessage =
| {
code: 0
method: 'subscribe'
result: {
instrument_name: 'ETH_CRO'
subscription: 'book.ETH_CRO.150'
channel: 'book'
depth: 150
data: [
{
bids: [number, number][]
asks: [number, number][]
t: 1659311999933
s: 788293808
}
]
}
}
| {
code: 0
method: 'subscribe'
result: {
instrument_name: 'DOT_USDT'
subscription: 'book.DOT_USDT.150'
channel: 'book.update'
depth: 150
data: [
{
update: { bids: [number, number][]; asks: [number, number][] }
t: 1659312000046
s: 763793123
}
]
}
}
| {
id: -1
code: 0
method: 'subscribe'
result: {
channel: 'book.update'
subscription: 'book.BTCUSD-PERP.50'
instrument_name: 'BTCUSD-PERP'
depth: 50
data: [
{
update: { asks: [string, string][]; bids: [string, string][] }
t: 1653992578436
tt: 1653992578428
u: 72560693920
pu: 72560688000
cs: 380529173
}
]
}
}
type CryptoComTickerMessage =
| {
code: 0
method: 'subscribe'
result: {
instrument_name: 'GODS_USDT'
subscription: 'ticker.GODS_USDT'
channel: 'ticker'
data: [
{
i: 'GODS_USDT'
b: 0.4262
k: 0.4272
a: 0.4272
t: 1659311999946
v: 100623.01
vv: 42986.1541
h: 0.4624
l: 0.4229
c: -0.0062
pc: -1.4302
}
]
}
}
| CryptoComDerivativesTickerMessage
type CryptoComDerivativesTickerMessage = {
id: -1
code: 0
method: 'subscribe'
result: {
channel: 'ticker'
instrument_name: 'BTCUSD-PERP'
subscription: 'ticker.BTCUSD-PERP'
data: [
{
h: '32222.5'
l: '30240.0'
a: '31611.0'
c: '0.0320'
b: '31613.0'
k: '31613.5'
i: 'BTCUSD-PERP'
v: '13206.4884'
vv: '433945264.39'
oi: '318.5162'
t: 1653992543383
}
]
}
}
type CryptoComIndexMessage = {
id: -1
method: 'subscribe'
code: 0
result: {
instrument_name: 'BTCUSD-INDEX'
subscription: 'index.BTCUSD-INDEX'
channel: 'index'
data: [{ v: '31601.35'; t: 1653992545000 }]
}
}
type CryptoComMarkPriceMessage = {
id: 1
method: 'subscribe'
code: 0
result: {
instrument_name: 'BTCUSD-PERP'
subscription: 'mark.BTCUSD-PERP'
channel: 'mark'
data: [{ v: '31606.3'; t: 1653992543000 }]
}
}
type CryptoComFundingMessage = {
id: -1
method: 'subscribe'
code: 0
result: {
instrument_name: 'BTCUSD-PERP'
subscription: 'funding.BTCUSD-PERP'
channel: 'funding'
data: [{ v: '0.00000700'; t: 1653992579000 }]
}
}
type CryptoComEstFundingMessage = {
id: 1
method: 'subscribe'
code: 0
result: {
instrument_name: 'AAVEUSD-PERP'
subscription: 'estimatedfunding.AAVEUSD-PERP'
channel: 'estimatedfunding'
data: [{ v: '0.000039493'; t: 1727308799000 }]
}
}