Skip to content

Commit

Permalink
Merge pull request #101 from White-Whale-Defi-Platform/feat/orderbook
Browse files Browse the repository at this point in the history
Feat/orderbook
  • Loading branch information
SirTLB authored Sep 5, 2023
2 parents 9b1cc5a + 52d8988 commit 8eb2fa6
Show file tree
Hide file tree
Showing 28 changed files with 1,029 additions and 288 deletions.
2 changes: 1 addition & 1 deletion src/chains/defaults/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export { getFlashArbMessages } from "./messages/getFlashArbMessages";
export { messageFactory } from "./messages/messageFactory";
export { getPoolStates } from "./queries/getPoolState";
export { initPools } from "./queries/getPoolState";
42 changes: 42 additions & 0 deletions src/chains/defaults/messages/getSwapMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { toUtf8 } from "@cosmjs/encoding";
import { EncodeObject } from "@cosmjs/proto-signing";
import { MsgExecuteContract } from "cosmjs-types/cosmwasm/wasm/v1/tx";

import { Asset, isNativeAsset } from "../../../core/types/base/asset";
import { Pool } from "../../../core/types/base/pool";

/**
*
*/
export function getSwapMessage(
pool: Pool,
offerAsset: Asset,
walletAddress: string,
beliefPrice: string,
maxSpread = 0.005,
) {
const msg = {
swap: {
max_spread: String(maxSpread),
offer_asset: offerAsset,
// belief_price: beliefPrice,
},
};
const encodedMsgObject: EncodeObject = {
typeUrl: "/cosmwasm.wasm.v1.MsgExecuteContract",
value: MsgExecuteContract.fromJSON({
funds: [
{
denom: isNativeAsset(offerAsset.info)
? offerAsset.info.native_token.denom
: offerAsset.info.token.contract_addr,
amount: offerAsset.amount,
},
],
sender: walletAddress,
contract: pool.address,
msg: toUtf8(JSON.stringify(msg)),
}),
};
return encodedMsgObject;
}
22 changes: 22 additions & 0 deletions src/chains/defaults/messages/messageFactory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { EncodeObject } from "@cosmjs/proto-signing";

import { OptimalTrade } from "../../../core/arbitrage/arbitrage";
import { OptimalOrderbookTrade } from "../../../core/arbitrage/optimizers/orderbookOptimizer";
import { getOrderbookArbMessages } from "../../inj/messages/getOrderbookArbMessage";
import { getFlashArbMessages } from "./getFlashArbMessages";
/**
*
*/
export function messageFactory(
arbTrade: OptimalTrade | OptimalOrderbookTrade,
publicAddress: string,
flashloancontract?: string,
): [Array<EncodeObject>, number] | undefined {
if (arbTrade.path["orderbook" as keyof typeof arbTrade.path] !== undefined) {
return getOrderbookArbMessages(<OptimalOrderbookTrade>arbTrade, publicAddress);
} else if (flashloancontract !== undefined) {
return getFlashArbMessages(<OptimalTrade>arbTrade, publicAddress, flashloancontract);
} else {
return undefined;
}
}
1 change: 1 addition & 0 deletions src/chains/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//export all known setups for chains
export * as defaults from "./defaults";
export * as injective from "./inj";
export * as juno from "./juno";
export * as terra from "./terra";
2 changes: 2 additions & 0 deletions src/chains/inj/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export * from "../defaults";
export { getOrderbookState } from "./queries/getOrderbookState";
export { initOrderbooks } from "./queries/initOrderbook";
61 changes: 61 additions & 0 deletions src/chains/inj/messages/getOrderbookArbMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { EncodeObject } from "@cosmjs/proto-signing";

import { OptimalOrderbookTrade } from "../../../core/arbitrage/optimizers/orderbookOptimizer";
import { toChainAsset, toChainPrice } from "../../../core/types/base/asset";
import { OrderSequence } from "../../../core/types/base/path";
import { outGivenIn } from "../../../core/types/base/pool";
import { getSwapMessage } from "../../defaults/messages/getSwapMessage";
import { getMarketSpotOrderMessage } from "./getSpotOrderMessage";

/**
*
*/
export function getOrderbookArbMessages(
arbTrade: OptimalOrderbookTrade,
publicAddress: string,
): [Array<EncodeObject>, number] {
if (arbTrade.path.orderSequence === OrderSequence.AmmFirst) {
//buy on the amm, transfer to trading account, sell the inj there, withdraw the usdt to injective account
const [outGivenIn0, outInfo0] = outGivenIn(arbTrade.path.pool, arbTrade.offerAsset);

const price = toChainPrice(arbTrade.offerAsset, { amount: String(outGivenIn0), info: outInfo0 });
const offerAsset = toChainAsset(arbTrade.offerAsset);
const msg0 = getSwapMessage(arbTrade.path.pool, offerAsset, publicAddress, price);

const offerAsset1 = {
amount: String(outGivenIn0),
info: outInfo0,
};

const msg1 = getMarketSpotOrderMessage(arbTrade, publicAddress, offerAsset1, 2);

return [[msg0, msg1], 2];
} else {
const offerAsset1 = {
amount: String(arbTrade.outGivenIn),
info: arbTrade.path.orderbook.baseAssetInfo,
};
const msg0 = getMarketSpotOrderMessage(arbTrade, publicAddress, offerAsset1, 1);

const [outGivenIn1, outInfo1] = outGivenIn(arbTrade.path.pool, {
amount: String(arbTrade.outGivenIn),
info: offerAsset1.info,
});

const belief_price = toChainPrice(
{
amount: String(arbTrade.outGivenIn),
info: offerAsset1.info,
},
{ amount: String(outGivenIn1), info: outInfo1 },
);
const offerAsset = toChainAsset({
amount: String(arbTrade.outGivenIn),
info: offerAsset1.info,
});

const msg1 = getSwapMessage(arbTrade.path.pool, offerAsset, publicAddress, belief_price);

return [[msg0, msg1], 2];
}
}
48 changes: 48 additions & 0 deletions src/chains/inj/messages/getSpotOrderMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { MsgCreateSpotMarketOrder, spotPriceToChainPriceToFixed } from "@injectivelabs/sdk-ts";
import { BigNumberInBase } from "@injectivelabs/utils/dist/cjs/classes";

import { OptimalOrderbookTrade } from "../../../core/arbitrage/optimizers/orderbookOptimizer";
import { Asset, isMatchingAssetInfos } from "../../../core/types/base/asset";
import { SpotMarketOrderMessage } from "../../../core/types/messages/spotorders";

/**
*'/injective.exchange.v1beta1.MsgCreateSpotMarketOrder'.
*/
export function getMarketSpotOrderMessage(
arbTrade: OptimalOrderbookTrade,
injectiveAddress: string,
offerAsset: Asset,
orderType: 1 | 2,
) {
let decimals = 6;
if (isMatchingAssetInfos(offerAsset.info, arbTrade.path.orderbook.baseAssetInfo)) {
decimals = arbTrade.path.orderbook.baseAssetDecimals - arbTrade.path.orderbook.quoteAssetDecimals;
} else {
decimals = arbTrade.path.orderbook.quoteAssetDecimals - arbTrade.path.orderbook.quoteAssetDecimals;
}

const beliefPriceOrderbook = spotPriceToChainPriceToFixed({
value: Math.round(arbTrade.worstPrice * 1000) / 1000,
baseDecimals: arbTrade.path.orderbook.baseAssetDecimals,
quoteDecimals: arbTrade.path.orderbook.quoteAssetDecimals,
});

let orderSize = +new BigNumberInBase(offerAsset.amount).toWei(decimals).toFixed();
orderSize =
Math.floor(+orderSize / arbTrade.path.orderbook.minQuantityIncrement) *
arbTrade.path.orderbook.minQuantityIncrement;

const marketSpotOrderMsg: SpotMarketOrderMessage = {
marketId: arbTrade.path.orderbook.marketId,
subaccountId: "",
injectiveAddress: injectiveAddress,
orderType: orderType,
feeRecipient: injectiveAddress,
price: beliefPriceOrderbook,
quantity: String(orderSize),
};
return {
typeUrl: "/injective.exchange.v1beta1.MsgCreateSpotMarketOrder",
value: MsgCreateSpotMarketOrder.fromJSON(marketSpotOrderMsg),
};
}
51 changes: 51 additions & 0 deletions src/chains/inj/queries/getOrderbookState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { BigNumberInBase, BigNumberInWei } from "@injectivelabs/utils";

import { ChainOperator } from "../../../core/chainOperator/chainoperator";
import { isNativeAsset } from "../../../core/types/base/asset";
import { Order, Orderbook } from "../../../core/types/base/orderbook";

/**
*
*/
export async function getOrderbookState(chainOperator: ChainOperator, orderbooks: Array<Orderbook>) {
await Promise.all(
orderbooks.map(async (orderbook) => {
const ob = await chainOperator.queryOrderbook(orderbook.marketId);
if (!ob) {
console.log("cannot fetch orderbook: ", orderbook.marketId);
return;
}
orderbook.sells = [];
orderbook.buys = [];
let quantityDecimals: number;
let priceDecimals: number;
if (isNativeAsset(orderbook.baseAssetInfo) && orderbook.baseAssetInfo.native_token.denom === "inj") {
quantityDecimals = 12;
priceDecimals = 12;
} else {
quantityDecimals = 0;
priceDecimals = 0;
}
ob.buys.map((buy) => {
const quantity = new BigNumberInWei(buy.quantity).toBase(quantityDecimals);
const price = new BigNumberInBase(buy.price).toWei(priceDecimals);
const buyOrder: Order = {
quantity: +quantity.toFixed(),
price: +price.toFixed(),
type: "buy",
};
orderbook.buys.push(buyOrder);
ob.sells.map((sell) => {
const quantity = new BigNumberInWei(sell.quantity).toBase(quantityDecimals);
const price = new BigNumberInBase(sell.price).toWei(priceDecimals);
const sellOrder: Order = {
quantity: +quantity.toFixed(),
price: +price.toFixed(),
type: "sell",
};
orderbook.sells.push(sellOrder);
});
});
}),
);
}
37 changes: 37 additions & 0 deletions src/chains/inj/queries/initOrderbook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { ChainOperator } from "../../../core/chainOperator/chainoperator";
import { AssetInfo } from "../../../core/types/base/asset";
import { DexConfig } from "../../../core/types/base/configs";
import { Orderbook } from "../../../core/types/base/orderbook";
import { identity } from "../../../core/types/identity";
import { getOrderbookState } from "./getOrderbookState";
/**
*
*/
export async function initOrderbooks(
chainoperator: ChainOperator,
botConfig: DexConfig,
): Promise<Array<Orderbook> | undefined> {
const orderbooks: Array<Orderbook> = [];
for (const orderbookAddress of botConfig.orderbooks) {
const marketInfo = await chainoperator.queryMarket(orderbookAddress);
if (!marketInfo) {
console.log("cannot fetch market: ", orderbookAddress);
return;
}
const baseAssetInfo: AssetInfo = { native_token: { denom: marketInfo.baseDenom } };
const quoteAssetInfo: AssetInfo = { native_token: { denom: marketInfo.quoteDenom } };
const ob = identity<Orderbook>({
baseAssetInfo: baseAssetInfo,
quoteAssetInfo: quoteAssetInfo,
baseAssetDecimals: marketInfo.baseToken?.decimals ?? 6,
quoteAssetDecimals: marketInfo.quoteToken?.decimals ?? 6,
minQuantityIncrement: marketInfo.minQuantityTickSize ?? 10e3,
buys: [],
sells: [],
marketId: orderbookAddress,
});
orderbooks.push(ob);
}
await getOrderbookState(chainoperator, orderbooks);
return orderbooks;
}
59 changes: 42 additions & 17 deletions src/core/arbitrage/arbitrage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Asset } from "../types/base/asset";
import { DexConfig, LiquidationConfig } from "../types/base/configs";
import { AnchorOverseer } from "../types/base/overseer";
import { Path } from "../types/base/path";
import { getOptimalTrade } from "./optimizers/analyticalOptimizer";
import { isOrderbookPath, OrderbookPath, Path } from "../types/base/path";
import { getOptimalTrade as getOptimalAmmTrade } from "./optimizers/analyticalOptimizer";
import { getOptimalTrade as getOptimalOrderbookTrade, OptimalOrderbookTrade } from "./optimizers/orderbookOptimizer";

export interface OptimalTrade {
offerAsset: Asset;
Expand All @@ -12,8 +13,25 @@ export interface OptimalTrade {
/**
*
*/
export function trySomeArb(paths: Array<Path>, botConfig: DexConfig): OptimalTrade | undefined {
const optimalTrade: OptimalTrade | undefined = getOptimalTrade(paths, botConfig.offerAssetInfo);
export function tryAmmArb(paths: Array<Path>, botConfig: DexConfig): OptimalTrade | undefined {
const optimalTrade: OptimalTrade | undefined = getOptimalAmmTrade(paths, botConfig.offerAssetInfo);

if (!optimalTrade) {
return undefined;
} else {
if (!isAboveThreshold(botConfig, optimalTrade)) {
return undefined;
} else {
return optimalTrade;
}
}
}

/**
*
*/
export function tryOrderbookArb(paths: Array<OrderbookPath>, botConfig: DexConfig): OptimalOrderbookTrade | undefined {
const optimalTrade: OptimalOrderbookTrade | undefined = getOptimalOrderbookTrade(paths, botConfig.offerAssetInfo);

if (!optimalTrade) {
return undefined;
Expand Down Expand Up @@ -45,17 +63,24 @@ export function tryLiquidationArb(
/**
*
*/
function isAboveThreshold(botConfig: DexConfig, 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;
function isAboveThreshold(botConfig: DexConfig, optimalTrade: OptimalTrade | OptimalOrderbookTrade): boolean {
if (isOrderbookPath(optimalTrade.path)) {
return optimalTrade.profit >= Array.from(botConfig.profitThresholds.values())[0];
} else {
// 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
);
}
}
Loading

0 comments on commit 8eb2fa6

Please sign in to comment.