UNPKG

tardis-dev

Version:

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

240 lines (208 loc) 6.55 kB
import { parseμs, upperCaseSymbols } from '../handy' import { BookChange, BookPriceLevel, BookTicker, Trade } from '../types' import { Mapper } from './mapper' // https://docs.pro.coinbase.com/#websocket-feed export const coinbaseTradesMapper: Mapper<'coinbase', Trade> = { canHandle(message: CoinbaseTrade | CoinbaseLevel2Snapshot | CoinbaseLevel2Update) { return message.type === 'match' }, getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'match', symbols } ] }, *map(message: CoinbaseTrade, localTimestamp: Date): IterableIterator<Trade> { const timestamp = new Date(message.time) timestamp.μs = parseμs(message.time) yield { type: 'trade', symbol: message.product_id, exchange: 'coinbase', id: String(message.trade_id), price: Number(message.price), amount: Number(message.size), side: message.side === 'sell' ? 'buy' : 'sell', // coinbase side field indicates the maker order side timestamp, localTimestamp: localTimestamp } } } const mapUpdateBookLevel = (level: CoinbaseUpdateBookLevel) => { const price = Number(level[1]) const amount = Number(level[2]) return { price, amount } } const mapSnapshotBookLevel = (level: CoinbaseSnapshotBookLevel) => { 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 CoinbaseBookChangMapper implements Mapper<'coinbase', BookChange> { private readonly _symbolLastTimestampMap = new Map<string, Date>() canHandle(message: CoinbaseTrade | CoinbaseLevel2Snapshot | CoinbaseLevel2Update) { return message.type === 'l2update' || message.type === 'snapshot' } getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'snapshot', symbols } as const, { channel: 'l2update', symbols } as const ] } *map(message: CoinbaseLevel2Update | CoinbaseLevel2Snapshot, 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.μs = parseμs(message.time) } } else { timestamp = localTimestamp } yield { type: 'book_change', symbol: message.product_id, exchange: 'coinbase', isSnapshot: true, bids: message.bids.map(mapSnapshotBookLevel).filter(validAmountsOnly), asks: message.asks.map(mapSnapshotBookLevel).filter(validAmountsOnly), timestamp, localTimestamp } } else { // in very rare cases, Coinbase was returning timestamps that aren't valid, like: "time":"0001-01-01T00:00:00.000000Z" // but l2update message was still valid and we need to process it, in such case use timestamp of previous message let timestamp = new Date(message.time) if (timestamp.valueOf() < 0) { let previousValidTimestamp = this._symbolLastTimestampMap.get(message.product_id) if (previousValidTimestamp === undefined) { return } timestamp = previousValidTimestamp } else { timestamp.μs = parseμs(message.time) this._symbolLastTimestampMap.set(message.product_id, timestamp) } yield { type: 'book_change', symbol: message.product_id, exchange: 'coinbase', 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 coinbaseBookTickerMapper: Mapper<'coinbase', BookTicker> = { canHandle(message: CoinbaseTicker) { return message.type === 'ticker' }, getFilters(symbols?: string[]) { symbols = upperCaseSymbols(symbols) return [ { channel: 'ticker', symbols } ] }, *map(message: CoinbaseTicker, localTimestamp: Date): IterableIterator<BookTicker> { let timestamp = new Date(message.time) if (message.time === undefined || timestamp.valueOf() < 0) { timestamp = localTimestamp } else { timestamp.μs = parseμs(message.time) } yield { type: 'book_ticker', symbol: message.product_id, exchange: 'coinbase', askAmount: message.best_ask_size !== undefined ? Number(message.best_ask_size) : undefined, askPrice: message.best_ask !== undefined ? Number(message.best_ask) : undefined, bidPrice: message.best_bid !== undefined ? Number(message.best_bid) : undefined, bidAmount: message.best_bid_size !== undefined ? Number(message.best_bid_size) : undefined, timestamp, localTimestamp: localTimestamp } } } type CoinbaseTrade = { type: 'match' trade_id: number time: string product_id: string size: string price: string side: 'sell' | 'buy' } type CoinbaseSnapshotBookLevel = [string, string] type CoinbaseLevel2Snapshot = { type: 'snapshot' product_id: string bids: CoinbaseSnapshotBookLevel[] asks: CoinbaseSnapshotBookLevel[] time?: string } type CoinbaseUpdateBookLevel = ['buy' | 'sell', string, string] type CoinbaseLevel2Update = { type: 'l2update' product_id: string time: string changes: CoinbaseUpdateBookLevel[] } type CoinbaseTicker = | { type: 'ticker' sequence: 2349290585 product_id: 'CGLD-USD' price: '5.415' best_bid: '5.4149' best_ask: '5.4150' time: '2021-10-13T07:05:00.028961Z' best_bid_size: undefined best_ask_size: undefined } | { type: 'ticker' sequence: 50978628538 product_id: 'BTC-USD' price: '17165.16' open_24h: '16437.94' volume_24h: '42492.05081975' low_24h: '16423.37' high_24h: '17259.37' volume_30d: '1093827.95195495' best_bid: '17165.15' best_bid_size: '0.61540890' best_ask: '17167.76' best_ask_size: '0.18528568' side: 'sell' time: '2022-12-01T00:00:00.122581Z' trade_id: 463751434 last_size: '0.05' }