From 54b4dd470759a37c40a0a5ed707608ae89b71e00 Mon Sep 17 00:00:00 2001 From: Jon Thia Date: Mon, 30 Mar 2026 14:31:52 +1100 Subject: [PATCH 1/2] ft: remove non whitelisted mm's from evaluations --- .../tests/tradeMetricsProcessor.test.ts | 78 +++++++++++++++++++ src/publishers/tradeMetricsProcessor.ts | 7 ++ 2 files changed, 85 insertions(+) diff --git a/src/publishers/tests/tradeMetricsProcessor.test.ts b/src/publishers/tests/tradeMetricsProcessor.test.ts index d34326b..cf17d6b 100644 --- a/src/publishers/tests/tradeMetricsProcessor.test.ts +++ b/src/publishers/tests/tradeMetricsProcessor.test.ts @@ -286,4 +286,82 @@ describe('tradeMetricsProcessor', () => { }, ]); }); + + it('does not emit maker_not_competitive for a fill maker without indicative quotes', async () => { + const metrics = createMetricSinks(); + const quoteState = new Map([ + ['market_mms_perp_0', ['quoted-maker']], + [ + 'mm_quotes_v2_perp_0_quoted-maker', + { + ts: 1710000000000, + quotes: [{ bid_price: 99900000, bid_size: 1000000000 }], + }, + ], + ]); + + const { processFillEvent } = createTradeMetricsProcessor({ + redisClientPrefix: 'dlob:', + indicativeQuoteMaxAgeMs: 1000, + indicativeQuotesCacheTtlMs: 250, + spotMarketPrecisionResolver: () => undefined, + publisherRedisClient: { + publish: async () => 1, + }, + indicativeQuotesRedisClient: { + smembers: async (key) => quoteState.get(key) ?? [], + get: async (key) => quoteState.get(key), + }, + metrics, + }); + + const fillEvent: FillEvent = { + ts: 1710000000000, + marketIndex: 0, + marketType: 'perp', + filler: 'mock-filler', + takerFee: 0, + makerFee: 0, + quoteAssetAmountSurplus: 0, + baseAssetAmountFilled: 1, + quoteAssetAmountFilled: 100, + taker: 'mock-taker', + takerOrderId: 1, + takerOrderDirection: 'short', + takerOrderBaseAssetAmount: 1, + takerOrderCumulativeBaseAssetAmountFilled: 1, + takerOrderCumulativeQuoteAssetAmountFilled: 100, + maker: 'external-maker', + makerOrderId: 2, + makerOrderDirection: 'long', + makerOrderBaseAssetAmount: 1, + makerOrderCumulativeBaseAssetAmountFilled: 1, + makerOrderCumulativeQuoteAssetAmountFilled: 100, + oraclePrice: 100, + txSig: 'mock-2', + slot: 2, + fillRecordId: 2, + action: 'fill', + actionExplanation: 'none', + referrerReward: 0, + bitFlags: 0, + }; + + await processFillEvent(fillEvent); + + expect(metrics.indicativeQuoteEvaluationCount.calls).not.toEqual( + expect.arrayContaining([ + { + value: 1, + attributes: { + maker: 'external-maker', + market_index: 0, + market_type: 'perp', + side: 'long', + result: 'maker_not_competitive', + }, + }, + ]) + ); + }); }); diff --git a/src/publishers/tradeMetricsProcessor.ts b/src/publishers/tradeMetricsProcessor.ts index ed7e026..6e99421 100644 --- a/src/publishers/tradeMetricsProcessor.ts +++ b/src/publishers/tradeMetricsProcessor.ts @@ -298,8 +298,15 @@ export const createTradeMetricsProcessor = ({ }); } + const fillMakerEvaluation = fillEvent.maker + ? marketQuoteEvaluations.find( + (evaluation) => evaluation.maker === fillEvent.maker + ) + : undefined; if ( fillEvent.maker && + fillMakerEvaluation && + fillMakerEvaluation.totalQuoteValueOnBook > 0 && !marketQuotes.find((quote) => quote.maker === fillEvent.maker) ) { metrics.indicativeQuoteEvaluationCount.add(1, { From 596f199b795aaf49732188bb361930c7e60ed237 Mon Sep 17 00:00:00 2001 From: Jon Thia Date: Mon, 30 Mar 2026 15:05:42 +1100 Subject: [PATCH 2/2] update ind quote freshness logic --- .../tests/tradeMetricsProcessor.test.ts | 92 +++++++++++++++++++ src/publishers/tradeMetricsProcessor.ts | 16 ++-- 2 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/publishers/tests/tradeMetricsProcessor.test.ts b/src/publishers/tests/tradeMetricsProcessor.test.ts index cf17d6b..b31d357 100644 --- a/src/publishers/tests/tradeMetricsProcessor.test.ts +++ b/src/publishers/tests/tradeMetricsProcessor.test.ts @@ -116,6 +116,7 @@ describe('tradeMetricsProcessor', () => { get: async (key) => quoteState.get(key), }, metrics, + nowMsProvider: () => 1710000000000, }); const fillEvent: FillEvent = { @@ -313,6 +314,7 @@ describe('tradeMetricsProcessor', () => { get: async (key) => quoteState.get(key), }, metrics, + nowMsProvider: () => 1710000000000, }); const fillEvent: FillEvent = { @@ -364,4 +366,94 @@ describe('tradeMetricsProcessor', () => { ]) ); }); + + it('treats quotes as fresh based on evaluation time rather than fill timestamp', async () => { + const metrics = createMetricSinks(); + const quoteState = new Map([ + ['market_mms_perp_0', ['good-maker']], + [ + 'mm_quotes_v2_perp_0_good-maker', + { + ts: 1710000000000, + quotes: [{ bid_price: 100100000, bid_size: 1000000000 }], + }, + ], + ]); + + const { processFillEvent } = createTradeMetricsProcessor({ + redisClientPrefix: 'dlob:', + indicativeQuoteMaxAgeMs: 1000, + indicativeQuotesCacheTtlMs: 250, + spotMarketPrecisionResolver: () => undefined, + publisherRedisClient: { + publish: async () => 1, + }, + indicativeQuotesRedisClient: { + smembers: async (key) => quoteState.get(key) ?? [], + get: async (key) => quoteState.get(key), + }, + metrics, + nowMsProvider: () => 1710000000500, + }); + + const fillEvent: FillEvent = { + ts: 1709999999, + marketIndex: 0, + marketType: 'perp', + filler: 'mock-filler', + takerFee: 0, + makerFee: 0, + quoteAssetAmountSurplus: 0, + baseAssetAmountFilled: 1, + quoteAssetAmountFilled: 100.1, + taker: 'mock-taker', + takerOrderId: 1, + takerOrderDirection: 'short', + takerOrderBaseAssetAmount: 1, + takerOrderCumulativeBaseAssetAmountFilled: 1, + takerOrderCumulativeQuoteAssetAmountFilled: 100.1, + maker: 'good-maker', + makerOrderId: 2, + makerOrderDirection: 'long', + makerOrderBaseAssetAmount: 1, + makerOrderCumulativeBaseAssetAmountFilled: 1, + makerOrderCumulativeQuoteAssetAmountFilled: 100.1, + oraclePrice: 100, + txSig: 'mock-3', + slot: 3, + fillRecordId: 3, + action: 'fill', + actionExplanation: 'none', + referrerReward: 0, + bitFlags: 0, + }; + + await processFillEvent(fillEvent); + + expect(metrics.indicativePresenceCount.calls).toEqual([ + { + value: 1, + attributes: { + maker: 'good-maker', + market_index: 0, + market_type: 'perp', + side: 'long', + }, + }, + ]); + expect(metrics.indicativeQuoteEvaluationCount.calls).toEqual( + expect.arrayContaining([ + { + value: 1, + attributes: { + maker: 'good-maker', + market_index: 0, + market_type: 'perp', + side: 'long', + result: 'competitive', + }, + }, + ]) + ); + }); }); diff --git a/src/publishers/tradeMetricsProcessor.ts b/src/publishers/tradeMetricsProcessor.ts index 6e99421..bc5bc6f 100644 --- a/src/publishers/tradeMetricsProcessor.ts +++ b/src/publishers/tradeMetricsProcessor.ts @@ -6,7 +6,6 @@ import { getIndicativeDirectionBucket, getFillPrice, getFillSide, - getFillTimestampMs, getMakerMetricAttrs, getQuoteTimestampMs, getQuoteValueOnBook, @@ -103,6 +102,7 @@ export const createTradeMetricsProcessor = ({ publisherRedisClient, indicativeQuotesRedisClient, metrics, + nowMsProvider = Date.now, onError, }: { redisClientPrefix: string; @@ -112,6 +112,7 @@ export const createTradeMetricsProcessor = ({ publisherRedisClient: PublisherRedisClient; indicativeQuotesRedisClient: IndicativeQuotesRedisClient; metrics: TradeMetricsSinks; + nowMsProvider?: () => number; onError?: (error: unknown) => void; }) => { const marketQuoteCache = new Map(); @@ -120,7 +121,7 @@ export const createTradeMetricsProcessor = ({ fillEvent: FillEvent, side: 'long' | 'short', fillPrice: number, - fillTsMs: number + evaluationTsMs: number ): Promise => { const marketKey = `${fillEvent.marketType}_${fillEvent.marketIndex}_${side}`; const mapQuoteBlob = ( @@ -141,7 +142,7 @@ export const createTradeMetricsProcessor = ({ ); const quoteTsMs = getQuoteTimestampMs(quoteBlob); const quoteAgeMs = - quoteTsMs !== undefined ? fillTsMs - quoteTsMs : Infinity; + quoteTsMs !== undefined ? evaluationTsMs - quoteTsMs : Infinity; const isFresh = quoteAgeMs >= 0 && quoteAgeMs <= indicativeQuoteMaxAgeMs; return { @@ -158,7 +159,7 @@ export const createTradeMetricsProcessor = ({ const cached = marketQuoteCache.get(marketKey); if ( cached && - Date.now() - cached.fetchedAtMs <= indicativeQuotesCacheTtlMs + nowMsProvider() - cached.fetchedAtMs <= indicativeQuotesCacheTtlMs ) { return cached.quoteBlobs.map(({ maker, quoteBlob }) => mapQuoteBlob(maker, quoteBlob) @@ -169,7 +170,7 @@ export const createTradeMetricsProcessor = ({ const makers = await indicativeQuotesRedisClient.smembers(mmSetKey); if (!makers.length) { marketQuoteCache.set(marketKey, { - fetchedAtMs: Date.now(), + fetchedAtMs: nowMsProvider(), quoteBlobs: [], }); return []; @@ -188,7 +189,7 @@ export const createTradeMetricsProcessor = ({ quoteBlob: quoteBlobs[idx], })); marketQuoteCache.set(marketKey, { - fetchedAtMs: Date.now(), + fetchedAtMs: nowMsProvider(), quoteBlobs: cachedQuoteBlobs, }); @@ -222,11 +223,12 @@ export const createTradeMetricsProcessor = ({ metrics.marketFillCount.add(1, marketMetricAttrs); try { + const evaluationTsMs = nowMsProvider(); const marketQuoteEvaluations = await getMarketQuotes( fillEvent, fillSide, fillPrice, - getFillTimestampMs(fillEvent.ts) + evaluationTsMs ); const marketQuotes = marketQuoteEvaluations .map((evaluation) => evaluation.competitiveLiquidity)