Add buy_quote_calculator_test
This commit is contained in:
@@ -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];
|
||||
}
|
||||
|
130
packages/asset-buyer/test/buy_quote_calculator_test.ts
Normal file
130
packages/asset-buyer/test/buy_quote_calculator_test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user