diff --git a/.gitignore b/.gitignore index 88fbe9bd7a..857a61a123 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,14 @@ # Environment variables *.env +# Data +*.csv +*.parquet +*.png + +# Weights and biases +wandb + # user configs .configs diff --git a/lib/agent0/agent0/hyperdrive/interactive/interactive_hyperdrive.py b/lib/agent0/agent0/hyperdrive/interactive/interactive_hyperdrive.py index 792859a641..d9aa863be7 100644 --- a/lib/agent0/agent0/hyperdrive/interactive/interactive_hyperdrive.py +++ b/lib/agent0/agent0/hyperdrive/interactive/interactive_hyperdrive.py @@ -15,9 +15,8 @@ from chainsync import PostgresConfig from chainsync.dashboard.usernames import build_user_mapping from chainsync.db.base import add_addr_to_username, get_addr_to_username, get_username_to_user, initialize_session -from chainsync.db.hyperdrive import get_checkpoint_info -from chainsync.db.hyperdrive import get_current_wallet as chainsync_get_current_wallet from chainsync.db.hyperdrive import ( + get_checkpoint_info, get_latest_block_number_from_analysis_table, get_pool_analysis, get_pool_config, @@ -27,6 +26,7 @@ get_wallet_deltas, get_wallet_pnl, ) +from chainsync.db.hyperdrive import get_current_wallet as chainsync_get_current_wallet from chainsync.exec import acquire_data, data_analysis from eth_account.account import Account from eth_typing import BlockNumber, ChecksumAddress @@ -133,6 +133,8 @@ class Config: The upper bound on the governance lp fee that governance can set. max_governance_zombie_fee: FixedPoint The upper bound on the governance zombie fee that governance can set. + calc_pnl: bool + Whether to calculate pnl. Defaults to True. """ # Environment variables @@ -159,6 +161,7 @@ class Config: max_flat_fee: FixedPoint = FixedPoint("0.0015") # 0.15% max_governance_lp_fee: FixedPoint = FixedPoint("0.30") # 30% max_governance_zombie_fee: FixedPoint = FixedPoint("0.30") # 30% + calc_pnl: bool = True def __post_init__(self): # Random generator @@ -195,6 +198,7 @@ def __init__(self, chain: Chain, config: Config | None = None): full_path = os.path.realpath(__file__) current_file_dir, _ = os.path.split(full_path) abi_dir = os.path.join(current_file_dir, "..", "..", "..", "..", "..", "packages", "hyperdrive", "src", "abis") + self.calc_pnl = config.calc_pnl self.eth_config = EthConfig( artifacts_uri="not_used", @@ -381,6 +385,7 @@ def _run_blocking_data_pipeline(self, start_block: int | None = None) -> None: db_session=self.db_session, exit_on_catch_up=True, suppress_logs=True, + calc_pnl=self.calc_pnl, ) def _cleanup(self): @@ -618,8 +623,7 @@ def get_checkpoint_info(self, coerce_float: bool = True) -> pd.DataFrame: # DB read calls ensures data pipeline is caught up before returning if self.chain.experimental_data_threading: self._ensure_data_caught_up() - out = get_checkpoint_info(self.db_session, coerce_float=coerce_float) - return out + return get_checkpoint_info(self.db_session, coerce_float=coerce_float) def _add_username_to_dataframe(self, df: pd.DataFrame, addr_column: str): addr_to_username = get_addr_to_username(self.db_session) @@ -637,7 +641,8 @@ def _adjust_base_positions(self, in_df: pd.DataFrame, value_column: str, coerce_ if coerce_float: out_df.loc[row_idxs, value_column] += float(initial_balance) else: - out_df.loc[row_idxs, value_column] += Decimal(str(initial_balance)) + # Pandas is smart enough to handle "+=" for "Series[Unknown]" and "Decimal" + out_df.loc[row_idxs, value_column] += Decimal(str(initial_balance)) # type: ignore return out_df def get_current_wallet(self, coerce_float: bool = True) -> pd.DataFrame: diff --git a/lib/agent0/examples/interactive_econ.py b/lib/agent0/examples/interactive_econ.py deleted file mode 100644 index a7224bb629..0000000000 --- a/lib/agent0/examples/interactive_econ.py +++ /dev/null @@ -1,400 +0,0 @@ -# %% -"""Run experiments of economic activity. - -We want to better understand return profiles of participants in Hyperdrive. -To do so, we run various scenarios of plausible economic activity. -We target a certain amount of daily activity, as a percentage of the liquidity provided. -That trading activity is executed by a random agent named Rob. -The liquidity is provided by an agent named Larry. -At the end, we close out all positions, and evaluate results based off the WETH in their wallets. -""" -# Variables by themselves print out dataframes in a nice format in interactive mode -# pylint: disable=pointless-statement - -import datetime -import os -import sys -import time -from copy import deepcopy -from dataclasses import dataclass, field, fields -from typing import NamedTuple - -import numpy as np -import pandas as pd -from fixedpointmath import FixedPoint -from matplotlib import pyplot as plt - -from agent0.hyperdrive.interactive import InteractiveHyperdrive, LocalChain - -# pylint: disable=bare-except -# ruff: noqa: A001 (allow shadowing a python builtin) -# using the variable "max" -# pylint: disable=redefined-builtin -# don't make me use upper case variable names -# pylint: disable=invalid-name - - -# %% -# check interactive status -def running_interactive(): - """Check if we are running in interactive mode. - - Returns - ------- - bool - Whether we are running in interactive mode. - """ - try: - from IPython.core.getipython import get_ipython # pylint: disable=import-outside-toplevel - - return bool("ipykernel" in sys.modules and get_ipython()) - except ImportError: - return False - - -if RUNNING_INTERACTIVE := running_interactive(): - from IPython.display import display # pylint: disable=import-outside-toplevel - - print("Running in interactive mode.") -else: # being run from the terminal or something similar - display = print # pylint: disable=redefined-builtin,unused-import - print("Running in non-interactive mode.") - - -# %% -# config -@dataclass -class ExperimentConfig: # pylint: disable=too-many-instance-attributes - """Everything needed for my experiment.""" - - daily_volume_percentage_of_liquidity: float = 0.01 # 1% - term_days: int = 20 - float_fmt: str = ",.0f" - display_cols: list[str] = field( - default_factory=lambda: ["block_number", "username", "position", "pnl", "base_token_type", "maturity_time"] - ) - display_cols_with_hpr: list[str] = field( - default_factory=lambda: [ - "block_number", - "username", - "position", - "pnl", - "hpr", - "apr", - "base_token_type", - "maturity_time", - ] - ) - amount_of_liquidity: int = 10_000_000 - curve_fee: FixedPoint = FixedPoint("0.01") # 1%, 10% default - flat_fee: FixedPoint = FixedPoint("0.0001") # 1bps, 5bps default - governance_fee: FixedPoint = FixedPoint("0.1") # 10%, 15% default - randseed: int = 0 - - def __post_init__(self): - """Calculate parameters for the experiment.""" - self.term_seconds: int = 60 * 60 * 24 * self.term_days - - # used to scale up to the equivalent of a year - scaling_ratio = 365 / self.term_days - # this interest rate gives us the same price as a 3.% fixed rate for 1 year - rate_required_for_same_price: float = min(1, 0.035 * scaling_ratio) - self.starting_fixed_rate: FixedPoint = FixedPoint(rate_required_for_same_price) - self.starting_variable_rate: FixedPoint = FixedPoint(rate_required_for_same_price) - - -exp = ExperimentConfig() -for key in os.environ: - if key in fields(exp): - setattr(exp, key, os.environ[key]) -rng = np.random.default_rng(seed=exp.randseed) - -# %% -# set up chain -chain = LocalChain(LocalChain.Config()) - -# %% -# Parameters for pool initialization. If empty, defaults to default values, allows for custom values if needed -config = InteractiveHyperdrive.Config( - position_duration=exp.term_seconds, - checkpoint_duration=60 * 60 * 24, # 1 day - initial_liquidity=FixedPoint(20), - initial_fixed_rate=exp.starting_fixed_rate, - initial_variable_rate=exp.starting_variable_rate, - curve_fee=exp.curve_fee, - flat_fee=exp.flat_fee, - governance_lp_fee=exp.governance_fee, -) -MINIMUM_TRANSACTION_AMOUNT = config.minimum_transaction_amount -for k, v in config.__dict__.items(): - print(f"{k:26} : {v}") -print(f"{'term length':27}: {exp.term_days}") -interactive_hyperdrive = InteractiveHyperdrive(chain, config) -print(f"spot price = {interactive_hyperdrive.hyperdrive_interface.calc_spot_price()}") - -# %% -# set up agents -larry = interactive_hyperdrive.init_agent(base=FixedPoint(exp.amount_of_liquidity), name="larry") -larry.add_liquidity(base=FixedPoint(exp.amount_of_liquidity)) # 10 million -rob = interactive_hyperdrive.init_agent(base=FixedPoint(exp.amount_of_liquidity), name="rob") -# this verifies that spot price does not change after adding liquidity -print(f"spot price after adding liquidity = {interactive_hyperdrive.hyperdrive_interface.calc_spot_price()}") - -# %% -# do some trades -Max = NamedTuple("Max", [("base", FixedPoint), ("bonds", FixedPoint)]) -GetMax = NamedTuple("GetMax", [("long", Max), ("short", Max)]) - -start_time = time.time() - - -def get_max( - _interactive_hyperdrive: InteractiveHyperdrive, _share_price: FixedPoint, _current_base: FixedPoint -) -> GetMax: - """Get max trade sizes. - - Returns - ------- - GetMax - A NamedTuple containing the max long in base, max long in bonds, max short in bonds, and max short in base. - """ - max_long_base = _interactive_hyperdrive.hyperdrive_interface.calc_max_long(budget=_current_base) - max_long_shares = _interactive_hyperdrive.hyperdrive_interface.calc_shares_out_given_bonds_in_down(max_long_base) - max_long_bonds = max_long_shares * _share_price - max_short_bonds = FixedPoint(0) - try: # sourcery skip: do-not-use-bare-except - max_short_bonds = _interactive_hyperdrive.hyperdrive_interface.calc_max_short(budget=_current_base) - except Exception as exc: # pylint: disable=broad-exception-caught - print("Error calculating max short bonds: %s. ", exc) - max_short_shares = _interactive_hyperdrive.hyperdrive_interface.calc_shares_out_given_bonds_in_down(max_short_bonds) - max_short_base = max_short_shares * _share_price - return GetMax( - Max(max_long_base, max_long_bonds), - Max(max_short_base, max_short_bonds), - ) - - -# sourcery skip: avoid-builtin-shadow, do-not-use-bare-except, invert-any-all, -# remove-unnecessary-else, swap-if-else-branches -for day in range(exp.term_days): - amount_to_trade_base = FixedPoint(exp.amount_of_liquidity * exp.daily_volume_percentage_of_liquidity) - while amount_to_trade_base > MINIMUM_TRANSACTION_AMOUNT: - spot_price = interactive_hyperdrive.hyperdrive_interface.calc_spot_price() - share_price = interactive_hyperdrive.hyperdrive_interface.current_pool_state.pool_info.share_price - max = None - wallet = rob.wallet - event = None - if rng.random() < 0.5: # go long 50% of the time - if len(wallet.shorts) > 0: # check if we have shorts, and close them if we do - for maturity_time, short in wallet.shorts.copy().items(): - max = get_max(interactive_hyperdrive, share_price, rob.wallet.balance.amount) - amount_to_trade_bonds = ( - interactive_hyperdrive.hyperdrive_interface.calc_bonds_out_given_shares_in_down( - amount_to_trade_base / share_price - ) - ) - trade_size_bonds = min(amount_to_trade_bonds, short.balance, max.long.bonds) - if trade_size_bonds > MINIMUM_TRANSACTION_AMOUNT: - event = rob.close_short(maturity_time, trade_size_bonds) - amount_to_trade_base -= event.base_amount - if amount_to_trade_base <= 0: - break # stop looping across shorts if we've traded enough - if amount_to_trade_base > 0: - max = get_max(interactive_hyperdrive, share_price, rob.wallet.balance.amount) - trade_size_base = min(amount_to_trade_base, max.long.base) - if trade_size_base > MINIMUM_TRANSACTION_AMOUNT: - event = rob.open_long(trade_size_base) - amount_to_trade_base -= event.base_amount - else: # go short 50% of the time - if len(wallet.longs) > 0: # check if we have longs, and close them if we do - for maturity_time, long in wallet.longs.copy().items(): - max = get_max(interactive_hyperdrive, share_price, rob.wallet.balance.amount) - amount_to_trade_bonds = ( - interactive_hyperdrive.hyperdrive_interface.calc_bonds_out_given_shares_in_down( - amount_to_trade_base / share_price - ) - ) - trade_size_bonds = min(amount_to_trade_bonds, long.balance, max.short.bonds) - if trade_size_bonds > MINIMUM_TRANSACTION_AMOUNT: - event = rob.close_long(maturity_time, trade_size_bonds) - amount_to_trade_base -= event.base_amount - if amount_to_trade_base <= 0: - break # stop looping across longs if we've traded enough - if amount_to_trade_base > 0: - max = get_max(interactive_hyperdrive, share_price, rob.wallet.balance.amount) - amount_to_trade_bonds = interactive_hyperdrive.hyperdrive_interface.calc_bonds_out_given_shares_in_down( - amount_to_trade_base / share_price - ) - trade_size_bonds = min(amount_to_trade_bonds, max.short.bonds) - if trade_size_bonds > MINIMUM_TRANSACTION_AMOUNT: - event = rob.open_short(trade_size_bonds) - amount_to_trade_base -= event.base_amount - print(f"day {day}: {event}") # type: ignore (PossiblyUnboundVariable) - if amount_to_trade_base <= 0: - break # end the day if we've traded enough - chain.advance_time(datetime.timedelta(days=1), create_checkpoints=False) -print(f"experiment finished in {(time.time() - start_time):,.2f} seconds") - -# %% -# close all positions -print("wallets before liquidation:") -current_wallet = interactive_hyperdrive.get_current_wallet() -display( - current_wallet.loc[current_wallet.token_type != "WETH", exp.display_cols] - .style.format( - subset=[col for col in current_wallet.columns if current_wallet.dtypes[col] == "float64"], - formatter="{:" + exp.float_fmt + "}", - ) - .hide(axis="index") -) -rob.liquidate() -larry.remove_liquidity(shares=larry.wallet.lp_tokens) -print("wallets after liquidation:") -current_wallet = interactive_hyperdrive.get_current_wallet() -display( - current_wallet.loc[current_wallet.token_type != "WETH", exp.display_cols] - .style.format( - subset=[col for col in current_wallet.columns if current_wallet.dtypes[col] == "float64"], - formatter="{:" + exp.float_fmt + "}", - ) - .hide(axis="index") -) - -# %% -# show WETH balance after closing all positions -pool_info = interactive_hyperdrive.get_pool_state() -print(f"starting fixed rate is {float(pool_info.fixed_rate.iloc[0]):7.2%}") -print(f" ending fixed rate is {float(pool_info.fixed_rate.iloc[-1]):7.2%}") -governance_fees = float(interactive_hyperdrive.hyperdrive_interface.get_gov_fees_accrued(block_number=None)) -current_wallet = deepcopy(interactive_hyperdrive.get_current_wallet()) - -# index -non_weth_index = (current_wallet.token_type != "WETH") & (current_wallet.position > float(MINIMUM_TRANSACTION_AMOUNT)) -weth_index = current_wallet.token_type == "WETH" -# simple PNL based on WETH balance -current_wallet.loc[weth_index, ["pnl"]] = current_wallet.loc[weth_index, ["position"]].values - exp.amount_of_liquidity -# add HPR -current_wallet.loc[:, ["hpr"]] = current_wallet["pnl"] / (current_wallet["position"] - current_wallet["pnl"]) - -wallet_positions = deepcopy(interactive_hyperdrive.get_wallet_positions()) -wallet_positions_by_block = ( - wallet_positions.loc[wallet_positions.token_type == "WETH", :] - .pivot( - index="block_number", - columns="username", - values="position", - ) - .reset_index() -) -wallet_positions_by_time = ( - wallet_positions.loc[wallet_positions.token_type == "WETH", :] - .pivot( - index="timestamp", - columns="username", - values="position", - ) - .reset_index() -) -wallet_positions_by_block.loc[:, ["rob"]] = ( - wallet_positions_by_block["rob"].max() - wallet_positions_by_block["rob"] -).fillna(0) -wallet_positions_by_time.loc[:, ["rob"]] = ( - wallet_positions_by_time["rob"].max() - wallet_positions_by_time["rob"] -).fillna(0) -wallet_positions_by_block["block_number_delta"] = wallet_positions_by_block["block_number"].diff().fillna(0) -wallet_positions_by_time["timestamp_delta"] = wallet_positions_by_time["timestamp"].diff().dt.total_seconds().fillna(0) -average_by_block = np.average(wallet_positions_by_block["rob"], weights=wallet_positions_by_block["block_number_delta"]) -average_by_time = np.average(wallet_positions_by_time["rob"], weights=wallet_positions_by_time["timestamp_delta"]) -if RUNNING_INTERACTIVE: - fig, ax = plt.subplots(2, 1, figsize=(8, 8)) - ax[0].step(wallet_positions_by_block["block_number"], wallet_positions_by_block["rob"], label="rob's WETH spend") - ax[0].axhline(y=average_by_block, color="red", label=f"weighted average by block = {average_by_block:,.0f}") - ax[0].legend() - ax[1].step(wallet_positions_by_time["timestamp"], wallet_positions_by_time["rob"], label="rob's WETH spend") - ax[1].axhline(y=average_by_time, color="red", label=f"weighted average by time = {average_by_time:,.0f}") - ax[1].legend() - plt.show() -idx = weth_index & (current_wallet.username == "rob") -current_wallet.loc[idx, ["position"]] = average_by_time # type: ignore -current_wallet.loc[idx, ["hpr"]] = ( - current_wallet.loc[idx, ["pnl"]].astype("float").iloc[0].values - / current_wallet.loc[idx, ["position"]].astype("float").iloc[0].values -) # type: ignore - -# add governance row -new_row = current_wallet.iloc[len(current_wallet) - 1].copy() -new_row["username"] = "governance" -new_row["position"], new_row["pnl"] = governance_fees, governance_fees -new_row["hpr"] = np.inf -new_row["token_type"] = "WETH" -current_wallet = pd.concat([current_wallet, new_row.to_frame().T], ignore_index=True) - -# add total row -new_row = current_wallet.iloc[len(current_wallet) - 1].copy() -new_row["username"] = "total" -new_row["position"] = float(current_wallet["position"].values.sum()) # type: ignore -new_row["pnl"] = current_wallet.loc[current_wallet.token_type.values == "WETH", ["pnl"]].values.sum() # type: ignore -new_row["hpr"] = new_row["pnl"] / (new_row["position"] - new_row["pnl"]) -new_row["token_type"] = "WETH" -current_wallet = pd.concat([current_wallet, new_row.to_frame().T], ignore_index=True) - -# add share price row -new_row = current_wallet.iloc[len(current_wallet) - 1].copy() -new_row["username"] = "share price" -new_row["position"] = pool_info.share_price.iloc[-1] * 1e7 -new_row["pnl"] = pool_info.share_price.iloc[-1] * 1e7 - pool_info.share_price.iloc[0] * 1e7 -new_row["hpr"] = pool_info.share_price.iloc[-1] / pool_info.share_price.iloc[0] - 1 -new_row["token_type"] = "WETH" -current_wallet = pd.concat([current_wallet, new_row.to_frame().T], ignore_index=True) - -# re-index -non_weth_index = (current_wallet.token_type != "WETH") & (current_wallet.position > float(MINIMUM_TRANSACTION_AMOUNT)) -weth_index = current_wallet.token_type == "WETH" -# convert to float -current_wallet.position = current_wallet.position.astype(float) -current_wallet.pnl = current_wallet.pnl.astype(float) - -# time passed -time_passed_days = (pool_info.timestamp.iloc[-1] - pool_info.timestamp.iloc[0]).total_seconds() / 60 / 60 / 24 -print(f"time passed = {time_passed_days:.2f} days") -apr_factor = 365 / time_passed_days -print(f"to scale APR from HPR we multiply by {apr_factor:,.0f} (365/{time_passed_days:.2f})") -print(f"share price went from {pool_info.share_price.iloc[0]:.4f} to {pool_info.share_price.iloc[-1]:.4f}") -# add APR -current_wallet.loc[:, ["apr"]] = current_wallet.loc[:, ["hpr"]].values * apr_factor - -results1 = current_wallet.loc[non_weth_index, exp.display_cols] -results2 = current_wallet.loc[weth_index, exp.display_cols_with_hpr] -results1.to_csv("results1.csv") -results2.to_csv("results2.csv") -# display final results -if non_weth_index.sum() > 0: - print("material non-WETH positions:") - if RUNNING_INTERACTIVE: - display(results1.style.hide(axis="index")) - else: - print(results1) -else: - print("no material non-WETH positions") -print("WETH positions:") -if RUNNING_INTERACTIVE: - display( - results2.style.format( - subset=[ - col - for col in current_wallet.columns - if current_wallet.dtypes[col] == "float64" and col not in ["hpr", "apr"] - ], - formatter="{:" + exp.float_fmt + "}", - ) - .hide(axis="index") - .format( - subset=["hpr", "apr"], - formatter="{:.2%}", - ) - .hide(axis="columns", subset=["base_token_type", "maturity_time"]) - ) -else: - print(results2) - -# %% diff --git a/lib/chainsync/chainsync/analysis/calc_pnl.py b/lib/chainsync/chainsync/analysis/calc_pnl.py index 72f7d47120..ae85af7a9d 100644 --- a/lib/chainsync/chainsync/analysis/calc_pnl.py +++ b/lib/chainsync/chainsync/analysis/calc_pnl.py @@ -12,7 +12,9 @@ from web3.contract.contract import Contract -def calc_single_closeout(position: pd.Series, contract: Contract, pool_info: pd.DataFrame, min_output: int) -> Decimal: +def calc_single_closeout( + position: pd.Series, contract: Contract, min_output: int, lp_share_price: FixedPoint +) -> Decimal: """Calculate the closeout pnl for a single position. Arguments @@ -21,10 +23,10 @@ def calc_single_closeout(position: pd.Series, contract: Contract, pool_info: pd. The position to calculate the closeout pnl for (one row in current_wallet) contract: Contract The contract object - pool_info: pd.DataFrame - The pool info min_output: int The minimum output to be accepted, as part of slippage tolerance + lp_share_price: FixedPoint + The price of an LP share in units of the base token Returns ------- @@ -37,22 +39,14 @@ def calc_single_closeout(position: pd.Series, contract: Contract, pool_info: pd. # If no value, pnl is 0 if position["value"] == 0: return Decimal(0) - assert len(position.shape) == 1, "Only one position at a time" amount = FixedPoint(f"{position['value']:f}").scaled_value address = position["wallet_address"] tokentype = position["base_token_type"] sender = ChecksumAddress(HexAddress(HexStr(address))) preview_result = None maturity = 0 - if tokentype in ["LONG", "SHORT"]: - maturity = position["maturity_time"] - assert isinstance(maturity, Decimal) - maturity = int(maturity) - assert isinstance(maturity, int) - assert isinstance(tokentype, str) out_pnl = Decimal("nan") - if tokentype == "LONG": fn_args = ( maturity, @@ -91,46 +85,12 @@ def calc_single_closeout(position: pd.Series, contract: Contract, pool_info: pd. except Exception as exception: # pylint: disable=broad-except logging.warning("Exception caught, ignoring: %s", exception) - elif tokentype == "LP": - fn_args = ( - amount, - min_output, - ( # IHyperdrive.Options - address, # destination - True, # asBase - bytes(0), # extraData - ), - ) - # If this fails, keep as nan and continue iterating - try: - preview_result = smart_contract_preview_transaction( - contract, sender, "removeLiquidity", *fn_args, block_number=position["block_number"] - ) - out_pnl = Decimal( - preview_result["baseProceeds"] - # We assume all withdrawal shares are redeemable - + preview_result["withdrawalShares"] * pool_info["lp_share_price"].values[-1] - ) / Decimal(1e18) - except Exception as exception: # pylint: disable=broad-except - logging.warning("Exception caught, ignoring: %s", exception) - - elif tokentype == "WITHDRAWAL_SHARE": - fn_args = ( - amount, - min_output, - ( # IHyperdrive.Options - address, # destination - True, # asBase - bytes(0), # extraData - ), - ) - try: - # For PNL, we assume all withdrawal shares are redeemable - # even if there are no withdrawal shares available to withdraw - # Hence, we don't use preview transaction here - out_pnl = Decimal(amount * pool_info["lp_share_price"].values[-1]) / Decimal(1e18) - except Exception as exception: # pylint: disable=broad-except - logging.warning("Exception caught, ignoring: %s", exception) + # For PNL, we assume all withdrawal shares are redeemable + # even if there are no withdrawal shares available to withdraw + # Hence, we don't use preview transaction here + elif tokentype in ["LP", "WITHDRAWAL_SHARE"]: + out_pnl = amount * lp_share_price + out_pnl = Decimal(str(out_pnl)) / Decimal(1e18) else: # Should never get here raise ValueError(f"Unexpected token type: {tokentype}") @@ -138,7 +98,7 @@ def calc_single_closeout(position: pd.Series, contract: Contract, pool_info: pd. def calc_closeout_pnl( - current_wallet: pd.DataFrame, pool_info: pd.DataFrame, hyperdrive_contract: Contract + current_wallet: pd.DataFrame, hyperdrive_contract: Contract, lp_share_price: FixedPoint ) -> pd.DataFrame: """Calculate closeout value of agent positions. @@ -146,23 +106,20 @@ def calc_closeout_pnl( --------- current_wallet: pd.DataFrame A dataframe resulting from `get_current_wallet` that describes the current wallet position. - pool_info: pd.DataFrame - The pool info object. hyperdrive_contract: Contract The hyperdrive contract object. + lp_share_price: FixedPoint + The price of an LP share in units of the base token Returns ------- Decimal The closeout pnl """ - # Define a function to handle the calculation for each group - out_pnl = current_wallet.apply( + return current_wallet.apply( calc_single_closeout, # type: ignore contract=hyperdrive_contract, - pool_info=pool_info, min_output=0, + lp_share_price=lp_share_price, axis=1, ) - - return out_pnl diff --git a/lib/chainsync/chainsync/analysis/data_to_analysis.py b/lib/chainsync/chainsync/analysis/data_to_analysis.py index bf3f377fb6..0f7b578836 100644 --- a/lib/chainsync/chainsync/analysis/data_to_analysis.py +++ b/lib/chainsync/chainsync/analysis/data_to_analysis.py @@ -153,6 +153,7 @@ def data_to_analysis( pool_config: pd.Series, db_session: Session, hyperdrive_contract: Contract, + calc_pnl: bool = True, ) -> None: """Function to query postgres data tables and insert to analysis tables. Executes analysis on a batch of blocks, defined by start and end block. @@ -169,6 +170,8 @@ def data_to_analysis( The initialized db session. hyperdrive_contract: Contract The hyperdrive contract. + calc_pnl: bool + Whether to calculate pnl. Defaults to True. """ # Get data pool_info = get_pool_info(db_session, start_block, end_block, coerce_float=False) @@ -192,7 +195,10 @@ def data_to_analysis( # We can set a sample rate by doing batch processing on this function # since we only get the current wallet for the end_block wallet_pnl = get_current_wallet(db_session, end_block=end_block, coerce_float=False) - pnl_df = calc_closeout_pnl(wallet_pnl, pool_info, hyperdrive_contract) + if calc_pnl: + pnl_df = calc_closeout_pnl(wallet_pnl, hyperdrive_contract, pool_info["share_price"].iloc[-1]) + else: + pnl_df = np.nan # This sets the pnl to the current wallet dataframe, but there may be scaling issues here. # This is because the `CurrentWallet` table has one entry per change in wallet position, diff --git a/lib/chainsync/chainsync/exec/data_analysis.py b/lib/chainsync/chainsync/exec/data_analysis.py index dc432c929b..166a94ad98 100644 --- a/lib/chainsync/chainsync/exec/data_analysis.py +++ b/lib/chainsync/chainsync/exec/data_analysis.py @@ -36,6 +36,7 @@ def data_analysis( exit_on_catch_up: bool = False, exit_callback_fn: Callable[[], bool] | None = None, suppress_logs: bool = False, + calc_pnl: bool = True, ): """Execute the data acquisition pipeline. @@ -65,6 +66,8 @@ def data_analysis( Defaults to not set. suppress_logs: bool, optional If true, will suppress info logging from this function. Defaults to False. + calc_pnl: bool + Whether to calculate pnl. Defaults to True. """ # TODO implement logger instead of global logging to suppress based on module name. @@ -126,7 +129,9 @@ def data_analysis( analysis_end_block = latest_data_block_number + 1 if not suppress_logs: logging.info("Running batch %s to %s", analysis_start_block, analysis_end_block) - data_to_analysis(analysis_start_block, analysis_end_block, pool_config, db_session, hyperdrive_contract) + data_to_analysis( + analysis_start_block, analysis_end_block, pool_config, db_session, hyperdrive_contract, calc_pnl + ) curr_start_write_block = latest_data_block_number + 1 # Clean up resources on clean exit diff --git a/requirements.txt b/requirements.txt index 46ca018d33..8a615288f6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,4 @@ -e lib/hypertypes[base] --find-links="packages/hyperdrivepy" hyperdrivepy -rollbar \ No newline at end of file +rollbar