diff --git a/src/dlob-subscriber/DLOBSubscriberIO.ts b/src/dlob-subscriber/DLOBSubscriberIO.ts
index 0f7f52e..d692901 100644
--- a/src/dlob-subscriber/DLOBSubscriberIO.ts
+++ b/src/dlob-subscriber/DLOBSubscriberIO.ts
@@ -1,14 +1,17 @@
import {
BN,
+ BigNum,
DLOBSubscriber,
DLOBSubscriptionConfig,
DriftEnv,
L2OrderBookGenerator,
MarketType,
+ ONE,
Order,
OrderStatus,
OrderTriggerCondition,
OrderType,
+ PRICE_PRECISION,
PerpOperation,
PositionDirection,
ZERO,
@@ -23,11 +26,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 +40,7 @@ type wsMarketArgs = {
numVammOrders?: number;
fallbackL2Generators?: L2OrderBookGenerator[];
updateOnChange?: boolean;
+ tickSize?: BN;
};
require('dotenv').config();
@@ -123,6 +128,7 @@ export class DLOBSubscriberIO extends DLOBSubscriber {
includeVamm,
updateOnChange: false,
fallbackL2Generators: [],
+ tickSize: perpMarket?.amm?.orderTickSize ?? ONE,
});
}
for (const market of config.spotMarketInfos) {
@@ -137,6 +143,8 @@ 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,
});
}
}
@@ -423,6 +431,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..1b5f545 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;
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..f37b7a9 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,12 @@ 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: