diff --git a/changelog.d/20231025_203748_nagakingg_curve_prices_volume.rst b/changelog.d/20231025_203748_nagakingg_curve_prices_volume.rst new file mode 100644 index 000000000..5204de79d --- /dev/null +++ b/changelog.d/20231025_203748_nagakingg_curve_prices_volume.rst @@ -0,0 +1,27 @@ +Removed +------- +- Removed volume multiplier modes +- Removed pipelines.utils +- Removed PriceVolume.total_volumes() +- Removed volume from PoolDataCache +- Removed convex subgraph volume query +- Removed PoolDataCache + +Added +----- +- Added Curve Prices API volume query (network.curve_prices) +- Added pool_data.get_pool_volume() to fetch historical pool volume from + Curve Prices API + +Changed +------- +- Volume multipliers now computed individually for each asset pair +- Replaced pool_data.queries with folder +- Pool volume and volume limiting are only supported for Ethereum pools + pending updates to Curve Prices API + +Deprecated +---------- +- RAI3CRV pool is currently unsupported by simulation pipelines. It will + be reimplemented along with Stableswap-NG pools. + diff --git a/curvesim/exceptions/__init__.py b/curvesim/exceptions/__init__.py index e956c1745..6ad609ccb 100644 --- a/curvesim/exceptions/__init__.py +++ b/curvesim/exceptions/__init__.py @@ -59,6 +59,10 @@ class NetworkError(CurvesimException): """Error for network subpackage.""" +class ApiResultError(NetworkError): + """Raised when API results aren't as expected.""" + + class SimPoolError(CurvesimException): """Error in a SimPool operation.""" diff --git a/curvesim/iterators/price_samplers/price_volume.py b/curvesim/iterators/price_samplers/price_volume.py index add02223c..b7728d4d5 100644 --- a/curvesim/iterators/price_samplers/price_volume.py +++ b/curvesim/iterators/price_samplers/price_volume.py @@ -89,12 +89,3 @@ def __iter__(self) -> Iterator[PriceVolumeSample]: volumes = volumes.to_dict() yield PriceVolumeSample(price_timestamp, prices, volumes) # type:ignore - - def total_volumes(self): - """ - Returns - ------- - pandas.Series - Total volume for each pairwise coin combination, summed accross timestamps. - """ - return self.volumes.sum().to_dict() diff --git a/curvesim/network/curve_prices.py b/curvesim/network/curve_prices.py new file mode 100644 index 000000000..bf486e076 --- /dev/null +++ b/curvesim/network/curve_prices.py @@ -0,0 +1,129 @@ +""" +Network connector for Curve Prices API. +""" + +from typing import List + +from eth_utils import to_checksum_address +from pandas import DataFrame, to_datetime + +from curvesim.exceptions import ApiResultError, CurvesimValueError + +from .http import HTTP +from .utils import sync + +URL = "https://prices.curve.fi/v1/" + +CHAIN_ALIASES = {"mainnet": "ethereum"} + + +async def _get_pool_pair_volume( + pool_address, + main_token_address, + reference_token_address, + start_ts, + end_ts, + *, + chain="ethereum", + interval="day", +): + chain = _chain_from_alias(chain) + pool_address = to_checksum_address(pool_address) + main_token_address = to_checksum_address(main_token_address) + reference_token_address = to_checksum_address(reference_token_address) + + url = URL + f"volume/{chain}/{pool_address}" + params = { + "main_token": main_token_address, + "reference_token": reference_token_address, + "start": start_ts, + "end": end_ts, + "interval": interval, + } + r = await HTTP.get(url, params=params) + + try: + data = r["data"] + except KeyError as e: + raise ApiResultError( + "No historical volume returned for\n" + f"Pool: '{pool_address}', Chain: '{chain}',\n" + f"Tokens: (main: {main_token_address}, " + f"reference: {reference_token_address}),\n" + f"Timestamps: (start: {start_ts}, end: {end_ts})" + ) from e + + return data + + +async def get_pool_pair_volume( + pool_address: str, + main_token_address: str, + reference_token_address: str, + start_ts: int, + end_ts: int, + *, + chain: str = "ethereum", + interval: str = "day", +) -> DataFrame: + """ + Gets historical daily volume for a pair of coins traded in a Curve pool. + + Parameters + ---------- + pool_address: str + The Curve pool's address. + + main_token_address: str + Address for the token volume will be denominated in. + + reference_token_address: str + Address for the second token in the trading pair. + + start_ts: int + Posix timestamp (UTC) for start of query period. + + end_ts: int + Posix timestamp (UTC) for end of query period. + + chain: str, default "ethereum" + The pool's blockchain (note: currently only "ethereum" supported) + + interval: str, default "day" + The sampling interval for the data. Available values: week, day, hour + + Returns + ------- + DataFrame + Rows: DateTimeIndex; Columns: volume, fees + + """ + data: List[dict] = await _get_pool_pair_volume( + pool_address, + main_token_address, + reference_token_address, + start_ts, + end_ts, + chain=chain, + interval=interval, + ) + + df = DataFrame(data, columns=["timestamp", "volume", "fees"]) + df["timestamp"] = to_datetime(df["timestamp"], unit="s") + df.set_index("timestamp", inplace=True) + return df + + +def _chain_from_alias(chain): + if chain in CHAIN_ALIASES: # pylint: disable=consider-using-get + chain = CHAIN_ALIASES[chain] + + if chain != "ethereum": + raise CurvesimValueError( + "Curve Prices API currently only supports Ethereum chain." + ) + + return chain + + +get_pool_pair_volume_sync = sync(get_pool_pair_volume) diff --git a/curvesim/network/subgraph.py b/curvesim/network/subgraph.py index 6ba8592d0..35bd50fde 100644 --- a/curvesim/network/subgraph.py +++ b/curvesim/network/subgraph.py @@ -2,7 +2,6 @@ Network connector for subgraphs """ -from asyncio import gather from datetime import datetime, timedelta, timezone from decimal import Decimal @@ -161,91 +160,6 @@ async def symbol_address(symbol, chain, env="prod"): return addr -async def _volume(address, chain, env, days=60, end=None): - if end is None: - t_end = datetime.now(timezone.utc).replace( - hour=0, minute=0, second=0, microsecond=0 - ) - else: - t_end = datetime.fromtimestamp(end, tz=timezone.utc) - logger.info("Volume end date: %s", t_end) - t_start = t_end - timedelta(days=days) - - # pylint: disable=consider-using-f-string - q = """ - { - swapVolumeSnapshots( - orderBy: timestamp, - orderDirection: desc, - where: - { - pool: "%s" - period: "86400" - timestamp_gte: %d - timestamp_lt: %d - } - ) - { - volume - timestamp - } - } - """ % ( - address.lower(), - int(t_start.timestamp()), - int(t_end.timestamp()), - ) - - data = await convex(chain, q, env) - snapshots = data["swapVolumeSnapshots"] - num_snapshots = len(snapshots) - - if num_snapshots < days: - logger.warning("Only %s/%s days of pool volume returned.", num_snapshots, days) - - return snapshots - - -async def volume(addresses, chain, env="prod", days=60, end=None): - """ - Retrieves historical volume for a pool or multiple pools. - - Parameters - ---------- - addresses : str or iterable of str - The pool address(es). - - chain : str - The blockchain the pool or pools are on. - - days : int, default=60 - Number of days to fetch data for. - - Returns - ------- - list of float - A list of total volumes for each day. - - """ - if isinstance(addresses, str): - r = await _volume(addresses, chain, env, days=days, end=end) - vol = [float(e["volume"]) for e in r] - - else: - tasks = [] - for addr in addresses: - tasks.append(_volume(addr, chain, env, days=days, end=end)) - - r = await gather(*tasks) - - vol = [] - for _r in r: - _vol = [float(e["volume"]) for e in _r] - vol.append(_vol) - - return vol - - async def _pool_snapshot(address, chain, env, end_ts=None): if not end_ts: end_date = datetime.now(timezone.utc) @@ -453,7 +367,6 @@ async def pool_snapshot(address, chain, env="prod", end_ts=None): convex_sync = sync(convex) symbol_address_sync = sync(symbol_address) -volume_sync = sync(volume) pool_snapshot_sync = sync(pool_snapshot) @@ -564,6 +477,3 @@ async def redemption_prices( print("Symbol:", symbol) address = symbol_address_sync(symbol, chain) print("Address:", address) - _volume_sync = sync(_volume) - volumes = _volume_sync(address, chain, days=2) - print("Volumes:", volumes) diff --git a/curvesim/pipelines/utils.py b/curvesim/pipelines/utils.py deleted file mode 100644 index 55c3660bb..000000000 --- a/curvesim/pipelines/utils.py +++ /dev/null @@ -1,115 +0,0 @@ -"""Miscellaneous utility functions.""" -from math import factorial - -from numpy import append, array - -from curvesim.logging import get_logger -from curvesim.pool import CurveCryptoPool, CurveMetaPool, CurvePool - -logger = get_logger(__name__) - - -def compute_volume_multipliers(pool_vol, market_vol, n, pool_type, mode=1): - """ - Computes volume multipliers (vol_mult) used for volume limiting. - - Parameters - ---------- - pool_vol : float, int, or list - Total volume for the pool over the simulation period. - - market_vol : dict - Total market volume for each token pair over the simulation period. - - n : int - The number of token-types in the pool (e.g., DAI, USDC, USDT = 3) - - pool_type : str - "CurvePool" or "CurveMetaPool" - - vol_mode : int, default=1 - Modes for computing the volume multiplier: - - 1: limits trade volumes proportionally to market volume for each pair - 2: limits trade volumes equally across pairs - 3: mode 2 for trades with meta-pool asset, mode 1 for basepool-only trades - - """ - pairs, market_vol = zip(*market_vol.items()) - pool_vol_array = array(pool_vol) - market_vol_array = array(market_vol) - - try: - get_vol_mult = _pool_functions[pool_type] - except KeyError as e: - raise TypeError( - f"Pool type {pool_type} not supported by volume limiter." - ) from e - - vol_mult = get_vol_mult(pool_vol_array, market_vol_array, n, mode) - vol_mult_dict = dict(zip(pairs, vol_mult)) - - logger.info("Volume Multipliers: %s", _format_info_str(vol_mult_dict)) - return vol_mult_dict - - -def _pool_vol_mult(pool_vol, market_vol, n, mode): - if mode == 1: - vol_mult = [pool_vol / market_vol.sum()] * n - - elif mode == 2: - vol_mult = pool_vol.repeat(n) / n / market_vol - - elif mode == 3: - logger.info("Vol_mode=3 only available for meta-pools. Reverting to vol_mode=1") - vol_mult = [pool_vol / market_vol.sum()] * n - - else: - raise ValueError(f"Mode must be integer 1, 2, or 3, not {mode}.") - - return vol_mult - - -def _metapool_vol_mult(pool_vol, market_vol, n, mode): - pool_vol_meta = pool_vol[0] - pool_vol_base = pool_vol[1] - mkt_vol_meta = market_vol[0 : n[1]] - mkt_vol_base = market_vol[n[1] :] - - n_base_pairs = int(factorial(n[1]) / 2 * factorial(n[1] - 2)) - - if mode == 1: - vol_mult = append( - pool_vol_meta / mkt_vol_meta.sum().repeat(n[1]), - pool_vol_base / mkt_vol_base.sum().repeat(n_base_pairs), - ) - - elif mode == 2: - vol_mult = append( - pool_vol_meta.repeat(n[1]) / n[1] / mkt_vol_meta, - pool_vol_base.repeat(n_base_pairs) / n_base_pairs / mkt_vol_base, - ) - - elif mode == 3: - vol_mult = append( - pool_vol_meta.repeat(n[1]) / n[1] / mkt_vol_meta, - pool_vol_base / mkt_vol_base.sum().repeat(n_base_pairs), - ) - - else: - raise ValueError(f"Mode must be integer 1, 2, or 3, not {mode}.") - - return vol_mult - - -def _format_info_str(vol_mult_dict): - info = [f"{base}/{quote}: {mult}" for (base, quote), mult in vol_mult_dict.items()] - new_line = "\n " - return new_line + new_line.join(info) - - -_pool_functions = { - CurvePool: _pool_vol_mult, - CurveMetaPool: _metapool_vol_mult, - CurveCryptoPool: _pool_vol_mult, -} diff --git a/curvesim/pipelines/vol_limited_arb/__init__.py b/curvesim/pipelines/vol_limited_arb/__init__.py index 4d8f86b29..e7b03b09e 100644 --- a/curvesim/pipelines/vol_limited_arb/__init__.py +++ b/curvesim/pipelines/vol_limited_arb/__init__.py @@ -9,11 +9,10 @@ from curvesim.logging import get_logger from curvesim.metrics import init_metrics, make_results from curvesim.pool import get_sim_pool -from curvesim.pool_data.cache import PoolDataCache +from curvesim.pool_data import get_pool_volume from .. import run_pipeline from ..common import DEFAULT_METRICS -from ..utils import compute_volume_multipliers from .strategy import VolumeLimitedStrategy logger = get_logger(__name__) @@ -22,7 +21,6 @@ # pylint: disable-next=too-many-locals def pipeline( pool_metadata, - pool_data_cache=None, *, variable_params=None, fixed_params=None, @@ -31,7 +29,6 @@ def pipeline( src="coingecko", data_dir="data", vol_mult=None, - vol_mode=1, ncpu=None, end=None, ): @@ -83,16 +80,6 @@ def pipeline( {('DAI', 'USDC'): 0.1, ('DAI', 'USDT'): 0.1, ('USDC', 'USDT'): 0.1} - - vol_mode : int, default=1 - Modes for limiting trade volume. - - 1: limits trade volumes proportionally to market volume for each pair - - 2: limits trade volumes equally across pairs - - 3: mode 2 for trades with meta-pool asset, mode 1 for basepool-only trades - ncpu : int, default=os.cpu_count() Number of cores to use. @@ -104,10 +91,7 @@ def pipeline( cpu_count = os.cpu_count() ncpu = cpu_count if cpu_count is not None else 1 - if pool_data_cache is None: - pool_data_cache = PoolDataCache(pool_metadata, days=days, end=end) - - pool = get_sim_pool(pool_metadata, pool_data_cache=pool_data_cache) + pool = get_sim_pool(pool_metadata) # pylint: disable-next=abstract-class-instantiated param_sampler = ParameterizedPoolIterator(pool, variable_params, fixed_params) @@ -116,15 +100,10 @@ def pipeline( ) if vol_mult is None: - total_pool_volume = pool_data_cache.volume - total_market_volume = price_sampler.total_volumes() - vol_mult = compute_volume_multipliers( - total_pool_volume, - total_market_volume, - pool_metadata.n, - pool_metadata.pool_type, - mode=vol_mode, - ) + pool_volume = get_pool_volume(pool_metadata, days=days, end=end) + vol_mult = pool_volume.sum() / price_sampler.volumes.sum() + logger.info("Volume Multipliers:\n%s", vol_mult.to_string()) + vol_mult = vol_mult.to_dict() metrics = metrics or DEFAULT_METRICS metrics = init_metrics(metrics, pool=pool) diff --git a/curvesim/pool/__init__.py b/curvesim/pool/__init__.py index c22db155b..677283d1f 100644 --- a/curvesim/pool/__init__.py +++ b/curvesim/pool/__init__.py @@ -210,7 +210,6 @@ def get_sim_pool( balanced=True, balanced_base=True, custom_kwargs=None, - pool_data_cache=None, end_ts=None, env="prod", ): @@ -239,10 +238,6 @@ def get_sim_pool( custom_kwargs: dict, optional Used for passing additional kwargs to the pool's `__init__`. - pool_data_cache: PoolDataCache, optional - Pass in custom type that pulls/holds market prices and volume. - This will be deprecated in the future. - Returns ------- :class:`SimPool` @@ -281,7 +276,7 @@ def get_sim_pool( custom_keys = POOL_TYPE_TO_CUSTOM_KWARGS.get(pool_type, []) for key in custom_keys: try: - init_kwargs[key] = custom_kwargs.get(key) or getattr(pool_data_cache, key) + init_kwargs[key] = custom_kwargs.get(key) except KeyError as e: raise CurvesimValueError(f"'{pool_type.__name__}' needs '{key}'.") from e diff --git a/curvesim/pool_data/__init__.py b/curvesim/pool_data/__init__.py index a42e39385..a00749a80 100644 --- a/curvesim/pool_data/__init__.py +++ b/curvesim/pool_data/__init__.py @@ -1,73 +1,12 @@ """ -Tools for fetching pool state and metadata. +Tools for fetching pool metadata and volume. Currently supports stableswap pools, metapools, rebasing (RAI) metapools, and 2-token cryptopools. """ -__all__ = [ - "from_address", - "get_data_cache", - "get_metadata", -] +__all__ = ["get_metadata", "get_pool_volume"] -from curvesim.pool_data.metadata import PoolMetaData -from .cache import PoolDataCache -from .queries import from_address - - -def get_data_cache(address, chain="mainnet", days=60, end=None): - """ - Fetch historical volume and redemption price data and return - in a cache object. - - Deprecation warning: this will likely be removed in a future release. - - Parameters - ---------- - address : str - Pool address prefixed with “0x”. - - chain : str - Chain/layer2 identifier, e.g. “mainnet”, “arbitrum”, “optimism". - - Returns - ------- - :class:`PoolDataCache` - - """ - # TODO: validate function arguments - metadata_dict = from_address(address, chain, end_ts=end) - pool_data = PoolDataCache(metadata_dict, days=days, end=end) - - return pool_data - - -def get_metadata( - address, - chain="mainnet", - env="prod", - end_ts=None, -): - """ - Pulls pool state and metadata from daily snapshot. - - Parameters - ---------- - address : str - Pool address prefixed with “0x”. - - chain : str - Chain/layer2 identifier, e.g. “mainnet”, “arbitrum”, “optimism". - - Returns - ------- - :class:`~curvesim.pool_data.metadata.PoolMetaDataInterface` - - """ - # TODO: validate function arguments - metadata_dict = from_address(address, chain, env=env, end_ts=end_ts) - metadata = PoolMetaData(metadata_dict) - - return metadata +from .queries.metadata import get_metadata +from .queries.pool_volume import get_pool_volume diff --git a/curvesim/pool_data/cache.py b/curvesim/pool_data/cache.py deleted file mode 100644 index fdf1eed1d..000000000 --- a/curvesim/pool_data/cache.py +++ /dev/null @@ -1,158 +0,0 @@ -from curvesim.exceptions import CurvesimValueError -from curvesim.logging import get_logger -from curvesim.network.subgraph import redemption_prices_sync as _redemption_prices -from curvesim.network.subgraph import volume_sync as _volume -from curvesim.pool.stableswap.metapool import CurveMetaPool -from curvesim.pool_data.metadata.base import PoolMetaDataInterface - -from .metadata import PoolMetaData - -logger = get_logger(__name__) - - -class PoolDataCache: - """ - Container for fetching and caching historical volume and redemption price data. - - Deprecation warning: this will likely be removed in a future release. - """ - - def __init__(self, metadata_dict, cache_data=False, days=60, end=None): - """ - Parameters - ---------- - metadata_dict : dict, :class:`PoolMetaDataInterface` - Pool metadata in the format returned by - :func:`curvesim.network.subgraph.pool_snapshot`. - - cache_data : bool, optional - If True, fetches and caches historical volume and redemption price. - - days : int, default=60 - Number of days to pull data for if caching. - - """ - if isinstance(metadata_dict, dict): - self.metadata = PoolMetaData(metadata_dict) - elif isinstance(metadata_dict, PoolMetaDataInterface): - self.metadata = metadata_dict - else: - raise CurvesimValueError( - "Metadata must be of type dict or PoolMetaDataInterface." - ) - - self.days = days - self.end = end - - self._cached_volume = None - self._cached_redemption_prices = None - - if cache_data: - self.set_cache() - - def set_cache(self): - """ - Fetches and caches historical volume and redemption price data. - - Parameters - ---------- - days : int, default=60 - number of days to pull data for - """ - self._cached_volume = self._get_volume() - self._cached_redemption_prices = self._get_redemption_prices() - - def clear_cache(self): - """ - Clears any cached data. - """ - self._cached_volume = None - self._cached_redemption_prices = None - - @property - def volume(self): - """ - Fetches the pool's historical volume over the specified number of days. - - Parameters - ---------- - days : int, default=60 - Number of days to pull data for. - - store : bool, default=False - If true, caches the fetched data. - - get_cache : bool, default=True - If true, returns cached data when available. - - Returns - ------- - numpy.ndarray - Total volume summed across the specified number of days. - - """ - if self._cached_volume is not None: - logger.info("Getting cached historical volume...") - return self._cached_volume - - return self._get_volume() - - def _get_volume(self): - logger.info("Fetching historical volume...") - addresses = self.metadata.address - chain = self.metadata.chain - days = self.days - end = self.end - - if issubclass(self.metadata.pool_type, CurveMetaPool): - # pylint: disable-next=protected-access - basepool_address = self.metadata._dict["basepool"]["address"] - addresses = [addresses, basepool_address] - vol = _volume(addresses, chain, days=days, end=end) - summed_vol = [sum(v) for v in vol] - else: - vol = _volume(addresses, chain, days=days, end=end) - summed_vol = sum(vol) - - return summed_vol - - @property - def redemption_prices(self): - """ - Fetches the pool's redemption price over the specified number of days. - - Note: only returns data for RAI3CRV pool. Otherwise, returns None. - - Parameters - ---------- - days : int, default=60 - Number of days to pull data for. - - store : bool, default=False - If True, caches the fetched data. - - get_cache : bool, default=True - If True, returns cached data when available. - - Returns - ------- - pandas.DataFrame - Timestamped redemption prices across the specified number of days. - - """ - if self._cached_redemption_prices is not None: - logger.info("Getting cached redemption prices...") - return self._cached_redemption_prices - - return self._get_redemption_prices() - - def _get_redemption_prices(self): - address = self.metadata.address - chain = self.metadata.chain - - days = self.days - end = self.end - - r = _redemption_prices(address, chain, days=days, end=end) - - return r diff --git a/curvesim/pool_data/queries/__init__.py b/curvesim/pool_data/queries/__init__.py new file mode 100644 index 000000000..e732838e2 --- /dev/null +++ b/curvesim/pool_data/queries/__init__.py @@ -0,0 +1,3 @@ +""" +Functions for fetching data about Curve pools. +""" diff --git a/curvesim/pool_data/queries.py b/curvesim/pool_data/queries/metadata.py similarity index 57% rename from curvesim/pool_data/queries.py rename to curvesim/pool_data/queries/metadata.py index e61bb7135..c4b967b6e 100644 --- a/curvesim/pool_data/queries.py +++ b/curvesim/pool_data/queries/metadata.py @@ -1,8 +1,11 @@ +""" +Functions to get pool metadata for Curve pools. +""" +from curvesim.network.subgraph import pool_snapshot_sync, symbol_address_sync +from curvesim.network.web3 import underlying_coin_info_sync +from curvesim.pool_data.metadata import PoolMetaData from curvesim.utils import get_event_loop -from ..network.subgraph import pool_snapshot_sync, symbol_address_sync -from ..network.web3 import underlying_coin_info_sync - def from_address(address, chain, env="prod", end_ts=None): """ @@ -50,3 +53,32 @@ def from_symbol(symbol, chain, env): data = from_address(address, chain, env) return data + + +def get_metadata( + address, + chain="mainnet", + env="prod", + end_ts=None, +): + """ + Pulls pool state and metadata from daily snapshot. + + Parameters + ---------- + address : str + Pool address prefixed with “0x”. + + chain : str + Chain/layer2 identifier, e.g. “mainnet”, “arbitrum”, “optimism". + + Returns + ------- + :class:`~curvesim.pool_data.metadata.PoolMetaDataInterface` + + """ + # TODO: validate function arguments + metadata_dict = from_address(address, chain, env=env, end_ts=end_ts) + metadata = PoolMetaData(metadata_dict) + + return metadata diff --git a/curvesim/pool_data/queries/pool_volume.py b/curvesim/pool_data/queries/pool_volume.py new file mode 100644 index 000000000..3d0525476 --- /dev/null +++ b/curvesim/pool_data/queries/pool_volume.py @@ -0,0 +1,128 @@ +""" +Functions to get historical volume for Curve pools. +""" + +from datetime import datetime, timezone +from math import comb +from typing import List, Optional, Tuple, Union + +from pandas import DataFrame, Series + +from curvesim.logging import get_logger +from curvesim.network.curve_prices import get_pool_pair_volume_sync +from curvesim.pool_data.metadata import PoolMetaDataInterface +from curvesim.utils import get_event_loop, get_pairs + +from .metadata import get_metadata + +logger = get_logger(__name__) + + +def get_pool_volume( + metadata_or_address: Union[PoolMetaDataInterface, str], + days: int = 60, + end: Optional[int] = None, + chain: Optional[str] = "mainnet", +) -> DataFrame: + """ + Gets historical daily volume for each pair of coins traded in a Curve pool. + + Parameters + ---------- + metadata_or_address: PoolMetaDataInterface or str + Pool metadata or pool address to fetch metadata. + + days: int, defaults to 60 + Number of days to pull volume data for. + + end: int, defaults to start of current date + Posix timestamp of the last time to pull data for. + + chain: str, default "mainnet" + Chain to use if pool address is provided to fetch metadata. + + Returns + ------- + DataFrame + Rows: DateTimeIndex, Columns: pairwise tuples of coin symbols + + """ + + logger.info("Fetching historical pool volume...") + + if isinstance(metadata_or_address, str): + pool_metadata: PoolMetaDataInterface = get_metadata(metadata_or_address, chain) + else: + pool_metadata = metadata_or_address + + pair_data = _get_pair_data(pool_metadata) + start_ts, end_ts = _process_timestamps(days, end) + loop = get_event_loop() + + volumes: dict[Tuple[str, str], Series] = {} + for pool_address, pair_addresses, pair_symbols in pair_data: + data: DataFrame = get_pool_pair_volume_sync( + pool_address, + *pair_addresses, + start_ts, + end_ts, + chain=pool_metadata.chain, + event_loop=loop, + ) + volumes[pair_symbols] = data["volume"] + + volume_df = _make_volume_df(volumes, days) + return volume_df + + +def _get_pair_data(pool_metadata) -> List[Tuple[str, Tuple[str, str], Tuple[str, str]]]: + coin_addresses = _get_coin_addresses(pool_metadata) + pair_symbols = get_pairs(pool_metadata.coin_names) + pair_addresses = get_pairs(coin_addresses) + + if isinstance(pool_metadata.n, list): + pool_addresses = _get_metapool_addresses(pool_metadata) + else: + pool_addresses = [pool_metadata.address] * comb(pool_metadata.n, 2) + return list(zip(pool_addresses, pair_addresses, pair_symbols)) + + +def _get_coin_addresses(pool_metadata) -> List[str]: + # pylint: disable=protected-access + if "wrapper" in pool_metadata._dict["coins"]: + return pool_metadata._dict["coins"]["wrapper"]["addresses"] + + return pool_metadata.coins + + +def _get_metapool_addresses(pool_metadata) -> List[str]: + n_meta = pool_metadata.n[0] - 1 + n_base = pool_metadata.n[1] + address_meta = pool_metadata.address + # pylint: disable-next=protected-access + address_base = pool_metadata._dict["basepool"]["address"] + + n_pairs_meta = comb(n_meta, 2) + n_meta * n_base + n_pairs_base = comb(n_base, 2) + + return [address_meta] * n_pairs_meta + [address_base] * n_pairs_base + + +def _process_timestamps(days, end) -> Tuple[int, int]: + end = end or int( + datetime.now(timezone.utc) + .replace(hour=0, minute=0, second=0, microsecond=0) + .timestamp() + ) + start = end - days * 86400 + return start, end + + +def _make_volume_df(volumes, days) -> DataFrame: + df = DataFrame(volumes) + df.columns = df.columns.to_flat_index() + if len(df) > days: + df = df[-days:] + logger.info("Days of volume returned:\n%s", df.count().to_string()) + df.fillna(0, inplace=True) + return df diff --git a/curvesim/sim/__init__.py b/curvesim/sim/__init__.py index 073a6df60..8d2094e1d 100644 --- a/curvesim/sim/__init__.py +++ b/curvesim/sim/__init__.py @@ -23,7 +23,6 @@ def autosim( pool=None, chain="mainnet", pool_metadata=None, - pool_data_cache=None, env="prod", **kwargs, ): @@ -56,10 +55,6 @@ def autosim( .. note:: Either `pool` or `pool_metadata` must be provided. - pool_data_cache: PoolDataCache, optional - Cached data used in sims. Useful for replication of results and - avoiding re-fetches of data. - A: int or iterable of int, optional Amplification coefficient. This controls the curvature of the stableswap bonding curve. Increased values makes the curve @@ -149,7 +144,6 @@ def autosim( results = volume_limited_arbitrage( pool_metadata, - pool_data_cache, variable_params=p_var, fixed_params=p_fixed, **kwargs, diff --git a/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_run.pickle b/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_run.pickle index a8fb61718..a17ab9efd 100644 Binary files a/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_run.pickle and b/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_run.pickle differ diff --git a/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_trade.pickle b/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_trade.pickle index d48886c5d..dcdf528c6 100644 Binary files a/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_trade.pickle and b/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_per_trade.pickle differ diff --git a/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_summary.pickle b/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_summary.pickle index 31249240d..6fd52c489 100644 Binary files a/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_summary.pickle and b/test/data/0x4ebdf703948ddcea3b11f675b4d1fba9d2414a14-results_summary.pickle differ diff --git a/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_run.pickle b/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_run.pickle index 97eaa3993..841787190 100644 Binary files a/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_run.pickle and b/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_run.pickle differ diff --git a/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_trade.pickle b/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_trade.pickle index f6c11ee32..ef148a1df 100644 Binary files a/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_trade.pickle and b/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_per_trade.pickle differ diff --git a/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_summary.pickle b/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_summary.pickle index d2c084956..b72f3a984 100644 Binary files a/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_summary.pickle and b/test/data/0xbebc44782c7db0a1a60cb6fe97d0b483032ff1c7-results_summary.pickle differ diff --git a/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_run.pickle b/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_run.pickle index 4dee07dd5..9c2a6b0aa 100644 Binary files a/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_run.pickle and b/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_run.pickle differ diff --git a/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_trade.pickle b/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_trade.pickle index 8380d23b4..db6b19283 100644 Binary files a/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_trade.pickle and b/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_per_trade.pickle differ diff --git a/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_summary.pickle b/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_summary.pickle index 00598949a..4ddc046f8 100644 Binary files a/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_summary.pickle and b/test/data/0xd632f22692fac7611d2aa1c0d552930d43caed3b-results_summary.pickle differ diff --git a/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_run.pickle b/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_run.pickle index ba4cf8af4..fb7b10678 100644 Binary files a/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_run.pickle and b/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_run.pickle differ diff --git a/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_trade.pickle b/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_trade.pickle index 98284988a..de27030bb 100644 Binary files a/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_trade.pickle and b/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_per_trade.pickle differ diff --git a/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_summary.pickle b/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_summary.pickle index 8bc4405ec..3c31debaf 100644 Binary files a/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_summary.pickle and b/test/data/0xdebf20617708857ebe4f679508e7b7863a8a8eee-results_summary.pickle differ diff --git a/test/integration/test_get_pool_volume.py b/test/integration/test_get_pool_volume.py new file mode 100644 index 000000000..bb8f726bf --- /dev/null +++ b/test/integration/test_get_pool_volume.py @@ -0,0 +1,37 @@ +from curvesim.pool_data import get_pool_volume +from curvesim.pool_data.metadata import PoolMetaData +from curvesim.utils import get_pairs + +from ..unit.test_pool_metadata import ( + cryptopool_test_metadata, + metapool_test_metadata, + pool_test_metadata, + tricrypto_ng_test_metadata, +) + + +def test_get_pool_volume(): + """Test the volume query.""" + metadata_list = [ + cryptopool_test_metadata, + metapool_test_metadata, + pool_test_metadata, + tricrypto_ng_test_metadata, + ] + + for metadata in metadata_list: + pool_metadata = PoolMetaData(metadata) + + # Test using metadata + volumes1 = get_pool_volume(pool_metadata, days=2, end=1698292800) + assert len(volumes1) == 2 + assert volumes1.columns.to_list() == get_pairs(pool_metadata.coin_names) + + # Test using address and chain + address = pool_metadata.address + chain = pool_metadata.chain + volumes2 = get_pool_volume(address, chain=chain, days=2, end=1698292800) + assert len(volumes2) == 2 + assert volumes2.columns.to_list() == get_pairs(pool_metadata.coin_names) + + assert all(volumes1 == volumes2) diff --git a/test/integration/test_subgraph.py b/test/integration/test_subgraph.py index 0e0ae10f4..401386ba7 100644 --- a/test/integration/test_subgraph.py +++ b/test/integration/test_subgraph.py @@ -1,25 +1,12 @@ import pytest from curvesim.exceptions import SubgraphError -from curvesim.network.subgraph import _volume, pool_snapshot +from curvesim.network.subgraph import pool_snapshot from curvesim.network.utils import sync ZERO_ADDRESS = "0x" + "00" * 20 -def test_convex_subgraph_volume_query(): - """Test the volume query.""" - - chain = "mainnet" - address = "0xbEbc44782C7dB0a1A60Cb6fe97d0b483032FF1C7" - _volume_sync = sync(_volume) - volumes = _volume_sync(address, chain, env="prod", days=2, end=1686700800) - assert len(volumes) == 2 - - volumes = _volume_sync(ZERO_ADDRESS, chain, env="prod", days=2, end=1686700800) - assert len(volumes) == 0 - - def test_convex_subgraph_stableswap_snapshot_query(): """Test the pool snapshot query for stableswap.""" diff --git a/test/unit/test_pool_volume.py b/test/unit/test_pool_volume.py new file mode 100644 index 000000000..11bf27d3c --- /dev/null +++ b/test/unit/test_pool_volume.py @@ -0,0 +1,87 @@ +from math import comb + +from curvesim.pool_data.queries.pool_volume import _get_pair_data + + +class DummyMetadata: + def __init__(self, address, coin_names, coin_addresses, n, basepool_address=None): + self.address = address + self.coin_names = coin_names + self.coins = coin_addresses + self.n = n + self._dict = { + "basepool": {"address": basepool_address}, + "coins": coin_addresses, + } + + +def make_dummy_metadata(n): + if isinstance(n, list): + n_total = sum(n) - 1 + else: + n_total = n + + kwargs = { + "address": "pool_address", + "coin_names": ["SYM" + str(i) for i in range(n_total)], + "coin_addresses": ["ADR" + str(i) for i in range(n_total)], + "n": n, + "basepool_address": "basepool_address", + } + + return DummyMetadata(**kwargs), n_total + + +def test_get_pair_data(): + for n in range(2, 5): + pool_metadata, n_total = make_dummy_metadata(n) + pair_data = _get_pair_data(pool_metadata) + pair_data = tuple(pair_data) + pool_addresses, pair_addresses, pair_symbols = zip(*pair_data) + + # Ensure correct length + assert len(pair_data) == comb(n_total, 2) + + # Ensure correct pool address + assert pool_addresses == ("pool_address",) * comb(n_total, 2) + + # Ensure all pairs unique + assert len(pair_addresses) == len(set(pair_addresses)) # all pairs unique + assert len(pair_symbols) == len(set(pair_symbols)) # all pairs unique + + # Ensure all items present + assert {x for pair in pair_addresses for x in pair} == set(pool_metadata.coins) + assert {x for pair in pair_symbols for x in pair} == set( + pool_metadata.coin_names + ) + + +def test_get_pair_data_metapool(): + n_list = [[n1, n2] for n1 in range(2, 4) for n2 in range(2, 5)] + for n in n_list: + pool_metadata, n_total = make_dummy_metadata(n) + pair_data = _get_pair_data(pool_metadata) + pool_addresses, pair_addresses, pair_symbols = zip(*pair_data) + + # Ensure correct length + assert len(pair_data) == comb(n_total, 2) + + # Ensure correct pool addresses + n_meta = n[0] - 1 + n_base = n[1] + n_pairs_meta = comb(n_meta, 2) + n_meta * n_base + n_pairs_base = comb(n_base, 2) + assert ( + pool_addresses + == ("pool_address",) * n_pairs_meta + ("basepool_address",) * n_pairs_base + ) + + # Ensure all pairs unique + assert len(pair_addresses) == len(set(pair_addresses)) # all pairs unique + assert len(pair_symbols) == len(set(pair_symbols)) # all pairs unique + + # Ensure all items present + assert {x for pair in pair_addresses for x in pair} == set(pool_metadata.coins) + assert {x for pair in pair_symbols for x in pair} == set( + pool_metadata.coin_names + )