UNPKG

tardis-dev

Version:

Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js

329 lines (281 loc) 9.43 kB
import { addMinutes, upperCaseSymbols } from '../handy' import { BookChange, BookPriceLevel, BookTicker, DerivativeTicker, Trade } from '../types' import { Mapper, PendingTickerInfoHelper } from './mapper' export const coinbaseInternationalTradesMapper: Mapper<'coinbase-international', Trade> = { canHandle(message: CoinbaseInternationalTradeMessage) { return message.channel === 'MATCH' && message.type === 'UPDATE' }, getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'MATCH', symbols } ] }, *map(message: CoinbaseInternationalTradeMessage, localTimestamp: Date): IterableIterator<Trade> { yield { type: 'trade', symbol: message.product_id, exchange: 'coinbase-international', id: message.match_id, price: Number(message.trade_price), amount: Number(message.trade_qty), side: message.aggressor_side === 'SELL' ? 'sell' : message.aggressor_side === 'BUY' ? 'buy' : 'unknown', timestamp: new Date(message.time), localTimestamp: localTimestamp } } } const mapUpdateBookLevel = (level: CoinbaseInternationalUpdateBookLevel) => { const price = Number(level[1]) const amount = Number(level[2]) return { price, amount } } const mapSnapshotBookLevel = (level: CoinbaseInternationalSnapshotBookLevel) => { const price = Number(level[0]) const amount = Number(level[1]) return { price, amount } } const validAmountsOnly = (level: BookPriceLevel) => { if (Number.isNaN(level.amount)) { return false } if (level.amount < 0) { return false } return true } export class CoinbaseInternationalBookChangMapper implements Mapper<'coinbase-international', BookChange> { canHandle(message: CoinbaseInternationalLevel2Snapshot | CoinbaseInternationalLevel2Update) { return message.channel === 'LEVEL2' && (message.type === 'SNAPSHOT' || message.type === 'UPDATE') } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'LEVEL2', symbols } as const ] } *map( message: CoinbaseInternationalLevel2Snapshot | CoinbaseInternationalLevel2Update, localTimestamp: Date ): IterableIterator<BookChange> { if (message.type === 'SNAPSHOT') { let timestamp if (message.time !== undefined) { timestamp = new Date(message.time) if (timestamp.valueOf() < 0) { timestamp = localTimestamp } } else { timestamp = localTimestamp } yield { type: 'book_change', symbol: message.product_id, exchange: 'coinbase-international', isSnapshot: true, bids: message.bids.map(mapSnapshotBookLevel).filter(validAmountsOnly), asks: message.asks.map(mapSnapshotBookLevel).filter(validAmountsOnly), timestamp, localTimestamp } } else { let timestamp = new Date(message.time) yield { type: 'book_change', symbol: message.product_id, exchange: 'coinbase-international', isSnapshot: false, bids: message.changes.filter((c) => c[0] === 'BUY').map(mapUpdateBookLevel), asks: message.changes.filter((c) => c[0] === 'SELL').map(mapUpdateBookLevel), timestamp, localTimestamp: localTimestamp } } } } export const coinbaseInternationalBookTickerMapper: Mapper<'coinbase-international', BookTicker> = { canHandle(message: CoinbaseInternationalLevel1Message) { return message.channel === 'LEVEL1' && (message.type === 'SNAPSHOT' || message.type === 'UPDATE') }, getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'LEVEL1', symbols } ] }, *map(message: CoinbaseInternationalLevel1Message, localTimestamp: Date): IterableIterator<BookTicker> { let timestamp = new Date(message.time) if (message.time === undefined || timestamp.valueOf() < 0) { timestamp = localTimestamp } yield { type: 'book_ticker', symbol: message.product_id, exchange: 'coinbase-international', askAmount: message.ask_qty !== undefined ? Number(message.ask_qty) : undefined, askPrice: message.ask_price !== undefined ? Number(message.ask_price) : undefined, bidPrice: message.bid_price !== undefined ? Number(message.bid_price) : undefined, bidAmount: message.bid_qty !== undefined ? Number(message.bid_qty) : undefined, timestamp, localTimestamp: localTimestamp } } } export class CoinbaseInternationalDerivativeTickerMapper implements Mapper<'coinbase-international', DerivativeTicker> { private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper() canHandle(message: CoinbaseInternationalTradeMessage | CoinbaseInternationalRiskMessage | CoinbaseInternationalFundingMessage) { // perps only if (message.product_id === undefined || message.product_id.endsWith('-PERP') === false) { return false } if (message.channel === 'MATCH' && message.type === 'UPDATE') { return true } if (message.channel === 'FUNDING' && message.type === 'UPDATE') { return true } if (message.channel === 'RISK') { return true } return false } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'MATCH', symbols } as const, { channel: 'RISK', symbols } as const, { channel: 'FUNDING', symbols } as const ] } *map( message: CoinbaseInternationalTradeMessage | CoinbaseInternationalRiskMessage | CoinbaseInternationalFundingMessage, localTimestamp: Date ): IterableIterator<DerivativeTicker> { if (message.channel === 'MATCH') { const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.product_id, 'coinbase-international') pendingTickerInfo.updateLastPrice(Number(message.trade_price)) return } const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.product_id, 'coinbase-international') if (message.channel === 'RISK') { if (message.index_price !== undefined) { pendingTickerInfo.updateIndexPrice(Number(message.index_price)) } if (message.mark_price !== undefined) { pendingTickerInfo.updateMarkPrice(Number(message.mark_price)) } if (message.open_interest !== undefined) { pendingTickerInfo.updateOpenInterest(Number(message.open_interest)) } } if (message.channel === 'FUNDING') { let nextFundingTime = new Date(message.time) if (message.is_final === false) { // If the field is_final is false, the message indicates the predicted funding rate for the next funding interval. // https://docs.cdp.coinbase.com/intx/docs/websocket-channels#funding-channel nextFundingTime.setUTCMinutes(0, 0, 0) nextFundingTime = addMinutes(nextFundingTime, 60) pendingTickerInfo.updateFundingTimestamp(nextFundingTime) } pendingTickerInfo.updateFundingRate(Number(message.funding_rate)) } pendingTickerInfo.updateTimestamp(new Date(message.time)) if (pendingTickerInfo.hasChanged()) { yield pendingTickerInfo.getSnapshot(localTimestamp) } } } // TODO: real-time type CoinbaseInternationalTradeMessage = { sequence: 80 match_id: '374491377330814981' trade_price: '0.009573' trade_qty: '1651' aggressor_side: 'BUY' | 'SELL' | 'OPENING_FILL' channel: 'MATCH' type: 'UPDATE' time: '2024-10-30T10:55:02.069Z' product_id: 'MEW-PERP' } type CoinbaseInternationalSnapshotBookLevel = [string, string] type CoinbaseInternationalLevel2Snapshot = { sequence: 81053126 bids: CoinbaseInternationalSnapshotBookLevel[] asks: CoinbaseInternationalSnapshotBookLevel[] channel: 'LEVEL2' type: 'SNAPSHOT' time: '2024-11-06T23:59:59.812Z' product_id: 'BB-PERP' } type CoinbaseInternationalUpdateBookLevel = ['BUY' | 'SELL', string, string] type CoinbaseInternationalLevel2Update = { sequence: 162 changes: CoinbaseInternationalUpdateBookLevel[] channel: 'LEVEL2' type: 'UPDATE' time: '2024-10-30T10:55:02.348Z' product_id: 'NOT-PERP' } type CoinbaseInternationalLevel1Message = | { sequence: 65960075 bid_price: '27.03' bid_qty: '24.404' ask_price: '27.037' ask_qty: '32.302' channel: 'LEVEL1' type: 'SNAPSHOT' time: '2024-11-07T00:00:00.121Z' product_id: 'AVAX-PERP' } | { sequence: 120100774 bid_price: '2719.96' bid_qty: '0.3676' ask_price: '2720.25' ask_qty: '0.919' channel: 'LEVEL1' type: 'UPDATE' time: '2024-11-07T00:00:59.979Z' product_id: 'ETH-USDC' } type CoinbaseInternationalRiskMessage = { sequence: 108523490 limit_up: '0.5107' limit_down: '0.4621' index_price: '0.4864755122500001' mark_price: '0.4863' settlement_price: '0.4864' open_interest: '153090' channel: 'RISK' type: 'UPDATE' time: '2024-11-07T00:00:59.950Z' product_id: 'ENA-PERP' } type CoinbaseInternationalFundingMessage = { sequence: 108521023 funding_rate: '0.000009' is_final: false channel: 'FUNDING' type: 'UPDATE' time: '2024-11-07T00:00:51.068Z' product_id: 'DEGEN-PERP' }