tardis-dev
Version:
Convenient access to tick-level historical and real-time cryptocurrency market data via Node.js
353 lines (311 loc) • 9.53 kB
text/typescript
import { asNonZeroNumberOrUndefined } from '../handy.ts'
import { BookChange, BookTicker, DerivativeTicker, Liquidation, Trade } from '../types.ts'
import { Mapper, PendingTickerInfoHelper } from './mapper.ts'
function parseChannelMarketId(channel: string): string | undefined {
const colonIndex = channel.indexOf(':')
if (colonIndex < 0) {
return undefined
}
const suffix = channel.slice(colonIndex + 1)
if (suffix === 'all') {
return undefined
}
return suffix
}
export class LighterTradesMapper implements Mapper<'lighter', Trade> {
canHandle(message: LighterTradeMessage) {
return message.type === 'update/trade'
}
getFilters(symbols?: string[]) {
return [
{
channel: 'trade' as const,
symbols
}
]
}
*map(message: LighterTradeMessage, localTimestamp: Date): IterableIterator<Trade> {
for (const trade of message.trades) {
yield {
type: 'trade',
symbol: trade.market_id.toString(),
exchange: 'lighter',
id: trade.trade_id_str,
price: Number(trade.price),
amount: Number(trade.size),
side: trade.is_maker_ask ? 'buy' : 'sell',
timestamp: new Date(trade.timestamp),
localTimestamp
}
}
}
}
export class LighterLiquidationMapper implements Mapper<'lighter', Liquidation> {
canHandle(message: LighterTradeMessage) {
return message.type === 'update/trade'
}
getFilters(symbols?: string[]) {
return [
{
channel: 'trade' as const,
symbols
}
]
}
*map(message: LighterTradeMessage, localTimestamp: Date): IterableIterator<Liquidation> {
if (!Array.isArray(message.liquidation_trades)) {
return
}
for (const trade of message.liquidation_trades) {
if (trade.type !== 'liquidation') {
continue
}
yield {
type: 'liquidation',
symbol: trade.market_id.toString(),
exchange: 'lighter',
id: trade.trade_id_str,
price: Number(trade.price),
amount: Number(trade.size),
side: trade.is_maker_ask ? 'buy' : 'sell',
timestamp: new Date(trade.timestamp),
localTimestamp
}
}
}
}
export class LighterBookChangeMapper implements Mapper<'lighter', BookChange> {
canHandle(message: LighterOrderBookMessage) {
return message.type === 'subscribed/order_book' || message.type === 'update/order_book'
}
getFilters(symbols?: string[]) {
return [
{
channel: 'order_book' as const,
symbols
}
]
}
*map(message: LighterOrderBookMessage, localTimestamp: Date): IterableIterator<BookChange> {
const symbol = parseChannelMarketId(message.channel)
if (symbol === undefined) return
yield {
type: 'book_change',
symbol,
exchange: 'lighter',
isSnapshot: message.type === 'subscribed/order_book',
bids: message.order_book.bids.map(this.mapLevel),
asks: message.order_book.asks.map(this.mapLevel),
timestamp: new Date(message.timestamp),
localTimestamp
}
}
private mapLevel(level: LighterLevel) {
return {
price: Number(level.price),
amount: Number(level.size)
}
}
}
export class LighterBookTickerMapper implements Mapper<'lighter', BookTicker> {
canHandle(message: LighterTickerMessage) {
return message.type === 'update/ticker'
}
getFilters(symbols?: string[]) {
return [
{
channel: 'ticker' as const,
symbols
}
]
}
*map(message: LighterTickerMessage, localTimestamp: Date): IterableIterator<BookTicker> {
const symbol = parseChannelMarketId(message.channel)
if (symbol === undefined) return
yield {
type: 'book_ticker',
symbol,
exchange: 'lighter',
askAmount: asNonZeroNumberOrUndefined(message.ticker?.a?.size),
askPrice: asNonZeroNumberOrUndefined(message.ticker?.a?.price),
bidPrice: asNonZeroNumberOrUndefined(message.ticker?.b?.price),
bidAmount: asNonZeroNumberOrUndefined(message.ticker?.b?.size),
timestamp: new Date(message.timestamp),
localTimestamp
}
}
}
export class LighterDerivativeTickerMapper implements Mapper<'lighter', DerivativeTicker> {
private readonly pendingTickerInfoHelper = new PendingTickerInfoHelper()
canHandle(message: LighterMarketStatsMessage) {
return message.type === 'update/market_stats'
}
getFilters(_symbols?: string[]) {
return [
{
channel: 'market_stats' as const,
symbols: []
}
]
}
*map(message: LighterMarketStatsMessage, localTimestamp: Date): IterableIterator<DerivativeTicker> {
for (const entry of this.iterateMarketStats(message)) {
const pendingTickerInfo = this.pendingTickerInfoHelper.getPendingTickerInfo(entry.market_id.toString(), 'lighter')
pendingTickerInfo.updateMarkPrice(Number(entry.mark_price))
pendingTickerInfo.updateIndexPrice(Number(entry.index_price))
pendingTickerInfo.updateFundingRate(Number(entry.current_funding_rate))
pendingTickerInfo.updateLastPrice(Number(entry.last_trade_price))
pendingTickerInfo.updateOpenInterest(Number(entry.open_interest))
if (pendingTickerInfo.hasChanged()) {
pendingTickerInfo.updateTimestamp(new Date(message.timestamp))
yield pendingTickerInfo.getSnapshot(localTimestamp)
}
}
}
private *iterateMarketStats(message: LighterMarketStatsMessage): IterableIterator<LighterMarketStats> {
if (message.channel === 'market_stats:all') {
for (const key of Object.keys(message.market_stats)) {
yield message.market_stats[key]
}
return
}
yield message.market_stats
}
}
type LighterLevel = {
price: string
size: string
}
type LighterOrderBook = {
asks: LighterLevel[]
bids: LighterLevel[]
code: number
nonce: number
begin_nonce: number
offset: number
last_updated_at: number
}
type LighterOrderBookMessage = {
type: 'subscribed/order_book' | 'update/order_book'
channel: `order_book:${number}`
last_updated_at: number
offset: number
timestamp: number
order_book: LighterOrderBook
}
type LighterTicker = {
s: string
a?: Partial<LighterLevel>
b?: Partial<LighterLevel>
last_updated_at: number
}
type LighterTickerMessage = {
type: 'subscribed/ticker' | 'update/ticker'
channel: `ticker:${number}`
last_updated_at: number
nonce: number
ticker: LighterTicker
timestamp: number
}
type LighterTrade = {
trade_id: number
trade_id_str: string
tx_hash: string
type: 'trade' | 'liquidation' | 'deleverage' | 'market-settlement'
market_id: number
size: string
price: string
usd_amount: string
ask_id: number
ask_id_str: string
bid_id: number
bid_id_str: string
ask_client_id: number
ask_client_id_str: string
bid_client_id: number
bid_client_id_str: string
ask_account_id: number
bid_account_id: number
is_maker_ask: boolean
block_height: number
timestamp: number
taker_fee?: number
taker_position_size_before?: string
taker_entry_quote_before?: string
taker_initial_margin_fraction_before?: number
taker_position_sign_changed?: boolean
taker_allocated_margin_usdc_before?: number
maker_fee?: number
maker_position_size_before?: string
maker_entry_quote_before?: string
maker_initial_margin_fraction_before?: number
maker_position_sign_changed?: boolean
transaction_time: number
ask_account_pnl?: string
bid_account_pnl?: string
}
type LighterTradeMessage = {
type: 'subscribed/trade' | 'update/trade'
channel: `trade:${number}`
nonce: number
trades: LighterTrade[]
liquidation_trades?: LighterTrade[]
}
type LighterMarketStats = {
symbol: string
market_id: number
index_price: string
mark_price: string
mid_price: string
open_interest: string
open_interest_limit: string
funding_clamp_small: string
funding_clamp_big: string
last_trade_price: string
current_funding_rate: string
funding_rate: string
funding_timestamp: number
daily_base_token_volume: number
daily_quote_token_volume: number
daily_price_low: number
daily_price_high: number
daily_price_change: number
}
type LighterMarketStatsAllMessage = {
type: 'subscribed/market_stats' | 'update/market_stats'
channel: 'market_stats:all'
timestamp: number
market_stats: Record<string, LighterMarketStats>
}
type LighterMarketStatsMarketIdMessage = {
type: 'subscribed/market_stats' | 'update/market_stats'
channel: `market_stats:${number}`
timestamp: number
market_stats: LighterMarketStats
}
type LighterMarketStatsMessage = LighterMarketStatsAllMessage | LighterMarketStatsMarketIdMessage
type LighterSpotMarketStats = {
symbol: string
market_id: number
index_price: string
mid_price: string
last_trade_price: string
daily_base_token_volume: number
daily_quote_token_volume: number
daily_price_low: number
daily_price_high: number
daily_price_change: number
}
type LighterSpotMarketStatsAllMessage = {
type: 'subscribed/spot_market_stats' | 'update/spot_market_stats'
channel: 'spot_market_stats:all'
timestamp: number
spot_market_stats: Record<string, LighterSpotMarketStats>
}
type LighterSpotMarketStatsMarketIdMessage = {
type: 'subscribed/spot_market_stats' | 'update/spot_market_stats'
channel: `spot_market_stats:${number}`
timestamp: number
spot_market_stats: LighterSpotMarketStats
}
type LighterSpotMarketStatsMessage = LighterSpotMarketStatsAllMessage | LighterSpotMarketStatsMarketIdMessage