diff options
Diffstat (limited to 'packages/order-utils')
-rw-r--r-- | packages/order-utils/CHANGELOG.json | 4 | ||||
-rw-r--r-- | packages/order-utils/src/index.ts | 2 | ||||
-rw-r--r-- | packages/order-utils/src/rate_utils.ts | 48 | ||||
-rw-r--r-- | packages/order-utils/src/sorting_utils.ts | 54 | ||||
-rw-r--r-- | packages/order-utils/test/rate_utils_test.ts | 55 | ||||
-rw-r--r-- | packages/order-utils/test/sorting_utils_test.ts | 67 |
6 files changed, 230 insertions, 0 deletions
diff --git a/packages/order-utils/CHANGELOG.json b/packages/order-utils/CHANGELOG.json index a81eb077d..a2dcf13c4 100644 --- a/packages/order-utils/CHANGELOG.json +++ b/packages/order-utils/CHANGELOG.json @@ -18,6 +18,10 @@ }, { "note": "Dependencies updated" + }, + { + "note": "Added rateUtils and sortingUtils", + "pr": 953 } ], "timestamp": 1534210131 diff --git a/packages/order-utils/src/index.ts b/packages/order-utils/src/index.ts index 681fbc904..2b1c92973 100644 --- a/packages/order-utils/src/index.ts +++ b/packages/order-utils/src/index.ts @@ -25,3 +25,5 @@ export { EIP712Utils } from './eip712_utils'; export { OrderValidationUtils } from './order_validation_utils'; export { ExchangeTransferSimulator } from './exchange_transfer_simulator'; export { marketUtils } from './market_utils'; +export { rateUtils } from './rate_utils'; +export { sortingUtils } from './sorting_utils'; diff --git a/packages/order-utils/src/rate_utils.ts b/packages/order-utils/src/rate_utils.ts new file mode 100644 index 000000000..c9ca72c59 --- /dev/null +++ b/packages/order-utils/src/rate_utils.ts @@ -0,0 +1,48 @@ +import { schemas } from '@0xproject/json-schemas'; +import { Order } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; + +import { assert } from './assert'; +import { constants } from './constants'; + +export const rateUtils = { + /** + * Takes an order and calculates the fee adjusted rate (takerAsset/makerAsset) by calculating how much takerAsset + * is required to cover the fees (feeRate * takerFee), adding the takerAssetAmount and dividing by makerAssetAmount + * @param order An object that conforms to the order interface + * @param feeRate The market rate of ZRX denominated in takerAssetAmount + * (ex. feeRate is 0.1 takerAsset/ZRX if it takes 1 unit of takerAsset to buy 10 ZRX) + * Defaults to 0 + * @return The rate (takerAsset/makerAsset) of the order adjusted for fees + */ + getFeeAdjustedRateOfOrder(order: Order, feeRate: BigNumber = constants.ZERO_AMOUNT): BigNumber { + assert.doesConformToSchema('order', order, schemas.orderSchema); + assert.isBigNumber('feeRate', feeRate); + assert.assert( + feeRate.gte(constants.ZERO_AMOUNT), + `Expected feeRate: ${feeRate} to be greater than or equal to 0`, + ); + const takerAssetAmountNeededToPayForFees = order.takerFee.mul(feeRate); + const totalTakerAssetAmount = takerAssetAmountNeededToPayForFees.plus(order.takerAssetAmount); + const rate = totalTakerAssetAmount.div(order.makerAssetAmount); + return rate; + }, + /** + * Takes a fee order (makerAssetData corresponds to ZRX and takerAssetData corresponds to WETH) and calculates + * the fee adjusted rate (WETH/ZRX) by dividing the takerAssetAmount by the makerAmount minus the takerFee + * @param feeOrder An object that conforms to the order interface + * @return The rate (WETH/ZRX) of the fee order adjusted for fees + */ + getFeeAdjustedRateOfFeeOrder(feeOrder: Order): BigNumber { + assert.doesConformToSchema('feeOrder', feeOrder, schemas.orderSchema); + const zrxAmountAfterFees = feeOrder.makerAssetAmount.sub(feeOrder.takerFee); + assert.assert( + zrxAmountAfterFees.greaterThan(constants.ZERO_AMOUNT), + `Expected takerFee: ${JSON.stringify(feeOrder.takerFee)} to be less than makerAssetAmount: ${JSON.stringify( + feeOrder.makerAssetAmount, + )}`, + ); + const rate = feeOrder.takerAssetAmount.div(zrxAmountAfterFees); + return rate; + }, +}; diff --git a/packages/order-utils/src/sorting_utils.ts b/packages/order-utils/src/sorting_utils.ts new file mode 100644 index 000000000..8811bcaf8 --- /dev/null +++ b/packages/order-utils/src/sorting_utils.ts @@ -0,0 +1,54 @@ +import { schemas } from '@0xproject/json-schemas'; +import { Order } from '@0xproject/types'; +import { BigNumber } from '@0xproject/utils'; +import * as _ from 'lodash'; + +import { assert } from './assert'; +import { constants } from './constants'; +import { rateUtils } from './rate_utils'; + +export const sortingUtils = { + /** + * Takes an array of orders and sorts them by takerAsset/makerAsset rate in ascending order (best rate first). + * Adjusts the rate of each order according to the feeRate and takerFee for that order. + * @param orders An array of objects that extend the Order interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @param feeRate The market rate of ZRX denominated in takerAssetAmount + * (ex. feeRate is 0.1 takerAsset/ZRX if it takes 1 unit of takerAsset to buy 10 ZRX) + * Defaults to 0 + * @return The input orders sorted by rate in ascending order + */ + sortOrdersByFeeAdjustedRate<T extends Order>(orders: T[], feeRate: BigNumber = constants.ZERO_AMOUNT): T[] { + assert.doesConformToSchema('orders', orders, schemas.ordersSchema); + assert.isBigNumber('feeRate', feeRate); + const rateCalculator = (order: Order) => rateUtils.getFeeAdjustedRateOfOrder(order, feeRate); + const sortedOrders = sortOrders(orders, rateCalculator); + return sortedOrders; + }, + /** + * Takes an array of fee orders (makerAssetData corresponds to ZRX and takerAssetData corresponds to WETH) + * and sorts them by rate in ascending order (best rate first). Adjusts the rate according to the takerFee. + * @param feeOrders An array of objects that extend the Order interface. All orders should specify ZRX as + * the makerAsset and WETH as the takerAsset. + * @return The input orders sorted by rate in ascending order + */ + sortFeeOrdersByFeeAdjustedRate(feeOrders: Order[]): Order[] { + assert.doesConformToSchema('feeOrders', feeOrders, schemas.ordersSchema); + const rateCalculator = rateUtils.getFeeAdjustedRateOfFeeOrder.bind(rateUtils); + const sortedOrders = sortOrders(feeOrders, rateCalculator); + return sortedOrders; + }, +}; + +type RateCalculator = (order: Order) => BigNumber; + +// takes an array of orders, copies them, and sorts the copy based on the rate definition provided by rateCalculator +function sortOrders<T extends Order>(orders: T[], rateCalculator: RateCalculator): T[] { + const copiedOrders = _.cloneDeep(orders); + copiedOrders.sort((firstOrder, secondOrder) => { + const firstOrderRate = rateCalculator(firstOrder); + const secondOrderRate = rateCalculator(secondOrder); + return firstOrderRate.comparedTo(secondOrderRate); + }); + return copiedOrders; +} diff --git a/packages/order-utils/test/rate_utils_test.ts b/packages/order-utils/test/rate_utils_test.ts new file mode 100644 index 000000000..2e299c209 --- /dev/null +++ b/packages/order-utils/test/rate_utils_test.ts @@ -0,0 +1,55 @@ +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { rateUtils } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('rateUtils', () => { + const testOrder = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(100), + takerFee: new BigNumber(20), + }); + describe('#getFeeAdjustedRateOfOrder', () => { + it('throws when feeRate is less than zero', async () => { + const feeRate = new BigNumber(-1); + expect(() => rateUtils.getFeeAdjustedRateOfOrder(testOrder, feeRate)).to.throw( + 'Expected feeRate: -1 to be greater than or equal to 0', + ); + }); + it('correctly calculates fee adjusted rate when feeRate is provided', async () => { + const feeRate = new BigNumber(2); // ZRX costs 2 units of takerAsset per 1 unit of ZRX + const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfOrder(testOrder, feeRate); + // the order actually takes 100 + (2 * 20) takerAsset units to fill 100 units of makerAsset + expect(feeAdjustedRate).to.bignumber.equal(new BigNumber(1.4)); + }); + it('correctly calculates fee adjusted rate when no feeRate is provided', async () => { + const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfOrder(testOrder); + // because no feeRate was provided we just assume 0 fees + // the order actually takes 100 takerAsset units to fill 100 units of makerAsset + expect(feeAdjustedRate).to.bignumber.equal(new BigNumber(1)); + }); + }); + describe('#getFeeAdjustedRateOfFeeOrder', () => { + it('throws when takerFee exceeds makerAssetAmount', async () => { + const badOrder = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerFee: new BigNumber(101), + }); + expect(() => rateUtils.getFeeAdjustedRateOfFeeOrder(badOrder)).to.throw( + 'Expected takerFee: "101" to be less than makerAssetAmount: "100"', + ); + }); + it('correctly calculates fee adjusted rate', async () => { + const feeAdjustedRate = rateUtils.getFeeAdjustedRateOfFeeOrder(testOrder); + // the order actually takes 100 takerAsset units to fill (100 - 20) units of makerAsset + expect(feeAdjustedRate).to.bignumber.equal(new BigNumber(1.25)); + }); + }); +}); diff --git a/packages/order-utils/test/sorting_utils_test.ts b/packages/order-utils/test/sorting_utils_test.ts new file mode 100644 index 000000000..016432505 --- /dev/null +++ b/packages/order-utils/test/sorting_utils_test.ts @@ -0,0 +1,67 @@ +import { BigNumber } from '@0xproject/utils'; +import * as chai from 'chai'; +import 'mocha'; + +import { sortingUtils } from '../src'; + +import { chaiSetup } from './utils/chai_setup'; +import { testOrderFactory } from './utils/test_order_factory'; + +chaiSetup.configure(); +const expect = chai.expect; + +describe('sortingUtils', () => { + describe('#sortOrdersByFeeAdjustedRate', () => { + const feeRate = new BigNumber(1); // ZRX costs 1 unit of takerAsset per 1 unit of ZRX + // rate: 2 takerAsset / makerAsset + const testOrder1 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(200), + }); + // rate: 1 takerAsset / makerAsset + const testOrder2 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(100), + }); + // rate: 2.5 takerAsset / makerAsset + const testOrder3 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(200), + takerFee: new BigNumber(50), + }); + it('correctly sorts by fee adjusted rate when feeRate is Provided', async () => { + const orders = [testOrder1, testOrder2, testOrder3]; + const sortedOrders = sortingUtils.sortOrdersByFeeAdjustedRate(orders, feeRate); + expect(sortedOrders).to.deep.equal([testOrder2, testOrder1, testOrder3]); + }); + it('correctly sorts by fee adjusted rate when no feeRate is Provided', async () => { + const orders = [testOrder1, testOrder2, testOrder3]; + const sortedOrders = sortingUtils.sortOrdersByFeeAdjustedRate(orders); + expect(sortedOrders).to.deep.equal([testOrder2, testOrder1, testOrder3]); + }); + }); + describe('#sortFeeOrdersByFeeAdjustedRate', () => { + // rate: 200 takerAsset / makerAsset + const testOrder1 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(200), + takerFee: new BigNumber(99), + }); + // rate: 1 takerAsset / makerAsset + const testOrder2 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(100), + }); + // rate: 4 takerAsset / makerAsset + const testOrder3 = testOrderFactory.generateTestSignedOrder({ + makerAssetAmount: new BigNumber(100), + takerAssetAmount: new BigNumber(200), + takerFee: new BigNumber(50), + }); + it('correctly sorts by fee adjusted rate', async () => { + const orders = [testOrder1, testOrder2, testOrder3]; + const sortedOrders = sortingUtils.sortFeeOrdersByFeeAdjustedRate(orders); + expect(sortedOrders).to.deep.equal([testOrder2, testOrder3, testOrder1]); + }); + }); +}); |