Skip to content

Commit 1929e2b

Browse files
Merge pull request #603 from drift-labs/jonthia/be-247-indicative-liquidity-dashboard
hotfix: maker ind pub key to authority & subaccount mapping
2 parents a5df8cd + 4ea5b22 commit 1929e2b

4 files changed

Lines changed: 226 additions & 13 deletions

File tree

.env.trades.local.example

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ METRICS_PORT=9465
1212
INDICATIVE_QUOTES_MAX_AGE_MS=1000
1313
INDICATIVE_QUOTES_CACHE_TTL_MS=250
1414

15+
INDICATIVE_TO_MAKER_AUTHORITY_MAP='{"7yivAV6EE7PofhvnjnGJMsL2qrnxVXzvGgrkSDQiveK6":"GXyE3Snk3pPYX4Nz9QRVBrnBfbJRTAQYxuy5DRdnebAn","CHmpeeywt1xkXUubKnCRV3DymSJ1xtMVSRhZcJepBamU":"BxTExiVRt9EHe4b47ZDQLDGxee1hPexvkmaDFMLZTDvv","wvTxWGdSG6aV4zShf3CFzPPwTuw6eZsZTrpRNRSkVL9":"E69Pb3EoqrEYjNzFuw2JyEerwyPBFVdGZwmWoDzT9LVL","BroTbC7irNtmQdxcq5BxgYXkkdgJqYWccfBVydt6nMhe":"EibQ2VYpzj18qSdEBkmxWVzde7FzamTxVG9rZyY689Yj"}'
16+
1517
ENABLE_MOCK_FILL_ENDPOINT=true
1618
MOCK_ONLY_MODE=true
1719
MOCK_FILL_PORT=9470
1820

1921
MOCK_MARKET_TYPE=perp
2022
MOCK_MARKET_INDEX=0
21-
MOCK_QUOTES_JSON=[{"maker":"good-maker","side":"long","price":100,"size":2},{"maker":"bad-maker","side":"long","price":99,"size":1}]
22-
MOCK_FILLS_JSON=[{"maker":"good-maker","side":"long","fillPrice":100,"fillSize":1,"oraclePrice":100}]
23+
MOCK_QUOTES_JSON=[{"maker":"CHmpeeywt1xkXUubKnCRV3DymSJ1xtMVSRhZcJepBamU","side":"long","price":99.8,"size":10},{"maker":"CHmpeeywt1xkXUubKnCRV3DymSJ1xtMVSRhZcJepBamU","side":"long","price":99.9,"size":2},{"maker":"wvTxWGdSG6aV4zShf3CFzPPwTuw6eZsZTrpRNRSkVL9","side":"long","price":101,"size":1},{"maker":"CHmpeeywt1xkXUubKnCRV3DymSJ1xtMVSRhZcJepBamU","side":"short","price":101.1,"size":2}]
24+
MOCK_FILLS_JSON=[{"maker":"C13FZykQfLXKuMAMh2iuG7JxhQqd8otujNRAgVETU6id","side":"long","fillPrice":99.9,"fillSize":1,"oraclePrice":99.95}]

src/publishers/tests/tradeMetricsProcessor.test.ts

Lines changed: 159 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ describe('tradeMetricsProcessor', () => {
135135
takerOrderBaseAssetAmount: 1,
136136
takerOrderCumulativeBaseAssetAmountFilled: 1,
137137
takerOrderCumulativeQuoteAssetAmountFilled: 100.1,
138-
maker: 'good-maker',
138+
maker: 'maker-user-account',
139+
makerIndicativeKey: 'good-maker',
139140
makerOrderId: 2,
140141
makerOrderDirection: 'long',
141142
makerOrderBaseAssetAmount: 1,
@@ -367,6 +368,86 @@ describe('tradeMetricsProcessor', () => {
367368
);
368369
});
369370

371+
it('emits maker_not_competitive using the indicative maker label when indicative key is provided', async () => {
372+
const metrics = createMetricSinks();
373+
const quoteState = new Map<string, any>([
374+
['market_mms_perp_0', ['quoted-maker']],
375+
[
376+
'mm_quotes_v2_perp_0_quoted-maker',
377+
{
378+
ts: 1710000000000,
379+
quotes: [{ bid_price: 99900000, bid_size: 1000000000 }],
380+
},
381+
],
382+
]);
383+
384+
const { processFillEvent } = createTradeMetricsProcessor({
385+
redisClientPrefix: 'dlob:',
386+
indicativeQuoteMaxAgeMs: 1000,
387+
indicativeQuotesCacheTtlMs: 250,
388+
spotMarketPrecisionResolver: () => undefined,
389+
publisherRedisClient: {
390+
publish: async () => 1,
391+
},
392+
indicativeQuotesRedisClient: {
393+
smembers: async (key) => quoteState.get(key) ?? [],
394+
get: async (key) => quoteState.get(key),
395+
},
396+
metrics,
397+
nowMsProvider: () => 1710000000000,
398+
});
399+
400+
const fillEvent: FillEvent = {
401+
ts: 1710000000000,
402+
marketIndex: 0,
403+
marketType: 'perp',
404+
filler: 'mock-filler',
405+
takerFee: 0,
406+
makerFee: 0,
407+
quoteAssetAmountSurplus: 0,
408+
baseAssetAmountFilled: 1,
409+
quoteAssetAmountFilled: 100,
410+
taker: 'mock-taker',
411+
takerOrderId: 1,
412+
takerOrderDirection: 'short',
413+
takerOrderBaseAssetAmount: 1,
414+
takerOrderCumulativeBaseAssetAmountFilled: 1,
415+
takerOrderCumulativeQuoteAssetAmountFilled: 100,
416+
maker: 'maker-user-account',
417+
makerIndicativeKey: 'quoted-maker',
418+
makerOrderId: 2,
419+
makerOrderDirection: 'long',
420+
makerOrderBaseAssetAmount: 1,
421+
makerOrderCumulativeBaseAssetAmountFilled: 1,
422+
makerOrderCumulativeQuoteAssetAmountFilled: 100,
423+
oraclePrice: 100,
424+
txSig: 'mock-4',
425+
slot: 4,
426+
fillRecordId: 4,
427+
action: 'fill',
428+
actionExplanation: 'none',
429+
referrerReward: 0,
430+
bitFlags: 0,
431+
};
432+
433+
await processFillEvent(fillEvent);
434+
435+
expect(metrics.indicativeQuoteEvaluationCount.calls).toEqual(
436+
expect.arrayContaining([
437+
{
438+
value: 1,
439+
attributes: {
440+
maker: 'quoted-maker',
441+
market_index: 0,
442+
market_type: 'perp',
443+
side: 'long',
444+
result: 'maker_not_competitive',
445+
},
446+
},
447+
])
448+
);
449+
});
450+
370451
it('treats quotes as fresh based on evaluation time rather than fill timestamp', async () => {
371452
const metrics = createMetricSinks();
372453
const quoteState = new Map<string, any>([
@@ -456,4 +537,81 @@ describe('tradeMetricsProcessor', () => {
456537
])
457538
);
458539
});
540+
541+
it('matches competitive fills using precomputed indicative key', async () => {
542+
const metrics = createMetricSinks();
543+
const quoteState = new Map<string, any>([
544+
['market_mms_perp_0', ['indicative-maker']],
545+
[
546+
'mm_quotes_v2_perp_0_indicative-maker',
547+
{
548+
ts: 1710000000000,
549+
quotes: [{ bid_price: 100100000, bid_size: 1000000000 }],
550+
},
551+
],
552+
]);
553+
554+
const { processFillEvent } = createTradeMetricsProcessor({
555+
redisClientPrefix: 'dlob:',
556+
indicativeQuoteMaxAgeMs: 1000,
557+
indicativeQuotesCacheTtlMs: 250,
558+
spotMarketPrecisionResolver: () => undefined,
559+
publisherRedisClient: {
560+
publish: async () => 1,
561+
},
562+
indicativeQuotesRedisClient: {
563+
smembers: async (key) => quoteState.get(key) ?? [],
564+
get: async (key) => quoteState.get(key),
565+
},
566+
metrics,
567+
nowMsProvider: () => 1710000000000,
568+
});
569+
570+
const fillEvent: FillEvent = {
571+
ts: 1710000000000,
572+
marketIndex: 0,
573+
marketType: 'perp',
574+
filler: 'mock-filler',
575+
takerFee: 0,
576+
makerFee: 0,
577+
quoteAssetAmountSurplus: 0,
578+
baseAssetAmountFilled: 1,
579+
quoteAssetAmountFilled: 100.1,
580+
taker: 'mock-taker',
581+
takerOrderId: 1,
582+
takerOrderDirection: 'short',
583+
takerOrderBaseAssetAmount: 1,
584+
takerOrderCumulativeBaseAssetAmountFilled: 1,
585+
takerOrderCumulativeQuoteAssetAmountFilled: 100.1,
586+
maker: 'maker-user-account',
587+
makerIndicativeKey: 'indicative-maker',
588+
makerOrderId: 2,
589+
makerOrderDirection: 'long',
590+
makerOrderBaseAssetAmount: 1,
591+
makerOrderCumulativeBaseAssetAmountFilled: 1,
592+
makerOrderCumulativeQuoteAssetAmountFilled: 100.1,
593+
oraclePrice: 100,
594+
txSig: 'mock-5',
595+
slot: 5,
596+
fillRecordId: 5,
597+
action: 'fill',
598+
actionExplanation: 'none',
599+
referrerReward: 0,
600+
bitFlags: 0,
601+
};
602+
603+
await processFillEvent(fillEvent);
604+
605+
expect(metrics.indicativeCompetitiveFillCount.calls).toEqual([
606+
{
607+
value: 1,
608+
attributes: {
609+
maker: 'indicative-maker',
610+
market_index: 0,
611+
market_type: 'perp',
612+
side: 'long',
613+
},
614+
},
615+
]);
616+
});
459617
});

src/publishers/tradeMetricsProcessor.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export type FillEvent = {
3030
takerOrderCumulativeBaseAssetAmountFilled: number;
3131
takerOrderCumulativeQuoteAssetAmountFilled: number;
3232
maker?: string;
33+
makerIndicativeKey?: string;
3334
makerOrderId?: number;
3435
makerOrderDirection?: string;
3536
makerOrderBaseAssetAmount: number;
@@ -270,7 +271,7 @@ export const createTradeMetricsProcessor = ({
270271
result: 'competitive',
271272
});
272273

273-
if (fillEvent.maker === quote.maker) {
274+
if (fillEvent.makerIndicativeKey === quote.maker) {
274275
metrics.indicativeCompetitiveFillCount.add(1, attrs);
275276
metrics.indicativeCompetitiveCapturedNotional.add(
276277
Math.min(fillEvent.baseAssetAmountFilled, opportunitySize) *
@@ -300,19 +301,25 @@ export const createTradeMetricsProcessor = ({
300301
});
301302
}
302303

303-
const fillMakerEvaluation = fillEvent.maker
304+
const fillMakerEvaluation = fillEvent.makerIndicativeKey
304305
? marketQuoteEvaluations.find(
305-
(evaluation) => evaluation.maker === fillEvent.maker
306+
(evaluation) => evaluation.maker === fillEvent.makerIndicativeKey
306307
)
307308
: undefined;
308309
if (
309-
fillEvent.maker &&
310+
fillEvent.makerIndicativeKey &&
310311
fillMakerEvaluation &&
311312
fillMakerEvaluation.totalQuoteValueOnBook > 0 &&
312-
!marketQuotes.find((quote) => quote.maker === fillEvent.maker)
313+
!marketQuotes.find(
314+
(quote) => quote.maker === fillEvent.makerIndicativeKey
315+
)
313316
) {
314317
metrics.indicativeQuoteEvaluationCount.add(1, {
315-
...getMakerMetricAttrs(fillEvent, fillEvent.maker, fillSide),
318+
...getMakerMetricAttrs(
319+
fillEvent,
320+
fillMakerEvaluation.maker,
321+
fillSide
322+
),
316323
result: 'maker_not_competitive',
317324
});
318325
}

src/publishers/tradesPublisher.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,14 @@ const wsEndpoint = process.env.WS_ENDPOINT;
112112
const indicativeQuotesCacheTtlMs = process.env.INDICATIVE_QUOTES_CACHE_TTL_MS
113113
? parseInt(process.env.INDICATIVE_QUOTES_CACHE_TTL_MS)
114114
: 250;
115+
const indicativeToMakerAuthorityMap = process.env
116+
.INDICATIVE_TO_MAKER_AUTHORITY_MAP
117+
? (JSON.parse(process.env.INDICATIVE_TO_MAKER_AUTHORITY_MAP) as Record<
118+
string,
119+
string
120+
>)
121+
: {};
122+
const subaccountToIndicative = new Map<string, string>();
115123
const enableMockFillEndpoint =
116124
process.env.ENABLE_MOCK_FILL_ENDPOINT?.toLowerCase() === 'true';
117125
const mockOnlyMode = process.env.MOCK_ONLY_MODE?.toLowerCase() === 'true';
@@ -134,7 +142,13 @@ const startMockFillEndpoint = (
134142
app.use(express.json());
135143
app.post('/mockFill', async (req, res) => {
136144
try {
137-
await processFillEvent(req.body as FillEvent);
145+
const fillEvent = req.body as FillEvent;
146+
await processFillEvent({
147+
...fillEvent,
148+
makerIndicativeKey: fillEvent.maker
149+
? subaccountToIndicative.get(fillEvent.maker)
150+
: undefined,
151+
} as FillEvent);
138152
res.status(200).json({ ok: true });
139153
} catch (error) {
140154
logger.error('Failed to process mock fill:', error);
@@ -148,6 +162,33 @@ const startMockFillEndpoint = (
148162
});
149163
};
150164

165+
const preloadMakerAccountCache = async () => {
166+
for (const [indicativeMaker, authority] of Object.entries(
167+
indicativeToMakerAuthorityMap
168+
)) {
169+
try {
170+
const userAccounts =
171+
await driftClient.getUserAccountsAndAddressesForAuthority(
172+
new PublicKey(authority)
173+
);
174+
for (const userAccount of userAccounts) {
175+
subaccountToIndicative.set(
176+
userAccount.publicKey.toBase58(),
177+
indicativeMaker
178+
);
179+
}
180+
logger.info(
181+
`Preloaded ${userAccounts.length} subaccounts for indicative maker ${indicativeMaker}`
182+
);
183+
} catch (error) {
184+
logger.error(
185+
`Failed to preload subaccounts for indicative maker ${indicativeMaker}:`,
186+
error
187+
);
188+
}
189+
}
190+
};
191+
151192
const main = async () => {
152193
const wallet = new Wallet(new Keypair());
153194
const clearingHousePublicKey = new PublicKey(sdkConfig.DRIFT_PROGRAM_ID);
@@ -173,6 +214,8 @@ const main = async () => {
173214
await redisClient.connect();
174215
const indicativeQuotesRedisClient = new RedisClient({});
175216
await indicativeQuotesRedisClient.connect();
217+
await driftClient.subscribe();
218+
await preloadMakerAccountCache();
176219

177220
const { processFillEvent } = createTradeMetricsProcessor({
178221
redisClientPrefix,
@@ -216,8 +259,6 @@ const main = async () => {
216259
);
217260
logger.info(`Wallet pubkey: ${wallet.publicKey.toBase58()}`);
218261
logger.info(` . SOL balance: ${lamportsBalance / 10 ** 9}`);
219-
220-
await driftClient.subscribe();
221262
driftClient.eventEmitter.on('error', (e) => {
222263
logger.error(e);
223264
});
@@ -318,7 +359,12 @@ const main = async () => {
318359
})
319360
)
320361
.subscribe(async (fillEvent: FillEvent) => {
321-
await processFillEvent(fillEvent);
362+
await processFillEvent({
363+
...fillEvent,
364+
makerIndicativeKey: fillEvent.maker
365+
? subaccountToIndicative.get(fillEvent.maker)
366+
: undefined,
367+
});
322368
});
323369

324370
console.log('Publishing trades');

0 commit comments

Comments
 (0)