Implement RouterUtils.getPriceImpactForRoute (#108)
- Add a function in RouterUtils to allow calculation of price impact - Supports multi-split & multi-hop routes - bump to v0.11.1
This commit is contained in:
parent
42ca95a4bc
commit
7f05704f39
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@orca-so/whirlpools-sdk",
|
||||
"version": "0.11.0",
|
||||
"version": "0.11.1",
|
||||
"description": "Typescript SDK to interact with Orca's Whirlpool program.",
|
||||
"license": "Apache-2.0",
|
||||
"main": "dist/index.js",
|
||||
|
@ -64,4 +64,4 @@
|
|||
"url": "https://github.com/orca-so/whirlpools/issues"
|
||||
},
|
||||
"homepage": "https://www.orca.so"
|
||||
}
|
||||
}
|
|
@ -97,7 +97,9 @@ export async function getSwapFromRoute(
|
|||
]
|
||||
);
|
||||
|
||||
const inputAmount = quoteOne.amountSpecifiedIsInput ? quoteOne.estimatedAmountIn : quoteOne.otherAmountThreshold;
|
||||
const inputAmount = quoteOne.amountSpecifiedIsInput
|
||||
? quoteOne.estimatedAmountIn
|
||||
: quoteOne.otherAmountThreshold;
|
||||
addOrNative(mintOneA.toString(), quoteOne.aToB ? inputAmount : ZERO);
|
||||
addOrNative(mintOneB.toString(), !quoteOne.aToB ? inputAmount : ZERO);
|
||||
addOrNative(mintTwoA.toString(), ZERO);
|
||||
|
|
|
@ -93,7 +93,12 @@ export class PriceModule {
|
|||
AddressUtil.toStrings(filteredPoolAddresses)
|
||||
);
|
||||
|
||||
const tickArrayMap = await PriceModuleUtils.fetchTickArraysForPools(fetcher, poolMap, config, opts);
|
||||
const tickArrayMap = await PriceModuleUtils.fetchTickArraysForPools(
|
||||
fetcher,
|
||||
poolMap,
|
||||
config,
|
||||
opts
|
||||
);
|
||||
const mints = Array.from(
|
||||
Object.values(poolMap).reduce((acc, pool) => {
|
||||
acc.add(pool.tokenMintA.toBase58());
|
||||
|
|
|
@ -90,6 +90,7 @@ export type SubTradeRoute = {
|
|||
* @param vaultA The address of the first vault in the pool.
|
||||
* @param vaultB The address of the second vault in the pool.
|
||||
* @param quote The {@link SwapQuote} for this hop.
|
||||
* @param snapshot A snapshot of the whirlpool condition when this hop was made
|
||||
*/
|
||||
export type TradeHop = {
|
||||
amountIn: BN;
|
||||
|
@ -102,6 +103,17 @@ export type TradeHop = {
|
|||
vaultA: Address;
|
||||
vaultB: Address;
|
||||
quote: SwapQuote;
|
||||
snapshot: TradeHopSnapshot;
|
||||
};
|
||||
|
||||
/**
|
||||
* A snapshot of the whirlpool condition when a trade hop was made.
|
||||
* @category Router
|
||||
*/
|
||||
export type TradeHopSnapshot = {
|
||||
aToB: boolean;
|
||||
sqrtPrice: BN;
|
||||
feeRate: Percentage;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -8,10 +8,14 @@ import {
|
|||
} from "@orca-so/common-sdk";
|
||||
import { Account } from "@solana/spl-token";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import { ExecutableRoute, RoutingOptions, TradeRoute } from ".";
|
||||
import BN from "bn.js";
|
||||
import Decimal from "decimal.js";
|
||||
import { ExecutableRoute, RoutingOptions, Trade, TradeRoute } from ".";
|
||||
import { WhirlpoolContext } from "../../context";
|
||||
import { getSwapFromRoute } from "../../instructions/composites/swap-with-route";
|
||||
import { PREFER_CACHE } from "../../network/public/fetcher";
|
||||
import { U64 } from "../../utils/math/constants";
|
||||
import { PriceMath } from "../../utils/public";
|
||||
import { isWalletConnected } from "../../utils/wallet-utils";
|
||||
|
||||
/**
|
||||
|
@ -135,6 +139,60 @@ export class RouterUtils {
|
|||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the price impact for a route.
|
||||
* @param trade The trade the user used to derive the route.
|
||||
* @param route The route to calculate the price impact for.
|
||||
* @returns A Decimal object representing the percentage value of the price impact (ex. 3.01%)
|
||||
*/
|
||||
static getPriceImpactForRoute(trade: Trade, route: TradeRoute): Decimal {
|
||||
const { amountSpecifiedIsInput } = trade;
|
||||
|
||||
const totalBaseValue = route.subRoutes.reduce((acc, route) => {
|
||||
const directionalHops = amountSpecifiedIsInput
|
||||
? route.hopQuotes
|
||||
: route.hopQuotes.slice().reverse();
|
||||
const baseOutputs = directionalHops.reduce((acc, quote, index) => {
|
||||
const { snapshot } = quote;
|
||||
const { aToB, sqrtPrice, feeRate } = snapshot;
|
||||
// Inverse sqrt price will cause 1bps precision loss since ticks are spaces of 1bps
|
||||
const directionalSqrtPrice = aToB ? sqrtPrice : PriceMath.invertSqrtPriceX64(sqrtPrice);
|
||||
|
||||
// Convert from in/out -> base_out/in using the directional price & fee rate
|
||||
let nextBaseValue;
|
||||
const price = directionalSqrtPrice.mul(directionalSqrtPrice).div(U64);
|
||||
if (amountSpecifiedIsInput) {
|
||||
const amountIn = index === 0 ? quote.amountIn : acc[index - 1];
|
||||
const feeAdjustedAmount = amountIn
|
||||
.mul(feeRate.denominator.sub(feeRate.numerator))
|
||||
.div(feeRate.denominator);
|
||||
nextBaseValue = price.mul(feeAdjustedAmount).div(U64);
|
||||
} else {
|
||||
const amountOut = index === 0 ? quote.amountOut : acc[index - 1];
|
||||
const feeAdjustedAmount = amountOut.mul(U64).div(price);
|
||||
nextBaseValue = feeAdjustedAmount
|
||||
.mul(feeRate.denominator)
|
||||
.div(feeRate.denominator.sub(feeRate.numerator));
|
||||
}
|
||||
|
||||
acc.push(nextBaseValue);
|
||||
return acc;
|
||||
}, new Array<BN>());
|
||||
|
||||
return acc.add(baseOutputs[baseOutputs.length - 1]);
|
||||
}, new BN(0));
|
||||
|
||||
const totalBaseValueDec = new Decimal(totalBaseValue.toString());
|
||||
const totalAmountEstimatedDec = new Decimal(
|
||||
amountSpecifiedIsInput ? route.totalAmountOut.toString() : route.totalAmountIn.toString()
|
||||
);
|
||||
const priceImpact = amountSpecifiedIsInput
|
||||
? totalBaseValueDec.sub(totalAmountEstimatedDec).div(totalBaseValueDec)
|
||||
: totalAmountEstimatedDec.sub(totalBaseValueDec).div(totalAmountEstimatedDec);
|
||||
|
||||
return priceImpact.mul(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the tick arrays addresses that are touched by a route.
|
||||
* @param route The route to get the tick arrays from.
|
||||
|
|
|
@ -5,7 +5,7 @@ import BN from "bn.js";
|
|||
import { SwapErrorCode } from "../errors/errors";
|
||||
import { PREFER_CACHE, WhirlpoolAccountFetcherInterface } from "../network/public/fetcher";
|
||||
import { SwapQuoteParam, swapQuoteWithParams } from "../quotes/public";
|
||||
import { Path } from "../utils/public";
|
||||
import { Path, PoolUtil } from "../utils/public";
|
||||
import { SwapQuoteRequest, batchBuildSwapQuoteParams } from "./batch-swap-quote";
|
||||
import { RoutingOptions, Trade, TradeHop } from "./public";
|
||||
|
||||
|
@ -124,6 +124,11 @@ function populateQuoteMap(
|
|||
vaultA,
|
||||
vaultB,
|
||||
quote,
|
||||
snapshot: {
|
||||
aToB: swapParam.aToB,
|
||||
sqrtPrice: whirlpoolData.sqrtPrice,
|
||||
feeRate: PoolUtil.getFeeRate(whirlpoolData.feeRate),
|
||||
},
|
||||
};
|
||||
} catch (e: any) {
|
||||
const errorCode: SwapErrorCode = e.errorCode;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
import { ONE, U64_MAX } from "@orca-so/common-sdk";
|
||||
|
||||
export const U64 = U64_MAX.add(ONE);
|
|
@ -126,6 +126,8 @@ export class PriceMath {
|
|||
|
||||
/**
|
||||
* Utility to invert the price Pb/Pa to Pa/Pb
|
||||
* NOTE: precision is lost in this conversion
|
||||
*
|
||||
* @param price Pb / Pa
|
||||
* @param decimalsA Decimals of original token A (i.e. token A in the given Pb / Pa price)
|
||||
* @param decimalsB Decimals of original token B (i.e. token B in the given Pb / Pa price)
|
||||
|
@ -139,6 +141,8 @@ export class PriceMath {
|
|||
|
||||
/**
|
||||
* Utility to invert the sqrtPriceX64 from X64 repr. of sqrt(Pb/Pa) to X64 repr. of sqrt(Pa/Pb)
|
||||
* NOTE: precision is lost in this conversion
|
||||
*
|
||||
* @param sqrtPriceX64 X64 representation of sqrt(Pb / Pa)
|
||||
* @returns inverted sqrtPriceX64, i.e. X64 representation of sqrt(Pa / Pb)
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,281 @@
|
|||
import { Percentage } from "@orca-so/common-sdk";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import * as assert from "assert";
|
||||
import BN from "bn.js";
|
||||
import Decimal from "decimal.js";
|
||||
import { PriceMath, RouterUtils } from "../../../src";
|
||||
import { U64 } from "../../../src/utils/math/constants";
|
||||
|
||||
const maxDecimalAccuracy = 4;
|
||||
describe("RouterUtil - Price Impact tests", () => {
|
||||
// Mock a Orca -> USDC ExactIn trade that has no split route and goes through a single hop (ORCA -> USDC)
|
||||
it("ExactIn, a->b true, single-hop, 1 split", () => {
|
||||
const params: RouteTestParam = {
|
||||
amountSpecifiedIsInput: true,
|
||||
totalAmountIn: new BN("1000000"),
|
||||
totalAmountOut: new BN("581050"),
|
||||
subRouteParams: [
|
||||
{
|
||||
hops: [
|
||||
{
|
||||
aToB: true,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("14082503933855903449"),
|
||||
amountIn: new BN("1000000"),
|
||||
amountOut: new BN("581050"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { trade, routes } = buildRouteTest(params);
|
||||
const impact = RouterUtils.getPriceImpactForRoute(trade, routes).toDecimalPlaces(
|
||||
maxDecimalAccuracy
|
||||
);
|
||||
const expect = calculateImpact(params).toDecimalPlaces(maxDecimalAccuracy);
|
||||
assert.equal(impact.toString(), expect.toString());
|
||||
});
|
||||
|
||||
// Mock a Orca -> USDC ExactOut trade that has no split route and goes through a single hop (ORCA -> USDC)
|
||||
it("ExactOut, a->b false, single-hop, 1 split", () => {
|
||||
const params: RouteTestParam = {
|
||||
amountSpecifiedIsInput: false,
|
||||
totalAmountIn: new BN("5833496"),
|
||||
totalAmountOut: new BN("10000000"),
|
||||
subRouteParams: [
|
||||
{
|
||||
hops: [
|
||||
{
|
||||
aToB: false,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("14067691597581169278"),
|
||||
amountIn: new BN("5833496"),
|
||||
amountOut: new BN("10000000"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { trade, routes } = buildRouteTest(params);
|
||||
const impact = RouterUtils.getPriceImpactForRoute(trade, routes).toDecimalPlaces(
|
||||
maxDecimalAccuracy
|
||||
);
|
||||
const expect = calculateImpact(params).toDecimalPlaces(maxDecimalAccuracy);
|
||||
assert.equal(impact.toString(), expect.toString());
|
||||
});
|
||||
|
||||
// Mock a ORCA -> USDC trade that has 2 split route and goes through a multi-hop (ORCA -> SOL -> USDC)
|
||||
it("ExactIn, mix a->b, single & multi-hop, 2 splits", () => {
|
||||
const params: RouteTestParam = {
|
||||
amountSpecifiedIsInput: true,
|
||||
totalAmountIn: new BN("40000000000"),
|
||||
totalAmountOut: new BN("22277933969"),
|
||||
subRouteParams: [
|
||||
{
|
||||
hops: [
|
||||
{
|
||||
aToB: false,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("3363616053614750676"),
|
||||
amountIn: new BN("32000000000"),
|
||||
amountOut: new BN("925083736236"),
|
||||
},
|
||||
{
|
||||
aToB: true,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("2567715337494939945"),
|
||||
amountIn: new BN("925083736236"),
|
||||
amountOut: new BN("17871834810"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
hops: [
|
||||
{
|
||||
aToB: true,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("14082503933855903449"),
|
||||
amountIn: new BN("8000000000"),
|
||||
amountOut: new BN("4406099159"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { trade, routes } = buildRouteTest(params);
|
||||
const impact = RouterUtils.getPriceImpactForRoute(trade, routes).toDecimalPlaces(
|
||||
maxDecimalAccuracy
|
||||
);
|
||||
const expect = calculateImpact(params).toDecimalPlaces(maxDecimalAccuracy);
|
||||
assert.equal(impact.toString(), expect.toString());
|
||||
});
|
||||
|
||||
// Mock an ExactOut ORCA -> USDC trade that has 2 split route and goes through a multi-hop (ORCA -> SOL -> USDC)
|
||||
it("ExactOut, mix a->b, single & multi-hop, 2 splits", () => {
|
||||
const params: RouteTestParam = {
|
||||
amountSpecifiedIsInput: false,
|
||||
totalAmountIn: new BN("64800628033"),
|
||||
totalAmountOut: new BN("34000000000"),
|
||||
subRouteParams: [
|
||||
{
|
||||
hops: [
|
||||
{
|
||||
aToB: true,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("14067691597581169278"),
|
||||
amountIn: new BN("13107594181"),
|
||||
amountOut: new BN("6800000000"),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
hops: [
|
||||
{
|
||||
aToB: false,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("3366318822902200326"),
|
||||
amountIn: new BN("51693033852"),
|
||||
amountOut: new BN("1403541983350"),
|
||||
},
|
||||
{
|
||||
aToB: true,
|
||||
feeRate: Percentage.fromFraction(3000, 1000000),
|
||||
sqrtPrice: new BN("2572953144905521240"),
|
||||
amountIn: new BN("1403541983350"),
|
||||
amountOut: new BN("27200000000"),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const { trade, routes } = buildRouteTest(params);
|
||||
const impact = RouterUtils.getPriceImpactForRoute(trade, routes).toDecimalPlaces(
|
||||
maxDecimalAccuracy
|
||||
);
|
||||
const expect = calculateImpact(params).toDecimalPlaces(maxDecimalAccuracy);
|
||||
assert.equal(impact.toString(), expect.toString());
|
||||
});
|
||||
|
||||
// NOTE: The precision kept in these calculation slightly differs from the U64 calculation that we get from the RouterUtil function.
|
||||
function calculateImpact(params: RouteTestParam): Decimal {
|
||||
const { amountSpecifiedIsInput, totalAmountIn, totalAmountOut } = params;
|
||||
|
||||
const finalBaseValue = params.subRouteParams
|
||||
.map((subRoute) => {
|
||||
const { hops } = subRoute;
|
||||
const directionalHops = amountSpecifiedIsInput ? hops : hops.slice().reverse();
|
||||
const hopResults: Decimal[] = new Array(hops.length);
|
||||
directionalHops.forEach((hop, index) => {
|
||||
const { aToB, feeRate, sqrtPrice, amountIn, amountOut } = hop;
|
||||
const directionalSqrtPrice = aToB
|
||||
? new Decimal(sqrtPrice.toString())
|
||||
: new Decimal(PriceMath.invertSqrtPriceX64(sqrtPrice).toString());
|
||||
const directionalPrice = directionalSqrtPrice
|
||||
.pow(2)
|
||||
.div(U64.toString())
|
||||
.div(U64.toString());
|
||||
if (amountSpecifiedIsInput) {
|
||||
const amountInDec =
|
||||
index === 0 ? new Decimal(amountIn.toString()) : hopResults[index - 1];
|
||||
const amountOutDec = amountInDec
|
||||
.times(new Decimal(1).sub(feeRate.toDecimal()))
|
||||
.times(directionalPrice);
|
||||
hopResults[index] = amountOutDec.round();
|
||||
} else {
|
||||
const amountOutDec =
|
||||
index === 0 ? new Decimal(amountOut.toString()) : hopResults[index - 1];
|
||||
const amountInDec = amountOutDec
|
||||
.div(new Decimal(1).sub(feeRate.toDecimal()))
|
||||
.div(directionalPrice);
|
||||
hopResults[index] = amountInDec.round();
|
||||
}
|
||||
});
|
||||
return hopResults[hops.length - 1];
|
||||
})
|
||||
.reduce((acc, cur) => acc.add(cur), new Decimal(0));
|
||||
|
||||
if (amountSpecifiedIsInput) {
|
||||
const totalAmountOutDec = new Decimal(totalAmountOut.toString());
|
||||
return finalBaseValue.sub(totalAmountOutDec).div(finalBaseValue).mul(100);
|
||||
} else {
|
||||
const totalAmountInDec = new Decimal(totalAmountIn.toString());
|
||||
return totalAmountInDec.sub(finalBaseValue).div(totalAmountInDec).mul(100);
|
||||
}
|
||||
}
|
||||
|
||||
type TradeHopTestParam = {
|
||||
aToB: boolean;
|
||||
feeRate: Percentage;
|
||||
sqrtPrice: BN;
|
||||
amountIn: BN;
|
||||
amountOut: BN;
|
||||
};
|
||||
type SubRouteTestParam = {
|
||||
hops: TradeHopTestParam[];
|
||||
};
|
||||
type RouteTestParam = {
|
||||
amountSpecifiedIsInput: boolean;
|
||||
subRouteParams: SubRouteTestParam[];
|
||||
totalAmountIn: BN;
|
||||
totalAmountOut: BN;
|
||||
};
|
||||
function buildRouteTest(params: RouteTestParam) {
|
||||
return {
|
||||
trade: {
|
||||
tokenIn: "orcaEKTdK7LKz57vaAYr9QeNsVEPfiu6QeMU1kektZE",
|
||||
tokenOut: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
|
||||
tradeAmount: new BN(0),
|
||||
amountSpecifiedIsInput: params.amountSpecifiedIsInput,
|
||||
},
|
||||
routes: {
|
||||
subRoutes: params.subRouteParams.map((subRouteParam) => {
|
||||
return {
|
||||
hopQuotes: subRouteParam.hops.map((hopParam) => {
|
||||
return {
|
||||
amountIn: hopParam.amountIn,
|
||||
amountOut: hopParam.amountOut,
|
||||
whirlpool: PublicKey.default,
|
||||
inputMint: PublicKey.default,
|
||||
outputMint: PublicKey.default,
|
||||
mintA: PublicKey.default,
|
||||
mintB: PublicKey.default,
|
||||
vaultA: PublicKey.default,
|
||||
vaultB: PublicKey.default,
|
||||
quote: {
|
||||
amount: new BN(0),
|
||||
otherAmountThreshold: new BN(0),
|
||||
sqrtPriceLimit: new BN(0),
|
||||
amountSpecifiedIsInput: params.amountSpecifiedIsInput,
|
||||
aToB: hopParam.aToB,
|
||||
tickArray0: PublicKey.default,
|
||||
tickArray1: PublicKey.default,
|
||||
tickArray2: PublicKey.default,
|
||||
estimatedAmountIn: new BN(0),
|
||||
estimatedAmountOut: new BN(0),
|
||||
estimatedEndTickIndex: 0,
|
||||
estimatedEndSqrtPrice: new BN(0),
|
||||
estimatedFeeAmount: new BN(0),
|
||||
},
|
||||
snapshot: {
|
||||
aToB: hopParam.aToB,
|
||||
feeRate: hopParam.feeRate,
|
||||
sqrtPrice: hopParam.sqrtPrice,
|
||||
},
|
||||
};
|
||||
}),
|
||||
path: {
|
||||
startTokenMint: "startTokenMint",
|
||||
endTokenMint: "endTokenMint",
|
||||
edges: [],
|
||||
},
|
||||
splitPercent: 30,
|
||||
amountIn: new BN(0),
|
||||
amountOut: new BN(0),
|
||||
};
|
||||
}),
|
||||
totalAmountIn: params.totalAmountIn,
|
||||
totalAmountOut: params.totalAmountOut,
|
||||
},
|
||||
};
|
||||
}
|
||||
});
|
Loading…
Reference in New Issue