Skip to content

Commit

Permalink
Bug/profit calculation and path cooldown (#50)
Browse files Browse the repository at this point in the history
* feat: add cooldown feature to paths, calculate fees based on msgs

We use a cooldown feature on paths to prevent the bot for overtrading a
path whiles the arb is already gone. if it did one attempt cooldown is
set to true, on next block arrival the bot sets all paths cooldowns back
to false.
Also, this commit changes the way fees are attached to TXs, because fees
do not only scale with amount of pools, but also amount of messages used
for a pooltrade, therefore the amount of msgs is used as an indexer for
the amount of txfees to attach to a tx

* bug: fix profit calculation based on tradesize and skipbid

* chore: more comments on profit calc and try-catch on mempool tx
  • Loading branch information
SirTLB authored Feb 17, 2023
1 parent 3313a2d commit b7d3852
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 133 deletions.
47 changes: 23 additions & 24 deletions src/core/arbitrage/arbitrage.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Asset, isNativeAsset } from "../types/base/asset";
import { Asset } from "../types/base/asset";
import { BotConfig } from "../types/base/botConfig";
import { Path } from "../types/base/path";
import { getOptimalTrade } from "./optimizers/analyticalOptimizer";
Expand All @@ -12,34 +12,33 @@ export interface OptimalTrade {
*
*/
export function trySomeArb(paths: Array<Path>, botConfig: BotConfig): OptimalTrade | undefined {
const [path, tradesize, profit] = getOptimalTrade(paths, botConfig.offerAssetInfo);
const optimalTrade: OptimalTrade | undefined = getOptimalTrade(paths, botConfig.offerAssetInfo);

if (path === undefined) {
if (!optimalTrade) {
return undefined;
} else {
const profitThreshold =
botConfig.profitThresholds.get(path.pools.length) ??
Array.from(botConfig.profitThresholds.values())[botConfig.profitThresholds.size];
if (profit < profitThreshold) {
if (!isAboveThreshold(botConfig, optimalTrade)) {
return undefined;
} else {
console.log("optimal tradesize: ", tradesize, " with profit: ", profit);
console.log("path: "),
path.pools.map((pool) => {
console.log(
pool.address,
isNativeAsset(pool.assets[0].info)
? pool.assets[0].info.native_token.denom
: pool.assets[0].info.token.contract_addr,
pool.assets[0].amount,
isNativeAsset(pool.assets[1].info)
? pool.assets[1].info.native_token.denom
: pool.assets[1].info.token.contract_addr,
pool.assets[1].amount,
);
});
const offerAsset: Asset = { amount: String(tradesize), info: botConfig.offerAssetInfo };
return { path, offerAsset, profit };
return optimalTrade;
}
}
}

/**
*
*/
function isAboveThreshold(botConfig: BotConfig, optimalTrade: OptimalTrade): boolean {
// We dont know the number of message required to execute the trade, so the profit threshold will be set to the most conservative value: nr_of_pools*2-1
const profitThreshold =
botConfig.profitThresholds.get((optimalTrade.path.pools.length - 1) * 2 + 1) ??
Array.from(botConfig.profitThresholds.values())[botConfig.profitThresholds.size - 1];
if (botConfig.skipConfig) {
const skipBidRate = botConfig.skipConfig.skipBidRate;
return (
(1 - skipBidRate) * optimalTrade.profit - (botConfig.flashloanFee / 100) * +optimalTrade.offerAsset.amount >
profitThreshold
); //profit - skipbid*profit - flashloanfee*tradesize must be bigger than the set PROFIT_THRESHOLD + TX_FEE. The TX fees dont depend on tradesize nor profit so are set in config
} else
return optimalTrade.profit - (botConfig.flashloanFee / 100) * +optimalTrade.offerAsset.amount > profitThreshold;
}
1 change: 1 addition & 0 deletions src/core/arbitrage/graph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function getPaths(graph: Graph, startingAsset: AssetInfo, depth: number):
}
paths.push({
pools: poolList,
cooldown: false,
});
}
return paths;
Expand Down
21 changes: 14 additions & 7 deletions src/core/arbitrage/optimizers/analyticalOptimizer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AssetInfo } from "../../types/base/asset";
import { Path } from "../../types/base/path";
import { getAssetsOrder, outGivenIn } from "../../types/base/pool";
import { OptimalTrade } from "../arbitrage";

// function to get the optimal tradsize and profit for a single path.
// it assumes the token1 from pool1 is the same asset as token1 from pool2 and
Expand Down Expand Up @@ -114,20 +115,26 @@ function getTradesizeAndProfitForPath(path: Path, offerAssetInfo: AssetInfo): [n
* @param paths Type `Array<Path>` to check for arbitrage.
* @param offerAssetInfo Type `AssetInfo` to start the arbitrage from.
*/
export function getOptimalTrade(paths: Array<Path>, offerAssetInfo: AssetInfo): [Path | undefined, number, number] {
export function getOptimalTrade(paths: Array<Path>, offerAssetInfo: AssetInfo): OptimalTrade | undefined {
let maxTradesize = 0;
let maxProfit = 0;
let maxPath;

paths.map((path: Path) => {
const [tradesize, profit] = getOptimalTradeForPath(path, offerAssetInfo);
if (profit > maxProfit && tradesize > 0) {
maxProfit = profit;
maxTradesize = tradesize;
maxPath = path;
if (!path.cooldown) {
const [tradesize, profit] = getOptimalTradeForPath(path, offerAssetInfo);
if (profit > maxProfit && tradesize > 0) {
maxProfit = profit;
maxTradesize = tradesize;
maxPath = path;
}
}
});
return [maxPath, maxTradesize, maxProfit];
if (maxPath) {
return { path: maxPath, offerAsset: { amount: String(maxTradesize), info: offerAssetInfo }, profit: maxProfit };
} else {
return undefined;
}
}

/** Given an ordered route, calculate the optimal amount into the first pool that maximizes the profit of swapping through the route
Expand Down
10 changes: 9 additions & 1 deletion src/core/types/arbitrageloops/mempoolLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ export class MempoolLoop {

if (arbTrade) {
await this.trade(arbTrade);
arbTrade.path.cooldown = true;
break;
}
}
Expand All @@ -128,6 +129,10 @@ export class MempoolLoop {
*
*/
public reset() {
// reset all paths that are on cooldown
this.paths.forEach((path) => {
path.cooldown = false;
});
this.totalBytes = 0;
flushTxMemory();
}
Expand All @@ -136,6 +141,9 @@ export class MempoolLoop {
*
*/
private async trade(arbTrade: OptimalTrade) {
if (arbTrade.path.cooldown) {
return;
}
const [msgs, nrOfMessages] = this.messageFunction(
arbTrade,
this.account.address,
Expand All @@ -151,7 +159,7 @@ export class MempoolLoop {
};

const TX_FEE =
this.botConfig.txFees.get(arbTrade.path.pools.length) ??
this.botConfig.txFees.get(nrOfMessages) ??
Array.from(this.botConfig.txFees.values())[this.botConfig.txFees.size - 1];

// sign, encode and broadcast the transaction
Expand Down
8 changes: 6 additions & 2 deletions src/core/types/arbitrageloops/skipLoop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ export class SkipLoop extends MempoolLoop {
const arbTrade: OptimalTrade | undefined = this.arbitrageFunction(this.paths, this.botConfig);
if (arbTrade) {
await this.skipTrade(arbTrade, trade);
break;
arbTrade.path.cooldown = true; //set the cooldown of this path to true so we dont trade it again in next callbacks
}
}
}
Expand All @@ -90,6 +90,10 @@ export class SkipLoop extends MempoolLoop {
*
*/
private async skipTrade(arbTrade: OptimalTrade, toArbTrade: MempoolTrade) {
if (arbTrade.path.cooldown) {
// dont execute if path is on cooldown
return;
}
if (
!this.botConfig.skipConfig?.useSkip ||
this.botConfig.skipConfig?.skipRpcUrl === undefined ||
Expand Down Expand Up @@ -131,7 +135,7 @@ export class SkipLoop extends MempoolLoop {

//if gas fee cannot be found in the botconfig based on pathlengths, pick highest available
const TX_FEE =
this.botConfig.txFees.get(arbTrade.path.pools.length) ??
this.botConfig.txFees.get(nrOfWasms) ??
Array.from(this.botConfig.txFees.values())[this.botConfig.txFees.size - 1];

const txRaw: TxRaw = await this.botClients.SigningCWClient.sign(
Expand Down
16 changes: 5 additions & 11 deletions src/core/types/base/botConfig.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Coin, StdFee } from "@cosmjs/stargate";
import { StdFee } from "@cosmjs/stargate";
import { assert } from "console";

import { NativeAssetInfo } from "./asset";
Expand All @@ -16,6 +16,7 @@ export interface BotConfig {
maxPathPools: number;
mappingFactoryRouter: Array<{ factory: string; router: string }>;
flashloanRouterAddress: string;
flashloanFee: number;
offerAssetInfo: NativeAssetInfo;
mnemonic: string;
useMempool: boolean;
Expand Down Expand Up @@ -64,23 +65,15 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig {
skipBidRate: envs.SKIP_BID_RATE === undefined ? 0 : +envs.SKIP_BID_RATE,
};
}
const FLASHLOAN_FEE = +envs.FLASHLOAN_FEE;
const PROFIT_THRESHOLD = +envs.PROFIT_THRESHOLD;

//set all required fees for the depth of the hops set by user;
const GAS_FEES = new Map<number, Coin>();
const TX_FEES = new Map<number, StdFee>();
const PROFIT_THRESHOLDS = new Map<number, number>();
for (let hops = 2; hops <= MAX_PATH_HOPS; hops++) {
for (let hops = 2; hops <= (MAX_PATH_HOPS - 1) * 2 + 1; hops++) {
const gasFee = { denom: envs.BASE_DENOM, amount: String(GAS_USAGE_PER_HOP * hops * +GAS_UNIT_PRICE) };
GAS_FEES.set(hops, gasFee);
TX_FEES.set(hops, { amount: [gasFee], gas: String(GAS_USAGE_PER_HOP * hops) });
const profitThreshold: number =
skipConfig === undefined
? PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE / 100) + +gasFee.amount //dont use skip bid on top of the threshold, include flashloan fee and gas fee
: PROFIT_THRESHOLD / (1 - FLASHLOAN_FEE / 100) +
+gasFee.amount +
skipConfig.skipBidRate * PROFIT_THRESHOLD; //need extra profit to provide the skip bid
const profitThreshold: number = PROFIT_THRESHOLD + +gasFee.amount;
PROFIT_THRESHOLDS.set(hops, profitThreshold);
}
const botConfig: BotConfig = {
Expand All @@ -90,6 +83,7 @@ export function setBotConfig(envs: NodeJS.ProcessEnv): BotConfig {
maxPathPools: MAX_PATH_HOPS,
mappingFactoryRouter: FACTORIES_TO_ROUTERS_MAPPING,
flashloanRouterAddress: envs.FLASHLOAN_ROUTER_ADDRESS,
flashloanFee: +envs.FLASHLOAN_FEE,
offerAssetInfo: OFFER_ASSET_INFO,
mnemonic: envs.WALLET_MNEMONIC,
useMempool: envs.USE_MEMPOOL == "1" ? true : false,
Expand Down
1 change: 1 addition & 0 deletions src/core/types/base/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ import { Pool } from "./pool";

export interface Path {
pools: Array<Pool>;
cooldown: boolean;
}
Loading

0 comments on commit b7d3852

Please sign in to comment.