diff --git a/README.md b/README.md index 3295e422..b6ae395f 100644 --- a/README.md +++ b/README.md @@ -109,6 +109,7 @@ Other optional arguments are: - `--pool-update-interval`, Option to specify time (in minutes) between pools updates, default is 15 minutes, Will override the 'POOL_UPDATE_INTERVAL' in env variables - `--self-fund-orders`, Specifies owned order to get funded once their vault goes below the specified threshold, example: token,vaultId,threshold,toptupamount;token,vaultId,threshold,toptupamount;... . Will override the 'SELF_FUND_ORDERS' in env variables - `--route`, Specifies the routing mode 'multi' or 'single' or 'full', default is 'single'. Will override the 'ROUTE' in env variables +- `--exec-record-size`, Option for specifying the count of latest rounds reports are used for calculating avg execution performance, default is 50, Will override the 'EXEC_RECORD_SIZE' in env variables - `-w` or `--wallet-count`, Number of wallet to submit transactions with, requirs `--mnemonic`. Will override the 'WALLET_COUNT' in env variables - `-t` or `--topup-amount`, The initial topup amount of excess wallets, requirs `--mnemonic`. Will override the 'TOPUP_AMOUNT' in env variables - `-V` or `--version`, output the version number @@ -252,6 +253,9 @@ SELF_FUND_ORDERS= # Specifies the routing mode 'multi' or 'single' or 'full', default is 'single' ROUTE="single" + +# Option for specifying the count of latest rounds reports are used for calculating avg execution performance, default is 50 +EXEC_RECORD_SIZE= ``` If both env variables and CLI argument are set, the CLI arguments will be prioritized and override the env variables. diff --git a/example.env b/example.env index 2a4eac6a..63d709ac 100644 --- a/example.env +++ b/example.env @@ -82,6 +82,9 @@ SELF_FUND_ORDERS= # Specifies the routing mode 'multi' or 'single' or 'full', default is 'single' ROUTE="single" +# Option for specifying the count of latest rounds reports are used for calculating avg execution performance, default is 50 +EXEC_RECORD_SIZE= + # test rpcs vars TEST_POLYGON_RPC= diff --git a/src/cli.ts b/src/cli.ts index 63a92535..fb2421b1 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -48,6 +48,7 @@ const ENV_OPTIONS = { maxRatio: process?.env?.MAX_RATIO?.toLowerCase() === "true" ? true : false, bundle: process?.env?.NO_BUNDLE?.toLowerCase() === "true" ? false : true, timeout: process?.env?.TIMEOUT, + execRecordSize: process?.env?.EXEC_RECORD_SIZE, flashbotRpc: process?.env?.FLASHBOT_RPC, hops: process?.env?.HOPS, retries: process?.env?.RETRIES, @@ -163,6 +164,10 @@ const getOptions = async (argv: any, version?: string) => { "--route ", "Specifies the routing mode 'multi' or 'single' or 'full', default is 'single'. Will override the 'ROUTE' in env variables", ) + .option( + "--exec-record-size ", + "Option for specifying the count of latest rounds reports are used for calculating avg execution performance, default is 50, Will override the 'EXEC_RECORD_SIZE' in env variables", + ) .description( [ "A NodeJS app to find and take arbitrage trades for Rain Orderbook orders against some DeFi liquidity providers, requires NodeJS v18 or higher.", @@ -191,6 +196,7 @@ const getOptions = async (argv: any, version?: string) => { cmdOptions.maxRatio = cmdOptions.maxRatio || ENV_OPTIONS.maxRatio; cmdOptions.flashbotRpc = cmdOptions.flashbotRpc || ENV_OPTIONS.flashbotRpc; cmdOptions.timeout = cmdOptions.timeout || ENV_OPTIONS.timeout; + cmdOptions.execRecordSize = cmdOptions.execRecordSize || ENV_OPTIONS.execRecordSize || 50; cmdOptions.hops = cmdOptions.hops || ENV_OPTIONS.hops; cmdOptions.retries = cmdOptions.retries || ENV_OPTIONS.retries; cmdOptions.poolUpdateInterval = cmdOptions.poolUpdateInterval || ENV_OPTIONS.poolUpdateInterval; @@ -245,7 +251,7 @@ export const arbRound = async ( if (!ordersDetails.length) { span.setStatus({ code: SpanStatusCode.OK, message: "found no orders" }); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], oppCount: 0, successCount: 0, avgGasCost: undefined }; } } catch (e: any) { const snapshot = errorSnapshot("", e); @@ -254,12 +260,13 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], oppCount: 0, successCount: 0, avgGasCost: undefined }; } try { - let txs; - let foundOpp = false; + let oppCount = 0; + let successCount = 0; + const txs: string[] = []; const { reports = [], avgGasCost = undefined } = await clear( config, ordersDetails, @@ -267,27 +274,30 @@ export const arbRound = async ( ctx, ); if (reports && reports.length) { - txs = reports.map((v) => v.txUrl).filter((v) => !!v); + reports.forEach((v) => { + if (v.txUrl) txs.push(v.txUrl); + if (v?.successfull) successCount++; + if (v.status === ProcessPairReportStatus.FoundOpportunity) oppCount++; + }); if (txs.length) { - foundOpp = true; span.setAttribute("txUrls", txs); + } + if (successCount) { span.setAttribute("didClear", true); - span.setAttribute("foundOpp", true); - } else if ( - reports.some((v) => v.status === ProcessPairReportStatus.FoundOpportunity) - ) { - foundOpp = true; + } + if (oppCount) { span.setAttribute("foundOpp", true); } } else { span.setAttribute("didClear", false); + span.setAttribute("foundOpp", false); } if (avgGasCost) { span.setAttribute("avgGasCost", avgGasCost.toString()); } span.setStatus({ code: SpanStatusCode.OK }); span.end(); - return { txs, foundOpp, avgGasCost }; + return { txs, oppCount, successCount, avgGasCost }; } catch (e: any) { if (e?.startsWith?.("Failed to batch quote orders")) { span.setAttribute("severity", ErrorSeverity.LOW); @@ -301,7 +311,7 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], oppCount: 0, successCount: 0, avgGasCost: undefined }; } } catch (e: any) { const snapshot = errorSnapshot("Unexpected error occured", e); @@ -311,7 +321,7 @@ export const arbRound = async ( span.setAttribute("didClear", false); span.setAttribute("foundOpp", false); span.end(); - return { txs: [], foundOpp: false, avgGasCost: undefined }; + return { txs: [], oppCount: 0, successCount: 0, avgGasCost: undefined }; } }); }; @@ -351,6 +361,15 @@ export async function startup(argv: any, version?: string, tracer?: Tracer, ctx? if (/^[0-9]+$/.test(options.sleep)) roundGap = Number(options.sleep) * 1000; else throw "invalid sleep value, must be an integer greater than equal 0"; } + if (options.execRecordSize) { + if (typeof options.execRecordSize === "string") { + if (/^[0-9]+$/.test(options.sleep)) + options.execRecordSize = Number(options.execRecordSize); + else throw "invalid Execution Record Size value, must be an integer greater than 0"; + } else if (typeof options.execRecordSize !== "number") { + throw "invalid Execution Record Size value, must be an integer greater than 0"; + } + } if (options.poolUpdateInterval) { if (typeof options.poolUpdateInterval === "number") { _poolUpdateInterval = options.poolUpdateInterval; @@ -404,6 +423,22 @@ export async function startup(argv: any, version?: string, tracer?: Tracer, ctx? }; } +/** + * Calculates the opps count standard deviation from the avg of the given length of previous rounds records + */ +export const handleOppsRecord = ( + recordSize: number, + previousRecords: number[], + oppCount: number, +): number => { + const avg = Math.floor(previousRecords.reduce((a, b) => a + b, 0) / previousRecords.length); + previousRecords.push(oppCount); + if (previousRecords.length > recordSize) { + previousRecords.splice(0, previousRecords.length - recordSize); + } + return oppCount - avg; +}; + export const main = async (argv: any, version?: string) => { // startup otel to collect span, logs, etc // diag otel @@ -473,6 +508,7 @@ export const main = async (argv: any, version?: string) => { const wgc: ViemClient[] = []; const wgcBuffer: { address: string; count: number }[] = []; const botMinBalance = ethers.utils.parseUnits(options.botMinBalance); + const previousRoundsRecords: number[] = []; // run bot's processing orders in a loop // eslint-disable-next-line no-constant-condition @@ -538,24 +574,37 @@ export const main = async (argv: any, version?: string) => { try { await rotateProviders(config, update); const roundResult = await arbRound(tracer, roundCtx, options, config); - let txs, foundOpp, roundAvgGasCost; + let txs, roundAvgGasCost; + let oppCount = 0; + let successCount = 0; if (roundResult) { txs = roundResult.txs; - foundOpp = roundResult.foundOpp; + oppCount = roundResult.oppCount; + successCount = roundResult.successCount; roundAvgGasCost = roundResult.avgGasCost; } if (txs && txs.length) { roundSpan.setAttribute("txUrls", txs); - roundSpan.setAttribute("didClear", true); - roundSpan.setAttribute("foundOpp", true); - } else if (foundOpp) { + } + if (oppCount) { roundSpan.setAttribute("foundOpp", true); - roundSpan.setAttribute("didClear", false); } else { roundSpan.setAttribute("foundOpp", false); + } + if (successCount) { + roundSpan.setAttribute("didClear", true); + } else { roundSpan.setAttribute("didClear", false); } + // record opps stdvs + const oppsCountStdvs = handleOppsRecord( + options.execRecordSize, + previousRoundsRecords, + oppCount, + ); + roundSpan.setAttribute("opps-stdvs", oppsCountStdvs); + // keep avg gas cost if (roundAvgGasCost) { const _now = Date.now(); diff --git a/src/processOrders.ts b/src/processOrders.ts index 728e6abe..affd6920 100644 --- a/src/processOrders.ts +++ b/src/processOrders.ts @@ -690,6 +690,7 @@ export async function processPair(args: { sellToken: orderPairObject.sellToken, clearedAmount: clearActualAmount?.toString(), actualGasCost: ethers.utils.formatUnits(actualGasCost), + successfull: true, income, inputTokenIncome: inputTokenIncome ? ethers.utils.formatUnits(inputTokenIncome, toToken.decimals) @@ -735,6 +736,7 @@ export async function processPair(args: { buyToken: orderPairObject.buyToken, sellToken: orderPairObject.sellToken, actualGasCost: ethers.utils.formatUnits(actualGasCost), + successfull: false, }; result.reason = ProcessPairHaltReason.TxMineFailed; return Promise.reject(result); @@ -756,6 +758,7 @@ export async function processPair(args: { tokenPair: pair, buyToken: orderPairObject.buyToken, sellToken: orderPairObject.sellToken, + successfull: false, }; if (actualGasCost) { result.report.actualGasCost = ethers.utils.formatUnits(actualGasCost); diff --git a/src/types.ts b/src/types.ts index 23f992fe..29a916b4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -45,6 +45,7 @@ export type CliOptions = { selfFundOrders?: SelfFundOrder[]; tokens?: TokenDetails[]; route?: string; + execRecordSize: number; }; export type TokenDetails = { @@ -160,6 +161,7 @@ export type Report = { clearedOrders?: string[]; income?: BigNumber; netProfit?: BigNumber; + successfull?: boolean; }; export type RoundReport = { diff --git a/test/cli.test.js b/test/cli.test.js index 9b5f6b3e..003842f6 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -1,7 +1,7 @@ require("dotenv").config(); const { assert } = require("chai"); const mockServer = require("mockttp").getLocal(); -const { arbRound, startup } = require("../src/cli"); +const { arbRound, startup, handleOppsRecord } = require("../src/cli"); const { trace, context } = require("@opentelemetry/api"); const { Resource } = require("@opentelemetry/resources"); const { BasicTracerProvider } = require("@opentelemetry/sdk-trace-base"); @@ -48,7 +48,7 @@ describe("Test cli", async function () { }; const response = await arbRound(tracer, ctx, options, { mainAccount: {} }); - const expected = { txs: [], foundOpp: false, avgGasCost: undefined }; + const expected = { txs: [], oppCount: 0, successCount: 0, avgGasCost: undefined }; assert.deepEqual(response, expected); testSpan.end(); @@ -216,4 +216,13 @@ describe("Test cli", async function () { assert.deepEqual(result.config.rpcRecords, expected.config.rpcRecords); assert.equal(result.options.botMinBalance, expected.options.botMinBalance); }); + + it("test handleOppsRecord()", async function () { + const record = [3, 4, 5, 3, 3, 7, 4]; + const size = 7; + const currentOppCount = 1; + const result = handleOppsRecord(size, record, currentOppCount); + const expected = -3; + assert.equal(result, expected); + }); }); diff --git a/test/processPair.test.js b/test/processPair.test.js index d5913cbd..c8c790b4 100644 --- a/test/processPair.test.js +++ b/test/processPair.test.js @@ -126,6 +126,7 @@ describe("Test process pair", async function () { clearedOrders: [orderPairObject.takeOrders[0].id], inputTokenIncome: undefined, outputTokenIncome: undefined, + successfull: true, }, reason: undefined, error: undefined, @@ -205,6 +206,7 @@ describe("Test process pair", async function () { clearedOrders: [orderPairObject.takeOrders[0].id], inputTokenIncome: undefined, outputTokenIncome: undefined, + successfull: true, }, reason: undefined, error: undefined, @@ -601,6 +603,7 @@ describe("Test process pair", async function () { sellToken: orderPairObject.sellToken, txUrl: scannerUrl + "/tx/" + txHash, actualGasCost: formatUnits(effectiveGasPrice.mul(gasUsed)), + successfull: false, }, reason: ProcessPairHaltReason.TxMineFailed, error: undefined,