From b0287400505e85625fbba99b8462e258e894b745 Mon Sep 17 00:00:00 2001 From: Jacob Evans Date: Fri, 24 Jul 2020 16:47:44 +1000 Subject: [PATCH] added tests --- src/constants.ts | 5 + src/handlers/swap_handlers.ts | 37 ++-- src/services/swap_service.ts | 46 +++-- src/types.ts | 7 + src/utils/market_depth_utils.ts | 62 +++++-- test/market_depth_test.ts | 292 ++++++++++++++++++++++++++++++++ 6 files changed, 402 insertions(+), 47 deletions(-) create mode 100644 test/market_depth_test.ts diff --git a/src/constants.ts b/src/constants.ts index 91db7406bc..9154512d85 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -88,3 +88,8 @@ export const GST2_WALLET_ADDRESSES = { [ChainId.Kovan]: NULL_ADDRESS, [ChainId.Ganache]: NULL_ADDRESS, }; + +// Market Depth +export const MARKET_DEPTH_MAX_SAMPLES = 50; +export const MARKET_DEPTH_DEFAULT_DISTRIBUTION = 1.05; +export const MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC = 20; diff --git a/src/handlers/swap_handlers.ts b/src/handlers/swap_handlers.ts index 0b00a2e86d..72498ef24a 100644 --- a/src/handlers/swap_handlers.ts +++ b/src/handlers/swap_handlers.ts @@ -4,7 +4,12 @@ import * as express from 'express'; import * as HttpStatus from 'http-status-codes'; import { CHAIN_ID } from '../config'; -import { DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE, SWAP_DOCS_URL } from '../constants'; +import { + DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE, + MARKET_DEPTH_DEFAULT_DISTRIBUTION, + MARKET_DEPTH_MAX_SAMPLES, + SWAP_DOCS_URL, +} from '../constants'; import { InternalServerError, RevertAPIError, @@ -129,25 +134,21 @@ export class SwapHandlers { public async getMarketDepthAsync(req: express.Request, res: express.Response): Promise { const makerToken = getTokenMetadataIfExists(req.query.buyToken as string, CHAIN_ID); const takerToken = getTokenMetadataIfExists(req.query.sellToken as string, CHAIN_ID); - try { - const response = await this._swapService.calculateMarketDepthAsync({ - buyToken: makerToken, - sellToken: takerToken, - sellAmount: new BigNumber(req.query.sellAmount as string), - // tslint:disable-next-line:radix custom-no-magic-numbers - numSamples: req.query.numSamples ? parseInt(req.query.numSamples as string) : 100, - sampleDistributionBase: req.query.sampleDistributionBase - ? parseFloat(req.query.sampleDistributionBase as string) - : 1.05, - excludedSources: req.query.excludedSources === undefined + const response = await this._swapService.calculateMarketDepthAsync({ + buyToken: makerToken, + sellToken: takerToken, + sellAmount: new BigNumber(req.query.sellAmount as string), + // tslint:disable-next-line:radix custom-no-magic-numbers + numSamples: req.query.numSamples ? parseInt(req.query.numSamples as string) : MARKET_DEPTH_MAX_SAMPLES, + sampleDistributionBase: req.query.sampleDistributionBase + ? parseFloat(req.query.sampleDistributionBase as string) + : MARKET_DEPTH_DEFAULT_DISTRIBUTION, + excludedSources: + req.query.excludedSources === undefined ? [] : parseUtils.parseStringArrForERC20BridgeSources((req.query.excludedSources as string).split(',')), - }); - res.status(HttpStatus.OK).send({ ...response, buyToken: makerToken, sellToken: takerToken }); - } catch (e) { - console.log(e); - throw e; - } + }); + res.status(HttpStatus.OK).send({ ...response, buyToken: makerToken, sellToken: takerToken }); } private async _calculateSwapQuoteAsync( diff --git a/src/services/swap_service.ts b/src/services/swap_service.ts index 9e62702480..4f3710eb77 100644 --- a/src/services/swap_service.ts +++ b/src/services/swap_service.ts @@ -9,7 +9,6 @@ import { SwapQuoter, } from '@0x/asset-swapper'; import { SwapQuoteRequestOpts, SwapQuoterOpts } from '@0x/asset-swapper/lib/src/types'; -import { MarketDepth, MarketDepthSide } from '@0x/asset-swapper/lib/src/utils/market_operation_utils/types'; import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/contract-addresses'; import { ERC20TokenContract, WETH9Contract } from '@0x/contract-wrappers'; import { assetDataUtils, SupportedProvider } from '@0x/order-utils'; @@ -40,6 +39,7 @@ import { InsufficientFundsError } from '../errors'; import { logger } from '../logger'; import { TokenMetadatasForChains } from '../token_metadatas_for_networks'; import { + BucketedPriceDepth, CalaculateMarketDepthParams, CalculateSwapQuoteParams, GetSwapQuoteResponse, @@ -273,7 +273,12 @@ export class SwapService { return prices; } - public async calculateMarketDepthAsync(params: CalaculateMarketDepthParams): Promise { + public async calculateMarketDepthAsync( + params: CalaculateMarketDepthParams, + ): Promise<{ + asks: { dataByBucketPrice: BucketedPriceDepth[] }; + bids: { dataByBucketPrice: BucketedPriceDepth[] }; + }> { const { buyToken, sellToken, sellAmount, numSamples, sampleDistributionBase, excludedSources } = params; const marketDepth = await this._swapQuoter.getBidAskLiquidityForMakerTakerAssetPairAsync( buyToken.tokenAddress, @@ -286,27 +291,38 @@ export class SwapService { }, ); - const calculateDepthForSide = (rawDepthSide: MarketDepthSide, side: MarketOperation) => { - const depthSide = marketDepthUtils.normalizeMarketDepthToSampleOutput(rawDepthSide, side); - const [startPrice, endPrice] = marketDepthUtils.calculateStartEndBucketPrice(depthSide, side); - const buckets = marketDepthUtils.getBucketPrices(startPrice, endPrice, numSamples, sampleDistributionBase); - const distributedBuckets = marketDepthUtils.distributeSamplesToBuckets(depthSide, buckets, side); - // Scale the price by the token decimals - const scaledBuckets = distributedBuckets.map(b => ({ + const maxEndSlippagePercentage = 20; + const scalePriceByDecimals = (priceDepth: BucketedPriceDepth[]) => + priceDepth.map(b => ({ ...b, price: b.price.times(new BigNumber(10).pow(sellToken.decimals - buyToken.decimals)), })); - console.log({ startPrice, endPrice, buckets }); - return { dataByBucketPrice: scaledBuckets }; - }; + const askDepth = scalePriceByDecimals( + marketDepthUtils.calculateDepthForSide( + marketDepth.asks, + MarketOperation.Sell, + numSamples * 2, + sampleDistributionBase, + maxEndSlippagePercentage, + ), + ); + const bidDepth = scalePriceByDecimals( + marketDepthUtils.calculateDepthForSide( + marketDepth.bids, + MarketOperation.Buy, + numSamples * 2, + sampleDistributionBase, + maxEndSlippagePercentage, + ), + ); return { // We're buying buyToken and SELLING sellToken (DAI) (50k) // Price goes from HIGH to LOW - asks: calculateDepthForSide(marketDepth.asks, MarketOperation.Sell), + asks: { dataByBucketPrice: askDepth }, // We're BUYING sellToken (DAI) (50k) and selling buyToken // Price goes from LOW to HIGH - bids: calculateDepthForSide(marketDepth.bids, MarketOperation.Buy), - } as any; + bids: { dataByBucketPrice: bidDepth }, + }; } private async _getSwapQuoteForWethAsync( diff --git a/src/types.ts b/src/types.ts index 082a4861fd..e1c23e30bf 100644 --- a/src/types.ts +++ b/src/types.ts @@ -621,4 +621,11 @@ export interface CalaculateMarketDepthParams { sampleDistributionBase: number; excludedSources?: ERC20BridgeSource[]; } + +export interface BucketedPriceDepth { + cumulative: BigNumber; + price: BigNumber; + bucket: number; + bucketTotal: BigNumber; +} // tslint:disable-line:max-file-line-count diff --git a/src/utils/market_depth_utils.ts b/src/utils/market_depth_utils.ts index 3146fd5970..5db2e7ea99 100644 --- a/src/utils/market_depth_utils.ts +++ b/src/utils/market_depth_utils.ts @@ -9,8 +9,17 @@ import { BigNumber } from '@0x/utils'; import { Web3Wrapper } from '@0x/web3-wrapper'; import _ = require('lodash'); -import { ZERO } from '../constants'; -import { TokenMetadata } from '../types'; +import { + MARKET_DEPTH_DEFAULT_DISTRIBUTION, + MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC, + MARKET_DEPTH_MAX_SAMPLES, + ZERO, +} from '../constants'; +import { BucketedPriceDepth, TokenMetadata } from '../types'; + +// tslint:disable:custom-no-magic-numbers +const MAX_DECIMALS = 18; +const ONE_HUNDRED_PERC = 100; export const marketDepthUtils = { getBucketPrices: ( @@ -21,13 +30,16 @@ export const marketDepthUtils = { ): BigNumber[] => { const amount = endAmount.minus(startAmount); const distribution = [...Array(numSamples)].map((_v, i) => - new BigNumber(sampleDistributionBase).pow(i), + new BigNumber(sampleDistributionBase).pow(i).decimalPlaces(MAX_DECIMALS), ); const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution))); const amounts = stepSizes.map((_s, i) => { - return amount.times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)])).plus(startAmount); + return amount + .times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)])) + .plus(startAmount) + .decimalPlaces(MAX_DECIMALS); }); - return [startAmount, ...amounts, endAmount]; + return [startAmount, ...amounts]; }, calculateUnitPrice: ( input: BigNumber, @@ -58,7 +70,7 @@ export const marketDepthUtils = { return sampleAmounts; }, sampleNativeOrders: (path: Array>, targetInput: BigNumber): BigNumber => { - const sortedPath = path.sort((a, b) => a.output.dividedBy(a.input).comparedTo(b.output.dividedBy(b.input))); + const sortedPath = path.sort((a, b) => b.output.dividedBy(b.input).comparedTo(a.output.dividedBy(a.input))); let totalOutput = ZERO; let totalInput = ZERO; for (const fill of sortedPath) { @@ -71,11 +83,13 @@ export const marketDepthUtils = { totalInput = totalInput.plus(input); } if (totalInput.isLessThan(targetInput)) { + // TODO do I really want to do this return ZERO; } + console.log('native sample', { targetInput, totalInput, totalOutput }); return totalOutput; }, - normalizeMarketDepthToSampleOutput: (depthSide: MarketDepthSide, _operation: MarketOperation): MarketDepthSide => { + normalizeMarketDepthToSampleOutput: (depthSide: MarketDepthSide): MarketDepthSide => { // Native is not a "sampled" output, here we convert it to be a accumulated sample output const nativeIndexIfExists = depthSide.findIndex( s => s[0] && s[0].source === ERC20BridgeSource.Native && s[0].output, @@ -100,11 +114,15 @@ export const marketDepthUtils = { return normalizedDepth; }, - calculateStartEndBucketPrice: (depthSide: MarketDepthSide, side: MarketOperation): [BigNumber, BigNumber] => { + calculateStartEndBucketPrice: ( + depthSide: MarketDepthSide, + side: MarketOperation, + endSlippagePerc = 20, + ): [BigNumber, BigNumber] => { const pricesByAmount = depthSide .map(samples => samples - .map(s => (!s.output.isZero() ? s.output.dividedBy(s.input) : ZERO)) + .map(s => (!s.output.isZero() ? s.output.dividedBy(s.input).decimalPlaces(MAX_DECIMALS) : ZERO)) .filter(s => s.isGreaterThan(ZERO)), ) .filter(samples => samples.length > 0); @@ -112,15 +130,13 @@ export const marketDepthUtils = { let worstBestInBracket: BigNumber; if (side === MarketOperation.Sell) { // Sell we want to sell for a higher price as possible - console.log(pricesByAmount); bestInBracket = BigNumber.max(...pricesByAmount.map(s => BigNumber.max(...s))); - worstBestInBracket = BigNumber.min(...pricesByAmount.map(s => BigNumber.max(...s))); + worstBestInBracket = bestInBracket.times((ONE_HUNDRED_PERC - endSlippagePerc) / ONE_HUNDRED_PERC); } else { // Buy we want to buy for the lowest price possible bestInBracket = BigNumber.min(...pricesByAmount.map(s => BigNumber.min(...s))); - worstBestInBracket = BigNumber.max(...pricesByAmount.map(s => BigNumber.min(...s))); + worstBestInBracket = bestInBracket.times((ONE_HUNDRED_PERC + endSlippagePerc) / ONE_HUNDRED_PERC); } - // return [bestInBracket, worstBestInBracket.minus(bestInBracket).plus(worstBestInBracket)]; return [bestInBracket, worstBestInBracket]; }, @@ -140,7 +156,7 @@ export const marketDepthUtils = { const price = sample.output.dividedBy(sample.input); const bucketId = getBucketId(price); if (bucketId === -1) { - console.log('No bucket for price', price, source); + // No bucket available so we ignore continue; } const bucket = allocatedBuckets[bucketId]; @@ -155,4 +171,22 @@ export const marketDepthUtils = { }); return cumulativeBuckets; }, + + calculateDepthForSide: ( + rawDepthSide: MarketDepthSide, + side: MarketOperation, + numBuckets: number = MARKET_DEPTH_MAX_SAMPLES, + bucketDistribution: number = MARKET_DEPTH_DEFAULT_DISTRIBUTION, + maxEndSlippagePercentage: number = MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC, + ): BucketedPriceDepth[] => { + const depthSide = marketDepthUtils.normalizeMarketDepthToSampleOutput(rawDepthSide); + const [startPrice, endPrice] = marketDepthUtils.calculateStartEndBucketPrice( + depthSide, + side, + maxEndSlippagePercentage, + ); + const buckets = marketDepthUtils.getBucketPrices(startPrice, endPrice, numBuckets, bucketDistribution); + const distributedBuckets = marketDepthUtils.distributeSamplesToBuckets(depthSide, buckets, side); + return distributedBuckets; + }, }; diff --git a/test/market_depth_test.ts b/test/market_depth_test.ts new file mode 100644 index 0000000000..12e33aca45 --- /dev/null +++ b/test/market_depth_test.ts @@ -0,0 +1,292 @@ +import { ERC20BridgeSource } from '@0x/asset-swapper'; +import { expect } from '@0x/contracts-test-utils'; +import { MarketOperation } from '@0x/types'; +import { BigNumber } from '@0x/utils'; +import 'mocha'; + +import { ZERO } from '../src/constants'; +import { marketDepthUtils } from '../src/utils/market_depth_utils'; + +const B = v => new BigNumber(v); + +// tslint:disable:custom-no-magic-numbers + +const SUITE_NAME = 'market depth utils'; +describe(SUITE_NAME, () => { + describe('getBucketPrices', () => { + it('returns a range from start to end', async () => { + const start = B('1'); + const end = B('123'); + const num = 10; + const range = marketDepthUtils.getBucketPrices(start, end, num); + expect(range[0]).to.be.bignumber.eq('1'); + expect(range[10]).to.be.bignumber.eq('123'); + expect(range.length).to.be.eq(11); + }); + it('can go from high to low', async () => { + const start = B('123'); + const end = B('1'); + const num = 10; + const range = marketDepthUtils.getBucketPrices(start, end, num); + expect(range[0]).to.be.bignumber.eq('123'); + expect(range[10]).to.be.bignumber.eq('1'); + expect(range.length).to.be.eq(11); + }); + }); + describe('getSampleAmountsFromDepthSide', () => { + it('plucks out the input sample amounts', async () => { + const defaultSample = { output: B(10), source: ERC20BridgeSource.Uniswap }; + const sampleAmounts = marketDepthUtils.getSampleAmountsFromDepthSide([ + [ + { ...defaultSample, input: B(1) }, + { ...defaultSample, input: B(2) }, + ], + ]); + expect(sampleAmounts).to.deep.include(B(1)); + expect(sampleAmounts).to.deep.include(B(2)); + }); + it('ignores Native results if they are present', async () => { + const defaultSample = { output: B(10), source: ERC20BridgeSource.Uniswap }; + const nativeSample = { output: B(10), source: ERC20BridgeSource.Native }; + const sampleAmounts = marketDepthUtils.getSampleAmountsFromDepthSide([ + [{ ...defaultSample, input: B(1) }], + [ + { ...nativeSample, input: B(1) }, + { ...nativeSample, input: B(2) }, + ], + ]); + expect(sampleAmounts).to.deep.include(B(1)); + expect(sampleAmounts).to.not.deep.include(B(2)); + }); + it('plucks Native results if it has to', async () => { + const nativeSample = { output: B(10), source: ERC20BridgeSource.Native }; + const sampleAmounts = marketDepthUtils.getSampleAmountsFromDepthSide([ + [ + { ...nativeSample, input: B(1) }, + { ...nativeSample, input: B(2) }, + ], + ]); + expect(sampleAmounts).to.deep.include(B(1)); + expect(sampleAmounts).to.deep.include(B(2)); + }); + }); + describe('sampleNativeOrders', () => { + it('can partially fill a sample amount', async () => { + const nativePath = [{ input: B(100), output: B(200), source: ERC20BridgeSource.Native }]; + const output = marketDepthUtils.sampleNativeOrders(nativePath, B(10)); + expect(output).to.be.bignumber.eq(B(20)); + }); + it('returns zero if it cannot fully fill the amount', async () => { + const nativePath = [{ input: B(100), output: B(200), source: ERC20BridgeSource.Native }]; + const output = marketDepthUtils.sampleNativeOrders(nativePath, B(101)); + expect(output).to.be.bignumber.eq(ZERO); + }); + it('runs across multiple orders', async () => { + const nativePath = [ + { input: B(50), output: B(200), source: ERC20BridgeSource.Native }, + { input: B(50), output: B(50), source: ERC20BridgeSource.Native }, + ]; + const output = marketDepthUtils.sampleNativeOrders(nativePath, B(100)); + expect(output).to.be.bignumber.eq(B(250)); + }); + }); + describe('normalizeMarketDepthToSampleOutput', () => { + it('converts raw orders into samples for Native', async () => { + const nativePath = [ + { input: B(50), output: B(200), source: ERC20BridgeSource.Native }, + { input: B(50), output: B(50), source: ERC20BridgeSource.Native }, + ]; + const uniPath = [ + { input: B(1), output: B(10), source: ERC20BridgeSource.Uniswap }, + { input: B(2), output: B(20), source: ERC20BridgeSource.Uniswap }, + ]; + const results = marketDepthUtils.normalizeMarketDepthToSampleOutput([uniPath, nativePath]); + expect(results).to.deep.include(uniPath); + expect(results).to.deep.include([ + { input: B(1), output: B(4), source: ERC20BridgeSource.Native }, + { input: B(2), output: B(8), source: ERC20BridgeSource.Native }, + ]); + }); + }); + describe('calculateStartEndBucketPrice', () => { + const nativePath = [ + { input: B(1), output: B(4), source: ERC20BridgeSource.Native }, + { input: B(2), output: B(8), source: ERC20BridgeSource.Native }, + ]; + const uniPath = [ + { input: B(1), output: B(10), source: ERC20BridgeSource.Uniswap }, + { input: B(2), output: B(20), source: ERC20BridgeSource.Uniswap }, + ]; + describe('sell', () => { + it('starts at the best (highest) price and ends perc lower', async () => { + const [start, end] = marketDepthUtils.calculateStartEndBucketPrice( + [nativePath, uniPath], + MarketOperation.Sell, + 20, + ); + // Best price is the uniswap 1 for 10 + expect(start).to.be.bignumber.eq(B(10)); + expect(end).to.be.bignumber.eq(start.times(0.8)); + }); + }); + describe('buy', () => { + it('starts at the best (lowest) price and ends perc higher', async () => { + const [start, end] = marketDepthUtils.calculateStartEndBucketPrice( + [nativePath, uniPath], + MarketOperation.Buy, + 20, + ); + // Best price is the native 4 to receive 1 + expect(start).to.be.bignumber.eq(B(4)); + expect(end).to.be.bignumber.eq(start.times(1.2)); + }); + }); + }); + describe('distributeSamplesToBuckets', () => { + const nativePath = [ + { input: B(1), output: B(4), source: ERC20BridgeSource.Native }, + { input: B(2), output: B(8), source: ERC20BridgeSource.Native }, + ]; + const uniPath = [ + { input: B(1), output: B(10), source: ERC20BridgeSource.Uniswap }, + { input: B(2), output: B(20), source: ERC20BridgeSource.Uniswap }, + ]; + describe('sell', () => { + it('allocates the samples to the right bucket by price', async () => { + const buckets = [B(10), B(8), B(4), B(1)]; + const allocated = marketDepthUtils.distributeSamplesToBuckets( + [nativePath, uniPath], + buckets, + MarketOperation.Sell, + ); + const [first, second, third, fourth] = allocated; + expect(first[ERC20BridgeSource.Uniswap]).to.be.bignumber.eq(30); + expect(first.cumulative).to.be.bignumber.eq(30); + expect(first.bucketTotal).to.be.bignumber.eq(30); + expect(first.bucket).to.be.eq(0); + expect(first.price).to.be.bignumber.eq(10); + + expect(second.cumulative).to.be.bignumber.eq(30); + expect(second.bucketTotal).to.be.bignumber.eq(0); + expect(second.bucket).to.be.eq(1); + expect(second.price).to.be.bignumber.eq(8); + + expect(third.cumulative).to.be.bignumber.eq(42); + expect(third.bucketTotal).to.be.bignumber.eq(12); + expect(third[ERC20BridgeSource.Native]).to.be.bignumber.eq(12); + expect(third.bucket).to.be.eq(2); + expect(third.price).to.be.bignumber.eq(4); + + expect(fourth.cumulative).to.be.bignumber.eq(42); + expect(fourth.bucketTotal).to.be.bignumber.eq(0); + expect(fourth.bucket).to.be.eq(3); + expect(fourth.price).to.be.bignumber.eq(1); + }); + it('does not allocate to a bucket if there is none available', async () => { + const buckets = [B(10)]; + const badSource = [{ input: B(1), output: B(5), source: ERC20BridgeSource.Uniswap }]; + const allocated = marketDepthUtils.distributeSamplesToBuckets( + [badSource], + buckets, + MarketOperation.Sell, + ); + const [first] = allocated; + expect(first.cumulative).to.be.bignumber.eq(0); + expect(first.bucketTotal).to.be.bignumber.eq(0); + expect(first.bucket).to.be.eq(0); + expect(first.price).to.be.bignumber.eq(10); + }); + }); + describe('buy', () => { + it('allocates the samples to the right bucket by price', async () => { + const buckets = [B(1), B(4), B(10)]; + const allocated = marketDepthUtils.distributeSamplesToBuckets( + [nativePath, uniPath], + buckets, + MarketOperation.Buy, + ); + const [first, second, third] = allocated; + expect(first.cumulative).to.be.bignumber.eq(0); + expect(first.bucketTotal).to.be.bignumber.eq(0); + expect(first.bucket).to.be.eq(0); + expect(first.price).to.be.bignumber.eq(1); + + expect(second.cumulative).to.be.bignumber.eq(12); + expect(second.bucketTotal).to.be.bignumber.eq(12); + expect(second.bucket).to.be.eq(1); + expect(second.price).to.be.bignumber.eq(4); + expect(second[ERC20BridgeSource.Native]).to.be.bignumber.eq(12); + + expect(third.cumulative).to.be.bignumber.eq(42); + expect(third.bucketTotal).to.be.bignumber.eq(30); + expect(third[ERC20BridgeSource.Uniswap]).to.be.bignumber.eq(30); + expect(third.bucket).to.be.eq(2); + expect(third.price).to.be.bignumber.eq(10); + }); + }); + }); + describe('calculateDepthForSide', () => { + // Essentially orders not samples + const nativePath = [{ input: B(10), output: B(80), source: ERC20BridgeSource.Native }]; + it('calculates prices and allocates into buckets. Partial 0x', async () => { + const dexPaths = [ + [ + { input: B(1), output: B(10), source: ERC20BridgeSource.Uniswap }, + { input: B(2), output: B(11), source: ERC20BridgeSource.Uniswap }, + ], + [ + { input: B(1), output: B(0), source: ERC20BridgeSource.Curve }, + { input: B(2), output: B(0), source: ERC20BridgeSource.Curve }, + ], + ]; + const result = marketDepthUtils.calculateDepthForSide( + [nativePath, ...dexPaths], + MarketOperation.Sell, + 4, // buckets + 1, // distribution + 20, // max end perc + ); + expect(result).to.be.deep.eq([ + { price: B(10), bucket: 0, bucketTotal: B(10), cumulative: B(10), [ERC20BridgeSource.Uniswap]: B(10) }, + { price: B(9.5), bucket: 1, bucketTotal: ZERO, cumulative: B(10) }, + { price: B(9), bucket: 2, bucketTotal: ZERO, cumulative: B(10) }, + { price: B(8.5), bucket: 3, bucketTotal: ZERO, cumulative: B(10) }, + // Native is the sample for 1 (8) and thhe sample for 2 (16), since we didn't sample for 10 it does + // not contain the entire order + { price: B(8), bucket: 4, bucketTotal: B(24), cumulative: B(34), [ERC20BridgeSource.Native]: B(24) }, + ]); + }); + + // Skipped as it is showcasing a misrepresentation of Native orders + it.skip('calculates prices and allocates into buckets. Partial Uni', async () => { + const dexPaths = [ + [ + { input: B(1), output: B(10), source: ERC20BridgeSource.Uniswap }, + { input: B(2), output: B(11), source: ERC20BridgeSource.Uniswap }, + { input: B(10), output: B(0), source: ERC20BridgeSource.Uniswap }, + ], + [ + { input: B(1), output: B(0), source: ERC20BridgeSource.Curve }, + { input: B(2), output: B(0), source: ERC20BridgeSource.Curve }, + { input: B(10), output: B(0), source: ERC20BridgeSource.Curve }, + ], + ]; + const result = marketDepthUtils.calculateDepthForSide( + [nativePath, ...dexPaths], + MarketOperation.Sell, + 4, // buckets + 1, // distribution + 20, // max end perc + ); + expect(result).to.be.deep.eq([ + { price: B(10), bucket: 0, bucketTotal: B(10), cumulative: B(10), [ERC20BridgeSource.Uniswap]: B(10) }, + { price: B(9.5), bucket: 1, bucketTotal: ZERO, cumulative: B(10) }, + { price: B(9), bucket: 2, bucketTotal: ZERO, cumulative: B(10) }, + { price: B(8.5), bucket: 3, bucketTotal: ZERO, cumulative: B(10) }, + // Native is sampled for 1,2,10 and results in 104 + // This is more than liquiidity available so misrepresents Native as a source + { price: B(8), bucket: 4, bucketTotal: B(104), cumulative: B(114), [ERC20BridgeSource.Native]: B(104) }, + ]); + }); + }); +});