|
|
|
@@ -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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|