Return unoptimized quote in SwapQuote (#62)
* return unoptimized alternatives in SwapQuote * refactor: dedupe
This commit is contained in:
parent
475b608338
commit
db81a94adb
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -191,6 +191,8 @@ export interface SwapQuoteBase {
|
||||
worstCaseQuoteInfo: SwapQuoteInfo;
|
||||
sourceBreakdown: SwapQuoteOrdersBreakdown;
|
||||
quoteReport?: QuoteReport;
|
||||
unoptimizedQuoteInfo: SwapQuoteInfo;
|
||||
unoptimizedOrders: OptimizedMarketOrder[];
|
||||
isTwoHop: boolean;
|
||||
makerTokenDecimals: number;
|
||||
takerTokenDecimals: number;
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
optimizedOrders,
|
||||
operation,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
opts.gasSchedule,
|
||||
makerTokenDecimals,
|
||||
takerTokenDecimals,
|
||||
quoteReport,
|
||||
)
|
||||
: createSwapQuote(
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
optimizedOrders,
|
||||
operation,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
opts.gasSchedule,
|
||||
makerTokenDecimals,
|
||||
takerTokenDecimals,
|
||||
quoteReport,
|
||||
);
|
||||
const swapQuote = createSwapQuote(
|
||||
result,
|
||||
makerAssetData,
|
||||
takerAssetData,
|
||||
operation,
|
||||
assetFillAmount,
|
||||
gasPrice,
|
||||
opts.gasSchedule,
|
||||
);
|
||||
|
||||
// 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 {
|
||||
|
@ -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,
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user