Return unoptimized quote in SwapQuote (#62)

* return unoptimized alternatives in SwapQuote

* refactor: dedupe
This commit is contained in:
Xianny 2020-12-01 12:40:48 -08:00 committed by GitHub
parent 475b608338
commit db81a94adb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 139 additions and 114 deletions

View File

@ -84,7 +84,7 @@ export {
export { artifacts } from './artifacts';
export { InsufficientAssetLiquidityError } from './errors';
export { SwapQuoteConsumer } from './quote_consumers/swap_quote_consumer';
export { getSwapMinBuyAmount } from './quote_consumers/utils';
export { getSwapMinBuyAmount, getQuoteInfoMinBuyAmount } from './quote_consumers/utils';
export { SwapQuoter } from './swap_quoter';
export {
AffiliateFee,

View File

@ -1,8 +1,8 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperation, SwapQuote } from '../types';
import { ERC20BridgeSource } from '../utils/market_operation_utils/types';
import { MarketOperation, SwapQuote, SwapQuoteInfo } from '../types';
import { ERC20BridgeSource, OptimizedMarketOrder } from '../utils/market_operation_utils/types';
/**
* Compute the minimum buy token amount for market operations by inferring
@ -31,3 +31,34 @@ export function getSwapMinBuyAmount(quote: SwapQuote): BigNumber {
}
return quote.bestCaseQuoteInfo.makerAssetAmount.times(slipRatio).integerValue(BigNumber.ROUND_DOWN);
}
/**
* Same as `getSwapMinBuyAmount` but operates
* on a single quote info instead of using best and worst case
* Orders must be derived from the same path as the quote info
*/
export function getQuoteInfoMinBuyAmount(
quoteInfo: SwapQuoteInfo,
orders: OptimizedMarketOrder[],
marketOperation: MarketOperation,
): BigNumber {
if (marketOperation === MarketOperation.Buy) {
return quoteInfo.makerAssetAmount;
}
let slipRatio = new BigNumber(1);
// Infer the allowed maker asset slippage from any non-native order.
for (const o of orders) {
if (o.fills.length === 0 || o.fills[0].source === ERC20BridgeSource.Native) {
// No slippage on native orders.
continue;
}
const totalFillMakerAssetAmount = BigNumber.sum(...o.fills.map(f => f.output));
slipRatio = o.fillableMakerAssetAmount.div(totalFillMakerAssetAmount);
break;
}
if (slipRatio.gte(1)) {
// No slippage allowed across all orders.
return quoteInfo.makerAssetAmount;
}
return quoteInfo.makerAssetAmount.times(slipRatio).integerValue(BigNumber.ROUND_DOWN);
}

View File

@ -191,6 +191,8 @@ export interface SwapQuoteBase {
worstCaseQuoteInfo: SwapQuoteInfo;
sourceBreakdown: SwapQuoteOrdersBreakdown;
quoteReport?: QuoteReport;
unoptimizedQuoteInfo: SwapQuoteInfo;
unoptimizedOrders: OptimizedMarketOrder[];
isTwoHop: boolean;
makerTokenDecimals: number;
takerTokenDecimals: number;

View File

@ -26,7 +26,7 @@ import {
createSignedOrdersWithFillableAmounts,
getNativeOrderTokens,
} from './orders';
import { findOptimalPathAsync } from './path_optimizer';
import { fillsToSortedPaths, findOptimalPathAsync } from './path_optimizer';
import { DexOrderSampler, getSampleAmounts } from './sampler';
import { SourceFilters } from './source_filters';
import {
@ -543,6 +543,10 @@ export class MarketOperationUtils {
exchangeProxyOverhead: opts.exchangeProxyOverhead || (() => ZERO_AMOUNT),
};
// Find the unoptimized best rate to calculate savings from optimizer
const unoptimizedPath = fillsToSortedPaths(fills, side, inputAmount, optimizerOpts)[0].collapse(orderOpts);
// Find the optimal path
const optimalPath = await findOptimalPathAsync(side, fills, inputAmount, opts.runLimit, optimizerOpts);
const optimalPathRate = optimalPath ? optimalPath.adjustedRate() : ZERO_AMOUNT;
@ -559,6 +563,7 @@ export class MarketOperationUtils {
sourceFlags: SOURCE_FLAGS[ERC20BridgeSource.MultiHop],
marketSideLiquidity,
adjustedRate: bestTwoHopRate,
unoptimizedPath,
};
}
@ -591,6 +596,7 @@ export class MarketOperationUtils {
sourceFlags: collapsedPath.sourceFlags,
marketSideLiquidity,
adjustedRate: optimalPathRate,
unoptimizedPath,
};
}

View File

@ -21,14 +21,13 @@ export async function findOptimalPathAsync(
runLimit: number = 2 ** 8,
opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
): Promise<Path | undefined> {
const rates = rateBySourcePathId(side, fills, targetInput);
const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts));
// Sort fill arrays by descending adjusted completed rate.
const sortedPaths = paths.sort((a, b) => b.adjustedCompleteRate().comparedTo(a.adjustedCompleteRate()));
const sortedPaths = fillsToSortedPaths(fills, side, targetInput, opts);
if (sortedPaths.length === 0) {
return undefined;
}
let optimalPath = sortedPaths[0];
const rates = rateBySourcePathId(side, fills, targetInput);
for (const [i, path] of sortedPaths.slice(1).entries()) {
optimalPath = mixPaths(side, optimalPath, path, targetInput, runLimit * RUN_LIMIT_DECAY_FACTOR ** i, rates);
// Yield to event loop.
@ -37,6 +36,18 @@ export async function findOptimalPathAsync(
return optimalPath.isComplete() ? optimalPath : undefined;
}
// Sort fill arrays by descending adjusted completed rate.
export function fillsToSortedPaths(
fills: Fill[][],
side: MarketOperation,
targetInput: BigNumber,
opts: PathPenaltyOpts,
): Path[] {
const paths = fills.map(singleSourceFills => Path.create(side, singleSourceFills, targetInput, opts));
const sortedPaths = paths.sort((a, b) => b.adjustedCompleteRate().comparedTo(a.adjustedCompleteRate()));
return sortedPaths;
}
function mixPaths(
side: MarketOperation,
pathA: Path,

View File

@ -6,6 +6,7 @@ import { RfqtFirmQuoteValidator, RfqtRequestOpts, SignedOrderWithFillableAmounts
import { QuoteRequestor } from '../../utils/quote_requestor';
import { QuoteReport } from '../quote_report_generator';
import { CollapsedPath } from './path';
import { SourceFilters } from './source_filters';
/**
@ -343,6 +344,7 @@ export interface OptimizerResult {
liquidityDelivered: CollapsedFill[] | DexSample<MultiHopFillData>;
marketSideLiquidity: MarketSideLiquidity;
adjustedRate: BigNumber;
unoptimizedPath: CollapsedPath;
}
export interface OptimizerResultWithReport extends OptimizerResult {

View File

@ -22,8 +22,8 @@ import {
FillData,
GetMarketOrdersOpts,
OptimizedMarketOrder,
OptimizerResultWithReport,
} from './market_operation_utils/types';
import { QuoteReport } from './quote_report_generator';
import { QuoteFillResult, simulateBestCaseFill, simulateWorstCaseFill } from './quote_simulation';
import { getTokenFromAssetData, isSupportedAssetDataInOrders } from './utils';
@ -98,15 +98,13 @@ export class SwapQuoteCalculator {
if (result) {
const { makerAssetData, takerAssetData } = batchPrunedOrders[i][0];
return createSwapQuote(
result,
makerAssetData,
takerAssetData,
result.optimizedOrders,
operation,
assetFillAmounts[i],
gasPrice,
opts.gasSchedule,
result.marketSideLiquidity.makerTokenDecimals,
result.marketSideLiquidity.takerTokenDecimals,
);
} else {
return undefined;
@ -128,12 +126,6 @@ export class SwapQuoteCalculator {
}
// since prunedOrders do not have fillState, we will add a buffer of fillable orders to consider that some native are orders are partially filled
let optimizedOrders: OptimizedMarketOrder[];
let quoteReport: QuoteReport | undefined;
let sourceFlags: number = 0;
let makerTokenDecimals: number;
let takerTokenDecimals: number;
// Scale fees by gas price.
const _opts: GetMarketOrdersOpts = {
...opts,
@ -148,60 +140,94 @@ export class SwapQuoteCalculator {
? await this._marketOperationUtils.getMarketBuyOrdersAsync(prunedOrders, assetFillAmount, _opts)
: await this._marketOperationUtils.getMarketSellOrdersAsync(prunedOrders, assetFillAmount, _opts);
optimizedOrders = result.optimizedOrders;
quoteReport = result.quoteReport;
sourceFlags = result.sourceFlags;
makerTokenDecimals = result.marketSideLiquidity.makerTokenDecimals;
takerTokenDecimals = result.marketSideLiquidity.takerTokenDecimals;
// assetData information for the result
const { makerAssetData, takerAssetData } = prunedOrders[0];
const swapQuote =
sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop]
? createTwoHopSwapQuote(
const swapQuote = createSwapQuote(
result,
makerAssetData,
takerAssetData,
optimizedOrders,
operation,
assetFillAmount,
gasPrice,
opts.gasSchedule,
makerTokenDecimals,
takerTokenDecimals,
quoteReport,
)
: createSwapQuote(
makerAssetData,
takerAssetData,
optimizedOrders,
operation,
assetFillAmount,
gasPrice,
opts.gasSchedule,
makerTokenDecimals,
takerTokenDecimals,
quoteReport,
);
// Use the raw gas, not scaled by gas price
const exchangeProxyOverhead = opts.exchangeProxyOverhead(sourceFlags).toNumber();
const exchangeProxyOverhead = opts.exchangeProxyOverhead(result.sourceFlags).toNumber();
swapQuote.bestCaseQuoteInfo.gas += exchangeProxyOverhead;
swapQuote.worstCaseQuoteInfo.gas += exchangeProxyOverhead;
swapQuote.unoptimizedQuoteInfo.gas += exchangeProxyOverhead;
return swapQuote;
}
}
function createSwapQuote(
optimizerResult: OptimizerResultWithReport,
makerAssetData: string,
takerAssetData: string,
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
): SwapQuote {
const { optimizedOrders, quoteReport, sourceFlags, unoptimizedPath } = optimizerResult;
const isTwoHop = sourceFlags === SOURCE_FLAGS[ERC20BridgeSource.MultiHop];
// Calculate quote info
const { bestCaseQuoteInfo, worstCaseQuoteInfo, sourceBreakdown } = isTwoHop
? calculateTwoHopQuoteInfo(optimizedOrders, operation, gasSchedule)
: calculateQuoteInfo(optimizedOrders, operation, assetFillAmount, gasPrice, gasSchedule);
// Calculate the unoptimised alternative
const unoptimizedFillResult = simulateBestCaseFill({
gasPrice,
orders: unoptimizedPath.orders,
side: operation,
fillAmount: assetFillAmount,
opts: { gasSchedule },
});
const unoptimizedQuoteInfo = fillResultsToQuoteInfo(unoptimizedFillResult);
// Put together the swap quote
const { makerTokenDecimals, takerTokenDecimals } = optimizerResult.marketSideLiquidity;
const swapQuote = {
makerAssetData,
takerAssetData,
gasPrice,
orders: optimizedOrders,
bestCaseQuoteInfo,
worstCaseQuoteInfo,
unoptimizedQuoteInfo,
unoptimizedOrders: unoptimizedPath.orders,
sourceBreakdown,
makerTokenDecimals,
takerTokenDecimals,
quoteReport,
isTwoHop,
};
if (operation === MarketOperation.Buy) {
return {
...swapQuote,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
};
} else {
return {
...swapQuote,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
};
}
}
function calculateQuoteInfo(
optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
makerTokenDecimals: number,
takerTokenDecimals: number,
quoteReport?: QuoteReport,
): SwapQuote {
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
const bestCaseFillResult = simulateBestCaseFill({
gasPrice,
orders: optimizedOrders,
@ -218,49 +244,18 @@ function createSwapQuote(
opts: { gasSchedule },
});
const quoteBase = {
takerAssetData,
makerAssetData,
gasPrice,
return {
bestCaseQuoteInfo: fillResultsToQuoteInfo(bestCaseFillResult),
worstCaseQuoteInfo: fillResultsToQuoteInfo(worstCaseFillResult),
sourceBreakdown: getSwapQuoteOrdersBreakdown(bestCaseFillResult.fillAmountBySource),
orders: optimizedOrders,
quoteReport,
isTwoHop: false,
};
if (operation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
makerTokenDecimals,
takerTokenDecimals,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
makerTokenDecimals,
takerTokenDecimals,
};
}
}
function createTwoHopSwapQuote(
makerAssetData: string,
takerAssetData: string,
function calculateTwoHopQuoteInfo(
optimizedOrders: OptimizedMarketOrder[],
operation: MarketOperation,
assetFillAmount: BigNumber,
gasPrice: BigNumber,
gasSchedule: FeeSchedule,
makerTokenDecimals: number,
takerTokenDecimals: number,
quoteReport?: QuoteReport,
): SwapQuote {
): { bestCaseQuoteInfo: SwapQuoteInfo; worstCaseQuoteInfo: SwapQuoteInfo; sourceBreakdown: SwapQuoteOrdersBreakdown } {
const [firstHopOrder, secondHopOrder] = optimizedOrders;
const [firstHopFill] = firstHopOrder.fills;
const [secondHopFill] = secondHopOrder.fills;
@ -271,10 +266,7 @@ function createTwoHopSwapQuote(
}),
).toNumber();
const quoteBase = {
takerAssetData,
makerAssetData,
gasPrice,
return {
bestCaseQuoteInfo: {
makerAssetAmount: operation === MarketOperation.Sell ? secondHopFill.output : secondHopFill.input,
takerAssetAmount: operation === MarketOperation.Sell ? firstHopFill.input : firstHopFill.output,
@ -298,28 +290,7 @@ function createTwoHopSwapQuote(
hops: [firstHopFill.source, secondHopFill.source],
},
},
orders: optimizedOrders,
quoteReport,
isTwoHop: true,
};
if (operation === MarketOperation.Buy) {
return {
...quoteBase,
type: MarketOperation.Buy,
makerAssetFillAmount: assetFillAmount,
makerTokenDecimals,
takerTokenDecimals,
};
} else {
return {
...quoteBase,
type: MarketOperation.Sell,
takerAssetFillAmount: assetFillAmount,
makerTokenDecimals,
takerTokenDecimals,
};
}
}
function getSwapQuoteOrdersBreakdown(fillAmountBySource: { [source: string]: BigNumber }): SwapQuoteOrdersBreakdown {

View File

@ -37,6 +37,8 @@ export async function getFullyFillableSwapQuoteWithNoFeesAsync(
gasPrice,
bestCaseQuoteInfo: quoteInfo,
worstCaseQuoteInfo: quoteInfo,
unoptimizedQuoteInfo: quoteInfo,
unoptimizedOrders: orders.map(order => ({ ...order, fills: [] })),
sourceBreakdown: breakdown,
isTwoHop: false,
};