poc: rust router

This commit is contained in:
Jacob Evans
2021-07-03 13:28:36 +10:00
parent 7972c2ce4e
commit 292ce6ea84
4 changed files with 188 additions and 3 deletions

View File

@@ -165,11 +165,11 @@ function dexSamplesToFills(
if (!sample.gasUsed || sample.gasUsed.isZero()) {
throw new Error(`${sample.source} gas used missing or 0`);
}
const fee = gasPrice.times(sample.gasUsed);
let penalty = ZERO_AMOUNT;
if (i === 0) {
// Only the first fill in a DEX path incurs a penalty.
const fee = gasPrice.times(sample.gasUsed);
penalty = !outputAmountPerEth.isZero()
? outputAmountPerEth.times(fee)
: inputAmountPerEth.times(fee).times(output.dividedToIntegerBy(input));

View File

@@ -146,7 +146,7 @@ export class MeasuredSamplerContractOperation<
PathDeregister.getInstance().handleResult(this.source, this._deregisterKey, result);
}
if (this._log) {
logUtils.log({ source: this.source, fillData: this.fillData, ...result });
logUtils.log({ source: this.source, fillData: this.fillData, params: this._params, ...result });
}
return result;
}

View File

@@ -230,7 +230,7 @@ export class Path {
return true;
}
private _collapseFills(): ReadonlyArray<CollapsedFill> {
public _collapseFills(): ReadonlyArray<CollapsedFill> {
this.collapsedFills = [];
for (const fill of this.fills) {
const source = fill.source;

View File

@@ -1,5 +1,6 @@
import { BigNumber } from '@0x/utils';
import * as _ from 'lodash';
import { performance } from 'perf_hooks';
import { MarketOperation } from '../../types';
@@ -10,6 +11,180 @@ import { ERC20BridgeSource, Fill } from './types';
const RUN_LIMIT_DECAY_FACTOR = 0.5;
const util = require('util');
const { route } = require('neon-router');
const inputAccum = (f: Fill): BigNumber => (f.parent ? f.input.plus(inputAccum(f.parent)) : f.input);
const adjustedOutputAccum = (f: Fill): BigNumber => {
if (!f.parent) {
return f.adjustedOutput;
}
const adjustedOutputParentAcc = adjustedOutputAccum(f.parent);
const feePerc = f.parent.adjustedOutput.dividedBy(f.parent.output);
return feePerc.times(f.output).plus(adjustedOutputParentAcc);
};
export function findOptimalRustPath(input: BigNumber, allFills: Fill[][], optimal: Path): Path {
// Track sample id's to integers (required by rust router)
const sampleIdLookup: { [key: string]: number } = {};
let sampleIdCounter = 0;
const fillToSampleId = (s: { source: string; sourcePathId: string; index: number }): number => {
const key = `${s.source}-${s.sourcePathId}-${s.index}`;
if (sampleIdLookup[key]) {
return sampleIdLookup[key];
} else {
sampleIdLookup[key] = ++sampleIdCounter;
return sampleIdLookup[key];
}
};
const adjustedParsedFills = allFills.map(fills => {
const adjustedFills: Fill[] = [];
// Samples are turned into Fills
// Fills are dependent on their parent and have their parents information "subtracted" from them
// e.g a samples for [1,10,100] => [5,50,500] look like [1, 9, 91] => [5, 40, 400]
for (let i = 0; i < fills.length; i++) {
const parsedFill: Fill = { ...fills[i] };
if (parsedFill.index !== 0) {
const parent = adjustedFills[i - 1];
parsedFill.parent = parent;
parsedFill.input = parsedFill.input.plus(parent.input);
parsedFill.output = parsedFill.output.plus(parent.output);
// Adjusted output is only modified for the first fill
const feePerc = parent.adjustedOutput.dividedBy(parent.output);
parsedFill.adjustedOutput = feePerc.times(parsedFill.output);
}
adjustedFills.push(parsedFill);
}
return adjustedFills;
});
const pathsIn = adjustedParsedFills.map((fs: any) => ({
ids: fs.map((f: any) => fillToSampleId(f)),
inputs: fs.map((f: any) => parseInt(f.input.toString())),
outputs: fs.map((f: any, i: number) => parseInt(f.adjustedOutput.toString())),
}));
const pathOut = {
ids: optimal.fills.map(s => fillToSampleId(s)),
inputs: optimal.fills.map(s => parseInt(inputAccum(s).toString())),
outputs: optimal.fills.map(s => parseInt(adjustedOutputAccum(s).toString())),
};
const rustArgs = {
side: 'Sell',
// HACK: There can be off by 1 errors, somewhere...
targetInput: parseInt(input.plus(1).toString()),
pathsIn,
pathOut,
};
// console.log(util.inspect({ rustArgs }, { depth: null }));
const before = performance.now();
const rustRoute: number[] = route(rustArgs);
console.log('Rust perf (real):', performance.now() - before);
// Our route as input and perc
const optimalFillsByPathId = _.groupBy(optimal._collapseFills(), o => o.sourcePathId);
const fillsByPathId = _.groupBy(_.flatten(adjustedParsedFills), o => o.sourcePathId);
const out: BigNumber[] = [];
const outPerc: BigNumber[] = [];
for (const sourcePathId of Object.keys(fillsByPathId)) {
const fs = optimalFillsByPathId[sourcePathId];
if (fs) {
outPerc.push(
fs
.reduce((prev, curr) => prev.plus(curr.input), new BigNumber(0))
.dividedBy(input)
.times(100),
);
out.push(fs.reduce((prev, curr) => prev.plus(curr.input), new BigNumber(0)));
} else {
outPerc.push(new BigNumber(0));
out.push(new BigNumber(0));
}
}
const sourcePathKeys = Object.keys(fillsByPathId);
const fakeFills: Fill[] = [];
const totalInputs = BigNumber.sum(...rustRoute);
for (let i = 0; i < rustRoute.length; i++) {
if (rustRoute[i] === 0) {
continue;
}
const rustInput = new BigNumber(rustRoute[i]);
// HACK: Handle the case where the router can under quote the input
// Set the first fill just a tad higher
const adjInput =
totalInputs.lt(input) && fakeFills.length === 0 ? rustInput.plus(input.minus(totalInputs)) : rustInput;
// Rust router has chosen this source;
const sourcePathKey = sourcePathKeys[i];
const fills = fillsByPathId[sourcePathKey];
let fill = fills[fills.length - 1];
// Descend to approach a closer fill for fillData which may not be consistent
// throughout the path (UniswapV3) and for a closer guesstimate at
// gas used
for (let k = fills.length - 1; k >= 0; k--) {
if (k === 0) {
fill = fills[0];
}
if (rustInput.isGreaterThan(fills[k].input)) {
// Between here and the previous fill
// HACK: Use the midpoint between the two
const left = fills[k];
const right = fills[k + 1];
if (left && right) {
const midPrice = left.output
.dividedBy(left.input)
.plus(right.output.dividedBy(right.input))
.dividedBy(2);
fill = {
...right, // default to the greater (for gas used)
input: rustInput,
output: midPrice.times(rustInput).decimalPlaces(0),
};
} else {
fill = left || right;
}
break;
}
}
const adjustedOutput = fill.output
.dividedBy(fill.input)
.times(adjInput)
.decimalPlaces(0);
fakeFills.push({
...fill,
input: adjInput,
output: adjustedOutput,
index: 0,
parent: undefined,
});
}
const fakePath = Path.create(MarketOperation.Sell, fakeFills, input);
console.log(
util.inspect(
{
// fillsByPathId: Object.keys(fillsByPathId).map(k => `${fillsByPathId[k][0].source}-${k}`),
rustPerc: rustRoute.map(n =>
new BigNumber(n)
.dividedBy(input)
.times(100)
.decimalPlaces(2),
),
ourPerc: outPerc.map(o => o.decimalPlaces(2)),
// rustRoute,
// fakePath: fakePath._collapseFills(),
},
{ depth: null },
),
);
return fakePath;
}
/**
* Find the optimal mixture of fills that maximizes (for sells) or minimizes
* (for buys) output, while meeting the input requirement.
@@ -21,6 +196,7 @@ export async function findOptimalPathAsync(
runLimit: number = 2 ** 8,
opts: PathPenaltyOpts = DEFAULT_PATH_PENALTY_OPTS,
): Promise<Path | undefined> {
let before = performance.now();
// Sort fill arrays by descending adjusted completed rate.
// Remove any paths which cannot impact the optimal path
const sortedPaths = reducePaths(fillsToSortedPaths(fills, side, targetInput, opts), side);
@@ -34,6 +210,15 @@ export async function findOptimalPathAsync(
// Yield to event loop.
await Promise.resolve();
}
console.log('TS perf:', performance.now() - before);
before = performance.now();
const rustPath = findOptimalRustPath(targetInput, fills, optimalPath);
console.log('Rust perf:', performance.now() - before);
if (process.env.RUST === 'true') {
return rustPath;
}
return optimalPath.isComplete() ? optimalPath : undefined;
}