Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .env.trades.local.example
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ METRICS_PORT=9465
INDICATIVE_QUOTES_MAX_AGE_MS=1000
INDICATIVE_QUOTES_CACHE_TTL_MS=250

INDICATIVE_TO_MAKER_AUTHORITY_MAP='{"7yivAV6EE7PofhvnjnGJMsL2qrnxVXzvGgrkSDQiveK6":"GXyE3Snk3pPYX4Nz9QRVBrnBfbJRTAQYxuy5DRdnebAn","CHmpeeywt1xkXUubKnCRV3DymSJ1xtMVSRhZcJepBamU":"BxTExiVRt9EHe4b47ZDQLDGxee1hPexvkmaDFMLZTDvv","wvTxWGdSG6aV4zShf3CFzPPwTuw6eZsZTrpRNRSkVL9":"E69Pb3EoqrEYjNzFuw2JyEerwyPBFVdGZwmWoDzT9LVL","BroTbC7irNtmQdxcq5BxgYXkkdgJqYWccfBVydt6nMhe":"EibQ2VYpzj18qSdEBkmxWVzde7FzamTxVG9rZyY689Yj"}'

ENABLE_MOCK_FILL_ENDPOINT=true
MOCK_ONLY_MODE=true
MOCK_FILL_PORT=9470

MOCK_MARKET_TYPE=perp
MOCK_MARKET_INDEX=0
MOCK_QUOTES_JSON=[{"maker":"good-maker","side":"long","price":100,"size":2},{"maker":"bad-maker","side":"long","price":99,"size":1}]
MOCK_FILLS_JSON=[{"maker":"good-maker","side":"long","fillPrice":100,"fillSize":1,"oraclePrice":100}]
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}]
MOCK_FILLS_JSON=[{"maker":"C13FZykQfLXKuMAMh2iuG7JxhQqd8otujNRAgVETU6id","side":"long","fillPrice":99.9,"fillSize":1,"oraclePrice":99.95}]
160 changes: 159 additions & 1 deletion src/publishers/tests/tradeMetricsProcessor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,8 @@ describe('tradeMetricsProcessor', () => {
takerOrderBaseAssetAmount: 1,
takerOrderCumulativeBaseAssetAmountFilled: 1,
takerOrderCumulativeQuoteAssetAmountFilled: 100.1,
maker: 'good-maker',
maker: 'maker-user-account',
makerIndicativeKey: 'good-maker',
makerOrderId: 2,
makerOrderDirection: 'long',
makerOrderBaseAssetAmount: 1,
Expand Down Expand Up @@ -367,6 +368,86 @@ describe('tradeMetricsProcessor', () => {
);
});

it('emits maker_not_competitive using the indicative maker label when indicative key is provided', async () => {
const metrics = createMetricSinks();
const quoteState = new Map<string, any>([
['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,
nowMsProvider: () => 1710000000000,
});

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: 'maker-user-account',
makerIndicativeKey: 'quoted-maker',
makerOrderId: 2,
makerOrderDirection: 'long',
makerOrderBaseAssetAmount: 1,
makerOrderCumulativeBaseAssetAmountFilled: 1,
makerOrderCumulativeQuoteAssetAmountFilled: 100,
oraclePrice: 100,
txSig: 'mock-4',
slot: 4,
fillRecordId: 4,
action: 'fill',
actionExplanation: 'none',
referrerReward: 0,
bitFlags: 0,
};

await processFillEvent(fillEvent);

expect(metrics.indicativeQuoteEvaluationCount.calls).toEqual(
expect.arrayContaining([
{
value: 1,
attributes: {
maker: 'quoted-maker',
market_index: 0,
market_type: 'perp',
side: 'long',
result: 'maker_not_competitive',
},
},
])
);
});

it('treats quotes as fresh based on evaluation time rather than fill timestamp', async () => {
const metrics = createMetricSinks();
const quoteState = new Map<string, any>([
Expand Down Expand Up @@ -456,4 +537,81 @@ describe('tradeMetricsProcessor', () => {
])
);
});

it('matches competitive fills using precomputed indicative key', async () => {
const metrics = createMetricSinks();
const quoteState = new Map<string, any>([
['market_mms_perp_0', ['indicative-maker']],
[
'mm_quotes_v2_perp_0_indicative-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: () => 1710000000000,
});

const fillEvent: FillEvent = {
ts: 1710000000000,
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: 'maker-user-account',
makerIndicativeKey: 'indicative-maker',
makerOrderId: 2,
makerOrderDirection: 'long',
makerOrderBaseAssetAmount: 1,
makerOrderCumulativeBaseAssetAmountFilled: 1,
makerOrderCumulativeQuoteAssetAmountFilled: 100.1,
oraclePrice: 100,
txSig: 'mock-5',
slot: 5,
fillRecordId: 5,
action: 'fill',
actionExplanation: 'none',
referrerReward: 0,
bitFlags: 0,
};

await processFillEvent(fillEvent);

expect(metrics.indicativeCompetitiveFillCount.calls).toEqual([
{
value: 1,
attributes: {
maker: 'indicative-maker',
market_index: 0,
market_type: 'perp',
side: 'long',
},
},
]);
});
});
19 changes: 13 additions & 6 deletions src/publishers/tradeMetricsProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type FillEvent = {
takerOrderCumulativeBaseAssetAmountFilled: number;
takerOrderCumulativeQuoteAssetAmountFilled: number;
maker?: string;
makerIndicativeKey?: string;
makerOrderId?: number;
makerOrderDirection?: string;
makerOrderBaseAssetAmount: number;
Expand Down Expand Up @@ -270,7 +271,7 @@ export const createTradeMetricsProcessor = ({
result: 'competitive',
});

if (fillEvent.maker === quote.maker) {
if (fillEvent.makerIndicativeKey === quote.maker) {
metrics.indicativeCompetitiveFillCount.add(1, attrs);
metrics.indicativeCompetitiveCapturedNotional.add(
Math.min(fillEvent.baseAssetAmountFilled, opportunitySize) *
Expand Down Expand Up @@ -300,19 +301,25 @@ export const createTradeMetricsProcessor = ({
});
}

const fillMakerEvaluation = fillEvent.maker
const fillMakerEvaluation = fillEvent.makerIndicativeKey
? marketQuoteEvaluations.find(
(evaluation) => evaluation.maker === fillEvent.maker
(evaluation) => evaluation.maker === fillEvent.makerIndicativeKey
)
: undefined;
if (
fillEvent.maker &&
fillEvent.makerIndicativeKey &&
fillMakerEvaluation &&
fillMakerEvaluation.totalQuoteValueOnBook > 0 &&
!marketQuotes.find((quote) => quote.maker === fillEvent.maker)
!marketQuotes.find(
(quote) => quote.maker === fillEvent.makerIndicativeKey
)
) {
metrics.indicativeQuoteEvaluationCount.add(1, {
...getMakerMetricAttrs(fillEvent, fillEvent.maker, fillSide),
...getMakerMetricAttrs(
fillEvent,
fillMakerEvaluation.maker,
fillSide
),
result: 'maker_not_competitive',
});
}
Expand Down
54 changes: 50 additions & 4 deletions src/publishers/tradesPublisher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,14 @@ const wsEndpoint = process.env.WS_ENDPOINT;
const indicativeQuotesCacheTtlMs = process.env.INDICATIVE_QUOTES_CACHE_TTL_MS
? parseInt(process.env.INDICATIVE_QUOTES_CACHE_TTL_MS)
: 250;
const indicativeToMakerAuthorityMap = process.env
.INDICATIVE_TO_MAKER_AUTHORITY_MAP
? (JSON.parse(process.env.INDICATIVE_TO_MAKER_AUTHORITY_MAP) as Record<
string,
string
>)
: {};
const subaccountToIndicative = new Map<string, string>();
const enableMockFillEndpoint =
process.env.ENABLE_MOCK_FILL_ENDPOINT?.toLowerCase() === 'true';
const mockOnlyMode = process.env.MOCK_ONLY_MODE?.toLowerCase() === 'true';
Expand All @@ -134,7 +142,13 @@ const startMockFillEndpoint = (
app.use(express.json());
app.post('/mockFill', async (req, res) => {
try {
await processFillEvent(req.body as FillEvent);
const fillEvent = req.body as FillEvent;
await processFillEvent({
...fillEvent,
makerIndicativeKey: fillEvent.maker
? subaccountToIndicative.get(fillEvent.maker)
: undefined,
} as FillEvent);
res.status(200).json({ ok: true });
} catch (error) {
logger.error('Failed to process mock fill:', error);
Expand All @@ -148,6 +162,33 @@ const startMockFillEndpoint = (
});
};

const preloadMakerAccountCache = async () => {
for (const [indicativeMaker, authority] of Object.entries(
indicativeToMakerAuthorityMap
)) {
try {
const userAccounts =
await driftClient.getUserAccountsAndAddressesForAuthority(
new PublicKey(authority)
);
for (const userAccount of userAccounts) {
subaccountToIndicative.set(
userAccount.publicKey.toBase58(),
indicativeMaker
);
}
logger.info(
`Preloaded ${userAccounts.length} subaccounts for indicative maker ${indicativeMaker}`
);
} catch (error) {
logger.error(
`Failed to preload subaccounts for indicative maker ${indicativeMaker}:`,
error
);
}
}
};

const main = async () => {
const wallet = new Wallet(new Keypair());
const clearingHousePublicKey = new PublicKey(sdkConfig.DRIFT_PROGRAM_ID);
Expand All @@ -173,6 +214,8 @@ const main = async () => {
await redisClient.connect();
const indicativeQuotesRedisClient = new RedisClient({});
await indicativeQuotesRedisClient.connect();
await driftClient.subscribe();
await preloadMakerAccountCache();

const { processFillEvent } = createTradeMetricsProcessor({
redisClientPrefix,
Expand Down Expand Up @@ -216,8 +259,6 @@ const main = async () => {
);
logger.info(`Wallet pubkey: ${wallet.publicKey.toBase58()}`);
logger.info(` . SOL balance: ${lamportsBalance / 10 ** 9}`);

await driftClient.subscribe();
driftClient.eventEmitter.on('error', (e) => {
logger.error(e);
});
Expand Down Expand Up @@ -318,7 +359,12 @@ const main = async () => {
})
)
.subscribe(async (fillEvent: FillEvent) => {
await processFillEvent(fillEvent);
await processFillEvent({
...fillEvent,
makerIndicativeKey: fillEvent.maker
? subaccountToIndicative.get(fillEvent.maker)
: undefined,
});
});

console.log('Publishing trades');
Expand Down
Loading