Improved ComparisonPrice function (#32)
* separated comparison price function into a new file, accounted for backup orders * removed scratch code * Adjusted documentation, object naming * Refactored comparisonPrice function to use adjusted rate from optimizer, used native order fee schedule to adjust for order fees * Small fixes to function, added unit tests * Adjusted fee calculation for comparisonPrice function * use available OptimalPathRate object * fix lint error in test, separate out fee calculation * Fixed market operation utils test, added additional checks for fee schedule * removed unused dep, prettier
This commit is contained in:
parent
89948b360c
commit
927fe2b58b
@ -0,0 +1,79 @@
|
||||
import { Web3Wrapper } from '@0x/dev-utils';
|
||||
import { BigNumber, logUtils } from '@0x/utils';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { MarketOperation } from '../../types';
|
||||
|
||||
import { COMPARISON_PRICE_DECIMALS } from './constants';
|
||||
import { ComparisonPrice, ERC20BridgeSource, FeeEstimate, FeeSchedule, MarketSideLiquidity } from './types';
|
||||
|
||||
/**
|
||||
* Takes in an optimizer response and returns a price for RFQT MMs to beat
|
||||
* returns the price of the taker asset in terms of the maker asset
|
||||
* So the RFQT MM should aim for a higher price
|
||||
* @param adjustedRate the adjusted rate (accounting for fees) from the optimizer, maker/taker
|
||||
* @param amount the amount specified by the client
|
||||
* @param marketSideLiquidity the results from querying liquidity sources
|
||||
* @param feeSchedule the fee schedule passed to the Optimizer
|
||||
* @return ComparisonPrice object with the prices for RFQ MMs to beat
|
||||
*/
|
||||
export function getComparisonPrices(
|
||||
adjustedRate: BigNumber,
|
||||
amount: BigNumber,
|
||||
marketSideLiquidity: MarketSideLiquidity,
|
||||
feeSchedule: FeeSchedule,
|
||||
): ComparisonPrice {
|
||||
let wholeOrder: BigNumber | undefined;
|
||||
let feeInEth: BigNumber | number;
|
||||
|
||||
// HACK: get the fee penalty of a single 0x native order
|
||||
// The FeeSchedule function takes in a `FillData` object and returns a fee estimate in ETH
|
||||
// We don't have fill data here, we just want the cost of a single native order, so we pass in undefined
|
||||
// This works because the feeSchedule returns a constant for Native orders, this will need
|
||||
// to be tweaked if the feeSchedule for native orders uses the fillData passed in
|
||||
// 2 potential issues: there is no native fee schedule or the fee schedule depends on fill data
|
||||
if (feeSchedule[ERC20BridgeSource.Native] === undefined) {
|
||||
logUtils.warn('ComparisonPrice function did not find native order fee schedule');
|
||||
|
||||
return { wholeOrder };
|
||||
} else {
|
||||
try {
|
||||
feeInEth = new BigNumber((feeSchedule[ERC20BridgeSource.Native] as FeeEstimate)(undefined));
|
||||
} catch {
|
||||
logUtils.warn('Native order fee schedule requires fill data');
|
||||
|
||||
return { wholeOrder };
|
||||
}
|
||||
}
|
||||
|
||||
// Calc native order fee penalty in output unit (maker units for sells, taker unit for buys)
|
||||
const feePenalty = !marketSideLiquidity.ethToOutputRate.isZero()
|
||||
? marketSideLiquidity.ethToOutputRate.times(feeInEth)
|
||||
: // if it's a sell, the input token is the taker token
|
||||
marketSideLiquidity.ethToInputRate
|
||||
.times(feeInEth)
|
||||
.times(marketSideLiquidity.side === MarketOperation.Sell ? adjustedRate : adjustedRate.pow(-1));
|
||||
|
||||
// the adjusted rate is defined as maker/taker
|
||||
// input is the taker token for sells, input is the maker token for buys
|
||||
const orderMakerAmount =
|
||||
marketSideLiquidity.side === MarketOperation.Sell ? adjustedRate.times(amount).plus(feePenalty) : amount;
|
||||
const orderTakerAmount =
|
||||
marketSideLiquidity.side === MarketOperation.Sell ? amount : amount.dividedBy(adjustedRate).minus(feePenalty);
|
||||
|
||||
if (orderTakerAmount.gt(0) && orderMakerAmount.gt(0)) {
|
||||
const optimalMakerUnitAmount = Web3Wrapper.toUnitAmount(
|
||||
// round up maker amount -- err to giving more competitive price
|
||||
orderMakerAmount.integerValue(BigNumber.ROUND_UP),
|
||||
marketSideLiquidity.makerTokenDecimals,
|
||||
);
|
||||
const optimalTakerUnitAmount = Web3Wrapper.toUnitAmount(
|
||||
// round down taker amount -- err to giving more competitive price
|
||||
orderTakerAmount.integerValue(BigNumber.ROUND_DOWN),
|
||||
marketSideLiquidity.takerTokenDecimals,
|
||||
);
|
||||
wholeOrder = optimalMakerUnitAmount.div(optimalTakerUnitAmount).decimalPlaces(COMPARISON_PRICE_DECIMALS);
|
||||
}
|
||||
|
||||
return { wholeOrder };
|
||||
}
|
@ -375,7 +375,7 @@ export const ONE_HOUR_IN_SECONDS = 60 * 60;
|
||||
export const ONE_SECOND_MS = 1000;
|
||||
export const NULL_BYTES = '0x';
|
||||
export const NULL_ADDRESS = '0x0000000000000000000000000000000000000000';
|
||||
export const COMPARISON_PRICE_DECIMALS = 5;
|
||||
export const COMPARISON_PRICE_DECIMALS = 10;
|
||||
|
||||
const EMPTY_BRIDGE_ADDRESSES: BridgeContractAddresses = {
|
||||
uniswapBridge: NULL_ADDRESS,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { RFQTIndicativeQuote } from '@0x/quote-server';
|
||||
import { SignedOrder } from '@0x/types';
|
||||
import { BigNumber, NULL_ADDRESS } from '@0x/utils';
|
||||
import { Web3Wrapper } from '@0x/web3-wrapper';
|
||||
import * as _ from 'lodash';
|
||||
|
||||
import { AssetSwapperContractAddresses, MarketOperation } from '../../types';
|
||||
@ -9,9 +8,9 @@ import { QuoteRequestor } from '../quote_requestor';
|
||||
import { getPriceAwareRFQRolloutFlags } from '../utils';
|
||||
|
||||
import { generateQuoteReport, QuoteReport } from './../quote_report_generator';
|
||||
import { getComparisonPrices } from './comparison_price';
|
||||
import {
|
||||
BUY_SOURCE_FILTER,
|
||||
COMPARISON_PRICE_DECIMALS,
|
||||
DEFAULT_GET_MARKET_ORDERS_OPTS,
|
||||
FEE_QUOTE_SOURCES,
|
||||
ONE_ETHER,
|
||||
@ -559,6 +558,7 @@ export class MarketOperationUtils {
|
||||
liquidityDelivered: bestTwoHopQuote,
|
||||
sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
|
||||
marketSideLiquidity,
|
||||
adjustedRate: bestTwoHopRate,
|
||||
};
|
||||
}
|
||||
|
||||
@ -584,11 +584,13 @@ export class MarketOperationUtils {
|
||||
}
|
||||
}
|
||||
const collapsedPath = optimalPath.collapse(orderOpts);
|
||||
|
||||
return {
|
||||
optimizedOrders: collapsedPath.orders,
|
||||
liquidityDelivered: collapsedPath.collapsedFills as CollapsedFill[],
|
||||
sourceFlags: collapsedPath.sourceFlags,
|
||||
marketSideLiquidity,
|
||||
adjustedRate: optimalPathRate,
|
||||
};
|
||||
}
|
||||
|
||||
@ -627,30 +629,17 @@ export class MarketOperationUtils {
|
||||
}
|
||||
|
||||
// If RFQ liquidity is enabled, make a request to check RFQ liquidity
|
||||
let comparisonPrice: BigNumber | undefined;
|
||||
let wholeOrderPrice: BigNumber | undefined;
|
||||
const { rfqt } = _opts;
|
||||
if (rfqt && rfqt.quoteRequestor && marketSideLiquidity.quoteSourceFilters.isAllowed(ERC20BridgeSource.Native)) {
|
||||
// Calculate a suggested price. For now, this is simply the overall price of the aggregation.
|
||||
if (optimizerResult) {
|
||||
const totalMakerAmount = BigNumber.sum(
|
||||
...optimizerResult.optimizedOrders.map(order => order.makerAssetAmount),
|
||||
);
|
||||
const totalTakerAmount = BigNumber.sum(
|
||||
...optimizerResult.optimizedOrders.map(order => order.takerAssetAmount),
|
||||
);
|
||||
if (totalMakerAmount.gt(0)) {
|
||||
const totalMakerAmountUnitAmount = Web3Wrapper.toUnitAmount(
|
||||
totalMakerAmount,
|
||||
marketSideLiquidity.makerTokenDecimals,
|
||||
);
|
||||
const totalTakerAmountUnitAmount = Web3Wrapper.toUnitAmount(
|
||||
totalTakerAmount,
|
||||
marketSideLiquidity.takerTokenDecimals,
|
||||
);
|
||||
comparisonPrice = totalMakerAmountUnitAmount
|
||||
.div(totalTakerAmountUnitAmount)
|
||||
.decimalPlaces(COMPARISON_PRICE_DECIMALS);
|
||||
}
|
||||
wholeOrderPrice = getComparisonPrices(
|
||||
optimizerResult.adjustedRate,
|
||||
amount,
|
||||
marketSideLiquidity,
|
||||
_opts.feeSchedule,
|
||||
).wholeOrder;
|
||||
}
|
||||
|
||||
const { isFirmPriceAwareEnabled, isIndicativePriceAwareEnabled } = getPriceAwareRFQRolloutFlags(
|
||||
@ -664,7 +653,7 @@ export class MarketOperationUtils {
|
||||
nativeOrders[0].takerAssetData,
|
||||
side,
|
||||
amount,
|
||||
comparisonPrice,
|
||||
wholeOrderPrice,
|
||||
_opts,
|
||||
);
|
||||
// Re-run optimizer with the new indicative quote
|
||||
@ -690,7 +679,7 @@ export class MarketOperationUtils {
|
||||
nativeOrders[0].takerAssetData,
|
||||
amount,
|
||||
side,
|
||||
comparisonPrice,
|
||||
wholeOrderPrice,
|
||||
rfqt,
|
||||
);
|
||||
if (firmQuotes.length > 0) {
|
||||
@ -731,7 +720,7 @@ export class MarketOperationUtils {
|
||||
_opts.rfqt ? _opts.rfqt.quoteRequestor : undefined,
|
||||
marketSideLiquidity,
|
||||
optimizerResult,
|
||||
comparisonPrice,
|
||||
wholeOrderPrice,
|
||||
);
|
||||
}
|
||||
return { ...optimizerResult, quoteReport };
|
||||
|
@ -340,6 +340,7 @@ export interface OptimizerResult {
|
||||
sourceFlags: number;
|
||||
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
|
||||
marketSideLiquidity: MarketSideLiquidity;
|
||||
adjustedRate: BigNumber;
|
||||
}
|
||||
|
||||
export interface OptimizerResultWithReport extends OptimizerResult {
|
||||
@ -391,3 +392,7 @@ export interface GenerateOptimizedOrdersOpts {
|
||||
allowFallback?: boolean;
|
||||
shouldBatchBridgeOrders?: boolean;
|
||||
}
|
||||
|
||||
export interface ComparisonPrice {
|
||||
wholeOrder: BigNumber | undefined;
|
||||
}
|
||||
|
122
packages/asset-swapper/test/comparison_price_test.ts
Normal file
122
packages/asset-swapper/test/comparison_price_test.ts
Normal file
@ -0,0 +1,122 @@
|
||||
// tslint:disable:custom-no-magic-numbers
|
||||
import { BigNumber } from '@0x/utils';
|
||||
import * as chai from 'chai';
|
||||
import * as _ from 'lodash';
|
||||
import 'mocha';
|
||||
|
||||
import { MarketOperation } from '../src/types';
|
||||
import { getComparisonPrices } from '../src/utils/market_operation_utils/comparison_price';
|
||||
import { SourceFilters } from '../src/utils/market_operation_utils/source_filters';
|
||||
import { DexSample, ERC20BridgeSource, MarketSideLiquidity } from '../src/utils/market_operation_utils/types';
|
||||
|
||||
import { chaiSetup } from './utils/chai_setup';
|
||||
|
||||
chaiSetup.configure();
|
||||
const expect = chai.expect;
|
||||
|
||||
const DAI_TOKEN = '0x6b175474e89094c44da98b954eedeac495271d0f';
|
||||
const ETH_TOKEN = '0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee';
|
||||
const GAS_PRICE = new BigNumber(50e9); // 50 gwei
|
||||
const NATIVE_ORDER_FEE = new BigNumber(220e3); // 220K gas
|
||||
|
||||
// DEX samples to fill in MarketSideLiquidity
|
||||
const kyberSample1: DexSample = {
|
||||
source: ERC20BridgeSource.Kyber,
|
||||
input: new BigNumber(10000),
|
||||
output: new BigNumber(10001),
|
||||
fillData: {},
|
||||
};
|
||||
const uniswapSample1: DexSample = {
|
||||
source: ERC20BridgeSource.UniswapV2,
|
||||
input: new BigNumber(10003),
|
||||
output: new BigNumber(10004),
|
||||
fillData: {},
|
||||
};
|
||||
const dexQuotes: DexSample[] = [kyberSample1, uniswapSample1];
|
||||
|
||||
const feeSchedule = {
|
||||
[ERC20BridgeSource.Native]: _.constant(GAS_PRICE.times(NATIVE_ORDER_FEE)),
|
||||
};
|
||||
|
||||
const buyMarketSideLiquidity: MarketSideLiquidity = {
|
||||
// needed params
|
||||
ethToOutputRate: new BigNumber(500),
|
||||
ethToInputRate: new BigNumber(1),
|
||||
side: MarketOperation.Buy,
|
||||
makerTokenDecimals: 18,
|
||||
takerTokenDecimals: 18,
|
||||
// extra
|
||||
inputAmount: new BigNumber(0),
|
||||
inputToken: ETH_TOKEN,
|
||||
outputToken: DAI_TOKEN,
|
||||
dexQuotes: [dexQuotes],
|
||||
nativeOrders: [],
|
||||
orderFillableAmounts: [],
|
||||
twoHopQuotes: [],
|
||||
rfqtIndicativeQuotes: [],
|
||||
quoteSourceFilters: new SourceFilters(),
|
||||
};
|
||||
|
||||
const sellMarketSideLiquidity: MarketSideLiquidity = {
|
||||
// needed params
|
||||
ethToOutputRate: new BigNumber(500),
|
||||
ethToInputRate: new BigNumber(1),
|
||||
side: MarketOperation.Sell,
|
||||
makerTokenDecimals: 18,
|
||||
takerTokenDecimals: 18,
|
||||
// extra
|
||||
inputAmount: new BigNumber(0),
|
||||
inputToken: ETH_TOKEN,
|
||||
outputToken: DAI_TOKEN,
|
||||
dexQuotes: [dexQuotes],
|
||||
nativeOrders: [],
|
||||
orderFillableAmounts: [],
|
||||
twoHopQuotes: [],
|
||||
rfqtIndicativeQuotes: [],
|
||||
quoteSourceFilters: new SourceFilters(),
|
||||
};
|
||||
|
||||
describe('getComparisonPrices', async () => {
|
||||
it('should create a proper comparison price for Sells', () => {
|
||||
// test selling 10 ETH for DAI
|
||||
// here, ETH is the input token
|
||||
// and DAI is the output token
|
||||
const AMOUNT = new BigNumber(10 * 1e18);
|
||||
// raw maker over taker rate, let's say is 500 flat
|
||||
const adjustedRate = new BigNumber(500);
|
||||
|
||||
const comparisonPrices = getComparisonPrices(adjustedRate, AMOUNT, sellMarketSideLiquidity, feeSchedule);
|
||||
|
||||
// expected outcome
|
||||
const EXPECTED_PRICE = new BigNumber('500.55');
|
||||
|
||||
expect(comparisonPrices.wholeOrder).to.deep.eq(EXPECTED_PRICE);
|
||||
});
|
||||
it('should create a proper comparison price for Buys', () => {
|
||||
// test buying 10 ETH with DAI
|
||||
// here, ETH is the input token
|
||||
// and DAI is the output token (now from the maker's perspective)
|
||||
const AMOUNT = new BigNumber(10 * 1e18);
|
||||
|
||||
// raw maker over taker rate, let's say is ETH/DAI rate is 500 flat
|
||||
const adjustedRate = new BigNumber(1).dividedBy(new BigNumber(500));
|
||||
|
||||
const comparisonPrices = getComparisonPrices(adjustedRate, AMOUNT, buyMarketSideLiquidity, feeSchedule);
|
||||
|
||||
// expected outcome
|
||||
const EXPECTED_PRICE = new BigNumber('0.0020022024');
|
||||
|
||||
expect(comparisonPrices.wholeOrder).to.deep.eq(EXPECTED_PRICE);
|
||||
});
|
||||
it('should not return a price if takerAmount is < 0', () => {
|
||||
// test selling 0.00001 ETH for DAI
|
||||
// this will result in a negative comparison price, but here we should return undefined
|
||||
const AMOUNT = new BigNumber(0.00001 * 1e18);
|
||||
// raw maker over taker rate, let's say is 500 flat
|
||||
const adjustedRate = new BigNumber(500);
|
||||
|
||||
const comparisonPrices = getComparisonPrices(adjustedRate, AMOUNT, sellMarketSideLiquidity, feeSchedule);
|
||||
|
||||
expect(comparisonPrices.wholeOrder === undefined);
|
||||
});
|
||||
});
|
@ -731,6 +731,11 @@ describe('MarketOperationUtils tests', () => {
|
||||
const mockedQuoteRequestor = TypeMoq.Mock.ofType(QuoteRequestor, TypeMoq.MockBehavior.Loose, false, {});
|
||||
|
||||
let requestedComparisonPrice: BigNumber | undefined;
|
||||
|
||||
// to get a comparisonPrice, you need a feeschedule for a native order
|
||||
const feeSchedule = {
|
||||
[ERC20BridgeSource.Native]: _.constant(new BigNumber(1)),
|
||||
};
|
||||
mockedQuoteRequestor
|
||||
.setup(mqr =>
|
||||
mqr.requestRfqtFirmQuotesAsync(
|
||||
@ -811,6 +816,7 @@ describe('MarketOperationUtils tests', () => {
|
||||
Web3Wrapper.toBaseUnitAmount(1, 18),
|
||||
{
|
||||
...DEFAULT_OPTS,
|
||||
feeSchedule,
|
||||
rfqt: {
|
||||
isIndicative: false,
|
||||
apiKey: 'foo',
|
||||
|
Loading…
x
Reference in New Issue
Block a user