From 3e2dbfc83c59c517d122d10e0c581a3e2ede967e Mon Sep 17 00:00:00 2001 From: David Sun Date: Thu, 27 Jun 2019 10:23:25 -0700 Subject: [PATCH] added testing to market utils for market sell --- packages/order-utils/src/index.ts | 4 +- packages/order-utils/src/market_utils.ts | 63 ++++++++- packages/order-utils/src/types.ts | 13 +- .../order-utils/test/market_utils_test.ts | 129 ++++++++++++++++++ 4 files changed, 200 insertions(+), 9 deletions(-) diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 476a0f4e0b..6118658492 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -73,6 +73,8 @@ export { TransferType, FindFeeOrdersThatCoverFeesForTargetOrdersOpts, FindOrdersThatCoverMakerAssetFillAmountOpts, + FindOrdersThatCoverTakerAssetFillAmountOpts, FeeOrdersAndRemainingFeeAmount, - OrdersAndRemainingFillAmount, + OrdersAndRemainingTakerFillAmount, + OrdersAndRemainingMakerFillAmount, } from './types'; diff --git a/packages/order-utils/src/market_utils.ts b/packages/order-utils/src/market_utils.ts index 600f04f80b..48b321c5c1 100644 --- a/packages/order-utils/src/market_utils.ts +++ b/packages/order-utils/src/market_utils.ts @@ -5,12 +5,16 @@ import * as _ from 'lodash'; import { assert } from './assert'; import { constants } from './constants'; +import { + orderCalculationUtils, +} from './order_calculation_utils'; import { FeeOrdersAndRemainingFeeAmount, FindFeeOrdersThatCoverFeesForTargetOrdersOpts, FindOrdersThatCoverMakerAssetFillAmountOpts, FindOrdersThatCoverTakerAssetFillAmountOpts, - OrdersAndRemainingFillAmount, + OrdersAndRemainingMakerFillAmount, + OrdersAndRemainingTakerFillAmount, } from './types'; export const marketUtils = { @@ -18,10 +22,61 @@ export const marketUtils = { orders: T[], takerAssetFillAmount: BigNumber, opts?: FindOrdersThatCoverTakerAssetFillAmountOpts, - ): OrdersAndRemainingFillAmount { + ): OrdersAndRemainingTakerFillAmount { assert.doesConformToSchema('orders', orders, schemas.ordersSchema); assert.isValidBaseUnitAmount('takerAssetFillAmount', takerAssetFillAmount); - + // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders + const remainingFillableTakerAssetAmounts = _.get( + opts, + 'remainingFillableTakerAssetAmounts', + _.map(orders, order => order.takerAssetAmount), + ) as BigNumber[]; + _.forEach(remainingFillableTakerAssetAmounts, (amount, index) => + assert.isValidBaseUnitAmount(`remainingFillableTakerAssetAmount[${index}]`, amount), + ); + assert.assert( + orders.length === remainingFillableTakerAssetAmounts.length, + 'Expected orders.length to equal opts.remainingFillableMakerAssetAmounts.length', + ); + // try to get slippageBufferAmount from opts, if it's not there, default to 0 + const slippageBufferAmount = _.get(opts, 'slippageBufferAmount', constants.ZERO_AMOUNT) as BigNumber; + assert.isValidBaseUnitAmount('opts.slippageBufferAmount', slippageBufferAmount); + // calculate total amount of makerAsset needed to be filled + const totalFillAmount = takerAssetFillAmount.plus(slippageBufferAmount); + // iterate through the orders input from left to right until we have enough makerAsset to fill totalFillAmount + const result = _.reduce( + orders, + ({ resultOrders, remainingFillAmount, ordersRemainingFillableTakerAssetAmounts }, order, index) => { + if (remainingFillAmount.isLessThanOrEqualTo(constants.ZERO_AMOUNT)) { + return { + resultOrders, + remainingFillAmount: constants.ZERO_AMOUNT, + ordersRemainingFillableTakerAssetAmounts, + }; + } else { + const takerAssetAmountAvailable = remainingFillableTakerAssetAmounts[index]; + const shouldIncludeOrder = takerAssetAmountAvailable.gt(constants.ZERO_AMOUNT); + // if there is no makerAssetAmountAvailable do not append order to resultOrders + // if we have exceeded the total amount we want to fill set remainingFillAmount to 0 + return { + resultOrders: shouldIncludeOrder ? _.concat(resultOrders, order) : resultOrders, + ordersRemainingFillableTakerAssetAmounts: shouldIncludeOrder + ? _.concat(ordersRemainingFillableTakerAssetAmounts, takerAssetAmountAvailable) + : ordersRemainingFillableTakerAssetAmounts, + remainingFillAmount: BigNumber.max( + constants.ZERO_AMOUNT, + remainingFillAmount.minus(takerAssetAmountAvailable), + ), + }; + } + }, + { + resultOrders: [] as T[], + remainingFillAmount: totalFillAmount, + ordersRemainingFillableTakerAssetAmounts: [] as BigNumber[], + }, + ); + return result; }, /** * Takes an array of orders and returns a subset of those orders that has enough makerAssetAmount @@ -37,7 +92,7 @@ export const marketUtils = { orders: T[], makerAssetFillAmount: BigNumber, opts?: FindOrdersThatCoverMakerAssetFillAmountOpts, - ): OrdersAndRemainingFillAmount { + ): OrdersAndRemainingMakerFillAmount { assert.doesConformToSchema('orders', orders, schemas.ordersSchema); assert.isValidBaseUnitAmount('makerAssetFillAmount', makerAssetFillAmount); // try to get remainingFillableMakerAssetAmounts from opts, if it's not there, use makerAssetAmount values from orders diff --git a/packages/order-utils/src/types.ts b/packages/order-utils/src/types.ts index e284f7168b..c58eafe036 100644 --- a/packages/order-utils/src/types.ts +++ b/packages/order-utils/src/types.ts @@ -34,19 +34,18 @@ export interface CreateOrderOpts { */ export interface FindOrdersThatCoverMakerAssetFillAmountOpts { remainingFillableMakerAssetAmounts?: BigNumber[]; - remainingFillableTakerAssetAmounts?: BigNumber[]; slippageBufferAmount?: BigNumber; } /** - * remainingFillableTakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter. + * remainingFillableMakerAssetAmount: An array of BigNumbers corresponding to the `orders` parameter. * You can use `OrderStateUtils` `@0x/order-utils` to perform blockchain lookups for these values. * Defaults to `makerAssetAmount` values from the orders param. * slippageBufferAmount: An additional amount of makerAsset to be covered by the result in case of trade collisions or partial fills. * Defaults to 0 */ export interface FindOrdersThatCoverTakerAssetFillAmountOpts { - remainingFillableMakerAssetAmounts?: BigNumber[]; + remainingFillableTakerAssetAmounts?: BigNumber[]; slippageBufferAmount?: BigNumber; } @@ -72,8 +71,14 @@ export interface FeeOrdersAndRemainingFeeAmount { remainingFeeAmount: BigNumber; } -export interface OrdersAndRemainingFillAmount { +export interface OrdersAndRemainingMakerFillAmount { resultOrders: T[]; ordersRemainingFillableMakerAssetAmounts: BigNumber[]; remainingFillAmount: BigNumber; } + +export interface OrdersAndRemainingTakerFillAmount { + resultOrders: T[]; + ordersRemainingFillableTakerAssetAmounts: BigNumber[]; + remainingFillAmount: BigNumber; +} diff --git a/packages/order-utils/test/market_utils_test.ts b/packages/order-utils/test/market_utils_test.ts index 42ea195bb8..b1853d2922 100644 --- a/packages/order-utils/test/market_utils_test.ts +++ b/packages/order-utils/test/market_utils_test.ts @@ -13,6 +13,135 @@ const expect = chai.expect; // tslint:disable: no-unused-expression describe('marketUtils', () => { + describe('#findOrdersThatCoverTakerAssetFillAmount', () => { + describe('no orders', () => { + it('returns empty and unchanged remainingFillAmount', async () => { + const fillAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + [], + fillAmount, + ); + expect(resultOrders).to.be.empty; + expect(remainingFillAmount).to.be.bignumber.equal(fillAmount); + }); + }); + describe('orders are completely fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const takerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + takerAssetAmount, + }, + 3, + ); + it('returns input orders and zero remainingFillAmount when input exactly matches requested fill amount', async () => { + // try to fill 20 units of makerAsset + // include 10 units of slippageBufferAmount + const fillAmount = new BigNumber(20); + const slippageBufferAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + inputOrders, + fillAmount, + { + slippageBufferAmount, + }, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns input orders and zero remainingFillAmount when input has more than requested fill amount', async () => { + // try to fill 15 units of makerAsset + // include 10 units of slippageBufferAmount + const fillAmount = new BigNumber(15); + const slippageBufferAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + inputOrders, + fillAmount, + { + slippageBufferAmount, + }, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns input orders and non-zero remainingFillAmount when input has less than requested fill amount', async () => { + // try to fill 30 units of makerAsset + // include 5 units of slippageBufferAmount + const fillAmount = new BigNumber(30); + const slippageBufferAmount = new BigNumber(5); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + inputOrders, + fillAmount, + { + slippageBufferAmount, + }, + ); + expect(resultOrders).to.be.deep.equal(inputOrders); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(5)); + }); + it('returns first order and zero remainingFillAmount when requested fill amount is exactly covered by the first order', async () => { + // try to fill 10 units of makerAsset + const fillAmount = new BigNumber(10); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + inputOrders, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[0]]); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + it('returns first two orders and zero remainingFillAmount when requested fill amount is over covered by the first two order', async () => { + // try to fill 15 units of makerAsset + const fillAmount = new BigNumber(15); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + inputOrders, + fillAmount, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[0], inputOrders[1]]); + expect(remainingFillAmount).to.be.bignumber.equal(constants.ZERO_AMOUNT); + }); + }); + describe('orders are partially fillable', () => { + // generate three signed orders each with 10 units of makerAsset, 30 total + const takerAssetAmount = new BigNumber(10); + const inputOrders = testOrderFactory.generateTestSignedOrders( + { + takerAssetAmount, + }, + 3, + ); + // generate remainingFillableMakerAssetAmounts that cover different partial fill scenarios + // 1. order is completely filled already + // 2. order is partially fillable + // 3. order is completely fillable + const remainingFillableTakerAssetAmounts = [constants.ZERO_AMOUNT, new BigNumber(5), takerAssetAmount]; + it('returns last two orders and non-zero remainingFillAmount when trying to fill original takerAssetAmounts', async () => { + // try to fill 30 units of takerAsset + const fillAmount = new BigNumber(30); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + inputOrders, + fillAmount, + { + remainingFillableTakerAssetAmounts, + }, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(15)); + }); + it('returns last two orders and zero remainingFillAmount when trying to fill exactly takerAssetAmounts remaining', async () => { + // try to fill 15 units of takerAsset + const fillAmount = new BigNumber(15); + const { resultOrders, remainingFillAmount } = marketUtils.findOrdersThatCoverTakerAssetFillAmount( + inputOrders, + fillAmount, + { + remainingFillableTakerAssetAmounts, + }, + ); + expect(resultOrders).to.be.deep.equal([inputOrders[1], inputOrders[2]]); + expect(remainingFillAmount).to.be.bignumber.equal(new BigNumber(0)); + }); + }); + }); describe('#findOrdersThatCoverMakerAssetFillAmount', () => { describe('no orders', () => { it('returns empty and unchanged remainingFillAmount', async () => {