[asset-swapper] clip native fill data (#2565)

* [asset-swapper] clip native fill data

* Test for createFillPaths

* CHANGELOG
This commit is contained in:
Jacob Evans
2020-04-25 08:44:48 +10:00
committed by GitHub
parent 160519e1fe
commit b34edcbf87
4 changed files with 91 additions and 11 deletions

View File

@@ -61,6 +61,10 @@
{
"note": "Fix sporadically failing quote simulation tests",
"pr": 2564
},
{
"note": "Apply Native order penalty inline with the target amount",
"pr": 2565
}
]
},

View File

@@ -35,7 +35,7 @@ export function createFillPaths(opts: {
const dexQuotes = opts.dexQuotes || [];
const ethToOutputRate = opts.ethToOutputRate || ZERO_AMOUNT;
// Create native fill paths.
const nativePath = nativeOrdersToPath(side, orders, ethToOutputRate, feeSchedule);
const nativePath = nativeOrdersToPath(side, orders, opts.targetInput, ethToOutputRate, feeSchedule);
// Create DEX fill paths.
const dexPaths = dexQuotesToPaths(side, dexQuotes, ethToOutputRate, feeSchedule);
return filterPaths([...dexPaths, nativePath].map(p => clipPathToInput(p, opts.targetInput)), excludedSources);
@@ -60,6 +60,7 @@ function filterPaths(paths: Fill[][], excludedSources: ERC20BridgeSource[]): Fil
function nativeOrdersToPath(
side: MarketOperation,
orders: SignedOrderWithFillableAmounts[],
targetInput: BigNumber = POSITIVE_INF,
ethToOutputRate: BigNumber,
fees: { [source: string]: BigNumber },
): Fill[] {
@@ -71,19 +72,26 @@ function nativeOrdersToPath(
const input = side === MarketOperation.Sell ? takerAmount : makerAmount;
const output = side === MarketOperation.Sell ? makerAmount : takerAmount;
const penalty = ethToOutputRate.times(fees[ERC20BridgeSource.Native] || 0);
const adjustedOutput = side === MarketOperation.Sell ? output.minus(penalty) : output.plus(penalty);
const rate = makerAmount.div(takerAmount);
// targetInput can be less than the order size
// whilst the penalty is constant, it affects the adjusted output
// only up until the target has been exhausted.
// A large order and an order at the exact target should be penalized
// the same.
const clippedInput = BigNumber.min(targetInput, input);
// scale the clipped output inline with the input
const clippedOutput = clippedInput.dividedBy(input).times(output);
const adjustedOutput =
side === MarketOperation.Sell ? clippedOutput.minus(penalty) : clippedOutput.plus(penalty);
const adjustedRate =
side === MarketOperation.Sell
? makerAmount.minus(penalty).div(takerAmount)
: makerAmount.div(takerAmount.plus(penalty));
side === MarketOperation.Sell ? adjustedOutput.div(clippedInput) : clippedInput.div(adjustedOutput);
// Skip orders with rates that are <= 0.
if (adjustedRate.lte(0)) {
continue;
}
path.push({
input,
output,
input: clippedInput,
output: clippedOutput,
rate,
adjustedRate,
adjustedOutput,
@@ -117,7 +125,7 @@ function dexQuotesToPaths(
const sample = quote[i];
const prevSample = i === 0 ? undefined : quote[i - 1];
const source = sample.source;
// Stop of the sample has zero output, which can occur if the source
// Stop if the sample has zero output, which can occur if the source
// cannot fill the full amount.
// TODO(dorothy-zbornak): Sometimes Kyber will dip to zero then pick back up.
if (sample.output.eq(0)) {

View File

@@ -96,7 +96,7 @@ function clipFillAdjustedOutput(fill: Fill, remainingInput: BigNumber): BigNumbe
return fill.adjustedOutput;
}
const penalty = fill.adjustedOutput.minus(fill.output);
return fill.output.times(remainingInput.div(fill.input)).plus(penalty);
return remainingInput.times(fill.rate).plus(penalty);
}
function getRate(side: MarketOperation, input: BigNumber, output: BigNumber): BigNumber {

View File

@@ -14,10 +14,18 @@ import { AssetProxyId, ERC20BridgeAssetData, SignedOrder } from '@0x/types';
import { BigNumber, fromTokenUnitAmount, hexUtils, NULL_ADDRESS } from '@0x/utils';
import * as _ from 'lodash';
import { MarketOperation, SignedOrderWithFillableAmounts } from '../src';
import { MarketOperationUtils } from '../src/utils/market_operation_utils/';
import { BUY_SOURCES, DEFAULT_CURVE_OPTS, SELL_SOURCES } from '../src/utils/market_operation_utils/constants';
import {
BUY_SOURCES,
DEFAULT_CURVE_OPTS,
POSITIVE_INF,
SELL_SOURCES,
ZERO_AMOUNT,
} from '../src/utils/market_operation_utils/constants';
import { createFillPaths } from '../src/utils/market_operation_utils/fills';
import { DexOrderSampler } from '../src/utils/market_operation_utils/sampler';
import { DexSample, ERC20BridgeSource } from '../src/utils/market_operation_utils/types';
import { DexSample, ERC20BridgeSource, NativeFillData } from '../src/utils/market_operation_utils/types';
// tslint:disable: custom-no-magic-numbers
describe('MarketOperationUtils tests', () => {
@@ -1017,5 +1025,65 @@ describe('MarketOperationUtils tests', () => {
});
});
});
describe('createFillPaths', () => {
const takerAssetAmount = new BigNumber(5000000);
const ethToOutputRate = new BigNumber(0.5);
// tslint:disable-next-line:no-object-literal-type-assertion
const smallOrder = {
chainId: 1,
makerAddress: 'SMALL_ORDER',
takerAddress: NULL_ADDRESS,
takerAssetAmount,
makerAssetAmount: takerAssetAmount.times(2),
makerFee: ZERO_AMOUNT,
takerFee: ZERO_AMOUNT,
makerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
takerAssetData: '0xf47261b0000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
makerFeeAssetData: '0x',
takerFeeAssetData: '0x',
fillableTakerAssetAmount: takerAssetAmount,
fillableMakerAssetAmount: takerAssetAmount.times(2),
fillableTakerFeeAmount: ZERO_AMOUNT,
} as SignedOrderWithFillableAmounts;
const largeOrder = {
...smallOrder,
makerAddress: 'LARGE_ORDER',
fillableMakerAssetAmount: smallOrder.fillableMakerAssetAmount.times(2),
fillableTakerAssetAmount: smallOrder.fillableTakerAssetAmount.times(2),
makerAssetAmount: smallOrder.makerAssetAmount.times(2),
takerAssetAmount: smallOrder.takerAssetAmount.times(2),
};
const orders = [smallOrder, largeOrder];
const feeSchedule = {
[ERC20BridgeSource.Native]: new BigNumber(2e5),
};
it('penalizes native fill based on target amount when target is smaller', () => {
const path = createFillPaths({
side: MarketOperation.Sell,
orders,
dexQuotes: [],
targetInput: takerAssetAmount.minus(1),
ethToOutputRate,
feeSchedule,
});
expect((path[0][0].fillData as NativeFillData).order.makerAddress).to.eq(smallOrder.makerAddress);
expect(path[0][0].input).to.be.bignumber.eq(takerAssetAmount.minus(1));
});
it('penalizes native fill based on available amount when target is larger', () => {
const path = createFillPaths({
side: MarketOperation.Sell,
orders,
dexQuotes: [],
targetInput: POSITIVE_INF,
ethToOutputRate,
feeSchedule,
});
expect((path[0][0].fillData as NativeFillData).order.makerAddress).to.eq(largeOrder.makerAddress);
expect((path[0][1].fillData as NativeFillData).order.makerAddress).to.eq(smallOrder.makerAddress);
});
});
});
// tslint:disable-next-line: max-file-line-count