tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
400 lines (349 loc) • 10.6 kB
text/typescript
import { Mapper, PendingTickerInfoHelper } from './mapper'
import { Trade, BookChange, DerivativeTicker } from '../types'
// phemex provides timestamps in nanoseconds
const fromNanoSecondsToDate = (nanos: number) => {
const microtimestamp = Math.floor(nanos / 1000)
const timestamp = new Date(microtimestamp / 1000)
timestamp.μs = microtimestamp % 1000
return timestamp
}
function getPriceScale(symbol: string) {
if (symbol.startsWith('s')) {
return 1e8
}
return 1e4
}
function getQtyScale(symbol: string) {
if (symbol.startsWith('s')) {
return 1e8
}
return 1
}
const COINS_STARTING_WITH_S = ['SOL', 'SUSHI', 'SNX', 'SAND', 'SRM', 'SKLU', 'SXP', 'STORJ', 'SFP', 'STG']
function getInstrumentType(symbol: string) {
if (/\d+$/.test(symbol)) {
return 'future'
}
if (COINS_STARTING_WITH_S.some((c) => symbol.startsWith(c)) || symbol.startsWith('S') === false) {
return 'perpetual'
}
return 'spot'
}
function getApiSymbolId(symbolId: string) {
const type = getInstrumentType(symbolId)
if (type === 'spot' && symbolId.startsWith('S')) {
return symbolId.charAt(0).toLowerCase() + symbolId.slice(1)
}
if (symbolId.startsWith('U100')) {
return symbolId.charAt(0).toLowerCase() + symbolId.slice(1)
}
if (symbolId === 'CETHUSD') {
return symbolId.charAt(0).toLowerCase() + symbolId.slice(1)
}
return symbolId
}
function getSymbols(symbols: string[]) {
const perpV2Symbols = symbols.filter((s) => getInstrumentType(s) === 'perpetual' && s.endsWith('USDT')).map(getApiSymbolId)
const otherSymbols = symbols.filter((s) => getInstrumentType(s) !== 'perpetual' || s.endsWith('USDT') == false).map(getApiSymbolId)
return {
perpV2Symbols,
otherSymbols
}
}
export const phemexTradesMapper: Mapper<'phemex', Trade> = {
canHandle(message: PhemexTradeMessage) {
return message.type === 'incremental' && ('trades' in message || 'trades_p' in message)
},
getFilters(symbols?: string[]) {
if (symbols == undefined || symbols.length === 0) {
return [
{
channel: 'trades'
} as const,
{
channel: 'trades_p'
} as const
]
}
const { perpV2Symbols, otherSymbols } = getSymbols(symbols)
const filters = []
if (perpV2Symbols.length > 0) {
filters.push({
channel: 'trades_p',
symbols: perpV2Symbols
} as const)
}
if (otherSymbols.length > 0) {
filters.push({
channel: 'trades',
symbols: otherSymbols
} as const)
}
return filters
},
*map(message: PhemexTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
if ('trades' in message) {
for (const [timestamp, side, priceEp, qty] of message.trades) {
const symbol = message.symbol
yield {
type: 'trade',
symbol: symbol.toUpperCase(),
exchange: 'phemex',
id: undefined,
price: priceEp / getPriceScale(symbol),
amount: qty / getQtyScale(symbol),
side: side === 'Buy' ? 'buy' : 'sell',
timestamp: fromNanoSecondsToDate(timestamp),
localTimestamp: localTimestamp
}
}
} else if ('trades_p' in message) {
for (const [timestamp, side, price, qty] of message.trades_p) {
const symbol = message.symbol
yield {
type: 'trade',
symbol: symbol.toUpperCase(),
exchange: 'phemex',
id: undefined,
price: Number(price),
amount: Number(qty),
side: side === 'Buy' ? 'buy' : 'sell',
timestamp: fromNanoSecondsToDate(timestamp),
localTimestamp: localTimestamp
}
}
}
}
}
const mapBookLevelForSymbol =
(symbol: string) =>
([priceEp, qty]: PhemexBookLevel) => {
return {
price: priceEp / getPriceScale(symbol),
amount: qty / getQtyScale(symbol)
}
}
function mapPerpBookLevel([price, amount]: [string, string]) {
return {
price: Number(price),
amount: Number(amount)
}
}
export const phemexBookChangeMapper: Mapper<'phemex', BookChange> = {
canHandle(message: PhemexBookMessage) {
return 'book' in message || 'orderbook_p' in message
},
getFilters(symbols?: string[]) {
if (symbols == undefined || symbols.length === 0) {
return [
{
channel: 'book'
} as const,
{
channel: 'orderbook_p'
} as const
]
}
const { perpV2Symbols, otherSymbols } = getSymbols(symbols)
const filters = []
if (perpV2Symbols.length > 0) {
filters.push({
channel: 'orderbook_p',
symbols: perpV2Symbols
} as const)
}
if (otherSymbols.length > 0) {
filters.push({
channel: 'book',
symbols: otherSymbols
} as const)
}
return filters
},
*map(message: PhemexBookMessage, localTimestamp: Date): IterableIterator<BookChange> {
const symbol = message.symbol
if ('book' in message) {
const mapBookLevel = mapBookLevelForSymbol(symbol)
yield {
type: 'book_change',
symbol: symbol.toUpperCase(),
exchange: 'phemex',
isSnapshot: message.type === 'snapshot',
bids: message.book.bids.map(mapBookLevel),
asks: message.book.asks.map(mapBookLevel),
timestamp: fromNanoSecondsToDate(message.timestamp),
localTimestamp
}
} else if ('orderbook_p' in message) {
yield {
type: 'book_change',
symbol: symbol.toUpperCase(),
exchange: 'phemex',
isSnapshot: message.type === 'snapshot',
bids: message.orderbook_p.bids.map(mapPerpBookLevel),
asks: message.orderbook_p.asks.map(mapPerpBookLevel),
timestamp: fromNanoSecondsToDate(message.timestamp),
localTimestamp
}
}
}
}
export class PhemexDerivativeTickerMapper implements Mapper<'phemex', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
canHandle(message: PhemexTicker) {
return 'market24h' in message || message.method === 'perp_market24h_pack_p.update'
}
getFilters(symbols?: string[]) {
if (symbols == undefined || symbols.length === 0) {
return [
{
channel: 'market24h'
} as const,
{
channel: 'perp_market24h_pack_p'
} as const
]
}
const { perpV2Symbols, otherSymbols } = getSymbols(symbols)
const filters = []
if (perpV2Symbols.length > 0) {
filters.push({
channel: 'perp_market24h_pack_p',
symbols: perpV2Symbols
} as const)
}
if (otherSymbols.length > 0) {
filters.push({
channel: 'market24h',
symbols: otherSymbols
} as const)
}
return filters
}
*map(message: PhemexTicker, localTimestamp: Date): IterableIterator<DerivativeTicker> {
if ('market24h' in message) {
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(message.market24h.symbol, 'phemex')
const phemexTicker = message.market24h
pendingTickerInfo.updateFundingRate(phemexTicker.fundingRate / 100000000)
pendingTickerInfo.updatePredictedFundingRate(phemexTicker.predFundingRate / 100000000)
pendingTickerInfo.updateIndexPrice(phemexTicker.indexPrice / 10000)
pendingTickerInfo.updateMarkPrice(phemexTicker.markPrice / 10000)
pendingTickerInfo.updateOpenInterest(phemexTicker.openInterest)
pendingTickerInfo.updateLastPrice(phemexTicker.close / 10000)
pendingTickerInfo.updateTimestamp(fromNanoSecondsToDate(message.timestamp))
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
} else {
for (let [
symbol,
_openRp,
_highRp,
_lowRp,
lastRp,
_volumeRq,
_turnoverRv,
openInterestRv,
indexRp,
markRp,
fundingRateRr,
predFundingRateRr
] of message.data) {
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(symbol, 'phemex')
pendingTickerInfo.updateFundingRate(Number(fundingRateRr))
pendingTickerInfo.updatePredictedFundingRate(Number(predFundingRateRr))
pendingTickerInfo.updateIndexPrice(Number(indexRp))
pendingTickerInfo.updateMarkPrice(Number(markRp))
pendingTickerInfo.updateOpenInterest(Number(openInterestRv))
pendingTickerInfo.updateLastPrice(Number(lastRp))
pendingTickerInfo.updateTimestamp(fromNanoSecondsToDate(message.timestamp))
if (pendingTickerInfo.hasChanged()) {
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
}
}
type PhemexTradeMessage =
| {
symbol: string
trades: [[number, 'Buy' | 'Sell', number, number]]
type: 'incremental' | 'snapshot'
}
| {
sequence: 79157171
symbol: 'BTCUSDT'
trades_p: [[1669198793402790477, 'Buy' | 'Sell', '16545.6', '0.7']]
type: 'snapshot' | 'incremental'
}
type PhemexBookLevel = [number, number]
type PhemexBookMessage =
| {
book: {
asks: PhemexBookLevel[]
bids: PhemexBookLevel[]
}
symbol: string
timestamp: number
type: 'incremental' | 'snapshot'
}
| {
depth: 0
orderbook_p: {
asks: [string, string][]
bids: [string, string][]
}
sequence: 80321058
symbol: 'BTCUSDT'
timestamp: 1669198850490348246
type: 'snapshot' | 'incremental'
}
type PhemexTicker =
| {
market24h: {
fundingRate: number
indexPrice: number
markPrice: number
openInterest: number
predFundingRate: number
symbol: string
close: number
}
timestamp: number
method: undefined
}
| {
data: [
[
'SOLUSDT',
'11.246',
'13.41',
'10.91',
'13.029',
'10445.82',
'127687.14224',
'0',
'13.03062296',
'13.03154351',
'0.0001',
'0.0001'
],
[
'BTCUSDT',
'15713.1',
'16626',
'15685.7',
'16545.6',
'1374.476',
'22296790.4579',
'0',
'16553.56998432',
'16554.73942506',
'0.0001',
'0.0001'
]
]
method: 'perp_market24h_pack_p.update'
timestamp: 1669198855202180601
type: 'incremental'
}