From 8b2d5daa562b1c1af1b5a118951169a5c4ee2395 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Tue, 28 Nov 2023 00:40:08 +0000 Subject: [PATCH 1/5] added help functions for overlapping strategies --- src/strategy-management/Toolkit.ts | 46 +++++++++++++++++++ src/strategy-management/utils.ts | 71 +++++++++++++++++++++++++++++ src/utils/numerics.ts | 2 +- tests/utils.spec.ts | 72 ++++++++++++++++++++++++++++++ 4 files changed, 190 insertions(+), 1 deletion(-) diff --git a/src/strategy-management/Toolkit.ts b/src/strategy-management/Toolkit.ts index 99cd525..52b01c0 100644 --- a/src/strategy-management/Toolkit.ts +++ b/src/strategy-management/Toolkit.ts @@ -46,8 +46,10 @@ const logger = new Logger('Toolkit.ts'); // Strategy utils import { + OverlappingDistributionResult, addFee, buildStrategyObject, + calculateOverlappingDistribution, decodeStrategy, encodeStrategy, normalizeRate, @@ -696,6 +698,50 @@ export class Toolkit { ); } + /** + * Calculates the parameters for the overlapping strategy. + * + * @param {string} baseToken - The address of the base token for the strategy. + * @param {string} quoteToken - The address of the quote token for the strategy. + * @param {string} buyPriceLow - The minimum buy price for the strategy, in in `quoteToken` per 1 `baseToken`, as a string. + * @param {string} buyBudget - The maximum budget for buying tokens in the strategy, in `quoteToken`, as a string. + * @param {string} sellPriceHigh - The maximum sell price for the strategy, in `quoteToken` per 1 `baseToken`, as a string. + * @param {string} marketPrice - The market price, in `quoteToken` per 1 `baseToken`, as a string. + * @param {string} spreadPercentage - The spread percentage, e.g. for 10%, enter `10`. + * @return {Promise} The result of the calculation. + */ + public async calculateOverlappingStrategyParams( + baseToken: string, + quoteToken: string, + buyPriceLow: string, + buyBudget: string, + sellPriceHigh: string, + marketPrice: string, + spreadPercentage: string + ): Promise { + logger.debug('calculateOverlappingStrategyParams called', arguments); + const decimals = this._decimals; + const baseDecimals = await decimals.fetchDecimals(baseToken); + const quoteDecimals = await decimals.fetchDecimals(quoteToken); + const result = calculateOverlappingDistribution( + baseDecimals, + quoteDecimals, + buyPriceLow, + sellPriceHigh, + marketPrice, + spreadPercentage, + buyBudget + ); + + logger.debug('calculateOverlappingStrategyParams info:', { + baseDecimals, + quoteDecimals, + result, + }); + + return result; + } + /** * Creates an unsigned transaction to create a strategy for buying and selling tokens of `baseToken` for price in `quoteToken` per 1 `baseToken`. * diff --git a/src/strategy-management/utils.ts b/src/strategy-management/utils.ts index 0e076ef..0e17bbd 100644 --- a/src/strategy-management/utils.ts +++ b/src/strategy-management/utils.ts @@ -5,6 +5,7 @@ import { formatUnits, parseUnits, tenPow, + trimDecimal, } from '../utils/numerics'; import { DecodedOrder, @@ -287,3 +288,73 @@ export function subtractFee( .div(PPM_RESOLUTION) .floor(); } + +export type OverlappingDistributionResult = { + buyPriceHigh: string; // in quote tkn per 1 base tkn + buyPriceMarginal: string; // in quote tkn per 1 base tkn + sellPriceLow: string; // in quote tkn per 1 base tkn + sellPriceMarginal: string; // in quote tkn per 1 base tkn + sellBudget: string; // in base tkn +}; + +export function calculateOverlappingDistribution( + baseTokenDecimals: number, + quoteTokenDecimals: number, + buyPriceLow: string, // in quote tkn per 1 base tkn + sellPriceHigh: string, // in quote tkn per 1 base tkn + marketPrice: string, // in quote tkn per 1 base tkn + spreadPercentage: string, // e.g. for 0.1% pass '0.1' + buyBudget: string // in quote tkn +): OverlappingDistributionResult { + const sellPriceHighDec = new Decimal(sellPriceHigh); + const buyPriceLowDec = new Decimal(buyPriceLow); + const marketPriceDec = new Decimal(marketPrice); + + const totalPriceRange = sellPriceHighDec.minus(buyPriceLowDec); + + const spread = totalPriceRange.mul(spreadPercentage).div(100); + + const buyPriceHigh = sellPriceHighDec.minus(spread); + + const sellPriceLow = buyPriceLowDec.plus(spread); + + const buyPriceRange = buyPriceHigh.minus(buyPriceLowDec); + + const sellPriceRange = sellPriceHighDec.minus(sellPriceLow); + + const buyLowRange = marketPriceDec.minus(buyPriceLowDec).minus(spread.div(2)); + + const buyLowBudgetRatio = buyLowRange.div(buyPriceRange); + + const buyOrderYint = new Decimal(buyBudget).div(buyLowBudgetRatio); + + const buyPriceMarginal = buyPriceLowDec.plus( + buyPriceRange.mul(buyLowBudgetRatio) + ); + + const geoMean = buyPriceLowDec.mul(sellPriceHighDec).sqrt(); + + const sellOrderYint = buyOrderYint.div(geoMean); + + const sellHighBudgetRatio = new Decimal(1).minus(buyLowBudgetRatio); + + const sellBudget = sellOrderYint.mul(sellHighBudgetRatio); + + const sellPriceMarginal = sellPriceHighDec.minus( + sellPriceRange.mul(sellHighBudgetRatio) + ); + + return { + buyPriceHigh: trimDecimal(buyPriceHigh.toString(), quoteTokenDecimals), + buyPriceMarginal: trimDecimal( + buyPriceMarginal.toString(), + quoteTokenDecimals + ), + sellPriceLow: trimDecimal(sellPriceLow.toString(), quoteTokenDecimals), + sellPriceMarginal: trimDecimal( + sellPriceMarginal.toString(), + quoteTokenDecimals + ), + sellBudget: trimDecimal(sellBudget.toString(), baseTokenDecimals), + }; +} diff --git a/src/utils/numerics.ts b/src/utils/numerics.ts index 33c23bc..8935917 100644 --- a/src/utils/numerics.ts +++ b/src/utils/numerics.ts @@ -31,7 +31,7 @@ export const DecToBn = (x: Decimal) => BigNumber.from(x.toFixed()); export const mulDiv = (x: BigNumber, y: BigNumber, z: BigNumber) => y.eq(z) ? x : x.mul(y).div(z); -function trimDecimal(input: string, precision: number): string { +export function trimDecimal(input: string, precision: number): string { const decimalIdx = input.indexOf('.'); if (decimalIdx !== -1) { return input.slice(0, decimalIdx + precision + 1); diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index 3c99f23..068164b 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -5,9 +5,81 @@ import { normalizeInvertedRate, subtractFee, addFee, + calculateOverlappingDistribution, } from '../src/strategy-management'; describe('utils', () => { + describe('calculateOverlappingDistribution', () => { + const testCases = [ + { + baseTokenDecimals: 18, + quoteTokenDecimals: 6, + buyPriceLow: '0.005', + sellPriceHigh: '0.03', + marketPrice: '0.007241', + spreadPercentage: '0.1', + buyBudget: '3090.190579', + buyPriceHigh: '0.029975', + sellPriceLow: '0.005025', + buyPriceMarginal: '0.007228', + sellPriceMarginal: '0.007253', + sellBudget: + '2575381.534852473816997609', + }, + { + baseTokenDecimals: 18, + quoteTokenDecimals: 6, + buyPriceLow: '1500', + sellPriceHigh: '2000', + marketPrice: '1600', + spreadPercentage: '0.1', + buyBudget: '100', + buyPriceHigh: '1999.5', + sellPriceLow: '1500.5', + buyPriceMarginal: '1599.75', + sellPriceMarginal: '1600.25', + sellBudget: '0.231374205622609422', + }, + ]; + + testCases.forEach( + ({ + baseTokenDecimals, + quoteTokenDecimals, + buyPriceLow, + sellPriceHigh, + marketPrice, + spreadPercentage, + buyBudget, + buyPriceHigh, + sellPriceLow, + buyPriceMarginal, + sellPriceMarginal, + sellBudget, + }) => { + it(`should successfully calculate overlapping distribution for inputs: + baseTokenDecimals: ${baseTokenDecimals}, quoteTokenDecimals: ${quoteTokenDecimals}, + buyPriceLow: ${buyPriceLow}, sellPriceHigh: ${sellPriceHigh}, + marketPrice: ${marketPrice}, spreadPercentage: ${spreadPercentage}, + buyBudget: ${buyBudget}`, () => { + const result = calculateOverlappingDistribution( + baseTokenDecimals, + quoteTokenDecimals, + buyPriceLow, + sellPriceHigh, + marketPrice, + spreadPercentage, + buyBudget + ); + expect(result.buyPriceHigh).to.equal(buyPriceHigh); + expect(result.sellPriceLow).to.equal(sellPriceLow); + expect(result.buyPriceMarginal).to.equal(buyPriceMarginal); + expect(result.sellPriceMarginal).to.equal(sellPriceMarginal); + expect(result.sellBudget).to.equal(sellBudget); + }); + } + ); + }); describe('parseUnits', () => { const testCases = [ { amount: '1', decimals: 0, expectedResult: '1' }, From e13ea1a32cf1ead0f40eca492e23e9ecc8a9dd05 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Tue, 28 Nov 2023 01:37:08 +0000 Subject: [PATCH 2/5] user now passes in marginal prices during strategy create and update --- src/strategy-management/Toolkit.ts | 51 +++++++++++++----------------- src/strategy-management/utils.ts | 33 ++++++++++++++++--- tests/encoders.spec.ts | 28 ++++++++++++++-- 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/src/strategy-management/Toolkit.ts b/src/strategy-management/Toolkit.ts index 52b01c0..194bbc3 100644 --- a/src/strategy-management/Toolkit.ts +++ b/src/strategy-management/Toolkit.ts @@ -791,9 +791,11 @@ export class Toolkit { baseToken: string, quoteToken: string, buyPriceLow: string, + buyPriceMarginal: string, buyPriceHigh: string, buyBudget: string, sellPriceLow: string, + sellPriceMarginal: string, sellPriceHigh: string, sellBudget: string, overrides?: PayableOverrides @@ -808,9 +810,11 @@ export class Toolkit { baseDecimals, quoteDecimals, buyPriceLow, + buyPriceMarginal, buyPriceHigh, buyBudget, sellPriceLow, + sellPriceMarginal, sellPriceHigh, sellBudget ); @@ -849,8 +853,8 @@ export class Toolkit { sellPriceHigh, sellBudget, }: StrategyUpdate, - buyMarginalPrice?: MarginalPriceOptions | string, - sellMarginalPrice?: MarginalPriceOptions | string, + buyPriceMarginal?: MarginalPriceOptions | string, + sellPriceMarginal?: MarginalPriceOptions | string, overrides?: PayableOverrides ): Promise { logger.debug('updateStrategy called', arguments); @@ -876,9 +880,19 @@ export class Toolkit { baseDecimals, quoteDecimals, buyPriceLow ?? originalStrategy.buyPriceLow, + buyPriceMarginal && + buyPriceMarginal !== MarginalPriceOptions.reset && + buyPriceMarginal !== MarginalPriceOptions.maintain + ? buyPriceMarginal + : originalStrategy.buyPriceMarginal, buyPriceHigh ?? originalStrategy.buyPriceHigh, buyBudget ?? originalStrategy.buyBudget, sellPriceLow ?? originalStrategy.sellPriceLow, + sellPriceMarginal && + sellPriceMarginal !== MarginalPriceOptions.reset && + sellPriceMarginal !== MarginalPriceOptions.maintain + ? sellPriceMarginal + : originalStrategy.sellPriceMarginal, sellPriceHigh ?? originalStrategy.sellPriceHigh, sellBudget ?? originalStrategy.sellBudget ); @@ -910,12 +924,12 @@ export class Toolkit { if (buyBudget !== undefined) { if ( - buyMarginalPrice === undefined || - buyMarginalPrice === MarginalPriceOptions.reset || + buyPriceMarginal === undefined || + buyPriceMarginal === MarginalPriceOptions.reset || encodedBN.order1.y.isZero() ) { newEncodedStrategy.order1.z = newEncodedStrategy.order1.y; - } else if (buyMarginalPrice === MarginalPriceOptions.maintain) { + } else if (buyPriceMarginal === MarginalPriceOptions.maintain) { // maintain the current ratio of y/z newEncodedStrategy.order1.z = mulDiv( encodedBN.order1.z, @@ -927,12 +941,12 @@ export class Toolkit { if (sellBudget !== undefined) { if ( - sellMarginalPrice === undefined || - sellMarginalPrice === MarginalPriceOptions.reset || + sellPriceMarginal === undefined || + sellPriceMarginal === MarginalPriceOptions.reset || encodedBN.order0.y.isZero() ) { newEncodedStrategy.order0.z = newEncodedStrategy.order0.y; - } else if (sellMarginalPrice === MarginalPriceOptions.maintain) { + } else if (sellPriceMarginal === MarginalPriceOptions.maintain) { // maintain the current ratio of y/z newEncodedStrategy.order0.z = mulDiv( encodedBN.order0.z, @@ -949,27 +963,6 @@ export class Toolkit { newEncodedStrategy.order0.z = newEncodedStrategy.order0.y; } - if ( - buyMarginalPrice !== undefined && - buyMarginalPrice !== MarginalPriceOptions.reset && - buyMarginalPrice !== MarginalPriceOptions.maintain - ) { - // TODO: set newEncodedStrategy.order1.z according to the given marginal price - throw new Error( - 'Support for custom marginal price is not implemented yet' - ); - } - if ( - sellMarginalPrice !== undefined && - sellMarginalPrice !== MarginalPriceOptions.reset && - sellMarginalPrice !== MarginalPriceOptions.maintain - ) { - // TODO: set newEncodedStrategy.order0.z according to the given marginal price - throw new Error( - 'Support for custom marginal price is not implemented yet' - ); - } - logger.debug('updateStrategy info:', { baseDecimals, quoteDecimals, diff --git a/src/strategy-management/utils.ts b/src/strategy-management/utils.ts index 0e17bbd..8e32d07 100644 --- a/src/strategy-management/utils.ts +++ b/src/strategy-management/utils.ts @@ -152,26 +152,36 @@ export function buildStrategyObject( baseDecimals: number, quoteDecimals: number, buyPriceLow: string, // in quote tkn per 1 base tkn + buyPriceMarginal: string, // in quote tkn per 1 base tkn buyPriceHigh: string, // in quote tkn per 1 base tkn buyBudget: string, // in quote tkn sellPriceLow: string, // in quote tkn per 1 base tkn + sellPriceMarginal: string, // in quote tkn per 1 base tkn sellPriceHigh: string, // in quote tkn per 1 base tkn sellBudget: string // in base tkn ): DecodedStrategy { logger.debug('buildStrategyObject called', arguments); if ( new Decimal(buyPriceLow).isNegative() || + new Decimal(buyPriceMarginal).isNegative() || new Decimal(buyPriceHigh).isNegative() || new Decimal(sellPriceLow).isNegative() || + new Decimal(sellPriceMarginal).isNegative() || new Decimal(sellPriceHigh).isNegative() ) { throw new Error('prices cannot be negative'); } if ( + new Decimal(buyPriceLow).gt(buyPriceMarginal) || new Decimal(buyPriceLow).gt(buyPriceHigh) || - new Decimal(sellPriceLow).gt(sellPriceHigh) + new Decimal(buyPriceMarginal).gt(buyPriceHigh) || + new Decimal(sellPriceLow).gt(sellPriceMarginal) || + new Decimal(sellPriceLow).gt(sellPriceHigh) || + new Decimal(sellPriceMarginal).gt(sellPriceHigh) ) { - throw new Error('low price must be lower than or equal to high price'); + throw new Error( + 'low/marginal price must be lower than or equal to marginal/high price' + ); } if ( new Decimal(buyBudget).isNegative() || @@ -184,9 +194,11 @@ export function buildStrategyObject( baseDecimals, quoteDecimals, buyPriceLow, + buyPriceMarginal, buyPriceHigh, buyBudget, sellPriceLow, + sellPriceMarginal, sellPriceHigh, sellBudget ); @@ -210,9 +222,11 @@ export function createOrders( baseTokenDecimals: number, quoteTokenDecimals: number, buyPriceLow: string, + buyPriceMarginal: string, buyPriceHigh: string, buyBudget: string, sellPriceLow: string, + sellPriceMarginal: string, sellPriceHigh: string, sellBudget: string ): { order0: DecodedOrder; order1: DecodedOrder } { @@ -230,6 +244,12 @@ export function createOrders( baseTokenDecimals ); + const marginalRate0 = normalizeInvertedRate( + sellPriceMarginal, + quoteTokenDecimals, + baseTokenDecimals + ); + const highestRate0 = normalizeInvertedRate( sellPriceLow, quoteTokenDecimals, @@ -247,6 +267,11 @@ export function createOrders( quoteTokenDecimals, baseTokenDecimals ); + const marginalRate1 = normalizeRate( + buyPriceMarginal, + quoteTokenDecimals, + baseTokenDecimals + ); const highestRate1 = normalizeRate( buyPriceHigh, quoteTokenDecimals, @@ -257,14 +282,14 @@ export function createOrders( liquidity: liquidity0.toString(), lowestRate: lowestRate0, highestRate: highestRate0, - marginalRate: highestRate0, + marginalRate: marginalRate0, }; const order1: DecodedOrder = { liquidity: liquidity1.toString(), lowestRate: lowestRate1, highestRate: highestRate1, - marginalRate: highestRate1, + marginalRate: marginalRate1, }; logger.debug('createOrders info:', { order0, order1 }); return { order0, order1 }; diff --git a/tests/encoders.spec.ts b/tests/encoders.spec.ts index 6a62b5a..3f15d23 100644 --- a/tests/encoders.spec.ts +++ b/tests/encoders.spec.ts @@ -191,8 +191,10 @@ describe('encoders', () => { quoteTokenDecimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); @@ -236,8 +238,10 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); @@ -284,12 +288,16 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); - }).to.throw('low price must be lower than or equal to high price'); + }).to.throw( + 'low/marginal price must be lower than or equal to marginal/high price' + ); }); it('should throw an error if sellPriceLow is greater than sellPriceHigh', () => { @@ -316,12 +324,16 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); - }).to.throw('low price must be lower than or equal to high price'); + }).to.throw( + 'low/marginal price must be lower than or equal to marginal/high price' + ); }); it('should throw an error if buyPriceLow is negative', () => { @@ -348,8 +360,10 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); @@ -380,8 +394,10 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); @@ -412,8 +428,10 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); @@ -444,8 +462,10 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); @@ -475,8 +495,10 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); @@ -516,8 +538,10 @@ describe('encoders', () => { quoteToken.decimals, buyPriceLow, buyPriceHigh, + buyPriceHigh, buyBudget, sellPriceLow, + sellPriceLow, sellPriceHigh, sellBudget ); From 71c2562db16fb1c772fc62ff5c8983200904447a Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Thu, 30 Nov 2023 10:17:53 +0000 Subject: [PATCH 3/5] better API and support for overlapping budget calc --- src/strategy-management/Toolkit.ts | 126 +++++++++++++++++++----- src/strategy-management/utils.ts | 149 +++++++++++++++++++---------- tests/utils.spec.ts | 108 ++++++++++++++------- 3 files changed, 278 insertions(+), 105 deletions(-) diff --git a/src/strategy-management/Toolkit.ts b/src/strategy-management/Toolkit.ts index 194bbc3..67be54e 100644 --- a/src/strategy-management/Toolkit.ts +++ b/src/strategy-management/Toolkit.ts @@ -8,6 +8,7 @@ import { tenPow, formatUnits, parseUnits, + trimDecimal, } from '../utils/numerics'; // Internal modules @@ -46,10 +47,11 @@ const logger = new Logger('Toolkit.ts'); // Strategy utils import { - OverlappingDistributionResult, addFee, buildStrategyObject, - calculateOverlappingDistribution, + calculateOverlappingBuyBudget, + calculateOverlappingPriceRanges, + calculateOverlappingSellBudget, decodeStrategy, encodeStrategy, normalizeRate, @@ -698,43 +700,123 @@ export class Toolkit { ); } + public async calculateOverlappingStrategyPrices( + quoteToken: string, + buyPriceLow: string, + sellPriceHigh: string, + marketPrice: string, + spreadPercentage: string + ): Promise<{ + buyPriceHigh: string; + buyPriceMarginal: string; + sellPriceLow: string; + sellPriceMarginal: string; + }> { + logger.debug('calculateOverlappingStrategyPrices called', arguments); + + const decimals = this._decimals; + const quoteDecimals = await decimals.fetchDecimals(quoteToken); + const prices = calculateOverlappingPriceRanges( + new Decimal(buyPriceLow), + new Decimal(sellPriceHigh), + new Decimal(marketPrice), + new Decimal(spreadPercentage) + ); + + const result = { + buyPriceHigh: trimDecimal(prices.buyPriceHigh.toString(), quoteDecimals), + buyPriceMarginal: trimDecimal( + prices.buyPriceMarginal.toString(), + quoteDecimals + ), + sellPriceLow: trimDecimal(prices.sellPriceLow.toString(), quoteDecimals), + sellPriceMarginal: trimDecimal( + prices.sellPriceMarginal.toString(), + quoteDecimals + ), + }; + + logger.debug('calculateOverlappingStrategyPrices info:', { + quoteDecimals, + result, + }); + + return result; + } + /** - * Calculates the parameters for the overlapping strategy. + * Calculates the sell budget given a buy budget of an overlapping strategy. * * @param {string} baseToken - The address of the base token for the strategy. - * @param {string} quoteToken - The address of the quote token for the strategy. * @param {string} buyPriceLow - The minimum buy price for the strategy, in in `quoteToken` per 1 `baseToken`, as a string. - * @param {string} buyBudget - The maximum budget for buying tokens in the strategy, in `quoteToken`, as a string. * @param {string} sellPriceHigh - The maximum sell price for the strategy, in `quoteToken` per 1 `baseToken`, as a string. * @param {string} marketPrice - The market price, in `quoteToken` per 1 `baseToken`, as a string. * @param {string} spreadPercentage - The spread percentage, e.g. for 10%, enter `10`. - * @return {Promise} The result of the calculation. + * @param {string} buyBudget - The budget for buying tokens in the strategy, in `quoteToken`, as a string. + * @return {Promise} The result of the calculation - the sell budget in token res in base token. */ - public async calculateOverlappingStrategyParams( + public async calculateOverlappingStrategySellBudget( baseToken: string, - quoteToken: string, buyPriceLow: string, - buyBudget: string, sellPriceHigh: string, marketPrice: string, - spreadPercentage: string - ): Promise { - logger.debug('calculateOverlappingStrategyParams called', arguments); + spreadPercentage: string, + buyBudget: string + ): Promise { + logger.debug('calculateOverlappingStrategySellBudget called', arguments); const decimals = this._decimals; const baseDecimals = await decimals.fetchDecimals(baseToken); - const quoteDecimals = await decimals.fetchDecimals(quoteToken); - const result = calculateOverlappingDistribution( - baseDecimals, - quoteDecimals, - buyPriceLow, - sellPriceHigh, - marketPrice, - spreadPercentage, - buyBudget + const budget = calculateOverlappingSellBudget( + new Decimal(buyPriceLow), + new Decimal(sellPriceHigh), + new Decimal(marketPrice), + new Decimal(spreadPercentage), + new Decimal(buyBudget) ); - logger.debug('calculateOverlappingStrategyParams info:', { + const result = trimDecimal(budget.toString(), baseDecimals); + + logger.debug('calculateOverlappingStrategySellBudget info:', { baseDecimals, + result, + }); + + return result; + } + + /** + * Calculates the buy budget given a sell budget of an overlapping strategy. + * + * @param {string} quoteToken - The address of the base token for the strategy. + * @param {string} buyPriceLow - The minimum buy price for the strategy, in in `quoteToken` per 1 `baseToken`, as a string. + * @param {string} sellPriceHigh - The maximum sell price for the strategy, in `quoteToken` per 1 `baseToken`, as a string. + * @param {string} marketPrice - The market price, in `quoteToken` per 1 `baseToken`, as a string. + * @param {string} spreadPercentage - The spread percentage, e.g. for 10%, enter `10`. + * @param {string} sellBudget - The budget for selling tokens in the strategy, in `baseToken`, as a string. + * @return {Promise} The result of the calculation - the buy budget in token res in quote token. + */ + public async calculateOverlappingStrategyBuyBudget( + quoteToken: string, + buyPriceLow: string, + sellPriceHigh: string, + marketPrice: string, + spreadPercentage: string, + sellBudget: string + ): Promise { + logger.debug('calculateOverlappingStrategyBuyBudget called', arguments); + const decimals = this._decimals; + const quoteDecimals = await decimals.fetchDecimals(quoteToken); + const budget = calculateOverlappingBuyBudget( + new Decimal(buyPriceLow), + new Decimal(sellPriceHigh), + new Decimal(marketPrice), + new Decimal(spreadPercentage), + new Decimal(sellBudget) + ); + + const result = trimDecimal(budget.toString(), quoteDecimals); + + logger.debug('calculateOverlappingStrategyBuyBudget info:', { quoteDecimals, result, }); diff --git a/src/strategy-management/utils.ts b/src/strategy-management/utils.ts index 8e32d07..2fa1e20 100644 --- a/src/strategy-management/utils.ts +++ b/src/strategy-management/utils.ts @@ -5,7 +5,6 @@ import { formatUnits, parseUnits, tenPow, - trimDecimal, } from '../utils/numerics'; import { DecodedOrder, @@ -314,72 +313,120 @@ export function subtractFee( .floor(); } -export type OverlappingDistributionResult = { - buyPriceHigh: string; // in quote tkn per 1 base tkn - buyPriceMarginal: string; // in quote tkn per 1 base tkn - sellPriceLow: string; // in quote tkn per 1 base tkn - sellPriceMarginal: string; // in quote tkn per 1 base tkn - sellBudget: string; // in base tkn -}; - -export function calculateOverlappingDistribution( - baseTokenDecimals: number, - quoteTokenDecimals: number, - buyPriceLow: string, // in quote tkn per 1 base tkn - sellPriceHigh: string, // in quote tkn per 1 base tkn - marketPrice: string, // in quote tkn per 1 base tkn - spreadPercentage: string, // e.g. for 0.1% pass '0.1' - buyBudget: string // in quote tkn -): OverlappingDistributionResult { - const sellPriceHighDec = new Decimal(sellPriceHigh); - const buyPriceLowDec = new Decimal(buyPriceLow); - const marketPriceDec = new Decimal(marketPrice); - - const totalPriceRange = sellPriceHighDec.minus(buyPriceLowDec); +export function calculateOverlappingPriceRanges( + buyPriceLow: Decimal, // in quote tkn per 1 base tkn + sellPriceHigh: Decimal, // in quote tkn per 1 base tkn + marketPrice: Decimal, // in quote tkn per 1 base tkn + spreadPercentage: Decimal // e.g. for 0.1% pass '0.1' +): { + buyPriceHigh: Decimal; + buyPriceMarginal: Decimal; + sellPriceLow: Decimal; + sellPriceMarginal: Decimal; + spread: Decimal; +} { + const totalPriceRange = sellPriceHigh.minus(buyPriceLow); const spread = totalPriceRange.mul(spreadPercentage).div(100); - const buyPriceHigh = sellPriceHighDec.minus(spread); - - const sellPriceLow = buyPriceLowDec.plus(spread); + const buyPriceHigh = sellPriceHigh.minus(spread); - const buyPriceRange = buyPriceHigh.minus(buyPriceLowDec); + const sellPriceLow = buyPriceLow.plus(spread); - const sellPriceRange = sellPriceHighDec.minus(sellPriceLow); + // buy marginal price is the market price minus 0.5 spread. But it can never be lower than buyPriceLow + const buyPriceMarginal = Decimal.max( + buyPriceLow, + marketPrice.minus(spread.div(2)) + ); - const buyLowRange = marketPriceDec.minus(buyPriceLowDec).minus(spread.div(2)); + // sell marginal price is the market price plus 0.5 spread. But ir can never be higher than sellPriceHigh + const sellPriceMarginal = Decimal.min( + sellPriceHigh, + marketPrice.plus(spread.div(2)) + ); - const buyLowBudgetRatio = buyLowRange.div(buyPriceRange); + return { + buyPriceHigh, + buyPriceMarginal, + sellPriceLow, + sellPriceMarginal, + spread, + }; +} - const buyOrderYint = new Decimal(buyBudget).div(buyLowBudgetRatio); +export function calculateOverlappingSellBudget( + buyPriceLow: Decimal, // in quote tkn per 1 base tkn + sellPriceHigh: Decimal, // in quote tkn per 1 base tkn + marketPrice: Decimal, // in quote tkn per 1 base tkn + spreadPercentage: Decimal, // e.g. for 0.1% pass '0.1' + buyBudget: Decimal // in quote tkn +): Decimal { + // zero buy budget means zero sell budget + if (buyBudget.isZero()) return new Decimal(0); - const buyPriceMarginal = buyPriceLowDec.plus( - buyPriceRange.mul(buyLowBudgetRatio) + const { buyPriceHigh, spread } = calculateOverlappingPriceRanges( + buyPriceLow, + sellPriceHigh, + marketPrice, + spreadPercentage ); - const geoMean = buyPriceLowDec.mul(sellPriceHighDec).sqrt(); + // if buy range takes the entire range then there's zero sell budget + if (marketPrice.minus(spread.div(2)).gte(buyPriceHigh)) return new Decimal(0); - const sellOrderYint = buyOrderYint.div(geoMean); + // if buy range is zero there's no point to this call + if (marketPrice.minus(spread.div(2)).lte(buyPriceLow)) { + throw new Error( + 'calculateOverlappingSellBudget called with zero buy range and non zero buy budget' + ); + } + const buyPriceRange = buyPriceHigh.minus(buyPriceLow); + const buyLowRange = marketPrice.minus(buyPriceLow).minus(spread.div(2)); + const buyLowBudgetRatio = buyLowRange.div(buyPriceRange); + const buyOrderYint = new Decimal(buyBudget).div(buyLowBudgetRatio); + const geoMeanOfBuyRange = buyPriceLow.mul(buyPriceHigh).sqrt(); + const sellOrderYint = buyOrderYint.div(geoMeanOfBuyRange); const sellHighBudgetRatio = new Decimal(1).minus(buyLowBudgetRatio); - const sellBudget = sellOrderYint.mul(sellHighBudgetRatio); - const sellPriceMarginal = sellPriceHighDec.minus( - sellPriceRange.mul(sellHighBudgetRatio) + return sellBudget; +} + +export function calculateOverlappingBuyBudget( + buyPriceLow: Decimal, // in quote tkn per 1 base tkn + sellPriceHigh: Decimal, // in quote tkn per 1 base tkn + marketPrice: Decimal, // in quote tkn per 1 base tkn + spreadPercentage: Decimal, // e.g. for 0.1% pass '0.1' + sellBudget: Decimal // in quote tkn +): Decimal { + // zero sell budget means zero buy budget + if (sellBudget.isZero()) return new Decimal(0); + + const { sellPriceLow, spread } = calculateOverlappingPriceRanges( + buyPriceLow, + sellPriceHigh, + marketPrice, + spreadPercentage ); - return { - buyPriceHigh: trimDecimal(buyPriceHigh.toString(), quoteTokenDecimals), - buyPriceMarginal: trimDecimal( - buyPriceMarginal.toString(), - quoteTokenDecimals - ), - sellPriceLow: trimDecimal(sellPriceLow.toString(), quoteTokenDecimals), - sellPriceMarginal: trimDecimal( - sellPriceMarginal.toString(), - quoteTokenDecimals - ), - sellBudget: trimDecimal(sellBudget.toString(), baseTokenDecimals), - }; + // if sell range takes the entire range then there's zero buy budget + if (marketPrice.plus(spread.div(2)).lte(sellPriceLow)) return new Decimal(0); + + // if sell range is zero there's no point to this call + if (marketPrice.plus(spread.div(2)).gte(sellPriceHigh)) { + throw new Error( + 'calculateOverlappingBuyBudget called with zero sell range and non zero sell budget' + ); + } + + const sellPriceRange = sellPriceHigh.minus(sellPriceLow); + const sellHighRange = sellPriceHigh.minus(marketPrice).minus(spread.div(2)); + const sellHighBudgetRatio = sellHighRange.div(sellPriceRange); + const sellOrderYint = new Decimal(sellBudget).div(sellHighBudgetRatio); + const geoMeanOfSellRange = sellPriceHigh.mul(sellPriceLow).sqrt(); + const buyOrderYint = sellOrderYint.mul(geoMeanOfSellRange); + const buyLowBudgetRatio = new Decimal(1).minus(sellHighBudgetRatio); + const buyBudget = buyOrderYint.mul(buyLowBudgetRatio); + return buyBudget; } diff --git a/tests/utils.spec.ts b/tests/utils.spec.ts index 068164b..b89e661 100644 --- a/tests/utils.spec.ts +++ b/tests/utils.spec.ts @@ -1,44 +1,54 @@ import { expect } from 'chai'; -import { formatUnits, parseUnits, BigNumber } from '../src/utils/numerics'; +import { + formatUnits, + parseUnits, + BigNumber, + Decimal, + trimDecimal, +} from '../src/utils/numerics'; import { normalizeRate, normalizeInvertedRate, subtractFee, addFee, - calculateOverlappingDistribution, + calculateOverlappingBuyBudget, + calculateOverlappingSellBudget, + calculateOverlappingPriceRanges, } from '../src/strategy-management'; +import { isAlmostEqual } from './test-utils'; describe('utils', () => { - describe('calculateOverlappingDistribution', () => { + describe('overlapping strategies', () => { const testCases = [ { baseTokenDecimals: 18, quoteTokenDecimals: 6, - buyPriceLow: '0.005', - sellPriceHigh: '0.03', - marketPrice: '0.007241', - spreadPercentage: '0.1', - buyBudget: '3090.190579', - buyPriceHigh: '0.029975', - sellPriceLow: '0.005025', - buyPriceMarginal: '0.007228', - sellPriceMarginal: '0.007253', - sellBudget: - '2575381.534852473816997609', + buyPriceLow: new Decimal('1500'), + sellPriceHigh: new Decimal('2000'), + marketPrice: new Decimal('1600'), + spreadPercentage: new Decimal('0.1'), + spread: new Decimal('0.5'), + buyBudget: new Decimal('100'), + buyPriceHigh: new Decimal('1999.5'), + sellPriceLow: new Decimal('1500.5'), + buyPriceMarginal: new Decimal('1599.75'), + sellPriceMarginal: new Decimal('1600.25'), + sellBudget: new Decimal('0.231403132822275197'), }, { baseTokenDecimals: 18, quoteTokenDecimals: 6, - buyPriceLow: '1500', - sellPriceHigh: '2000', - marketPrice: '1600', - spreadPercentage: '0.1', - buyBudget: '100', - buyPriceHigh: '1999.5', - sellPriceLow: '1500.5', - buyPriceMarginal: '1599.75', - sellPriceMarginal: '1600.25', - sellBudget: '0.231374205622609422', + buyPriceLow: new Decimal('0.005'), + sellPriceHigh: new Decimal('0.03'), + marketPrice: new Decimal('0.007241'), + spreadPercentage: new Decimal('0.1'), + spread: new Decimal('0.000025'), + buyBudget: new Decimal('3090.190579'), + buyPriceHigh: new Decimal('0.029975'), + sellPriceLow: new Decimal('0.005025'), + buyPriceMarginal: new Decimal('0.0072285'), + sellPriceMarginal: new Decimal('0.0072535'), + sellBudget: new Decimal('2576455.281630354877824211'), }, ]; @@ -50,6 +60,7 @@ describe('utils', () => { sellPriceHigh, marketPrice, spreadPercentage, + spread, buyBudget, buyPriceHigh, sellPriceLow, @@ -62,20 +73,53 @@ describe('utils', () => { buyPriceLow: ${buyPriceLow}, sellPriceHigh: ${sellPriceHigh}, marketPrice: ${marketPrice}, spreadPercentage: ${spreadPercentage}, buyBudget: ${buyBudget}`, () => { - const result = calculateOverlappingDistribution( - baseTokenDecimals, - quoteTokenDecimals, + const prices = calculateOverlappingPriceRanges( + buyPriceLow, + sellPriceHigh, + marketPrice, + spreadPercentage + ); + + expect(prices.buyPriceHigh.toString()).to.equal( + buyPriceHigh.toString() + ); + expect(prices.sellPriceLow.toString()).to.equal( + sellPriceLow.toString() + ); + expect(prices.buyPriceMarginal.toString()).to.equal( + buyPriceMarginal.toString() + ); + expect(prices.sellPriceMarginal.toString()).to.equal( + sellPriceMarginal.toString() + ); + expect(prices.spread.toString()).to.equal(spread.toString()); + + const sellRes = calculateOverlappingSellBudget( buyPriceLow, sellPriceHigh, marketPrice, spreadPercentage, buyBudget ); - expect(result.buyPriceHigh).to.equal(buyPriceHigh); - expect(result.sellPriceLow).to.equal(sellPriceLow); - expect(result.buyPriceMarginal).to.equal(buyPriceMarginal); - expect(result.sellPriceMarginal).to.equal(sellPriceMarginal); - expect(result.sellBudget).to.equal(sellBudget); + expect(trimDecimal(sellRes.toString(), baseTokenDecimals)).to.equal( + sellBudget.toString() + ); + + const buyRes = calculateOverlappingBuyBudget( + buyPriceLow, + sellPriceHigh, + marketPrice, + spreadPercentage, + sellBudget + ); + expect( + ...isAlmostEqual( + buyRes.toString(), + buyBudget.toString(), + '100', + '0.0003' + ) + ).to.be.true; }); } ); From 9b38c884c125e6ec5e38a04a14467d9ba9bad425 Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Thu, 30 Nov 2023 10:18:21 +0000 Subject: [PATCH 4/5] version bump --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b544208..57786b4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@bancor/carbon-sdk", "type": "module", "source": "src/index.ts", - "version": "0.0.85-DEV", + "version": "0.0.86-DEV", "description": "The SDK is a READ-ONLY tool, intended to facilitate working with Carbon contracts. It's a convenient wrapper around our matching algorithm, allowing programs and users get a ready to use transaction data that will allow them to manage strategies and fulfill trades", "main": "dist/index.js", "module": "dist/index.js", From 22196072a1d1daa5193328f6d8c28093e007796b Mon Sep 17 00:00:00 2001 From: Doron Zavelevsky Date: Thu, 30 Nov 2023 10:38:16 +0000 Subject: [PATCH 5/5] added tests for toolkit --- tests/toolkit.spec.ts | 43 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/toolkit.spec.ts b/tests/toolkit.spec.ts index 4f38359..42da216 100644 --- a/tests/toolkit.spec.ts +++ b/tests/toolkit.spec.ts @@ -98,6 +98,49 @@ describe('Toolkit', () => { decimalFetcher = () => 18; }); + describe('overlappingStrategies', () => { + it('should calculate strategy prices', async () => { + const toolkit = new Toolkit(apiMock, cacheMock, decimalFetcher); + const result = await toolkit.calculateOverlappingStrategyPrices( + 'quoteToken', + '1500', + '2000', + '1600', + '0.1' + ); + expect(result).to.deep.equal({ + buyPriceHigh: '1999.5', + buyPriceMarginal: '1599.75', + sellPriceLow: '1500.5', + sellPriceMarginal: '1600.25', + }); + }); + it('should calculate strategy sell budget', async () => { + const toolkit = new Toolkit(apiMock, cacheMock, decimalFetcher); + const result = await toolkit.calculateOverlappingStrategySellBudget( + 'baseToken', + '1500', + '2000', + '1600', + '0.1', + '100' + ); + expect(result).to.equal('0.231403132822275197'); + }); + it('should calculate strategy buy budget', async () => { + const toolkit = new Toolkit(apiMock, cacheMock, () => 6); + const result = await toolkit.calculateOverlappingStrategyBuyBudget( + 'quoteToken', + '1500', + '2000', + '1600', + '0.1', + '0.231403132822275197' + ); + expect(result).to.equal('100.029169'); + }); + }); + describe('hasLiquidityByPair', () => { it('should return true if there are orders', async () => { cacheMock.getOrdersByPair.resolves(orderMap);