Skip to content

Commit

Permalink
Implement the prediction of volatility (#84)
Browse files Browse the repository at this point in the history
* Implement the prediction of volatility

* Adapt endpoint

* feat: enhance implementation of slippage estimation

* feat: implement thorough tests for slippage calculations

* chore: make const coherent with the comment

Co-authored-by: Leandro <[email protected]>

* feat: add volatility details endpoint for investigating issues with slippage calculations

* fix: fix comment

* chore: simplify variance

* docs: improve documentation of tests

* chore: use the const for max/min slippage

* Fix comment

* Use const for max slippage

* Fix issue with the return schema

* Add shared lib and handle native currency

* Add script to easily test slippage tolerances

* Upload test-data

* Create utilities to better handle supported chains

* Remove redundant info

* Delete unused

---------

Co-authored-by: Leandro <[email protected]>
  • Loading branch information
anxolin and alfetopito authored Aug 22, 2024
1 parent 2fa7b68 commit be1b58d
Show file tree
Hide file tree
Showing 68 changed files with 36,080 additions and 121 deletions.
139 changes: 139 additions & 0 deletions apps/api/scripts/test-slippage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
const fs = require('fs');

const BASE_URL = 'http://localhost:3010/chains';

const NetworkToChainId = {
Mainnet: 1,
'Gnosis Chain': 100,
Arbitrum: 42161,
Sepolia: 11155111,
};

const PAIRS_TEST = {
Mainnet: [
{
pair: 'USDC-DAI',
quoteToken: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
baseToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
},
{
pair: 'DAI-ETH',
quoteToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
baseToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
},
{
pair: 'DAI-WETH',
quoteToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
baseToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
},
{
pair: 'PEPE-DAI',
quoteToken: '0x6982508145454Ce325dDbE47a25d4ec3d2311933',
baseToken: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
},
{
pair: 'PEPE-WETH',
quoteToken: '0x6982508145454Ce325dDbE47a25d4ec3d2311933',
baseToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
},
{
pair: 'COW-WETH',
quoteToken: '0xDEf1CA1fb7FBcDC777520aa7f396b4E015F497aB',
baseToken: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2',
},
],
'Gnosis Chain': [
{
pair: 'USDC-xDAI',
quoteToken: '0xDDAfbb505ad214D7b80b1f830fcCc89B60fb7A83',
baseToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
},
{
pair: 'sDAI-xDAI',
quoteToken: '0xaf204776c7245bF4147c2612BF6e5972Ee483701',
baseToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
},
{
pair: 'sDAI-wxDAI',
quoteToken: '0xaf204776c7245bF4147c2612BF6e5972Ee483701',
baseToken: '0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d',
},
{
pair: 'WETH-xDAI',
quoteToken: '0x6A023CCd1ff6F2045C3309768eAd9E68F978f6e1',
baseToken: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
},
],
Arbitrum: [
{
pair: 'USDC-DAI',
quoteToken: '0xaf88d065e77c8cC2239327C5EDb3A432268e5831',
baseToken: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
},
{
pair: 'DAI-WETH',
quoteToken: '0xDA10009cBd5D07dd0CeCc66161FC93D7c9000da1',
baseToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
},
{
pair: 'COW-WETH',
quoteToken: '0xcb8b5CD20BdCaea9a010aC1F8d835824F5C87A04',
baseToken: '0x82aF49447D8a07e3bd95BD0d56f35241523fBab1',
},
],
};

const fetchForMarket = async (chainId, quoteToken, baseToken, endpoint) => {
const url = `${BASE_URL}/${chainId}/markets/${quoteToken}-${baseToken}/${endpoint}`;
try {
const response = await fetch(url);
return response.json();
} catch (error) {
console.error(
`Error fetching slippage tolerance for ${quoteToken}-${baseToken} on chain ${chainId}:`,
error
);
return null;
}
};

const fetchVolatilityDetails = async (pair, chainId, quoteToken, baseToken) => {
const result = await fetchForMarket(
chainId,
quoteToken,
baseToken,
'volatilityDetails'
);

fs.writeFileSync(
`${chainId}-${pair}__volatilityDetails.json`,
JSON.stringify(result, null, 2)
);
};

const fetchSlippageTolerance = async (pair, chainId, quoteToken, baseToken) => {
const result = await fetchForMarket(
chainId,
quoteToken,
baseToken,
'slippageTolerance'
);
console.log(
`${pair}: ${result ? result.slippageBps : 'Error fetching data'}`
);
fs.writeFileSync(
`${chainId}-${pair}__slippageTolerance.json`,
JSON.stringify(result, null, 2)
);
};

(async function () {
for (const chain in PAIRS_TEST) {
console.log(`\nFetching slippage tolerances for ${chain}:`);
const chainId = NetworkToChainId[chain];
for (const { pair, quoteToken, baseToken } of PAIRS_TEST[chain]) {
await fetchSlippageTolerance(pair, chainId, quoteToken, baseToken);
await fetchVolatilityDetails(pair, chainId, quoteToken, baseToken);
}
}
})();
4 changes: 2 additions & 2 deletions apps/api/src/app/inversify.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const CACHE_TOKEN_INFO_SECONDS = ms('24h') / 1000; // 24h
import { Container } from 'inversify';
import {
SlippageService,
SlippageServiceMock,
SlippageServiceMain,
UsdService,
UsdServiceMain,
slippageServiceSymbol,
Expand Down Expand Up @@ -107,7 +107,7 @@ function getApiContainer(): Container {
// Services
apiContainer
.bind<SlippageService>(slippageServiceSymbol)
.to(SlippageServiceMock);
.to(SlippageServiceMain);

apiContainer.bind<UsdService>(usdServiceSymbol).to(UsdServiceMain);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { SlippageService, slippageServiceSymbol } from '@cowprotocol/services';
import {
SlippageService,
slippageServiceSymbol,
VolatilityDetails,
} from '@cowprotocol/services';
import { ChainIdSchema, ETHEREUM_ADDRESS_PATTERN } from '../../../../schemas';
import { FastifyInstance, FastifyPluginAsync } from 'fastify';
import { FastifyPluginAsync } from 'fastify';
import { FromSchema, JSONSchema } from 'json-schema-to-ts';
import { apiContainer } from '../../../../inversify.config';
import {
Expand Down Expand Up @@ -31,6 +35,18 @@ const routeSchema = {
},
} as const satisfies JSONSchema;

const queryStringSchema = {
type: 'object',
properties: {
orderKind: { type: 'string', enum: ['buy', 'sell'] },
partiallyFillable: { type: 'boolean' },
sellAmount: { type: 'string' },
buyAmount: { type: 'string' },
expirationTimeInSeconds: { type: 'number' },
feeAmount: { type: 'string' },
},
} as const satisfies JSONSchema;

const successSchema = {
type: 'object',
required: ['slippageBps'],
Expand All @@ -56,7 +72,8 @@ const slippageService: SlippageService = apiContainer.get(
);

const root: FastifyPluginAsync = async (fastify): Promise<void> => {
// example: http://localhost:3010/1/markets/0x6b175474e89094c44da98b954eedeac495271d0f-0x2260fac5e5542a773aa44fbcfedf7c193bc2c599/slippageTolerance
// example (basic): http://localhost:3010/1/markets/0x6b175474e89094c44da98b954eedeac495271d0f-0x2260fac5e5542a773aa44fbcfedf7c193bc2c599/slippageTolerance
// example (with optional params): http://localhost:3010/1/markets/0x6b175474e89094c44da98b954eedeac495271d0f-0x2260fac5e5542a773aa44fbcfedf7c193bc2c599/slippageTolerance?orderKind=sell&partiallyFillable=false&sellAmount=123456&expirationTimeInSeconds=1800
fastify.get<{
Params: RouteSchema;
Reply: SuccessSchema;
Expand All @@ -72,18 +89,63 @@ const root: FastifyPluginAsync = async (fastify): Promise<void> => {
},
async function (request, reply) {
const { chainId, baseTokenAddress, quoteTokenAddress } = request.params;

fastify.log.info(
`Get default slippage for market ${baseTokenAddress}-${quoteTokenAddress} on chain ${chainId}`
`Get default slippage for market ${baseTokenAddress}-${quoteTokenAddress} on chain ${chainId}. Query: %s'`,
JSON.stringify(request.query)
);
const slippageBps = await slippageService.getSlippageBps(
const slippageBps = await slippageService.getSlippageBps({
chainId,
baseTokenAddress,
quoteTokenAddress,
});
reply.header(
CACHE_CONTROL_HEADER,
getCacheControlHeaderValue(CACHE_SECONDS)
);
reply.send({ slippageBps });
}
);

fastify.get<{
Params: RouteSchema;
Reply: {
baseToken: VolatilityDetails | null;
quoteToken: VolatilityDetails | null;
};
}>(
'/volatilityDetails',
{
schema: {
params: routeSchema,
querystring: queryStringSchema,
},
},
async function (request, reply) {
const { chainId, baseTokenAddress, quoteTokenAddress } = request.params;

fastify.log.info(
`Get volatility details for market ${baseTokenAddress}-${quoteTokenAddress} on chain ${chainId}. Query: %s'`,
JSON.stringify(request.query)
);
const volatilityDetailsBase = await slippageService.getVolatilityDetails(
chainId,
baseTokenAddress
);

const volatilityDetailsQuote = await slippageService.getVolatilityDetails(
chainId,
quoteTokenAddress
);

reply.header(
CACHE_CONTROL_HEADER,
getCacheControlHeaderValue(CACHE_SECONDS)
);
reply.send({ slippageBps });
reply.send({
baseToken: volatilityDetailsBase,
quoteToken: volatilityDetailsQuote,
});
}
);
};
Expand Down
4 changes: 2 additions & 2 deletions apps/api/src/app/schemas.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { ALL_CHAIN_IDS } from '@cowprotocol/repositories';
import { AllChainIds } from '@cowprotocol/shared';

export const ChainIdSchema = {
title: 'Chain ID',
description: 'Chain ID',
enum: ALL_CHAIN_IDS,
enum: AllChainIds,
type: 'integer',
} as const;

Expand Down
2 changes: 1 addition & 1 deletion libs/repositories/src/Erc20Repository/Erc20Repository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';

export const erc20RepositorySymbol = Symbol.for('Erc20Repository');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Erc20RepositoryCache } from './Erc20RepositoryCache';
import { Erc20, Erc20Repository } from './Erc20Repository';
import { CacheRepository } from '../CacheRepository/CacheRepository';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';

describe('Erc20RepositoryCache', () => {
let erc20RepositoryCache: Erc20RepositoryCache;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { inject, injectable } from 'inversify';
import NodeCache from 'node-cache';
import { Erc20, Erc20Repository } from './Erc20Repository';
import { CacheRepository } from '../CacheRepository/CacheRepository';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import { getCacheKey, PartialCacheKey } from '../utils/cache';

const NULL_VALUE = 'null';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Erc20RepositoryViem } from './Erc20RepositoryViem';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import { PublicClient } from 'viem';
import { erc20Abi } from 'viem';
import { Erc20 } from './Erc20Repository';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { injectable } from 'inversify';
import { Erc20, Erc20Repository } from './Erc20Repository';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import { erc20Abi, getAddress, PublicClient } from 'viem';

@injectable()
Expand Down
2 changes: 1 addition & 1 deletion libs/repositories/src/UsdRepository/UsdRepository.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';

export const usdRepositorySymbol = Symbol.for('UsdRepository');

Expand Down
15 changes: 1 addition & 14 deletions libs/repositories/src/UsdRepository/UsdRepositoryCache.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { UsdRepositoryCache } from './UsdRepositoryCache';
import IORedis from 'ioredis';
import { UsdRepository } from './UsdRepository';
import { CacheRepositoryRedis } from '../CacheRepository/CacheRepositoryRedis';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import { WETH } from '../../test/mock';
import type { PricePoint } from './UsdRepository';

Expand Down Expand Up @@ -190,19 +190,6 @@ describe('UsdRepositoryCache', () => {
expect(proxyMock.getUsdPrices).not.toHaveBeenCalled();
});

it('should return prices from cache', async () => {
// GIVEN: cached prices
redisMock.get.mockResolvedValue(pricePoints100String);
proxyMock.getUsdPrices.mockResolvedValue([pricePoint200]);

// WHEN: Get USD prices
const prices = await usdRepositoryCache.getUsdPrices(chainId, WETH, '5m');

// THEN: We get the cached value
expect(prices).toEqual([pricePoint100]);
expect(proxyMock.getUsdPrices).not.toHaveBeenCalled();
});

it('should call the proxy if no cache, then cache the value', async () => {
// GIVEN: The value is not cached
redisMock.get.mockResolvedValue(null);
Expand Down
2 changes: 1 addition & 1 deletion libs/repositories/src/UsdRepository/UsdRepositoryCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
deserializePricePoints,
serializePricePoints,
} from './UsdRepository';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import ms from 'ms';
import { CacheRepository } from '../CacheRepository/CacheRepository';
import { getCacheKey, PartialCacheKey } from '../utils/cache';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Container } from 'inversify';
import { UsdRepositoryCoingecko } from './UsdRepositoryCoingecko';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import { WETH, DEFINITELY_NOT_A_TOKEN } from '../../test/mock';
import ms from 'ms';

Expand Down Expand Up @@ -111,7 +111,7 @@ describe('UsdRepositoryCoingecko', () => {
}
});

it('[daily] should return ~90 prices of WETH (~60min apart)', async () => {
it('[daily] should return ~90 prices of WETH (~24h apart)', async () => {
const prices = await usdRepositoryCoingecko.getUsdPrices(
SupportedChainId.MAINNET,
WETH,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
SimplePriceResponse,
coingeckoProClient,
} from '../datasources/coingecko';
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import { throwIfUnsuccessful } from '../utils/throwIfUnsuccessful';

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SupportedChainId } from '../types';
import { SupportedChainId } from '@cowprotocol/shared';
import { UsdRepositoryCow } from './UsdRepositoryCow';

import {
Expand Down
Loading

0 comments on commit be1b58d

Please sign in to comment.