Add buy_quote_calculator_test

This commit is contained in:
Brandon Millman
2018-10-03 13:23:48 -07:00
parent a21cf0ad83
commit 57b4396193
2 changed files with 246 additions and 31 deletions

View File

@@ -1,4 +1,4 @@
import { marketUtils } from '@0xproject/order-utils';
import { marketUtils, rateUtils } from '@0xproject/order-utils';
import { BigNumber } from '@0xproject/utils';
import * as _ from 'lodash';
@@ -21,6 +21,7 @@ export const buyQuoteCalculator = {
const feeOrders = feeOrdersAndFillableAmounts.orders;
const remainingFillableFeeAmounts = feeOrdersAndFillableAmounts.remainingFillableMakerAssetAmounts;
const slippageBufferAmount = assetBuyAmount.mul(slippagePercentage).round();
// find the orders that cover the desired assetBuyAmount (with slippage)
const {
resultOrders,
remainingFillAmount,
@@ -29,9 +30,11 @@ export const buyQuoteCalculator = {
remainingFillableMakerAssetAmounts,
slippageBufferAmount,
});
// if we do not have enough orders to cover the desired assetBuyAmount, throw
if (remainingFillAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientAssetLiquidity);
}
// given the orders calculated above, find the fee-orders that cover the desired assetBuyAmount (with slippage)
// TODO(bmillman): optimization
// update this logic to find the minimum amount of feeOrders to cover the worst case as opposed to
// finding order that cover all fees, this will help with estimating ETH and minimizing gas usage
@@ -40,49 +43,131 @@ export const buyQuoteCalculator = {
remainingFeeAmount,
feeOrdersRemainingFillableMakerAssetAmounts,
} = marketUtils.findFeeOrdersThatCoverFeesForTargetOrders(resultOrders, feeOrders, {
remainingFillableMakerAssetAmounts,
remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
remainingFillableFeeAmounts,
});
// if we do not have enough feeOrders to cover the fees, throw
if (remainingFeeAmount.gt(constants.ZERO_AMOUNT)) {
throw new Error(AssetBuyerError.InsufficientZrxLiquidity);
}
// assetData information for the result
const assetData = orders[0].makerAssetData;
// calculate minRate and maxRate by calculating min and max eth usage and then dividing into
// assetBuyAmount to get assetData / WETH, needs to take into account feePercentage as well
// minEthAmount = (sum(takerAssetAmount[i]) until sum(makerAssetAmount[i]) >= assetBuyAmount ) * (1 + feePercentage)
// maxEthAmount = (sum(takerAssetAmount[i]) until i == orders.length) * (1 + feePercentage)
const allOrders = _.concat(resultOrders, resultFeeOrders);
const allRemainingAmounts = _.concat(
ordersRemainingFillableMakerAssetAmounts,
feeOrdersRemainingFillableMakerAssetAmounts,
// compile the resulting trimmed set of orders for makerAsset and feeOrders that are needed for assetBuyAmount
const trimmedOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: resultOrders,
remainingFillableMakerAssetAmounts: ordersRemainingFillableMakerAssetAmounts,
};
const trimmedFeeOrdersAndFillableAmounts: OrdersAndFillableAmounts = {
orders: resultFeeOrders,
remainingFillableMakerAssetAmounts: feeOrdersRemainingFillableMakerAssetAmounts,
};
const minRate = calculateRate(
trimmedOrdersAndFillableAmounts,
trimmedFeeOrdersAndFillableAmounts,
assetBuyAmount,
feePercentage,
);
// in order to calculate the maxRate, reverse the ordersAndFillableAmounts such that they are sorted from worst rate to best rate
const maxRate = calculateRate(
reverseOrdersAndFillableAmounts(trimmedOrdersAndFillableAmounts),
reverseOrdersAndFillableAmounts(trimmedFeeOrdersAndFillableAmounts),
assetBuyAmount,
feePercentage,
);
let minEthAmount = constants.ZERO_AMOUNT;
let maxEthAmount = constants.ZERO_AMOUNT;
let cumulativeMakerAmount = constants.ZERO_AMOUNT;
_.forEach(allOrders, (order, index) => {
const remainingFillableMakerAssetAmount = allRemainingAmounts[index];
const claimableTakerAssetAmount = orderUtils.calculateRemainingTakerAssetAmount(
order,
remainingFillableMakerAssetAmount,
);
// taker asset is always assumed to be WETH
maxEthAmount = maxEthAmount.plus(claimableTakerAssetAmount);
if (cumulativeMakerAmount.lessThan(assetBuyAmount)) {
minEthAmount = minEthAmount.plus(claimableTakerAssetAmount);
}
cumulativeMakerAmount = cumulativeMakerAmount.plus(remainingFillableMakerAssetAmount);
});
const feeAdjustedMinRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount);
const feeAdjustedMaxRate = minEthAmount.mul(feePercentage + 1).div(assetBuyAmount);
return {
assetData,
orders: resultOrders,
feeOrders: resultFeeOrders,
minRate: feeAdjustedMinRate,
maxRate: feeAdjustedMaxRate,
minRate,
maxRate,
assetBuyAmount,
feePercentage,
};
},
};
function calculateRate(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
assetBuyAmount: BigNumber,
feePercentage: number,
): BigNumber {
// find the total eth and zrx needed to buy assetAmount from the resultOrders from left to right (best rate to worst rate)
const [minEthAmountToBuyAsset, minZrxAmountToBuyAsset] = findEthAndZrxAmountNeededToBuyAsset(
ordersAndFillableAmounts,
assetBuyAmount,
);
// find the total eth needed to buy fees
const minEthAmountToBuyFees = findEthAmountNeededToBuyFees(feeOrdersAndFillableAmounts, minZrxAmountToBuyAsset);
const finalMinEthAmount = minEthAmountToBuyAsset.plus(minEthAmountToBuyFees).mul(feePercentage + 1);
// divide into the assetBuyAmount in order to find rate of makerAsset / WETH
const result = assetBuyAmount.div(finalMinEthAmount);
return result;
}
// given an OrdersAndFillableAmounts, reverse the orders and remainingFillableMakerAssetAmounts properties
function reverseOrdersAndFillableAmounts(ordersAndFillableAmounts: OrdersAndFillableAmounts): OrdersAndFillableAmounts {
const ordersCopy = _.clone(ordersAndFillableAmounts.orders);
const remainingFillableMakerAssetAmountsCopy = _.clone(ordersAndFillableAmounts.remainingFillableMakerAssetAmounts);
return {
orders: ordersCopy.reverse(),
remainingFillableMakerAssetAmounts: remainingFillableMakerAssetAmountsCopy.reverse(),
};
}
function findEthAmountNeededToBuyFees(
feeOrdersAndFillableAmounts: OrdersAndFillableAmounts,
feeAmount: BigNumber,
): BigNumber {
const { orders, remainingFillableMakerAssetAmounts } = feeOrdersAndFillableAmounts;
const result = _.reduce(
orders,
(acc, order, index) => {
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const amountToFill = BigNumber.min(acc.remainingFeeAmount, remainingFillableMakerAssetAmount);
const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfFeeOrder(order);
const ethAmountForThisOrder = feeAdjustedRate.mul(amountToFill);
return {
ethAmount: acc.ethAmount.plus(ethAmountForThisOrder),
remainingFeeAmount: BigNumber.max(constants.ZERO_AMOUNT, acc.remainingFeeAmount.minus(amountToFill)),
};
},
{
ethAmount: constants.ZERO_AMOUNT,
remainingFeeAmount: feeAmount,
},
);
return result.ethAmount;
}
function findEthAndZrxAmountNeededToBuyAsset(
ordersAndFillableAmounts: OrdersAndFillableAmounts,
assetBuyAmount: BigNumber,
): [BigNumber, BigNumber] {
const { orders, remainingFillableMakerAssetAmounts } = ordersAndFillableAmounts;
const result = _.reduce(
orders,
(acc, order, index) => {
const remainingFillableMakerAssetAmount = remainingFillableMakerAssetAmounts[index];
const amountToFill = BigNumber.min(acc.remainingAssetBuyAmount, remainingFillableMakerAssetAmount);
const ethAmountForThisOrder = amountToFill
.mul(order.takerAssetAmount)
.dividedToIntegerBy(order.makerAssetAmount);
const zrxAmountForThisOrder = amountToFill.mul(order.takerFee).dividedToIntegerBy(order.makerAssetAmount);
return {
ethAmount: acc.ethAmount.plus(ethAmountForThisOrder),
zrxAmount: acc.ethAmount.plus(zrxAmountForThisOrder),
remainingAssetBuyAmount: BigNumber.max(
constants.ZERO_AMOUNT,
acc.remainingAssetBuyAmount.minus(amountToFill),
),
};
},
{
ethAmount: constants.ZERO_AMOUNT,
zrxAmount: constants.ZERO_AMOUNT,
remainingAssetBuyAmount: assetBuyAmount,
},
);
return [result.ethAmount, result.zrxAmount];
}

View File

@@ -0,0 +1,130 @@
import { orderFactory } from '@0xproject/order-utils/lib/src/order_factory';
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import * as _ from 'lodash';
import 'mocha';
import { AssetBuyerError, OrdersAndFillableAmounts } from '../src/types';
import { buyQuoteCalculator } from '../src/utils/buy_quote_calculator';
import { chaiSetup } from './utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
const NULL_BYTES = '0x';
// tslint:disable:custom-no-magic-numbers
describe('buyQuoteCalculator', () => {
describe('#calculate', () => {
let ordersAndFillableAmounts: OrdersAndFillableAmounts;
let feeOrdersAndFillableAmounts: 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)
const firstOrder = orderFactory.createOrder(
NULL_ADDRESS,
new BigNumber(400),
NULL_BYTES,
new BigNumber(100),
NULL_BYTES,
NULL_ADDRESS,
{
takerFee: new BigNumber(200),
},
);
const firstRemainingFillAmount = new BigNumber(200);
const secondOrder = orderFactory.createOrder(
NULL_ADDRESS,
new BigNumber(200),
NULL_BYTES,
new BigNumber(100),
NULL_BYTES,
NULL_ADDRESS,
{
takerFee: new BigNumber(100),
},
);
const secondRemainingFillAmount = secondOrder.makerAssetAmount;
const signedOrders = _.map([firstOrder, secondOrder], order => {
return {
...order,
signature: NULL_BYTES,
};
});
ordersAndFillableAmounts = {
orders: signedOrders,
remainingFillableMakerAssetAmounts: [firstRemainingFillAmount, secondRemainingFillAmount],
};
const feeOrder = orderFactory.createOrder(
NULL_ADDRESS,
new BigNumber(100),
NULL_BYTES,
new BigNumber(100),
NULL_BYTES,
NULL_ADDRESS,
);
const signedFeeOrder = {
...feeOrder,
signature: NULL_BYTES,
};
feeOrdersAndFillableAmounts = {
orders: [signedFeeOrder],
remainingFillableMakerAssetAmounts: [signedFeeOrder.makerAssetAmount],
};
});
it('should throw if not enough maker asset liquidity', () => {
// we have 400 makerAsset units available to fill but attempt to calculate a quote for 500 makerAsset units
expect(() =>
buyQuoteCalculator.calculate(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
new BigNumber(500),
0,
0,
),
).to.throw(AssetBuyerError.InsufficientAssetLiquidity);
});
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(() =>
buyQuoteCalculator.calculate(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
new BigNumber(300),
0,
0,
),
).to.throw(AssetBuyerError.InsufficientZrxLiquidity);
});
it('calculates a correct buyQuote', () => {
// we request 200 makerAsset 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 assetBuyAmount = new BigNumber(200);
const feePercentage = 0.02;
const slippagePercentage = 0;
const buyQuote = buyQuoteCalculator.calculate(
ordersAndFillableAmounts,
feeOrdersAndFillableAmounts,
assetBuyAmount,
feePercentage,
slippagePercentage,
);
// test if orders are correct
expect(buyQuote.orders).to.deep.equal([ordersAndFillableAmounts.orders[0]]);
expect(buyQuote.feeOrders).to.deep.equal([feeOrdersAndFillableAmounts.orders[0]]);
// test if rates are correct
const expectedMinEthToFill = new BigNumber(150);
const expectedMinRate = assetBuyAmount.div(expectedMinEthToFill.mul(feePercentage + 1));
expect(buyQuote.minRate).to.bignumber.equal(expectedMinRate);
// because we have no slippage protection, minRate is equal to maxRate
expect(buyQuote.maxRate).to.bignumber.equal(expectedMinRate);
// test if feePercentage gets passed through
expect(buyQuote.feePercentage).to.equal(feePercentage);
});
});
});