feat: Udate neon-router and use router estimated output amount (#354)

* feat: use Rust router estimated output amount when possible

* fix: use strings for sample ids, and increase samples in the rust router

* fix: remove unnecessary interpolation of out of range values

* fix: don't recalculate sampled dist sum in a loop

* fix: use 14 samples for rust router to work around interpolation issues

* fix: unintentional logic change

* fix: remove local dev plotting param from route fn call

* feat: make neon-router number of samples configurable

* chore: bump to newly published neon-router version

* fix: handle insufficient liquidity at all requested sources

* chore: update asset-swapper changelog
This commit is contained in:
Kim Persson 2021-12-29 12:08:24 +01:00 committed by GitHub
parent 8ae9f59f20
commit 285f98e9e9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 69 additions and 54 deletions

View File

@ -1,4 +1,13 @@
[
{
"version": "16.44.0",
"changes": [
{
"note": "Update neon-router and use router estimated output amount",
"pr": 354
}
]
},
{
"version": "16.43.0",
"changes": [

View File

@ -66,7 +66,7 @@
"@0x/contracts-zero-ex": "^0.30.1",
"@0x/dev-utils": "^4.2.9",
"@0x/json-schemas": "^6.3.0",
"@0x/neon-router": "^0.2.1",
"@0x/neon-router": "^0.3.1",
"@0x/protocol-utils": "^1.10.1",
"@0x/quote-server": "^6.0.6",
"@0x/types": "^3.3.4",

View File

@ -2103,4 +2103,5 @@ export const DEFAULT_GET_MARKET_ORDERS_OPTS: Omit<GetMarketOrdersOpts, 'gasPrice
shouldGenerateQuoteReport: true,
shouldIncludePriceComparisonsReport: false,
tokenAdjacencyGraph: { default: [] },
neonRouterNumSamples: 14,
};

View File

@ -443,6 +443,7 @@ export class MarketOperationUtils {
feeSchedule: _opts.feeSchedule,
allowFallback: _opts.allowFallback,
gasPrice: _opts.gasPrice,
neonRouterNumSamples: _opts.neonRouterNumSamples,
},
);
return optimizerResult;
@ -531,6 +532,7 @@ export class MarketOperationUtils {
penaltyOpts,
opts.feeSchedule,
this._sampler.chainId,
opts.neonRouterNumSamples,
);
} else {
optimalPath = await findOptimalPathJSAsync(side, fills, inputAmount, opts.runLimit, penaltyOpts);
@ -596,6 +598,7 @@ export class MarketOperationUtils {
allowFallback: _opts.allowFallback,
exchangeProxyOverhead: _opts.exchangeProxyOverhead,
gasPrice: _opts.gasPrice,
neonRouterNumSamples: _opts.neonRouterNumSamples,
};
if (nativeOrders.length === 0) {
@ -806,6 +809,7 @@ export class MarketOperationUtils {
sturdyPenaltyOpts,
opts.feeSchedule,
this._sampler.chainId,
opts.neonRouterNumSamples,
);
} else {
const sturdyFills = fills.filter(p => p.length > 0 && !fragileSources.includes(p[0].source));

View File

@ -17,7 +17,6 @@ import { DexSample, ERC20BridgeSource, FeeSchedule, Fill, FillData } from './typ
// tslint:disable: prefer-for-of custom-no-magic-numbers completed-docs no-bitwise
const RUN_LIMIT_DECAY_FACTOR = 0.5;
const RUST_ROUTER_NUM_SAMPLES = 200;
const FILL_QUOTE_TRANSFORMER_GAS_OVERHEAD = new BigNumber(150e3);
// NOTE: The Rust router will panic with less than 3 samples
const MIN_NUM_SAMPLE_INPUTS = 3;
@ -69,21 +68,6 @@ function calculateOuputFee(
}
}
// Use linear interpolation to approximate the output
// at a certain input somewhere between the two samples
// See https://en.wikipedia.org/wiki/Linear_interpolation
const interpolateOutputFromSamples = (
left: { input: BigNumber; output: BigNumber },
right: { input: BigNumber; output: BigNumber },
targetInput: BigNumber,
): BigNumber =>
left.output.plus(
right.output
.minus(left.output)
.dividedBy(right.input.minus(left.input))
.times(targetInput.minus(left.input)),
);
function findRoutesAndCreateOptimalPath(
side: MarketOperation,
samples: DexSample[][],
@ -91,21 +75,10 @@ function findRoutesAndCreateOptimalPath(
input: BigNumber,
opts: PathPenaltyOpts,
fees: FeeSchedule,
neonRouterNumSamples: number,
): Path | undefined {
const createFill = (sample: DexSample) =>
dexSamplesToFills(side, [sample], opts.outputAmountPerEth, opts.inputAmountPerEth, fees)[0];
// Track sample id's to integers (required by rust router)
const sampleIdLookup: { [key: string]: number } = {};
let sampleIdCounter = 0;
const sampleToId = (source: ERC20BridgeSource, index: number): number => {
const key = `${source}-${index}`;
if (sampleIdLookup[key]) {
return sampleIdLookup[key];
} else {
sampleIdLookup[key] = ++sampleIdCounter;
return sampleIdLookup[key];
}
};
const samplesAndNativeOrdersWithResults: Array<DexSample[] | NativeOrderWithFillableAmounts[]> = [];
const serializedPaths: SerializedPath[] = [];
@ -131,7 +104,7 @@ function findRoutesAndCreateOptimalPath(
// TODO(kimpers): Do we need to handle 0 entries, from eg Kyber?
const serializedPath = singleSourceSamplesWithOutput.reduce<SerializedPath>(
(memo, sample, sampleIdx) => {
memo.ids.push(sampleToId(sample.source, sampleIdx));
memo.ids.push(`${sample.source}-${serializedPaths.length}-${sampleIdx}`);
memo.inputs.push(sample.input.integerValue().toNumber());
memo.outputs.push(sample.output.integerValue().toNumber());
memo.outputFees.push(
@ -188,7 +161,7 @@ function findRoutesAndCreateOptimalPath(
.toNumber();
const outputFees = [fee, fee, fee];
// NOTE: ids can be the same for all fake samples
const id = sampleToId(ERC20BridgeSource.Native, idx);
const id = `${ERC20BridgeSource.Native}-${serializedPaths.length}-${idx}`;
const ids = [id, id, id];
const serializedPath: SerializedPath = {
@ -214,7 +187,9 @@ function findRoutesAndCreateOptimalPath(
const before = performance.now();
const allSourcesRustRoute = new Float64Array(rustArgs.pathsIn.length);
route(rustArgs, allSourcesRustRoute, RUST_ROUTER_NUM_SAMPLES);
const strategySourcesOutputAmounts = new Float64Array(rustArgs.pathsIn.length);
route(rustArgs, allSourcesRustRoute, strategySourcesOutputAmounts, neonRouterNumSamples);
DEFAULT_INFO_LOGGER(
{ router: 'neon-router', performanceMs: performance.now() - before, type: 'real' },
'Rust router real routing performance',
@ -224,18 +199,25 @@ function findRoutesAndCreateOptimalPath(
rustArgs.pathsIn.length === allSourcesRustRoute.length,
'different number of sources in the Router output than the input',
);
assert.assert(
rustArgs.pathsIn.length === strategySourcesOutputAmounts.length,
'different number of sources in the Router output amounts results than the input',
);
const routesAndSamples = _.zip(allSourcesRustRoute, samplesAndNativeOrdersWithResults);
const routesAndSamplesAndOutputs = _.zip(
allSourcesRustRoute,
samplesAndNativeOrdersWithResults,
strategySourcesOutputAmounts,
);
const adjustedFills: Fill[] = [];
const totalRoutedAmount = BigNumber.sum(...allSourcesRustRoute);
const scale = input.dividedBy(totalRoutedAmount);
for (const [routeInput, routeSamplesAndNativeOrders] of routesAndSamples) {
if (!routeInput || !routeSamplesAndNativeOrders) {
for (const [routeInput, routeSamplesAndNativeOrders, outputAmount] of routesAndSamplesAndOutputs) {
if (!routeInput || !routeSamplesAndNativeOrders || !outputAmount || !Number.isFinite(outputAmount)) {
continue;
}
// TODO(kimpers): [TKR-241] amounts are sometimes clipped in the router due to precisions loss for number/f64
// TODO(kimpers): [TKR-241] amounts are sometimes clipped in the router due to precision loss for number/f64
// we can work around it by scaling it and rounding up. However now we end up with a total amount of a couple base units too much
const rustInputAdjusted = BigNumber.min(
new BigNumber(routeInput).multipliedBy(scale).integerValue(BigNumber.ROUND_CEIL),
@ -270,22 +252,13 @@ function findRoutesAndCreateOptimalPath(
fill = createFill(routeSamples[0]);
}
if (rustInputAdjusted.isGreaterThan(routeSamples[k].input)) {
// Between here and the previous fill
// HACK: Use the midpoint between the two
const left = routeSamples[k];
const right = routeSamples[k + 1];
if (left && right) {
// Approximate how much output we get for the input with the surrounding samples
const interpolatedOutput = interpolateOutputFromSamples(
left,
right,
rustInputAdjusted,
).decimalPlaces(0, side === MarketOperation.Sell ? BigNumber.ROUND_FLOOR : BigNumber.ROUND_CEIL);
fill = createFill({
...right, // default to the greater (for gas used)
input: rustInputAdjusted,
output: interpolatedOutput,
output: new BigNumber(outputAmount),
});
} else {
assert.assert(Boolean(left || right), 'No valid sample to use');
@ -295,6 +268,7 @@ function findRoutesAndCreateOptimalPath(
}
}
// TODO(kimpers): remove once we have solved the rounding/precision loss issues in the Rust router
const scaleOutput = (output: BigNumber) =>
output
.dividedBy(fill.input)
@ -310,6 +284,10 @@ function findRoutesAndCreateOptimalPath(
});
}
if (adjustedFills.length === 0) {
return undefined;
}
const pathFromRustInputs = Path.create(side, adjustedFills, input);
return pathFromRustInputs;
@ -323,6 +301,7 @@ export function findOptimalRustPathFromSamples(
opts: PathPenaltyOpts,
fees: FeeSchedule,
chainId: ChainId,
neonRouterNumSamples: number,
): Path | undefined {
const before = performance.now();
const logPerformance = () =>
@ -331,7 +310,15 @@ export function findOptimalRustPathFromSamples(
'Rust router total routing performance',
);
const allSourcesPath = findRoutesAndCreateOptimalPath(side, samples, nativeOrders, input, opts, fees);
const allSourcesPath = findRoutesAndCreateOptimalPath(
side,
samples,
nativeOrders,
input,
opts,
fees,
neonRouterNumSamples,
);
if (!allSourcesPath) {
return undefined;
}
@ -345,7 +332,15 @@ export function findOptimalRustPathFromSamples(
const vipSourcesSamples = samples.filter(s => s[0] && vipSourcesSet.has(s[0].source));
if (vipSourcesSamples.length > 0) {
const vipSourcesPath = findRoutesAndCreateOptimalPath(side, vipSourcesSamples, [], input, opts, fees);
const vipSourcesPath = findRoutesAndCreateOptimalPath(
side,
vipSourcesSamples,
[],
input,
opts,
fees,
neonRouterNumSamples,
);
const { input: allSourcesInput, output: allSourcesOutput } = allSourcesPath.adjustedSize();
// NOTE: For sell quotes input is the taker asset and for buy quotes input is the maker asset

View File

@ -14,7 +14,8 @@ import { BatchedOperation, ERC20BridgeSource, LiquidityProviderRegistry, TokenAd
*/
export function getSampleAmounts(maxFillAmount: BigNumber, numSamples: number, expBase: number = 1): BigNumber[] {
const distribution = [...Array<BigNumber>(numSamples)].map((_v, i) => new BigNumber(expBase).pow(i));
const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution)));
const distributionSum = BigNumber.sum(...distribution);
const stepSizes = distribution.map(d => d.div(distributionSum));
const amounts = stepSizes.map((_s, i) => {
if (i === numSamples - 1) {
return maxFillAmount;

View File

@ -455,6 +455,10 @@ export interface GetMarketOrdersOpts {
* Default: 1.25.
*/
sampleDistributionBase: number;
/**
* Number of samples to use when creating fill curves with neon-router
*/
neonRouterNumSamples: number;
/**
* Fees for each liquidity source, expressed in gas.
*/
@ -599,6 +603,7 @@ export interface GenerateOptimizedOrdersOpts {
allowFallback?: boolean;
shouldBatchBridgeOrders?: boolean;
gasPrice: BigNumber;
neonRouterNumSamples: number;
}
export interface ComparisonPrice {

View File

@ -959,10 +959,10 @@
typedoc "~0.16.11"
yargs "^10.0.3"
"@0x/neon-router@^0.2.1":
version "0.2.1"
resolved "https://registry.yarnpkg.com/@0x/neon-router/-/neon-router-0.2.1.tgz#23bb3cedc0eafd55a8ba6b6ea8a59ee4c538064b"
integrity sha512-feCCKuox4staZl8lxLY4nf5U256NcDHrgvSFra5cU/TUhoblLHb8F7eWAC9ygpukZUCVFLy13mExkFQHXlEOYw==
"@0x/neon-router@^0.3.1":
version "0.3.1"
resolved "https://registry.yarnpkg.com/@0x/neon-router/-/neon-router-0.3.1.tgz#4ec13e750d1435357c4928d7f2521a2b4376f27e"
integrity sha512-M4ypTov9KyxsGJpYwobrld3Y2JOlR7U0XjR6BEQE2gQ1k3nie/1wNEI2J4ZjKw++RLDxdv/RCqhgA5VnINzjxA==
dependencies:
"@mapbox/node-pre-gyp" "^1.0.5"