diff --git a/src/agent0/core/hyperdrive/agent/hyperdrive_wallet.py b/src/agent0/core/hyperdrive/agent/hyperdrive_wallet.py index 931246c060..dd31da044e 100644 --- a/src/agent0/core/hyperdrive/agent/hyperdrive_wallet.py +++ b/src/agent0/core/hyperdrive/agent/hyperdrive_wallet.py @@ -62,6 +62,8 @@ class Short: """The amount of bonds that the position is short.""" maturity_time: int """The maturity time of the short.""" + open_vault_share_price: FixedPoint = FixedPoint(0) + """The vault share price when the short was opened.""" @dataclass(kw_only=True) diff --git a/src/agent0/core/hyperdrive/exec/execute_agent_trades.py b/src/agent0/core/hyperdrive/exec/execute_agent_trades.py index 5006994e61..6b808b144a 100644 --- a/src/agent0/core/hyperdrive/exec/execute_agent_trades.py +++ b/src/agent0/core/hyperdrive/exec/execute_agent_trades.py @@ -272,7 +272,9 @@ async def async_match_contract_call_to_trade( ), shorts={ trade_result.maturity_time_seconds: Short( - balance=trade_result.bond_amount, maturity_time=trade_result.maturity_time_seconds + open_vault_share_price=trade_result.vault_share_price, + balance=trade_result.bond_amount, + maturity_time=trade_result.maturity_time_seconds, ) }, ) @@ -290,7 +292,8 @@ async def async_match_contract_call_to_trade( ), shorts={ trade.maturity_time: Short( - balance=-trade_result.bond_amount, maturity_time=trade_result.maturity_time_seconds + balance=-trade_result.bond_amount, + maturity_time=trade_result.maturity_time_seconds ) }, ) diff --git a/src/agent0/core/hyperdrive/policies/lpandarb.py b/src/agent0/core/hyperdrive/policies/lpandarb.py index 0870980be9..060e3c05f9 100644 --- a/src/agent0/core/hyperdrive/policies/lpandarb.py +++ b/src/agent0/core/hyperdrive/policies/lpandarb.py @@ -3,6 +3,7 @@ from __future__ import annotations import logging +import time from copy import deepcopy from dataclasses import dataclass from typing import TYPE_CHECKING @@ -18,6 +19,7 @@ open_long_trade, open_short_trade, ) +from agent0.core.hyperdrive.agent.hyperdrive_wallet import Long from agent0.core.utilities.predict import predict_long, predict_short from agent0.ethpy.hyperdrive.state import PoolState @@ -37,12 +39,66 @@ # pylint: disable=too-many-arguments +def _measure_value( + wallet: HyperdriveWallet, + interface: HyperdriveReadInterface, + pool_state: PoolState | None = None, + spot_price: FixedPoint | None = None, + block_time: int | None = None, +) -> tuple[FixedPoint, FixedPoint]: + # either provide interface or all of the other arguments + pool_state = interface.current_pool_state if pool_state is None else pool_state + spot_price = interface.calc_spot_price(pool_state) if spot_price is None else spot_price + block_time = interface.get_block_timestamp(interface.get_current_block()) if block_time is None else block_time + assert isinstance(pool_state, PoolState), "pool_state must be a PoolState" + assert isinstance(spot_price, FixedPoint), "spot_price must be a FixedPoint" + assert isinstance(block_time, int), "block_time must be an int" + + position_duration = pool_state.pool_config.position_duration + value = wallet.balance.amount # base in wallet + old_lp_share_price = pool_state.pool_info.lp_share_price + logging.info("old_lp_share_price is %s", old_lp_share_price) + logging.info("=== predicted_pool_state ===") + for k,v in pool_state.__dict__.items(): + if k not in ["block", "pool_info", "pool_config"]: + logging.info("%s : %s", k, v) + logging.info("=== predicted_pool_info ===") + for k,v in pool_state.pool_info.__dict__.items(): + logging.info("%s : %s", k, v) + new_lp_share_price = interface.calc_present_value(pool_state=pool_state) / pool_state.pool_info.lp_total_supply * pool_state.pool_info.vault_share_price + logging.info("new_lp_share_price is %s (%s%.2f%%)", new_lp_share_price, "+" if new_lp_share_price > old_lp_share_price else "",(new_lp_share_price / old_lp_share_price - 1)* 100) + # LP position + simple_lp_value = wallet.lp_tokens * new_lp_share_price + closeout_lp_value = wallet.lp_tokens * new_lp_share_price + value += closeout_lp_value + for maturity, long in wallet.longs.items(): + normalized_time_remaining = max(maturity - block_time, 0) / FixedPoint(position_duration) + value += interface.calc_close_long(long.balance, normalized_time_remaining, pool_state) + for maturity, short in wallet.shorts.items(): + normalized_time_remaining = max(maturity - block_time, 0) / FixedPoint(position_duration) + open_checkpoint_time = maturity - position_duration + open_share_price = interface.get_checkpoint(open_checkpoint_time).vault_share_price + if block_time >= maturity: + close_share_price = interface.get_checkpoint(maturity).vault_share_price + else: + close_share_price = pool_state.pool_info.vault_share_price + value += interface.calc_close_short( + short.balance, + open_vault_share_price=open_share_price, + close_vault_share_price=close_share_price, + normalized_time_remaining=normalized_time_remaining, + pool_state=pool_state, + ) + return value, new_lp_share_price + + def arb_fixed_rate_down( interface: HyperdriveReadInterface, pool_state: PoolState, wallet: HyperdriveWallet, max_trade_amount_base: FixedPoint, min_trade_amount_bonds: FixedPoint, + arb_portion: FixedPoint, slippage_tolerance: FixedPoint | None = None, ) -> list[Trade[HyperdriveMarketAction]]: """Returns an action list for arbitraging the fixed rate down to the variable rate. @@ -59,6 +115,8 @@ def arb_fixed_rate_down( The maximum amount of base allowed to trade. min_trade_amount_bonds: FixedPoint The minimum amount of bonds needed to open a trade. + arb_portion: FixedPoint + The portion of the pool to arbitrage. slippage_tolerance: FixedPoint | None, optional The slippage tolerance for trades. Defaults to None. @@ -107,6 +165,116 @@ def arb_fixed_rate_down( max_long_shares * pool_state.pool_info.vault_share_price, max_trade_amount_base, ) + + original_total_value, new_lp_share_price = _measure_value(wallet, interface) + orignal_lp_value = wallet.lp_tokens * interface.current_pool_state.pool_info.lp_share_price + original_arb_value = original_total_value - orignal_lp_value + original_arb_portion = original_arb_value / original_total_value + new_arb_portion = FixedPoint(1) + iteration = 0 + while new_arb_portion > arb_portion: + iteration += 1 + logging.info("=== iteration %s ===", iteration) + new_block_time = interface.current_pool_state.block_time + 12 + new_maturity_time = new_block_time + interface.pool_config.position_duration + predicted_pool_state = deepcopy(interface.current_pool_state) + trade_outcome = predict_long( + hyperdrive_interface=interface, + pool_state=predicted_pool_state, + base=amount_base, + ) + predicted_wallet = deepcopy(wallet) + predicted_long = Long(maturity_time=new_maturity_time, balance=trade_outcome.user.bonds) + predicted_wallet.longs.update({new_maturity_time: predicted_long}) + logging.info("predicted_pool_state.pool_info.bond_reserves is %s", predicted_pool_state.pool_info.bond_reserves) + predicted_pool_state.pool_info.bond_reserves += trade_outcome.pool.bonds + logging.info("predicted_pool_state.pool_info.bond_reserves is %s", predicted_pool_state.pool_info.bond_reserves) + logging.info("trade_outcome.pool.bonds is %s", trade_outcome.pool.bonds) + logging.info("predicted_pool_state.pool_info.share_reserves is %s", predicted_pool_state.pool_info.share_reserves) + predicted_pool_state.pool_info.share_reserves += trade_outcome.pool.shares + logging.info("predicted_pool_state.pool_info.share_reserves is %s", predicted_pool_state.pool_info.share_reserves) + new_long_average_maturity_time = ( + predicted_pool_state.pool_info.longs_outstanding * predicted_pool_state.pool_info.long_average_maturity_time + + trade_outcome.user.bonds * new_maturity_time + ) / ( predicted_pool_state.pool_info.longs_outstanding + trade_outcome.user.bonds ) + logging.info("new_long_average_maturity_time is %s (%s)", new_long_average_maturity_time, type(new_long_average_maturity_time)) + logging.info("predicted_pool_state.pool_info.longs_outstanding is %s (%s)", predicted_pool_state.pool_info.longs_outstanding, type(predicted_pool_state.pool_info.longs_outstanding)) + predicted_pool_state.pool_info.longs_outstanding += trade_outcome.user.bonds + # predicted_pool_state.pool_info.long_exposure += trade_outcome.user.bonds + predicted_pool_state.exposure -= trade_outcome.user.bonds + predicted_pool_state.exposure = FixedPoint(-129338.867334504463130003) + logging.info("predicted_pool_state.pool_info.longs_outstanding is %s (%s)", predicted_pool_state.pool_info.longs_outstanding, type(predicted_pool_state.pool_info.longs_outstanding)) + logging.info("predicted_pool_state.pool_info.long_average_maturity_time is %s (%s)", predicted_pool_state.pool_info.long_average_maturity_time, type(predicted_pool_state.pool_info.long_average_maturity_time)) + predicted_pool_state.pool_info.long_average_maturity_time = new_long_average_maturity_time + logging.info("predicted_pool_state.pool_info.long_average_maturity_time is %s (%s)", predicted_pool_state.pool_info.long_average_maturity_time, type(predicted_pool_state.pool_info.long_average_maturity_time)) + logging.info("trade_outcome.pool.shares is %s", trade_outcome.pool.shares) + logging.info("predicted_pool_state.pool_info is %s", predicted_pool_state.pool_info) + old_spot_price = interface.calc_spot_price() + logging.info("old_spot_price is %s", old_spot_price) + new_spot_price = interface.calc_spot_price(pool_state=predicted_pool_state) + logging.info("new_spot_price is %s", new_spot_price) + delta_spot_price = new_spot_price - old_spot_price + logging.info("delta_spot_price is %s (%.2f%%)", delta_spot_price, delta_spot_price / old_spot_price * 100) + # new_total_value, new_lp_share_price = _measure_value( + # interface=interface, + # wallet=predicted_wallet, + # pool_state=predicted_pool_state, + # spot_price=new_spot_price, + # block_time=new_block_time, + # ) + # logging.info("new_total_value is %s", new_total_value) + # new_lp_value = predicted_wallet.lp_tokens * new_lp_share_price + # logging.info("new_lp_value is %s", new_lp_value) + # new_arb_value = new_total_value - new_lp_value + # logging.info("new_arb_value is %s", new_arb_value) + # new_arb_portion = new_arb_value / new_total_value + # logging.info("new_arb_portion is %s", new_arb_portion) + # overshoot_or_undershoot = (new_arb_portion - original_arb_portion) / (arb_portion - original_arb_portion) + # logging.info("overshoot_or_undershoot is %s", overshoot_or_undershoot) + + # # update trade size + # logging.info("amount_base is %s", old_amount_base := amount_base) + # amount_base /= overshoot_or_undershoot + # logging.info("amount_base is %s (%.2f%%)", amount_base, (amount_base / old_amount_base - 1) * 100) + + # # update prediction + # predicted_pool_state = deepcopy(interface.current_pool_state) + # trade_outcome = predict_long( + # hyperdrive_interface=interface, + # pool_state=predicted_pool_state, + # base=amount_base, + # ) + # predicted_wallet = deepcopy(wallet) + # predicted_long = Long(maturity_time=new_maturity_time, balance=trade_outcome.user.bonds) + # predicted_wallet.longs.update({new_maturity_time: predicted_long}) + # predicted_pool_state.pool_info.bond_reserves += trade_outcome.pool.bonds + # logging.info("trade_outcome.pool.bonds is %s", trade_outcome.pool.bonds) + # predicted_pool_state.pool_info.share_reserves += trade_outcome.pool.shares + # new_long_average_maturity_time = ( + # predicted_pool_state.pool_info.longs_outstanding * predicted_pool_state.pool_info.long_average_maturity_time + # + trade_outcome.user.bonds * new_maturity_time + # ) / ( predicted_pool_state.pool_info.longs_outstanding + trade_outcome.user.bonds ) + # predicted_pool_state.pool_info.longs_outstanding += trade_outcome.user.bonds + # # predicted_pool_state.pool_info.long_exposure += trade_outcome.user.bonds + # # predicted_pool_state.pool_info.long_average_maturity_time = new_long_average_maturity_time + + # # update new_arb_portion + # new_spot_price = interface.calc_spot_price(pool_state=predicted_pool_state) + # new_lp_share_price = predicted_pool_state.pool_info.lp_share_price + # new_total_value, new_lp_share_price = _measure_value( + # interface=interface, + # wallet=predicted_wallet, + # pool_state=predicted_pool_state, + # spot_price=new_spot_price, + # block_time=new_block_time, + # ) + # new_lp_value = predicted_wallet.lp_tokens * new_lp_share_price + # new_arb_value = new_total_value - new_lp_value + # new_arb_portion = new_arb_value / new_total_value + new_arb_portion = FixedPoint(0) + # logging.info("new_arb_portion is %s", new_arb_portion) + # time.sleep(0.5) + action_list.append(open_long_trade(amount_base, slippage_tolerance)) return action_list @@ -512,6 +680,7 @@ def action( wallet, max_trade_amount_base, self.min_trade_amount_bonds, + self.policy_config.arb_portion, self.slippage_tolerance, ) ) diff --git a/src/agent0/core/hyperdrive/policies/lpandarb_test.py b/src/agent0/core/hyperdrive/policies/lpandarb_test.py index a6fac0829e..eef16b50b7 100644 --- a/src/agent0/core/hyperdrive/policies/lpandarb_test.py +++ b/src/agent0/core/hyperdrive/policies/lpandarb_test.py @@ -3,14 +3,20 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING import pytest from fixedpointmath import FixedPoint +from agent0.core.hyperdrive import HyperdriveWallet from agent0.core.hyperdrive.interactive import ILocalChain, ILocalHyperdrive from agent0.core.hyperdrive.interactive.event_types import AddLiquidity, CloseLong, CloseShort, OpenLong, OpenShort from agent0.core.hyperdrive.interactive.i_local_hyperdrive_agent import ILocalHyperdriveAgent from agent0.core.hyperdrive.policies import PolicyZoo +from agent0.ethpy.hyperdrive.state import PoolState + +if TYPE_CHECKING: + from agent0.ethpy.hyperdrive import HyperdriveReadInterface # avoid unnecessary warning from using fixtures defined in outer scope # pylint: disable=redefined-outer-name @@ -290,7 +296,7 @@ def test_reduce_long(interactive_hyperdrive: ILocalHyperdrive, arbitrage_andy: I # give Andy a long event = arbitrage_andy.open_long(base=FixedPoint(10)) - # advance time to maturity + # advance halfway time to maturity interactive_hyperdrive.chain.advance_time(int(YEAR_IN_SECONDS / 2), create_checkpoints=False) # see if he reduces the long @@ -303,7 +309,6 @@ def test_reduce_long(interactive_hyperdrive: ILocalHyperdrive, arbitrage_andy: I @pytest.mark.anvil def test_reduce_short(interactive_hyperdrive: ILocalHyperdrive, arbitrage_andy: ILocalHyperdriveAgent): """Reduce a short position.""" - logging.info("starting fixed rate is %s", interactive_hyperdrive.interface.calc_fixed_rate()) # give Andy a short @@ -311,7 +316,7 @@ def test_reduce_short(interactive_hyperdrive: ILocalHyperdrive, arbitrage_andy: logging.info("fixed rate after open short is %s", interactive_hyperdrive.interface.calc_fixed_rate()) - # advance time to maturity + # advance time halfway to maturity interactive_hyperdrive.chain.advance_time(int(YEAR_IN_SECONDS / 2), create_checkpoints=False) # see if he reduces the short @@ -363,3 +368,129 @@ def test_safe_short_trading(interactive_hyperdrive: ILocalHyperdrive, manual_age assert len(action_result) == 2 # LP & Arb (no closing trades) assert isinstance(action_result[0], AddLiquidity) # LP first assert isinstance(action_result[1], OpenShort) # then arb + +@pytest.mark.anvil +def test_manage_budget( + interactive_hyperdrive: ILocalHyperdrive, arbitrage_andy: ILocalHyperdriveAgent, manual_agent: ILocalHyperdriveAgent +): + """Manage budget between LP and Arb at 50/50.""" + logging.info("starting fixed rate is %s", interactive_hyperdrive.interface.calc_fixed_rate()) + + value_before_trade, new_lp_share_price = _measure_value(arbitrage_andy.wallet, interactive_hyperdrive.interface) + # define LP portion of budget + arbitrage_andy.agent.policy.sub_policy.policy_config.lp_portion = FixedPoint("0.5") + # andy sets up his LP + event_list = arbitrage_andy.execute_policy_action() + logging.info("Andy executed %s", event_list) + value_after_trade, new_lp_share_price = _measure_value(arbitrage_andy.wallet, interactive_hyperdrive.interface) + lp_value = arbitrage_andy.wallet.lp_tokens * new_lp_share_price + lp_portion = lp_value / value_after_trade + arb_portion = (value_after_trade - lp_value) / value_after_trade + new_spot_price = interactive_hyperdrive.interface.calc_spot_price() + logging.info("spot price after adding liquidity is %s", new_spot_price) + logging.info("value before adding liquidity is %s", value_before_trade) + logging.info("value after adding liquidity is %s", value_after_trade) + logging.info("change is %s", value_after_trade - value_before_trade) + logging.info("budget breakdown is: %.5f LP, %.5f Arb", lp_portion, arb_portion) + assert value_after_trade < value_before_trade + + # manually open a short + manual_agent.open_short(bonds=FixedPoint(100_000_000)) + # andy should open a long + value_before_trade, new_lp_share_price = _measure_value(arbitrage_andy.wallet, interactive_hyperdrive.interface) + old_spot_price = interactive_hyperdrive.interface.calc_spot_price() + event_list = arbitrage_andy.execute_policy_action() + logging.info("Andy executed %s", event_list) + event = event_list[0] if isinstance(event_list, list) else event_list + assert isinstance(event, OpenLong), "Andy should have opened a long" + actual_pool_state = interactive_hyperdrive.interface.current_pool_state + logging.info("actual_pool_state.pool_info is %s", actual_pool_state.pool_info) + logging.info("=== actual_pool_state ===") + for k,v in actual_pool_state.__dict__.items(): + if k not in ["block", "pool_info", "pool_config"]: + logging.info("%s : %s", k, v) + logging.info("=== actual_pool_info ===") + for k,v in actual_pool_state.pool_info.__dict__.items(): + logging.info("%s : %s", k, v) + value_after_trade, new_lp_share_price = _measure_value(arbitrage_andy.wallet, interactive_hyperdrive.interface) + lp_value = arbitrage_andy.wallet.lp_tokens * new_lp_share_price + lp_portion = lp_value / value_after_trade + arb_portion = (value_after_trade - lp_value) / value_after_trade + new_spot_price = interactive_hyperdrive.interface.calc_spot_price() + logging.info("spot price after opening long is %s", new_spot_price) + delta_spot_price = new_spot_price - old_spot_price + logging.info("delta spot price is %s (%.2f%%)", delta_spot_price, delta_spot_price / old_spot_price * 100) + lp_share_price = actual_pool_state.pool_info.lp_share_price + logging.info("lp share price is %s", lp_share_price) + logging.info("value before opening long is %s", value_before_trade) + logging.info("value after opening long is %s", value_after_trade) + logging.info("change is %s", value_after_trade - value_before_trade) + logging.info("lp_value is %s", lp_value) + logging.info("arb_value is %s", value_after_trade - lp_value) + logging.info("budget breakdown is: %.5f LP, %.5f Arb", lp_portion, arb_portion) + assert arb_portion < FixedPoint(0.5) + +@pytest.mark.anvil +def test_exposure_change( + interactive_hyperdrive: ILocalHyperdrive, arbitrage_andy: ILocalHyperdriveAgent, manual_agent: ILocalHyperdriveAgent +): + arbitrage_andy.agent.policy.sub_policy.policy_config.lp_portion = FixedPoint("0.5") + event_list = arbitrage_andy.execute_policy_action() + logging.info("Andy executed %s", event_list) + logging.info("exposure after add liquidity is = %s", interactive_hyperdrive.interface.current_pool_state.exposure) + # manually open a short + manual_agent.open_short(bonds=FixedPoint(100_000_000)) + logging.info("exposure after open short is = %s", interactive_hyperdrive.interface.current_pool_state.exposure) + # andy should open a long + event_list = arbitrage_andy.execute_policy_action() + logging.info("Andy executed %s", event_list) + event = event_list[0] if isinstance(event_list, list) else event_list + assert isinstance(event, OpenLong), "Andy should have opened a long" + logging.info("exposure after open long is = %s", interactive_hyperdrive.interface.current_pool_state.exposure) + +def _measure_value( + wallet: HyperdriveWallet, + interface: HyperdriveReadInterface, + pool_state: PoolState | None = None, + spot_price: FixedPoint | None = None, + block_time: int | None = None, +) -> tuple[FixedPoint, FixedPoint]: + # either provide interface or all of the other arguments + pool_state = interface.current_pool_state if pool_state is None else pool_state + spot_price = interface.calc_spot_price(pool_state) if spot_price is None else spot_price + block_time = interface.get_block_timestamp(interface.get_current_block()) if block_time is None else block_time + assert isinstance(pool_state, PoolState), "pool_state must be a PoolState" + assert isinstance(spot_price, FixedPoint), "spot_price must be a FixedPoint" + assert isinstance(block_time, int), "block_time must be an int" + + position_duration = pool_state.pool_config.position_duration + value = wallet.balance.amount # base in wallet + old_lp_share_price = pool_state.pool_info.lp_share_price + logging.info("old_lp_share_price is %s", old_lp_share_price) + new_lp_share_price = interface.calc_present_value(pool_state=pool_state) / pool_state.pool_info.lp_total_supply * pool_state.pool_info.vault_share_price + logging.info("new_lp_share_price is %s (%s%.2f%%)", new_lp_share_price, "+" if new_lp_share_price > old_lp_share_price else "",(new_lp_share_price / old_lp_share_price - 1)* 100) + # LP position + simple_lp_value = wallet.lp_tokens * new_lp_share_price + closeout_lp_value = wallet.lp_tokens * new_lp_share_price + value += closeout_lp_value + for maturity, long in wallet.longs.items(): + normalized_time_remaining = max(maturity - block_time, 0) / FixedPoint(position_duration) + value += interface.calc_close_long(long.balance, normalized_time_remaining, pool_state) + for maturity, short in wallet.shorts.items(): + normalized_time_remaining = max(maturity - block_time, 0) / FixedPoint(position_duration) + open_checkpoint_time = maturity - position_duration + open_share_price = interface.get_checkpoint(open_checkpoint_time).vault_share_price + if block_time >= maturity: + close_share_price = interface.get_checkpoint(maturity).vault_share_price + else: + close_share_price = pool_state.pool_info.vault_share_price + value += interface.calc_close_short( + short.balance, + open_vault_share_price=open_share_price, + close_vault_share_price=close_share_price, + normalized_time_remaining=normalized_time_remaining, + pool_state=pool_state, + ) + return value, new_lp_share_price + + \ No newline at end of file diff --git a/src/agent0/ethpy/hyperdrive/transactions.py b/src/agent0/ethpy/hyperdrive/transactions.py index 97c40c7812..8d70518afd 100644 --- a/src/agent0/ethpy/hyperdrive/transactions.py +++ b/src/agent0/ethpy/hyperdrive/transactions.py @@ -162,6 +162,9 @@ def parse_logs(tx_receipt: TxReceipt, hyperdrive_contract: Contract, fn_name: st for value in fixedpoint_values: if value in log_args and hasattr(trade_result, camel_to_snake(value)): setattr(trade_result, camel_to_snake(value), FixedPoint(scaled_value=log_args[value])) + + # special handling for vault_share_price since it's not emitted, but calculated as base_amount / vault_share_amount + setattr(trade_result,"vault_share_price", FixedPoint(scaled_value=log_args["baseAmount"]) / FixedPoint(scaled_value=log_args["vaultShareAmount"])) return trade_result