Skip to content
This repository was archived by the owner on Oct 15, 2024. It is now read-only.

Commit

Permalink
added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
dekz committed Jul 24, 2020
1 parent 29d1e71 commit b028740
Show file tree
Hide file tree
Showing 6 changed files with 402 additions and 47 deletions.
5 changes: 5 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,8 @@ export const GST2_WALLET_ADDRESSES = {
[ChainId.Kovan]: NULL_ADDRESS,
[ChainId.Ganache]: NULL_ADDRESS,
};

// Market Depth
export const MARKET_DEPTH_MAX_SAMPLES = 50;
export const MARKET_DEPTH_DEFAULT_DISTRIBUTION = 1.05;
export const MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC = 20;
37 changes: 19 additions & 18 deletions src/handlers/swap_handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import * as express from 'express';
import * as HttpStatus from 'http-status-codes';

import { CHAIN_ID } from '../config';
import { DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE, SWAP_DOCS_URL } from '../constants';
import {
DEFAULT_QUOTE_SLIPPAGE_PERCENTAGE,
MARKET_DEPTH_DEFAULT_DISTRIBUTION,
MARKET_DEPTH_MAX_SAMPLES,
SWAP_DOCS_URL,
} from '../constants';
import {
InternalServerError,
RevertAPIError,
Expand Down Expand Up @@ -129,25 +134,21 @@ export class SwapHandlers {
public async getMarketDepthAsync(req: express.Request, res: express.Response): Promise<void> {
const makerToken = getTokenMetadataIfExists(req.query.buyToken as string, CHAIN_ID);
const takerToken = getTokenMetadataIfExists(req.query.sellToken as string, CHAIN_ID);
try {
const response = await this._swapService.calculateMarketDepthAsync({
buyToken: makerToken,
sellToken: takerToken,
sellAmount: new BigNumber(req.query.sellAmount as string),
// tslint:disable-next-line:radix custom-no-magic-numbers
numSamples: req.query.numSamples ? parseInt(req.query.numSamples as string) : 100,
sampleDistributionBase: req.query.sampleDistributionBase
? parseFloat(req.query.sampleDistributionBase as string)
: 1.05,
excludedSources: req.query.excludedSources === undefined
const response = await this._swapService.calculateMarketDepthAsync({
buyToken: makerToken,
sellToken: takerToken,
sellAmount: new BigNumber(req.query.sellAmount as string),
// tslint:disable-next-line:radix custom-no-magic-numbers
numSamples: req.query.numSamples ? parseInt(req.query.numSamples as string) : MARKET_DEPTH_MAX_SAMPLES,
sampleDistributionBase: req.query.sampleDistributionBase
? parseFloat(req.query.sampleDistributionBase as string)
: MARKET_DEPTH_DEFAULT_DISTRIBUTION,
excludedSources:
req.query.excludedSources === undefined
? []
: parseUtils.parseStringArrForERC20BridgeSources((req.query.excludedSources as string).split(',')),
});
res.status(HttpStatus.OK).send({ ...response, buyToken: makerToken, sellToken: takerToken });
} catch (e) {
console.log(e);
throw e;
}
});
res.status(HttpStatus.OK).send({ ...response, buyToken: makerToken, sellToken: takerToken });
}

private async _calculateSwapQuoteAsync(
Expand Down
46 changes: 31 additions & 15 deletions src/services/swap_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
SwapQuoter,
} from '@0x/asset-swapper';
import { SwapQuoteRequestOpts, SwapQuoterOpts } from '@0x/asset-swapper/lib/src/types';
import { MarketDepth, MarketDepthSide } from '@0x/asset-swapper/lib/src/utils/market_operation_utils/types';
import { ContractAddresses, getContractAddressesForChainOrThrow } from '@0x/contract-addresses';
import { ERC20TokenContract, WETH9Contract } from '@0x/contract-wrappers';
import { assetDataUtils, SupportedProvider } from '@0x/order-utils';
Expand Down Expand Up @@ -40,6 +39,7 @@ import { InsufficientFundsError } from '../errors';
import { logger } from '../logger';
import { TokenMetadatasForChains } from '../token_metadatas_for_networks';
import {
BucketedPriceDepth,
CalaculateMarketDepthParams,
CalculateSwapQuoteParams,
GetSwapQuoteResponse,
Expand Down Expand Up @@ -273,7 +273,12 @@ export class SwapService {
return prices;
}

public async calculateMarketDepthAsync(params: CalaculateMarketDepthParams): Promise<MarketDepth> {
public async calculateMarketDepthAsync(
params: CalaculateMarketDepthParams,
): Promise<{
asks: { dataByBucketPrice: BucketedPriceDepth[] };
bids: { dataByBucketPrice: BucketedPriceDepth[] };
}> {
const { buyToken, sellToken, sellAmount, numSamples, sampleDistributionBase, excludedSources } = params;
const marketDepth = await this._swapQuoter.getBidAskLiquidityForMakerTakerAssetPairAsync(
buyToken.tokenAddress,
Expand All @@ -286,27 +291,38 @@ export class SwapService {
},
);

const calculateDepthForSide = (rawDepthSide: MarketDepthSide, side: MarketOperation) => {
const depthSide = marketDepthUtils.normalizeMarketDepthToSampleOutput(rawDepthSide, side);
const [startPrice, endPrice] = marketDepthUtils.calculateStartEndBucketPrice(depthSide, side);
const buckets = marketDepthUtils.getBucketPrices(startPrice, endPrice, numSamples, sampleDistributionBase);
const distributedBuckets = marketDepthUtils.distributeSamplesToBuckets(depthSide, buckets, side);
// Scale the price by the token decimals
const scaledBuckets = distributedBuckets.map(b => ({
const maxEndSlippagePercentage = 20;
const scalePriceByDecimals = (priceDepth: BucketedPriceDepth[]) =>
priceDepth.map(b => ({
...b,
price: b.price.times(new BigNumber(10).pow(sellToken.decimals - buyToken.decimals)),
}));
console.log({ startPrice, endPrice, buckets });
return { dataByBucketPrice: scaledBuckets };
};
const askDepth = scalePriceByDecimals(
marketDepthUtils.calculateDepthForSide(
marketDepth.asks,
MarketOperation.Sell,
numSamples * 2,
sampleDistributionBase,
maxEndSlippagePercentage,
),
);
const bidDepth = scalePriceByDecimals(
marketDepthUtils.calculateDepthForSide(
marketDepth.bids,
MarketOperation.Buy,
numSamples * 2,
sampleDistributionBase,
maxEndSlippagePercentage,
),
);
return {
// We're buying buyToken and SELLING sellToken (DAI) (50k)
// Price goes from HIGH to LOW
asks: calculateDepthForSide(marketDepth.asks, MarketOperation.Sell),
asks: { dataByBucketPrice: askDepth },
// We're BUYING sellToken (DAI) (50k) and selling buyToken
// Price goes from LOW to HIGH
bids: calculateDepthForSide(marketDepth.bids, MarketOperation.Buy),
} as any;
bids: { dataByBucketPrice: bidDepth },
};
}

private async _getSwapQuoteForWethAsync(
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -621,4 +621,11 @@ export interface CalaculateMarketDepthParams {
sampleDistributionBase: number;
excludedSources?: ERC20BridgeSource[];
}

export interface BucketedPriceDepth {
cumulative: BigNumber;
price: BigNumber;
bucket: number;
bucketTotal: BigNumber;
}
// tslint:disable-line:max-file-line-count
62 changes: 48 additions & 14 deletions src/utils/market_depth_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,17 @@ import { BigNumber } from '@0x/utils';
import { Web3Wrapper } from '@0x/web3-wrapper';
import _ = require('lodash');

import { ZERO } from '../constants';
import { TokenMetadata } from '../types';
import {
MARKET_DEPTH_DEFAULT_DISTRIBUTION,
MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC,
MARKET_DEPTH_MAX_SAMPLES,
ZERO,
} from '../constants';
import { BucketedPriceDepth, TokenMetadata } from '../types';

// tslint:disable:custom-no-magic-numbers
const MAX_DECIMALS = 18;
const ONE_HUNDRED_PERC = 100;

export const marketDepthUtils = {
getBucketPrices: (
Expand All @@ -21,13 +30,16 @@ export const marketDepthUtils = {
): BigNumber[] => {
const amount = endAmount.minus(startAmount);
const distribution = [...Array<BigNumber>(numSamples)].map((_v, i) =>
new BigNumber(sampleDistributionBase).pow(i),
new BigNumber(sampleDistributionBase).pow(i).decimalPlaces(MAX_DECIMALS),
);
const stepSizes = distribution.map(d => d.div(BigNumber.sum(...distribution)));
const amounts = stepSizes.map((_s, i) => {
return amount.times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)])).plus(startAmount);
return amount
.times(BigNumber.sum(...[0, ...stepSizes.slice(0, i + 1)]))
.plus(startAmount)
.decimalPlaces(MAX_DECIMALS);
});
return [startAmount, ...amounts, endAmount];
return [startAmount, ...amounts];
},
calculateUnitPrice: (
input: BigNumber,
Expand Down Expand Up @@ -58,7 +70,7 @@ export const marketDepthUtils = {
return sampleAmounts;
},
sampleNativeOrders: (path: Array<DexSample<NativeFillData>>, targetInput: BigNumber): BigNumber => {
const sortedPath = path.sort((a, b) => a.output.dividedBy(a.input).comparedTo(b.output.dividedBy(b.input)));
const sortedPath = path.sort((a, b) => b.output.dividedBy(b.input).comparedTo(a.output.dividedBy(a.input)));
let totalOutput = ZERO;
let totalInput = ZERO;
for (const fill of sortedPath) {
Expand All @@ -71,11 +83,13 @@ export const marketDepthUtils = {
totalInput = totalInput.plus(input);
}
if (totalInput.isLessThan(targetInput)) {
// TODO do I really want to do this
return ZERO;
}
console.log('native sample', { targetInput, totalInput, totalOutput });
return totalOutput;
},
normalizeMarketDepthToSampleOutput: (depthSide: MarketDepthSide, _operation: MarketOperation): MarketDepthSide => {
normalizeMarketDepthToSampleOutput: (depthSide: MarketDepthSide): MarketDepthSide => {
// Native is not a "sampled" output, here we convert it to be a accumulated sample output
const nativeIndexIfExists = depthSide.findIndex(
s => s[0] && s[0].source === ERC20BridgeSource.Native && s[0].output,
Expand All @@ -100,27 +114,29 @@ export const marketDepthUtils = {
return normalizedDepth;
},

calculateStartEndBucketPrice: (depthSide: MarketDepthSide, side: MarketOperation): [BigNumber, BigNumber] => {
calculateStartEndBucketPrice: (
depthSide: MarketDepthSide,
side: MarketOperation,
endSlippagePerc = 20,
): [BigNumber, BigNumber] => {
const pricesByAmount = depthSide
.map(samples =>
samples
.map(s => (!s.output.isZero() ? s.output.dividedBy(s.input) : ZERO))
.map(s => (!s.output.isZero() ? s.output.dividedBy(s.input).decimalPlaces(MAX_DECIMALS) : ZERO))
.filter(s => s.isGreaterThan(ZERO)),
)
.filter(samples => samples.length > 0);
let bestInBracket: BigNumber;
let worstBestInBracket: BigNumber;
if (side === MarketOperation.Sell) {
// Sell we want to sell for a higher price as possible
console.log(pricesByAmount);
bestInBracket = BigNumber.max(...pricesByAmount.map(s => BigNumber.max(...s)));
worstBestInBracket = BigNumber.min(...pricesByAmount.map(s => BigNumber.max(...s)));
worstBestInBracket = bestInBracket.times((ONE_HUNDRED_PERC - endSlippagePerc) / ONE_HUNDRED_PERC);
} else {
// Buy we want to buy for the lowest price possible
bestInBracket = BigNumber.min(...pricesByAmount.map(s => BigNumber.min(...s)));
worstBestInBracket = BigNumber.max(...pricesByAmount.map(s => BigNumber.min(...s)));
worstBestInBracket = bestInBracket.times((ONE_HUNDRED_PERC + endSlippagePerc) / ONE_HUNDRED_PERC);
}
// return [bestInBracket, worstBestInBracket.minus(bestInBracket).plus(worstBestInBracket)];
return [bestInBracket, worstBestInBracket];
},

Expand All @@ -140,7 +156,7 @@ export const marketDepthUtils = {
const price = sample.output.dividedBy(sample.input);
const bucketId = getBucketId(price);
if (bucketId === -1) {
console.log('No bucket for price', price, source);
// No bucket available so we ignore
continue;
}
const bucket = allocatedBuckets[bucketId];
Expand All @@ -155,4 +171,22 @@ export const marketDepthUtils = {
});
return cumulativeBuckets;
},

calculateDepthForSide: (
rawDepthSide: MarketDepthSide,
side: MarketOperation,
numBuckets: number = MARKET_DEPTH_MAX_SAMPLES,
bucketDistribution: number = MARKET_DEPTH_DEFAULT_DISTRIBUTION,
maxEndSlippagePercentage: number = MARKET_DEPTH_END_PRICE_SLIPPAGE_PERC,
): BucketedPriceDepth[] => {
const depthSide = marketDepthUtils.normalizeMarketDepthToSampleOutput(rawDepthSide);
const [startPrice, endPrice] = marketDepthUtils.calculateStartEndBucketPrice(
depthSide,
side,
maxEndSlippagePercentage,
);
const buckets = marketDepthUtils.getBucketPrices(startPrice, endPrice, numBuckets, bucketDistribution);
const distributedBuckets = marketDepthUtils.distributeSamplesToBuckets(depthSide, buckets, side);
return distributedBuckets;
},
};
Loading

0 comments on commit b028740

Please sign in to comment.