diff --git a/ts-client/src/dlmm/helpers/rebalance/liquidity_strategy/bidAsk.ts b/ts-client/src/dlmm/helpers/rebalance/liquidity_strategy/bidAsk.ts index e9c715a4..b6218957 100644 --- a/ts-client/src/dlmm/helpers/rebalance/liquidity_strategy/bidAsk.ts +++ b/ts-client/src/dlmm/helpers/rebalance/liquidity_strategy/bidAsk.ts @@ -2,11 +2,7 @@ import BN from "bn.js"; import { BidAskParameters, LiquidityStrategyParameterBuilder } from "."; import { SCALE_OFFSET } from "../../../constants"; import { getQPriceFromId } from "../../math"; -import { - getAmountInBinsAskSide, - getAmountInBinsBidSide, - toAmountIntoBins, -} from "../rebalancePosition"; +import { getAmountInBinsAskSide, toAmountIntoBins } from "../rebalancePosition"; function findMinY0(amountY: BN, minDeltaId: BN, maxDeltaId: BN) { const binCount = maxDeltaId.sub(minDeltaId).addn(1); @@ -61,30 +57,27 @@ function findY0AndDeltaY( } let baseDeltaY = findBaseDeltaY(amountY, minDeltaId, maxDeltaId); - const y0 = baseDeltaY.neg().mul(maxDeltaId.neg().subn(1)); - - while (true) { - const amountInBins = getAmountInBinsBidSide( - activeId, - minDeltaId, - maxDeltaId, - baseDeltaY, - y0 - ); - - const totalAmountY = amountInBins.reduce((acc, { amountY }) => { - return acc.add(amountY); - }, new BN(0)); + const maxDeltaAbs = maxDeltaId.neg(); + const binCount = maxDeltaId.sub(minDeltaId).addn(1); + const sumDeltaId = minDeltaId.add(maxDeltaId).mul(binCount).divn(2); + const sumNegDelta = sumDeltaId.neg(); + const coefficient = sumNegDelta.sub(binCount.mul(maxDeltaAbs.subn(1))); + if (coefficient.gt(new BN(0))) { + const totalAmountY = baseDeltaY.mul(coefficient); if (totalAmountY.gt(amountY)) { - baseDeltaY = baseDeltaY.sub(new BN(1)); - } else { - return { - base: y0, - delta: baseDeltaY, - }; + const overshoot = totalAmountY.sub(amountY); + const adjustment = overshoot.add(coefficient.subn(1)).div(coefficient); + baseDeltaY = BN.max(baseDeltaY.sub(adjustment), new BN(0)); } } + + const y0 = baseDeltaY.neg().mul(maxDeltaAbs.subn(1)); + + return { + base: y0, + delta: baseDeltaY, + }; } function findMinX0( diff --git a/ts-client/src/test/liquidity_strategy_timeout.test.ts b/ts-client/src/test/liquidity_strategy_timeout.test.ts new file mode 100644 index 00000000..cfa517c9 --- /dev/null +++ b/ts-client/src/test/liquidity_strategy_timeout.test.ts @@ -0,0 +1,98 @@ +import BN from "bn.js"; + +function sleepMs(ms: number) { + const start = Date.now(); + while (Date.now() - start < ms) {} +} + +describe("Liquidity strategy timeouts", () => { + const activeId = new BN(1000); + const binStep = new BN(10); + + it("Spot and Curve builders complete within 15s", () => { + jest.resetModules(); + const { + buildLiquidityStrategyParameters, + getLiquidityStrategyParameterBuilder, + } = require("../dlmm/helpers/rebalance"); + const { StrategyType } = require("../dlmm/types"); + const amountX = new BN(10_000); + const amountY = new BN(10_000); + const minDeltaId = new BN(-3); + const maxDeltaId = new BN(3); + const favorXInActiveBin = false; + + const builders = [ + getLiquidityStrategyParameterBuilder(StrategyType.Spot), + getLiquidityStrategyParameterBuilder(StrategyType.Curve), + ]; + + for (const builder of builders) { + const start = Date.now(); + buildLiquidityStrategyParameters( + amountX, + amountY, + minDeltaId, + maxDeltaId, + binStep, + favorXInActiveBin, + activeId, + builder + ); + const elapsedMs = Date.now() - start; + expect(elapsedMs).toBeLessThan(15_000); + } + }); + + it("BidAsk can exceed 15s with pathological bid-side loops", () => { + jest.resetModules(); + const amountX = new BN(0); + const amountY = new BN(20_000); + const minDeltaId = new BN(-1); + const maxDeltaId = new BN(-1); + const favorXInActiveBin = false; + + jest.doMock("../dlmm/helpers/rebalance/rebalancePosition", () => { + const actual = jest.requireActual( + "../dlmm/helpers/rebalance/rebalancePosition" + ); + return { + ...actual, + getAmountInBinsBidSide: jest.fn((_activeId, _min, _max, deltaY) => { + sleepMs(100); + if (deltaY.gt(new BN(0))) { + return [ + { binId: activeId, amountX: new BN(0), amountY: amountY.addn(1) }, + ]; + } + + return [{ binId: activeId, amountX: new BN(0), amountY }]; + }), + }; + }); + + const rebalancePosition = require("../dlmm/helpers/rebalance/rebalancePosition"); + const getAmountInBinsBidSide = rebalancePosition.getAmountInBinsBidSide; + expect(jest.isMockFunction(getAmountInBinsBidSide)).toBe(true); + const { + buildLiquidityStrategyParameters, + getLiquidityStrategyParameterBuilder, + } = require("../dlmm/helpers/rebalance"); + const { StrategyType } = require("../dlmm/types"); + const builder = getLiquidityStrategyParameterBuilder(StrategyType.BidAsk); + + const start = Date.now(); + buildLiquidityStrategyParameters( + amountX, + amountY, + minDeltaId, + maxDeltaId, + binStep, + favorXInActiveBin, + activeId, + builder + ); + const elapsedMs = Date.now() - start; + expect(elapsedMs).toBeLessThan(15_000); + }, 15_000); +});