diff --git a/src/bots/filler.ts b/src/bots/filler.ts index c1696afa..537459f0 100644 --- a/src/bots/filler.ts +++ b/src/bots/filler.ts @@ -91,6 +91,7 @@ import { getNodeToTriggerSignature, sleepMs, } from '../utils'; +import { selectMakers } from '../makerSelection'; const MAX_TX_PACK_SIZE = 1230; //1232; const CU_PER_FILL = 260_000; // CU cost for a successful fill @@ -102,7 +103,7 @@ const FILL_ORDER_BACKOFF = 2000; // the time to wait before trying to a node in const THROTTLED_NODE_SIZE_TO_PRUNE = 10; // Size of throttled nodes to get to before pruning the map const TRIGGER_ORDER_COOLDOWN_MS = 1000; // the time to wait before trying to a node in the triggering map again const MAX_COMPUTE_UNIT_PRICE_MICRO_LAMPORTS = 20_000; // cap the computeUnitPrice to pay per fill tx -const MAX_MAKERS_PER_FILL = 6; // max number of unique makers to include per fill +export const MAX_MAKERS_PER_FILL = 6; // max number of unique makers to include per fill const SETTLE_PNL_CHUNKS = 4; const MAX_POSITIONS_PER_USER = 8; @@ -186,6 +187,8 @@ function logMessageForNodeToFill(node: NodeToFill, prefix?: string): string { return msg; } +export type MakerNodeMap = Map; + export class FillerBot implements Bot { public readonly name: string; public readonly dryRun: boolean; @@ -909,24 +912,32 @@ export class FillerBot implements Bot { }> { const makerInfos: Array = []; - // set to track whether maker account has already been included - const makersIncluded = new Set(); if (nodeToFill.makerNodes.length > 0) { + let makerNodesMap: MakerNodeMap = new Map(); for (const makerNode of nodeToFill.makerNodes) { if (this.isDLOBNodeThrottled(makerNode)) { continue; } + if (!makerNode.userAccount) { continue; } - const makerAccount = makerNode.userAccount; - if (makersIncluded.has(makerAccount)) { - continue; - } - if (makersIncluded.size >= MAX_MAKERS_PER_FILL) { - break; + if (makerNodesMap.has(makerNode.userAccount!)) { + makerNodesMap.get(makerNode.userAccount!)!.push(makerNode); + } else { + makerNodesMap.set(makerNode.userAccount!, [makerNode]); } + } + + if (makerNodesMap.size > MAX_MAKERS_PER_FILL) { + logger.info(`selecting from ${makerNodesMap.size} makers`); + makerNodesMap = selectMakers(makerNodesMap); + logger.info(`selected: ${Array.from(makerNodesMap.keys()).join(',')}`); + } + + for (const [makerAccount, makerNodes] of makerNodesMap) { + const makerNode = makerNodes[0]; const makerUserAccount = await this.getUserAccountFromMap(makerAccount); const makerAuthority = makerUserAccount.authority; @@ -934,12 +945,11 @@ export class FillerBot implements Bot { await this.userStatsMap!.mustGet(makerAuthority.toString()) ).userStatsAccountPublicKey; makerInfos.push({ - maker: new PublicKey(makerNode.userAccount), + maker: new PublicKey(makerAccount), makerUserAccount: makerUserAccount, order: makerNode.order, makerStats: makerUserStats, }); - makersIncluded.add(makerAccount); } } diff --git a/src/makerSelection.ts b/src/makerSelection.ts new file mode 100644 index 00000000..ff80b5c1 --- /dev/null +++ b/src/makerSelection.ts @@ -0,0 +1,69 @@ +import { BN, convertToNumber, divCeil, DLOBNode, ZERO } from '@drift-labs/sdk'; +import { MakerNodeMap, MAX_MAKERS_PER_FILL } from './bots/filler'; + +const PROBABILITY_PRECISION = new BN(1000); + +export function selectMakers(makerNodeMap: MakerNodeMap): MakerNodeMap { + const selectedMakers: MakerNodeMap = new Map(); + + while (selectedMakers.size < MAX_MAKERS_PER_FILL && makerNodeMap.size > 0) { + const maker = selectMaker(makerNodeMap); + if (maker === undefined) { + break; + } + const makerNodes = makerNodeMap.get(maker)!; + selectedMakers.set(maker, makerNodes); + makerNodeMap.delete(maker); + } + + return selectedMakers; +} + +function selectMaker(makerNodeMap: MakerNodeMap): string | undefined { + if (makerNodeMap.size === 0) { + return undefined; + } + + let totalLiquidity = ZERO; + for (const [_, dlobNodes] of makerNodeMap) { + totalLiquidity = totalLiquidity.add(getMakerLiquidity(dlobNodes)); + } + + const probabilities = []; + for (const [_, dlobNodes] of makerNodeMap) { + probabilities.push(getProbability(dlobNodes, totalLiquidity)); + } + + let makerIndex = 0; + const random = Math.random(); + let sum = 0; + for (let i = 0; i < probabilities.length; i++) { + sum += probabilities[i]; + if (random < sum) { + makerIndex = i; + break; + } + } + + return Array.from(makerNodeMap.keys())[makerIndex]; +} + +function getProbability(dlobNodes: DLOBNode[], totalLiquidity: BN): number { + const makerLiquidity = getMakerLiquidity(dlobNodes); + return convertToNumber( + divCeil(makerLiquidity.mul(PROBABILITY_PRECISION), totalLiquidity), + PROBABILITY_PRECISION + ); +} + +function getMakerLiquidity(dlobNodes: DLOBNode[]): BN { + return dlobNodes.reduce( + (acc, dlobNode) => + acc.add( + dlobNode.order!.baseAssetAmount.sub( + dlobNode.order!.baseAssetAmountFilled + ) + ), + ZERO + ); +} diff --git a/src/types.test.ts b/src/types.test.ts index 3f6f1a45..e0ad6514 100644 --- a/src/types.test.ts +++ b/src/types.test.ts @@ -1,6 +1,8 @@ import { expect } from 'chai'; import { BN, isVariant } from '@drift-labs/sdk'; import { TwapExecutionProgress } from './types'; +import { selectMakers } from './makerSelection'; +import { MAX_MAKERS_PER_FILL } from './bots/filler'; describe('TwapExecutionProgress', () => { const startTs = 1000; @@ -265,3 +267,64 @@ describe('TwapExecutionProgress', () => { expect(slice.toString()).to.be.equal(new BN(0).toString()); }); }); + +describe('selectMakers', () => { + let originalRandom: { (): number; (): number }; + + beforeEach(() => { + // Mock Math.random + let seed = 12345; + originalRandom = Math.random; + Math.random = () => { + const x = Math.sin(seed++) * 10000; + return x - Math.floor(x); + }; + }); + + afterEach(() => { + // Restore original Math.random + Math.random = originalRandom; + }); + + it('more than 6', function () { + // Mock DLOBNode and Order + const mockOrder = (filledAmount: number, orderId: number) => ({ + orderId, + baseAssetAmount: new BN(100), + baseAssetAmountFilled: new BN(filledAmount), + }); + + const mockDLOBNode = (filledAmount: number, orderId: number) => ({ + order: mockOrder(filledAmount, orderId), + // Include other necessary properties of DLOBNode if needed + }); + + const makerNodeMap = new Map([ + ['0', [mockDLOBNode(10, 0)]], + ['1', [mockDLOBNode(20, 1)]], + ['2', [mockDLOBNode(30, 2)]], + ['3', [mockDLOBNode(40, 3)]], + ['4', [mockDLOBNode(50, 4)]], + ['5', [mockDLOBNode(60, 5)]], + ['6', [mockDLOBNode(70, 6)]], + ['7', [mockDLOBNode(80, 7)]], + ['8', [mockDLOBNode(90, 8)]], + ]); + + // @ts-ignore + const selectedMakers = selectMakers(makerNodeMap); + + expect(selectedMakers).to.not.be.undefined; + expect(selectedMakers.size).to.be.equal(MAX_MAKERS_PER_FILL); + + expect(selectedMakers.get('0')).to.not.be.undefined; + expect(selectedMakers.get('1')).to.not.be.undefined; + expect(selectedMakers.get('2')).to.not.be.undefined; + expect(selectedMakers.get('3')).to.not.be.undefined; + expect(selectedMakers.get('4')).to.be.undefined; + expect(selectedMakers.get('5')).to.not.be.undefined; + expect(selectedMakers.get('6')).to.not.be.undefined; + expect(selectedMakers.get('7')).to.be.undefined; + expect(selectedMakers.get('8')).to.be.undefined; + }); +});