[asset-swapper] clip native fill data (#2565)
* [asset-swapper] clip native fill data * Test for createFillPaths * CHANGELOG
This commit is contained in:
@@ -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
|
||||
}
|
||||
]
|
||||
},
|
||||
|
@@ -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)) {
|
||||
|
@@ -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 {
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user