diff --git a/drift-common b/drift-common index 7917618..91bcf1a 160000 --- a/drift-common +++ b/drift-common @@ -1 +1 @@ -Subproject commit 7917618ae6a03e37d58f2084aeb5698bd0788250 +Subproject commit 91bcf1a17eac80d2ce479be5839bdd87a9d26397 diff --git a/src/dlob-subscriber/DLOBSubscriberIO.ts b/src/dlob-subscriber/DLOBSubscriberIO.ts index 0f7f52e..db7636b 100644 --- a/src/dlob-subscriber/DLOBSubscriberIO.ts +++ b/src/dlob-subscriber/DLOBSubscriberIO.ts @@ -5,6 +5,7 @@ import { DriftEnv, L2OrderBookGenerator, MarketType, + ONE, Order, OrderStatus, OrderTriggerCondition, @@ -12,6 +13,7 @@ import { PerpOperation, PositionDirection, ZERO, + getLimitPrice, isOperationPaused, isVariant, } from '@drift-labs/sdk'; @@ -23,11 +25,12 @@ import { addOracletoResponse, l2WithBNToStrings, parsePositiveIntArray, + publishGroupings, } from '../utils/utils'; import { setHealthStatus, HEALTH_STATUS } from '../core/metrics'; import { OffloadQueue } from '../utils/offload'; -type wsMarketArgs = { +export type wsMarketArgs = { marketIndex: number; marketType: MarketType; marketName: string; @@ -36,6 +39,7 @@ type wsMarketArgs = { numVammOrders?: number; fallbackL2Generators?: L2OrderBookGenerator[]; updateOnChange?: boolean; + tickSize?: BN; }; require('dotenv').config(); @@ -123,6 +127,7 @@ export class DLOBSubscriberIO extends DLOBSubscriber { includeVamm, updateOnChange: false, fallbackL2Generators: [], + tickSize: perpMarket?.amm?.orderTickSize ?? ONE, }); } for (const market of config.spotMarketInfos) { @@ -137,16 +142,39 @@ export class DLOBSubscriberIO extends DLOBSubscriber { config.spotMarketSubscribers[market.marketIndex].phoenix, config.spotMarketSubscribers[market.marketIndex].openbook, ].filter((a) => !!a), + tickSize: + config.spotMarketSubscribers[market.marketIndex].tickSize ?? ONE, }); } } override async updateDLOB(): Promise { await super.updateDLOB(); + const dlob = this.getDLOB(); let indicativeOrderId = 0; for (const marketArgs of this.marketArgs) { try { if (this.indicativeQuotesRedisClient) { + const oraclePriceData = isVariant(marketArgs.marketType, 'perp') + ? this.driftClient.getOracleDataForPerpMarket( + marketArgs.marketIndex + ) + : this.driftClient.getOracleDataForSpotMarket( + marketArgs.marketIndex + ); + const bestBid = dlob.getBestBid( + marketArgs.marketIndex, + this.slotSource.getSlot(), + marketArgs.marketType, + oraclePriceData + ); + const bestAsk = dlob.getBestAsk( + marketArgs.marketIndex, + this.slotSource.getSlot(), + marketArgs.marketType, + oraclePriceData + ); + const marketType = isVariant(marketArgs.marketType, 'perp') ? 'perp' : 'spot'; @@ -200,7 +228,6 @@ export class DLOBSubscriberIO extends DLOBSubscriber { if (quote['bid_size'] && quote['bid_price'] != null) { // Sanity check bid price and size - const indicativeBid: Order = Object.assign( {}, indicativeBaseOrder, @@ -216,15 +243,21 @@ export class DLOBSubscriberIO extends DLOBSubscriber { direction: PositionDirection.LONG, } ); - this.dlob.insertOrder( + const limitPrice = getLimitPrice( indicativeBid, - INDICATIVE_QUOTES_PUBKEY, - this.slotSource.getSlot(), - false + oraclePriceData, + this.slotSource.getSlot() ); - indicativeOrderId += 1; + if (limitPrice.lte(bestBid)) { + this.dlob.insertOrder( + indicativeBid, + INDICATIVE_QUOTES_PUBKEY, + this.slotSource.getSlot(), + false + ); + indicativeOrderId += 1; + } } - if (quote['ask_size'] && quote['ask_price'] != null) { const indicativeAsk: Order = Object.assign( {}, @@ -241,13 +274,20 @@ export class DLOBSubscriberIO extends DLOBSubscriber { direction: PositionDirection.SHORT, } ); - this.dlob.insertOrder( + const limitPrice = getLimitPrice( indicativeAsk, - INDICATIVE_QUOTES_PUBKEY, - this.slotSource.getSlot(), - false + oraclePriceData, + this.slotSource.getSlot() ); - indicativeOrderId += 1; + if (limitPrice.gte(bestAsk)) { + this.dlob.insertOrder( + indicativeAsk, + INDICATIVE_QUOTES_PUBKEY, + this.slotSource.getSlot(), + false + ); + indicativeOrderId += 1; + } } } } @@ -423,6 +463,15 @@ export class DLOBSubscriberIO extends DLOBSubscriber { l2Formatted_depth100 ); + publishGroupings( + l2Formatted, + marketArgs, + this.redisClient, + clientPrefix, + marketType, + this.indicativeQuotesRedisClient + ); + if (!this.indicativeQuotesRedisClient) { const bids = this.dlob .getBestMakers({ diff --git a/src/publishers/dlobPublisher.ts b/src/publishers/dlobPublisher.ts index 1d92ff2..c4e4bc8 100644 --- a/src/publishers/dlobPublisher.ts +++ b/src/publishers/dlobPublisher.ts @@ -20,6 +20,7 @@ import { PhoenixSubscriber, MarketType, OraclePriceData, + ONE, } from '@drift-labs/sdk'; import { RedisClient, RedisClientPrefix } from '@drift/common/clients'; @@ -301,6 +302,8 @@ const initializeAllMarketSubscribers = async (driftClient: DriftClient) => { } } } + + markets[market.marketIndex].tickSize = market?.orderTickSize ?? ONE; } return markets; @@ -524,7 +527,7 @@ const main = async () => { killSwitchSlotDiffThreshold: KILLSWITCH_SLOT_DIFF_THRESHOLD, protectedMakerView: false, indicativeQuotesRedisClient: indicativeRedisClient, - enableOffloadQueue + enableOffloadQueue, }); await dlobSubscriberIndicative.subscribe(); diff --git a/src/templates/full.html b/src/templates/full.html new file mode 100644 index 0000000..ae8c50c --- /dev/null +++ b/src/templates/full.html @@ -0,0 +1,787 @@ + + + + + + Drift Orderbook (full) + + + + + +
Disconnected
+ +
+
+
Bids
+
+ Price + Size + Total +
+
+
+
+
Asks
+
+ Price + Size + Total +
+
+
+
+ +
+
+ Connection Log + Show Log +
+
+
+ + + + diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 329a95c..1257e95 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -1,4 +1,6 @@ import { + BN, + BigNum, DriftClient, DriftEnv, L2OrderBook, @@ -19,6 +21,16 @@ import { logger } from './logger'; import { NextFunction, Request, Response } from 'express'; import FEATURE_FLAGS from './featureFlags'; import { Connection } from '@solana/web3.js'; +import { wsMarketArgs } from 'src/dlob-subscriber/DLOBSubscriberIO'; + +export const GROUPING_OPTIONS = [1, 10, 100, 500, 1000]; +export const GROUPING_DEPENDENCIES = { + 1: null, + 10: 1, + 100: 10, + 500: 100, + 1000: 100, +}; export const l2WithBNToStrings = (l2: L2OrderBook): any => { for (const key of Object.keys(l2)) { @@ -171,6 +183,119 @@ export const addMarketSlotToResponse = ( response['marketSlot'] = marketSlot; }; +export function aggregatePrices(entries, side, pricePrecision) { + const isAsk = side === 'ask'; + const result = new Map(); + + entries.forEach((entry) => { + const price = parseFloat(entry.price); + const data = { + size: parseFloat(entry.size), + sources: entry.sources || {}, + }; + + let bucketPrice, displayPrice; + if (isAsk) { + displayPrice = Math.ceil(price / pricePrecision) * pricePrecision; + bucketPrice = displayPrice; + } else { + displayPrice = Math.floor(price / pricePrecision) * pricePrecision; + bucketPrice = displayPrice; + } + + const bucketKey = Math.round(bucketPrice); + + if (!result.has(bucketKey)) { + result.set(bucketKey, { + size: 0, + price: displayPrice, + sources: {}, + }); + } + + const bucketData = result.get(bucketKey); + bucketData.size += data.size; + + if (data.sources) { + Object.entries(data.sources).forEach( + ([sourceKey, sourceSize]: [string, string]) => { + if (!bucketData.sources[sourceKey]) { + bucketData.sources[sourceKey] = 0; + } + bucketData.sources[sourceKey] += parseFloat(sourceSize); + } + ); + } + }); + + return Array.from(result.values()); +} + +export function publishGroupings( + l2Formatted, + marketArgs: wsMarketArgs, + redisClient: RedisClient, + clientPrefix: string, + marketType: string, + indicativeQuotesRedisClient: RedisClient +) { + const groupingResults = new Map(); + + GROUPING_OPTIONS.forEach((group) => { + const pricePrecision = BigNum.from(group).mul(marketArgs.tickSize).toNum(); + const dependency = GROUPING_DEPENDENCIES[group]; + + let fullAggregatedBids, fullAggregatedAsks; + + if (dependency && groupingResults.has(dependency)) { + const previousResults = groupingResults.get(dependency); + + fullAggregatedBids = aggregatePrices( + previousResults.bids, + 'bid', + pricePrecision + ).sort((a, b) => b[0] - a[0]); + + fullAggregatedAsks = aggregatePrices( + previousResults.asks, + 'ask', + pricePrecision + ).sort((a, b) => a[0] - b[0]); + } else { + fullAggregatedBids = aggregatePrices( + l2Formatted.bids, + 'bid', + pricePrecision + ).sort((a, b) => b[0] - a[0]); + + fullAggregatedAsks = aggregatePrices( + l2Formatted.asks, + 'ask', + pricePrecision + ).sort((a, b) => a[0] - b[0]); + } + + groupingResults.set(group, { + bids: fullAggregatedBids, + asks: fullAggregatedAsks, + }); + + const aggregatedBids = fullAggregatedBids.slice(0, 20); + const aggregatedAsks = fullAggregatedAsks.slice(0, 20); + const l2Formatted_grouped20 = Object.assign({}, l2Formatted, { + bids: aggregatedBids, + asks: aggregatedAsks, + }); + + redisClient.publish( + `${clientPrefix}orderbook_${marketType}_${ + marketArgs.marketIndex + }_grouped_${group}${indicativeQuotesRedisClient ? '_indicative' : ''}`, + l2Formatted_grouped20 + ); + }); +} + /** * Takes in a req.query like: `{ * marketName: 'SOL-PERP,BTC-PERP,ETH-PERP', @@ -484,6 +609,7 @@ export type SubscriberLookup = { phoenix?: PhoenixSubscriber; serum?: SerumSubscriber; openbook?: OpenbookV2Subscriber; + tickSize?: BN; }; }; diff --git a/src/wsConnectionManager.ts b/src/wsConnectionManager.ts index 39bf659..7e63771 100644 --- a/src/wsConnectionManager.ts +++ b/src/wsConnectionManager.ts @@ -3,7 +3,7 @@ import express from 'express'; import * as http from 'http'; import compression from 'compression'; import { WebSocket, WebSocketServer } from 'ws'; -import { sleep, selectMostRecentBySlot } from './utils/utils'; +import { sleep, selectMostRecentBySlot, GROUPING_OPTIONS } from './utils/utils'; import { register, Gauge, Counter } from 'prom-client'; import { DriftEnv, PerpMarkets, SpotMarkets } from '@drift-labs/sdk'; import { RedisClient, RedisClientPrefix } from '@drift/common/clients'; @@ -95,8 +95,15 @@ const getRedisChannelFromMessage = (message: any): string => { return `trades_${marketType}_${marketIndex}`; case 'orderbook': return `orderbook_${marketType}_${marketIndex}`; - case 'orderbook_indicative': + case 'orderbook_indicative': { + if ( + message.grouping && + GROUPING_OPTIONS.includes(parseInt(message.grouping)) + ) { + return `orderbook_${marketType}_${marketIndex}_grouped_${message.grouping}_indicative`; + } return `orderbook_${marketType}_${marketIndex}_indicative`; + } case 'priorityfees': return `priorityFees_${marketType}_${marketIndex}`; case undefined: