From 9401bb53e8681b0459c7edb7a30e44f59d2ef610 Mon Sep 17 00:00:00 2001 From: David Sun Date: Mon, 1 Jul 2019 16:09:52 -0700 Subject: [PATCH] added testing for generating swap quotes --- .../src/utils/swap_quote_calculator.ts | 1 - .../test/swap_quote_calculator_test.ts | 314 +++++++++++++++++- packages/asset-buyer/test/utils/swap_quote.ts | 40 ++- 3 files changed, 338 insertions(+), 17 deletions(-) diff --git a/packages/asset-buyer/src/utils/swap_quote_calculator.ts b/packages/asset-buyer/src/utils/swap_quote_calculator.ts index 4d2fbf1295..c6020d6557 100644 --- a/packages/asset-buyer/src/utils/swap_quote_calculator.ts +++ b/packages/asset-buyer/src/utils/swap_quote_calculator.ts @@ -358,7 +358,6 @@ function findTakerTokenAmountNeededToBuyZrx( order, makerFillAmount, ); - // TODO(dave4506) may remove if this is for affiliate fees (asset-buyer2.0) const extraFeeAmount = remainingFillableMakerAssetAmount.isGreaterThanOrEqualTo(adjustedMakerFillAmount) ? constants.ZERO_AMOUNT : adjustedMakerFillAmount.minus(makerFillAmount); diff --git a/packages/asset-buyer/test/swap_quote_calculator_test.ts b/packages/asset-buyer/test/swap_quote_calculator_test.ts index 3d68b98592..5a78841295 100644 --- a/packages/asset-buyer/test/swap_quote_calculator_test.ts +++ b/packages/asset-buyer/test/swap_quote_calculator_test.ts @@ -16,7 +16,295 @@ const expect = chai.expect; // tslint:disable:custom-no-magic-numbers describe('swapQuoteCalculator', () => { - describe('#calculate', () => { + describe('#calculateMarketSellSwapQuote', () => { + let firstOrder: SignedOrder; + let firstRemainingFillAmount: BigNumber; + let secondOrder: SignedOrder; + let secondRemainingFillAmount: BigNumber; + let ordersAndFillableAmounts: OrdersAndFillableAmounts; + let smallFeeOrderAndFillableAmount: OrdersAndFillableAmounts; + let allFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts; + beforeEach(() => { + // generate two orders for our desired maker asset + // the first order has a rate of 4 makerAsset / WETH with a takerFee of 200 ZRX and has only 200 / 400 makerAsset units left to fill (half fillable) + // the second order has a rate of 2 makerAsset / WETH with a takerFee of 100 ZRX and has 200 / 200 makerAsset units left to fill (completely fillable) + // generate one order for fees + // the fee order has a rate of 1 ZRX / WETH with no taker fee and has 100 ZRX left to fill (completely fillable) + firstOrder = orderFactory.createSignedOrderFromPartial({ + makerAssetAmount: new BigNumber(400), + takerAssetAmount: new BigNumber(100), + takerFee: new BigNumber(200), + }); + firstRemainingFillAmount = new BigNumber(200); + secondOrder = orderFactory.createSignedOrderFromPartial({ + makerAssetAmount: new BigNumber(200), + takerAssetAmount: new BigNumber(100), + takerFee: new BigNumber(100), + }); + secondRemainingFillAmount = secondOrder.makerAssetAmount; + ordersAndFillableAmounts = { + orders: [firstOrder, secondOrder], + remainingFillableMakerAssetAmounts: [firstRemainingFillAmount, secondRemainingFillAmount], + }; + const smallFeeOrder = orderFactory.createSignedOrderFromPartial({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(100), + }); + smallFeeOrderAndFillableAmount = { + orders: [smallFeeOrder], + remainingFillableMakerAssetAmounts: [smallFeeOrder.makerAssetAmount], + }; + const largeFeeOrder = orderFactory.createSignedOrderFromPartial({ + makerAssetAmount: new BigNumber(113), + takerAssetAmount: new BigNumber(200), + takerFee: new BigNumber(11), + }); + allFeeOrdersAndFillableAmounts = { + orders: [smallFeeOrder, largeFeeOrder], + remainingFillableMakerAssetAmounts: [ + smallFeeOrder.makerAssetAmount, + largeFeeOrder.makerAssetAmount.minus(largeFeeOrder.takerFee), + ], + }; + }); + describe('InsufficientLiquidityError', () => { + it('should throw if not enough taker asset liquidity (multiple orders)', () => { + // we have 150 takerAsset units available to fill but attempt to calculate a quote for 200 takerAsset units + const errorFunction = () => { + swapQuoteCalculator.calculateMarketSellSwapQuote( + ordersAndFillableAmounts, + smallFeeOrderAndFillableAmount, + new BigNumber(200), + 0, + false, + ); + }; + testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(150)); + }); + it('should throw if not enough taker asset liquidity (multiple orders with 20% slippage)', () => { + // we have 150 takerAsset units available to fill but attempt to calculate a quote for 200 makerAsset units + const errorFunction = () => { + swapQuoteCalculator.calculateMarketSellSwapQuote( + ordersAndFillableAmounts, + smallFeeOrderAndFillableAmount, + new BigNumber(200), + 0.2, + false, + ); + }; + testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(125)); + }); + it('should throw if not enough taker asset liquidity (multiple orders with 5% slippage)', () => { + // we have 150 takerAsset units available to fill but attempt to calculate a quote for 200 makerAsset units + const errorFunction = () => { + swapQuoteCalculator.calculateMarketSellSwapQuote( + ordersAndFillableAmounts, + smallFeeOrderAndFillableAmount, + new BigNumber(200), + 0.05, + false, + ); + }; + testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(142)); + }); + it('should throw if not enough taker asset liquidity (partially filled order)', () => { + const firstOrderAndFillableAmount: OrdersAndFillableAmounts = { + orders: [firstOrder], + remainingFillableMakerAssetAmounts: [firstRemainingFillAmount], + }; + + const errorFunction = () => { + swapQuoteCalculator.calculateMarketSellSwapQuote( + firstOrderAndFillableAmount, + smallFeeOrderAndFillableAmount, + new BigNumber(51), + 0, + false, + ); + }; + testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(50)); + }); + it('should throw if not enough taker asset liquidity (completely fillable order)', () => { + const completelyFillableOrder = orderFactory.createSignedOrderFromPartial({ + makerAssetAmount: new BigNumber(123), + takerAssetAmount: new BigNumber(80), + takerFee: new BigNumber(200), + }); + const completelyFillableOrdersAndFillableAmount: OrdersAndFillableAmounts = { + orders: [completelyFillableOrder], + remainingFillableMakerAssetAmounts: [completelyFillableOrder.makerAssetAmount], + }; + const errorFunction = () => { + swapQuoteCalculator.calculateMarketSellSwapQuote( + completelyFillableOrdersAndFillableAmount, + smallFeeOrderAndFillableAmount, + new BigNumber(81), + 0, + false, + ); + }; + testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(80)); + }); + it('should throw with 1 amount available if no slippage', () => { + const smallOrder = orderFactory.createSignedOrderFromPartial({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(1), + takerFee: new BigNumber(0), + }); + const errorFunction = () => { + swapQuoteCalculator.calculateMarketSellSwapQuote( + { orders: [smallOrder], remainingFillableMakerAssetAmounts: [smallOrder.makerAssetAmount] }, + smallFeeOrderAndFillableAmount, + new BigNumber(100), + 0, + false, + ); + }; + testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(1)); + }); + it('should throw with 0 available to fill if amount rounds to 0', () => { + const smallOrder = orderFactory.createSignedOrderFromPartial({ + makerAssetAmount: new BigNumber(1), + takerAssetAmount: new BigNumber(1), + takerFee: new BigNumber(0), + }); + const errorFunction = () => { + swapQuoteCalculator.calculateMarketSellSwapQuote( + { orders: [smallOrder], remainingFillableMakerAssetAmounts: [smallOrder.makerAssetAmount] }, + smallFeeOrderAndFillableAmount, + new BigNumber(100), + 0.2, + false, + ); + }; + testHelpers.expectInsufficientLiquidityError(expect, errorFunction, new BigNumber(0)); + }); + }); + it('should not throw if order is fillable', () => { + expect(() => + swapQuoteCalculator.calculateMarketSellSwapQuote( + ordersAndFillableAmounts, + allFeeOrdersAndFillableAmounts, + new BigNumber(125), + 0, + false, + ), + ).to.not.throw(); + }); + it('should throw if not enough ZRX liquidity', () => { + // we request 300 makerAsset units but the ZRX order is only enough to fill the first order, which only has 200 makerAssetUnits available + expect(() => + swapQuoteCalculator.calculateMarketSellSwapQuote( + ordersAndFillableAmounts, + smallFeeOrderAndFillableAmount, + new BigNumber(125), + 0, + false, + ), + ).to.throw(SwapQuoterError.InsufficientZrxLiquidity); + }); + it('calculates a correct swapQuote with no slippage', () => { + // we request 100 takerAsset units which can be filled using the first order + // the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder + const assetSellAmount = new BigNumber(50); + const slippagePercentage = 0; + const swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote( + ordersAndFillableAmounts, + smallFeeOrderAndFillableAmount, + assetSellAmount, + slippagePercentage, + false, + ); + // test if orders are correct + expect(swapQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]); + expect(swapQuote.feeOrders).to.deep.equal([smallFeeOrderAndFillableAmount.orders[0]]); + // test if rates are correct + // 50 eth to fill the first order + 100 eth for fees + const expectedMakerAssetAmountForTakerAsset = new BigNumber(200); + const expectedTakerAssetAmountForZrxFees = new BigNumber(100); + const expectedTotalTakerAssetAmount = assetSellAmount.plus( + expectedTakerAssetAmountForZrxFees, + ); + expect(swapQuote.bestCaseQuoteInfo.takerTokenAmount).to.bignumber.equal( + assetSellAmount, + ); + expect(swapQuote.bestCaseQuoteInfo.makerTokenAmount).to.bignumber.equal( + expectedMakerAssetAmountForTakerAsset, + ); + expect(swapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal( + expectedTakerAssetAmountForZrxFees, + ); + expect(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(expectedTotalTakerAssetAmount); + // because we have no slippage protection, minRate is equal to maxRate + expect(swapQuote.worstCaseQuoteInfo.takerTokenAmount).to.bignumber.equal( + assetSellAmount, + ); + expect(swapQuote.worstCaseQuoteInfo.makerTokenAmount).to.bignumber.equal( + expectedMakerAssetAmountForTakerAsset, + ); + expect(swapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal( + expectedTakerAssetAmountForZrxFees, + ); + expect(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal( + expectedTotalTakerAssetAmount, + ); + }); + it('calculates a correct swapQuote with slippage', () => { + // we request 50 takerAsset units which can be filled using the first order + // however with 50% slippage we are protecting the buy with 25 extra takerAssetUnits + // so we need enough orders to fill 75 takerAssetUnits + // 150 takerAssetUnits can only be filled using both orders + // the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder + const assetSellAmount = new BigNumber(50); + const slippagePercentage = 0.5; + const swapQuote = swapQuoteCalculator.calculateMarketSellSwapQuote( + ordersAndFillableAmounts, + allFeeOrdersAndFillableAmounts, + assetSellAmount, + slippagePercentage, + false, + ); + // test if orders are correct + expect(swapQuote.orders).to.deep.equal(ordersAndFillableAmounts.orders); + expect(swapQuote.feeOrders).to.deep.equal(allFeeOrdersAndFillableAmounts.orders); + // test if rates are correct + // 50 eth to fill the first order + 100 eth for fees + const expectedMakerAssetAmountForTakerAsset = new BigNumber(200); + const expectedTakerAssetAmountForZrxFees = new BigNumber(100); + const expectedTotalTakerAssetAmount = assetSellAmount.plus( + expectedTakerAssetAmountForZrxFees, + ); + expect(swapQuote.bestCaseQuoteInfo.takerTokenAmount).to.bignumber.equal( + assetSellAmount, + ); + expect(swapQuote.bestCaseQuoteInfo.makerTokenAmount).to.bignumber.equal( + expectedMakerAssetAmountForTakerAsset, + ); + expect(swapQuote.bestCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal( + expectedTakerAssetAmountForZrxFees, + ); + expect(swapQuote.bestCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal(expectedTotalTakerAssetAmount); + // 100 eth to fill the first order + 208 eth for fees + const expectedWorstMakerAssetAmountForTakerAsset = new BigNumber(100); + const expectedWorstTakerAssetAmountForZrxFees = new BigNumber(99); + const expectedWorstTotalTakerAssetAmount = assetSellAmount.plus( + expectedWorstTakerAssetAmountForZrxFees, + ); + expect(swapQuote.worstCaseQuoteInfo.takerTokenAmount).to.bignumber.equal( + assetSellAmount, + ); + expect(swapQuote.worstCaseQuoteInfo.makerTokenAmount).to.bignumber.equal( + expectedWorstMakerAssetAmountForTakerAsset, + ); + expect(swapQuote.worstCaseQuoteInfo.feeTakerTokenAmount).to.bignumber.equal( + expectedWorstTakerAssetAmountForZrxFees, + ); + expect(swapQuote.worstCaseQuoteInfo.totalTakerTokenAmount).to.bignumber.equal( + expectedWorstTotalTakerAssetAmount, + ); + }); + }); + describe('#calculateMarketBuySwapQuote', () => { let firstOrder: SignedOrder; let firstRemainingFillAmount: BigNumber; let secondOrder: SignedOrder; @@ -71,7 +359,7 @@ describe('swapQuoteCalculator', () => { it('should throw if not enough maker asset liquidity (multiple orders)', () => { // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units const errorFunction = () => { - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( ordersAndFillableAmounts, smallFeeOrderAndFillableAmount, new BigNumber(500), @@ -84,7 +372,7 @@ describe('swapQuoteCalculator', () => { it('should throw if not enough maker asset liquidity (multiple orders with 20% slippage)', () => { // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units const errorFunction = () => { - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( ordersAndFillableAmounts, smallFeeOrderAndFillableAmount, new BigNumber(500), @@ -97,7 +385,7 @@ describe('swapQuoteCalculator', () => { it('should throw if not enough maker asset liquidity (multiple orders with 5% slippage)', () => { // we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units const errorFunction = () => { - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( ordersAndFillableAmounts, smallFeeOrderAndFillableAmount, new BigNumber(600), @@ -114,7 +402,7 @@ describe('swapQuoteCalculator', () => { }; const errorFunction = () => { - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( firstOrderAndFillableAmount, smallFeeOrderAndFillableAmount, new BigNumber(201), @@ -135,7 +423,7 @@ describe('swapQuoteCalculator', () => { remainingFillableMakerAssetAmounts: [completelyFillableOrder.makerAssetAmount], }; const errorFunction = () => { - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( completelyFillableOrdersAndFillableAmount, smallFeeOrderAndFillableAmount, new BigNumber(124), @@ -152,7 +440,7 @@ describe('swapQuoteCalculator', () => { takerFee: new BigNumber(0), }); const errorFunction = () => { - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( { orders: [smallOrder], remainingFillableMakerAssetAmounts: [smallOrder.makerAssetAmount] }, smallFeeOrderAndFillableAmount, new BigNumber(600), @@ -169,7 +457,7 @@ describe('swapQuoteCalculator', () => { takerFee: new BigNumber(0), }); const errorFunction = () => { - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( { orders: [smallOrder], remainingFillableMakerAssetAmounts: [smallOrder.makerAssetAmount] }, smallFeeOrderAndFillableAmount, new BigNumber(600), @@ -182,7 +470,7 @@ describe('swapQuoteCalculator', () => { }); it('should not throw if order is fillable', () => { expect(() => - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( ordersAndFillableAmounts, allFeeOrdersAndFillableAmounts, new BigNumber(300), @@ -194,7 +482,7 @@ describe('swapQuoteCalculator', () => { it('should throw if not enough ZRX liquidity', () => { // we request 300 makerAsset units but the ZRX order is only enough to fill the first order, which only has 200 makerAssetUnits available expect(() => - swapQuoteCalculator.calculate( + swapQuoteCalculator.calculateMarketBuySwapQuote( ordersAndFillableAmounts, smallFeeOrderAndFillableAmount, new BigNumber(300), @@ -208,7 +496,7 @@ describe('swapQuoteCalculator', () => { // the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder const assetBuyAmount = new BigNumber(200); const slippagePercentage = 0; - const swapQuote = swapQuoteCalculator.calculate( + const swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote( ordersAndFillableAmounts, smallFeeOrderAndFillableAmount, assetBuyAmount, @@ -243,7 +531,7 @@ describe('swapQuoteCalculator', () => { expectedTotalTakerAssetAmount, ); }); - it('calculates a correct swapQuote with with slippage', () => { + it('calculates a correct swapQuote with slippage', () => { // we request 200 makerAsset units which can be filled using the first order // however with 50% slippage we are protecting the buy with 100 extra makerAssetUnits // so we need enough orders to fill 300 makerAssetUnits @@ -251,7 +539,7 @@ describe('swapQuoteCalculator', () => { // the first order requires a fee of 100 ZRX from the taker which can be filled by the feeOrder const assetBuyAmount = new BigNumber(200); const slippagePercentage = 0.5; - const swapQuote = swapQuoteCalculator.calculate( + const swapQuote = swapQuoteCalculator.calculateMarketBuySwapQuote( ordersAndFillableAmounts, allFeeOrdersAndFillableAmounts, assetBuyAmount, diff --git a/packages/asset-buyer/test/utils/swap_quote.ts b/packages/asset-buyer/test/utils/swap_quote.ts index 14de239a48..0f21724050 100644 --- a/packages/asset-buyer/test/utils/swap_quote.ts +++ b/packages/asset-buyer/test/utils/swap_quote.ts @@ -3,7 +3,7 @@ import { SignedOrder } from '@0x/types'; import { BigNumber } from '@0x/utils'; import * as _ from 'lodash'; -import { SwapQuote } from '../../src'; +import { MarketBuySwapQuote, MarketSellSwapQuote } from '../../src'; const ZERO_BIG_NUMBER = new BigNumber(0); @@ -25,11 +25,11 @@ export const getSignedOrdersWithNoFees = ( ); }; -export const getFullyFillableSwapQuoteWithNoFees = ( +export const getFullyFillableMarketBuySwapQuoteWithNoFees = ( makerAssetData: string, takerAssetData: string, orders: SignedOrder[], -): SwapQuote => { +): MarketBuySwapQuote => { const makerAssetFillAmount = _.reduce( orders, (a: BigNumber, c: SignedOrder) => a.plus(c.makerAssetAmount), @@ -41,6 +41,7 @@ export const getFullyFillableSwapQuoteWithNoFees = ( ZERO_BIG_NUMBER, ); const quoteInfo = { + makerTokenAmount: makerAssetFillAmount, takerTokenAmount: totalTakerTokenAmount, feeTakerTokenAmount: ZERO_BIG_NUMBER, totalTakerTokenAmount, @@ -56,3 +57,36 @@ export const getFullyFillableSwapQuoteWithNoFees = ( worstCaseQuoteInfo: quoteInfo, }; }; + +export const getFullyFillableMarketSellSwapQuoteWithNoFees = ( + makerAssetData: string, + takerAssetData: string, + orders: SignedOrder[], +): MarketSellSwapQuote => { + const makerAssetFillAmount = _.reduce( + orders, + (a: BigNumber, c: SignedOrder) => a.plus(c.makerAssetAmount), + ZERO_BIG_NUMBER, + ); + const totalTakerTokenAmount = _.reduce( + orders, + (a: BigNumber, c: SignedOrder) => a.plus(c.takerAssetAmount), + ZERO_BIG_NUMBER, + ); + const quoteInfo = { + makerTokenAmount: makerAssetFillAmount, + takerTokenAmount: totalTakerTokenAmount, + feeTakerTokenAmount: ZERO_BIG_NUMBER, + totalTakerTokenAmount, + }; + + return { + makerAssetData, + takerAssetData, + orders, + feeOrders: [], + takerAssetFillAmount: totalTakerTokenAmount, + bestCaseQuoteInfo: quoteInfo, + worstCaseQuoteInfo: quoteInfo, + }; +};