From 787518dbf9ce58ac80c4f05d2a681c8e21b92377 Mon Sep 17 00:00:00 2001 From: Mihai Date: Wed, 21 Jun 2023 23:27:27 -0400 Subject: [PATCH 01/28] init --- elfpy/utils/apeworx_integrations.py | 52 +++++++++++++++++++++ examples/evm_bots.py | 72 +++++++++++------------------ 2 files changed, 80 insertions(+), 44 deletions(-) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index 28c2a41762..9cfc246420 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -1,14 +1,19 @@ """Helper functions for integrating the sim repo with solidity contracts via Apeworx.""" from __future__ import annotations +# std libs import json import logging import os +import re +from time import sleep from collections import namedtuple from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, Callable +# third party libs +import requests import ape import numpy as np import pandas as pd @@ -22,9 +27,12 @@ from ape_accounts.accounts import KeyfileAccount from fixedpointmath import FixedPoint +# custom libs from elfpy import MAXIMUM_BALANCE_MISMATCH_IN_WEI, SECONDS_IN_YEAR, WEI, simulators, time, types from elfpy.markets.hyperdrive import AssetIdPrefix, HyperdriveMarketState, HyperdrivePricingModel, hyperdrive_assets from elfpy.markets.hyperdrive.hyperdrive_market import HyperdriveMarket +from elfpy.bots import BotConfig +from elfpy.math import FixedPoint from elfpy.simulators.config import Config from elfpy.utils import outputs as output_utils from elfpy.utils import sim_utils @@ -120,6 +128,50 @@ class DefaultHyperdriveConfig: position_duration_seconds: int = checkpoint_duration_seconds * checkpoints target_liquidity = FixedPoint(1 * 10**6) +def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: + """Get devnet addresses from address file.""" + if addresses is None: + addresses = {} + deployed_addresses = {} + # get deployed addresses from local file, if it exists + address_file_path = bot_config.scratch["project_dir"] / "hyperdrive_solidity/artifacts/addresses.json" + if os.path.exists(address_file_path): + logging.info("Loading addresses.json from local file. This should only be used for development.") + with open(address_file_path, "r", encoding="utf-8") as file: + deployed_addresses = json.load(file) + else: # otherwise get deployed addresses from artifacts server + logging.info( + "Attempting to load addresses.json, which requires waiting for the contract deployment to complete." + ) + num_attempts = 120 + for attempt_num in range(num_attempts): + logging.info("\tAttempt %s out of %s to %s", attempt_num + 1, num_attempts, bot_config.artifacts_url) + try: + response = requests.get(f"{bot_config.artifacts_url}/addresses.json", timeout=10) + if response.status_code == 200: + deployed_addresses = response.json() + break + except requests.exceptions.ConnectionError as exc: + logging.info("Connection error: %s", exc) + sleep(1) + logging.info("Contracts deployed; addresses loaded.") + if "baseToken" in deployed_addresses: + addresses["baseToken"] = deployed_addresses["baseToken"] + logging.info("found devnet base address: %s", addresses["baseToken"]) + else: + addresses["baseToken"] = None + if "mockHyperdrive" in deployed_addresses: + addresses["hyperdrive"] = deployed_addresses["mockHyperdrive"] + logging.info("found devnet hyperdrive address: %s", addresses["hyperdrive"]) + elif "hyperdrive" in deployed_addresses: + addresses["hyperdrive"] = deployed_addresses["hyperdrive"] + logging.info("found devnet hyperdrive address: %s", addresses["hyperdrive"]) + else: + addresses["hyperdrive"] = None + if "mockHyperdriveMath" in deployed_addresses: + addresses["hyperdriveMath"] = deployed_addresses["mockHyperdriveMath"] + logging.info("found devnet hyperdriveMath address: %s", addresses["hyperdriveMath"]) + return addresses def get_hyperdrive_config(hyperdrive_instance) -> dict: """Get the hyperdrive config from a deployed hyperdrive contract. diff --git a/examples/evm_bots.py b/examples/evm_bots.py index e673c9a670..43d97fca3b 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -7,7 +7,6 @@ import argparse import json import logging -import os from datetime import datetime from pathlib import Path from time import sleep @@ -17,7 +16,7 @@ # external lib import ape import numpy as np -import requests +import pandas as pd from ape import accounts from ape.api import ProviderAPI from ape.contracts import ContractInstance @@ -43,47 +42,6 @@ ape_logger.set_level(logging.ERROR) - -def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str]) -> tuple[dict[str, str], str]: - """Get devnet addresses from address file.""" - deployed_addresses = {} - # get deployed addresses from local file, if it exists - address_file_path = bot_config.scratch["project_dir"] / "hyperdrive_solidity/artifacts/addresses.json" - if os.path.exists(address_file_path): - with open(address_file_path, "r", encoding="utf-8") as file: - deployed_addresses = json.load(file) - else: # otherwise get deployed addresses from artifacts server - logging.info( - "Attempting to load addresses.json, which requires waiting for the contract deployment to complete." - ) - num_attempts = 120 - for attempt_num in range(num_attempts): - logging.info("\tAttempt %s out of %s to %s", attempt_num + 1, num_attempts, bot_config.artifacts_url) - try: - response = requests.get(f"{bot_config.artifacts_url}/addresses.json", timeout=10) - if response.status_code == 200: - deployed_addresses = response.json() - break - except requests.exceptions.ConnectionError as exc: - logging.info("Connection error: %s", exc) - sleep(1) - logging.info("Contracts deployed; addresses loaded.") - if "baseToken" in deployed_addresses: - addresses["baseToken"] = deployed_addresses["baseToken"] - logging.info("found devnet base address: %s", addresses["baseToken"]) - else: - addresses["baseToken"] = None - if "mockHyperdrive" in deployed_addresses: - addresses["hyperdrive"] = deployed_addresses["mockHyperdrive"] - logging.info("found devnet hyperdrive address: %s", addresses["hyperdrive"]) - else: - addresses["hyperdrive"] = None - if "mockHyperdriveMath" in deployed_addresses: - addresses["hyperdriveMath"] = deployed_addresses["mockHyperdriveMath"] - logging.info("found devnet hyperdriveMath address: %s", addresses["hyperdriveMath"]) - return addresses - - def get_accounts(bot_config: BotConfig) -> list[KeyfileAccount]: """Generate dev accounts and turn on auto-sign.""" num = sum(bot_config.scratch[f"num_{bot}"] for bot in bot_config.scratch["bot_names"]) @@ -519,6 +477,25 @@ def do_policy( return no_crash_streak +def dump_state(provider, bot_config, block_number, rng, hyperdrive_instance): + """Dump relevant pieces of information: full node state from anvil, random generator state, and trade history.""" + dump_name = f"state_dump_rng_{bot_config.random_seed}_block_{block_number}" # we name our dumps in this house + node_state_hex = provider._make_request("anvil_dumpState", parameters={}) # pylint: disable=protected-access + node_state_dump_file_name = f"{dump_name}_node_state.json" + with open(bot_config.scratch["state_dump_file_path"] / node_state_dump_file_name, "w", encoding="utf-8") as file: + json.dump(node_state_hex, file) + random_state_dump_file_name = f"{dump_name}_random_state.json" + with open(bot_config.scratch["state_dump_file_path"] / random_state_dump_file_name, "w", encoding="utf-8") as file: + print(f"printing {rng.bit_generator.state=}") + json.dump(rng.bit_generator.state, file) + on_chain_trade_info = ape_utils.get_on_chain_trade_info(hyperdrive_instance) # get all trades ever + trade_history_dump_file_name = f"{dump_name}_trade_history.csv" + df_share_price = pd.DataFrame(on_chain_trade_info.share_price.items(), columns=["block_number", "share_price"]) + pd.merge(on_chain_trade_info.trades, df_share_price, on="block_number").to_csv( + bot_config.scratch["state_dump_file_path"] / trade_history_dump_file_name, index=False + ) + + def main( bot_config: BotConfig, rng: NumpyGenerator, @@ -530,6 +507,10 @@ def main( # pylint: disable=too-many-locals # Custom parameters for this experiment bot_config.scratch["project_dir"] = Path.cwd().parent if Path.cwd().name == "examples" else Path.cwd() + bot_config.scratch["state_dump_file_path"] = bot_config.scratch["project_dir"] / "state_dumps" + # make dir if it doesn't exist + if not bot_config.scratch["state_dump_file_path"].exists(): + bot_config.scratch["state_dump_file_path"].mkdir() if "num_louie" not in bot_config.scratch: bot_config.scratch["num_louie"]: int = 1 if "num_sally" not in bot_config.scratch: @@ -553,7 +534,7 @@ def main( "goerli_hyperdrive": "0xB311B825171AF5A60d69aAD590B857B1E5ed23a2", } if bot_config.devnet: - addresses = get_devnet_addresses(bot_config, addresses) + addresses = ape_utils.get_devnet_addresses(bot_config, addresses) pricing_model = HyperdrivePricingModel() no_crash_streak = 0 last_executed_block = 0 @@ -572,6 +553,9 @@ def main( block_timestamp = latest_block.timestamp if block_number > last_executed_block: log_and_show_block_info(provider, no_crash_streak, block_number, block_timestamp) + # dump state at the beginning of every new block + dump_state(provider, bot_config, block_number, rng, hyperdrive_instance) + # create market object needed for agent execution elfpy_market = ape_utils.create_elfpy_market( pricing_model, hyperdrive_instance, hyperdrive_config, block_number, block_timestamp, start_timestamp ) From 331fdff2d02cef1e4e7e79e2caa899ff73599f49 Mon Sep 17 00:00:00 2001 From: Mihai Date: Mon, 26 Jun 2023 16:02:12 -0400 Subject: [PATCH 02/28] make trade history work --- .gitignore | 3 + elfpy/bots/bot_config.py | 4 + elfpy/utils/apeworx_integrations.py | 97 ++++++----- examples/evm_bots.py | 243 ++++++++++++++++++++++------ tests/test_transformers.py | 4 +- 5 files changed, 246 insertions(+), 105 deletions(-) diff --git a/.gitignore b/.gitignore index b35a9aa5cf..18a36fe7cb 100644 --- a/.gitignore +++ b/.gitignore @@ -86,6 +86,9 @@ no_crash.txt *.gif examples/pics examples/abi/ +state_dumps/ +config_json_generator.py +*.json # Local artifacts artifacts/ diff --git a/elfpy/bots/bot_config.py b/elfpy/bots/bot_config.py index 18c9e7b613..a909df4ba0 100644 --- a/elfpy/bots/bot_config.py +++ b/elfpy/bots/bot_config.py @@ -48,6 +48,10 @@ class BotConfig(types.FrozenClass): # In general the bot will be more risk averse as it grows to infinity. # A value of 0 will usually disable it. risk_threshold: float = 0.0 + # whether or not to dump state at every block + dump_state: bool = False + # whether to initialize state from a specific point + load_state_id: str | None = None # scratch space for any application-specific & extraneous parameters scratch: dict[Any, Any] = field(default_factory=dict) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index 9cfc246420..44e8dfc02f 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -128,6 +128,7 @@ class DefaultHyperdriveConfig: position_duration_seconds: int = checkpoint_duration_seconds * checkpoints target_liquidity = FixedPoint(1 * 10**6) + def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: """Get devnet addresses from address file.""" if addresses is None: @@ -173,6 +174,7 @@ def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None logging.info("found devnet hyperdriveMath address: %s", addresses["hyperdriveMath"]) return addresses + def get_hyperdrive_config(hyperdrive_instance) -> dict: """Get the hyperdrive config from a deployed hyperdrive contract. @@ -323,30 +325,29 @@ def get_market_state_from_contract(hyperdrive_contract: ContractInstance, **kwar ) -def get_on_chain_trade_info(hyperdrive_contract: ContractInstance, block_number: int | None = None) -> OnChainTradeInfo: +OnChainTradeInfo = namedtuple( + "OnChainTradeInfo", ["trades", "unique_maturities", "unique_ids", "unique_block_numbers", "share_price"] +) + + +def get_trade_history(hyperdrive_contract: ContractInstance, block_number: int | None = None) -> pd.DataFrame: r"""Get all trades from hyperdrive contract. Arguments --------- hyperdrive_contract : `ape.contracts.base.ContractInstance `_ Contract pointing to the initialized Hyperdrive (or MockHyperdriveTestnet) smart contract. + block_number : int, Optional + The block number at which to start querying. If not provided, query from the beginning. Returns ------- - OnChainTradeInfo - Named tuple containing the following fields: - - trades : pd.DataFrame - DataFrame containing all trades from the Hyperdrive contract. - - unique_maturities : list - List of unique maturity timestamps across all assets. - - unique_ids : list - List of unique ids across all assets. - - unique_block_numbers_ : list - List of unique block numbers across all trades. - - share_price_ - Map of share price to block number. + pd.DataFrame | None + History of all trade events. """ trades = hyperdrive_contract.TransferSingle.query("*", start_block=block_number or 0, stop_block=block_number) + if len(trades) == 0: + return None trades = pd.concat( # flatten event_arguments [ trades.loc[:, [c for c in trades.columns if c != "event_arguments"]], @@ -357,32 +358,22 @@ def get_on_chain_trade_info(hyperdrive_contract: ContractInstance, block_number: tuple_series = trades.apply(func=lambda x: hyperdrive_assets.decode_asset_id(int(x["id"])), axis=1) # type: ignore trades["prefix"], trades["maturity_timestamp"] = zip(*tuple_series) # split into two columns trades["trade_type"] = trades["prefix"].apply(lambda x: AssetIdPrefix(x).name) - - unique_maturities_ = trades["maturity_timestamp"].unique() - unique_maturities_ = unique_maturities_[unique_maturities_ != 0] - - unique_ids_: np.ndarray = trades["id"].unique() - unique_ids_ = unique_ids_[unique_ids_ != 0] - - unique_block_numbers_ = trades["block_number"].unique() - share_price_ = { block_number_: hyperdrive_contract.getPoolInfo(block_identifier=int(block_number_))["sharePrice"] - for block_number_ in unique_block_numbers_ + for block_number_ in trades["block_number"].unique() } - for block_number_, price in share_price_.items(): - logging.debug(("block_number_={}, price={}", block_number_, price)) - - return OnChainTradeInfo(trades, unique_maturities_, unique_ids_, unique_block_numbers_, share_price_) + df_share_price = pd.DataFrame(share_price_.items(), columns=["block_number", "share_price"]) + return pd.merge(trades, df_share_price, on="block_number") -def get_wallet_from_onchain_trade_info( +def get_wallet_from_trade_history( address: str, - info: OnChainTradeInfo, + trade_history: pd.DataFrame, hyperdrive_contract: ContractInstance, base_contract: ContractInstance, index: int = 0, add_to_existing_wallet: Wallet | None = None, + tolerance = None, ) -> Wallet: # pylint: disable=too-many-arguments, too-many-branches @@ -392,8 +383,8 @@ def get_wallet_from_onchain_trade_info( --------- address : str Address of the wallet. - info : OnChainTradeInfo - On-chain trade info. + trade_history : pd.DataFrame + History of all trade events. hyperdrive_contract : `ape.contracts.base.ContractInstance `_ Contract pointing to the initialized Hyperdrive (or MockHyperdriveTestnet) smart contract. base_contract : `ape.contracts.base.ContractInstance `_ @@ -407,6 +398,8 @@ def get_wallet_from_onchain_trade_info( Wallet with Short, Long, and LP positions. """ # TODO: remove restriction forcing Wallet index to be an int (issue #415) + if tolerance is None: + tolerance = MAXIMUM_BALANCE_MISMATCH_IN_WEI if add_to_existing_wallet is None: wallet = Wallet( address=index, @@ -416,13 +409,13 @@ def get_wallet_from_onchain_trade_info( ) else: wallet = add_to_existing_wallet - for position_id in info.unique_ids: # loop across all unique positions - trades_in_position = ((info.trades["from"] == address) | (info.trades["to"] == address)) & ( - info.trades["id"] == position_id - ) - logging.info("found %s trades for %s in position %s", sum(trades_in_position), address[:8], position_id) - positive_balance = int(info.trades.loc[(trades_in_position) & (info.trades["to"] == address), "value"].sum()) - negative_balance = int(info.trades.loc[(trades_in_position) & (info.trades["from"] == address), "value"].sum()) + for position_id in trade_history["id"].unique(): # loop across all unique positions + from_agent = trade_history["from"] == address + to_agent = trade_history["to"] == address + relevant_trades = ((from_agent) | (to_agent)) & (trade_history["id"] == position_id) + logging.debug("found %s trades for %s in position %s", sum(relevant_trades), address[:8], position_id) + positive_balance = int(trade_history.loc[relevant_trades & (to_agent), "value"].sum()) + negative_balance = int(trade_history.loc[relevant_trades & (from_agent), "value"].sum()) balance = positive_balance - negative_balance logging.debug( "balance %s = positive_balance %s - negative_balance %s", balance, positive_balance, negative_balance @@ -430,34 +423,34 @@ def get_wallet_from_onchain_trade_info( asset_prefix, maturity = hyperdrive_assets.decode_asset_id(position_id) asset_type = AssetIdPrefix(asset_prefix).name mint_time = maturity - SECONDS_IN_YEAR - logging.info(" => %s(%s) maturity=%s mint_time=%s", asset_type, asset_prefix, maturity, mint_time) + logging.debug(" => %s(%s) maturity=%s mint_time=%s", asset_type, asset_prefix, maturity, mint_time) on_chain_balance = 0 # verify our calculation against the onchain balance - if add_to_existing_wallet is None: + if add_to_existing_wallet is None and position_id != 0: on_chain_balance = hyperdrive_contract.balanceOf(position_id, address) # only do balance checks if not marignal update - if abs(balance - on_chain_balance) > MAXIMUM_BALANCE_MISMATCH_IN_WEI: + if abs(balance - on_chain_balance) > tolerance: raise ValueError( f"events {balance=} and {on_chain_balance=} disagree by " - f"more than {MAXIMUM_BALANCE_MISMATCH_IN_WEI} wei for {address}" + f"more than {tolerance} wei for {address}" ) - logging.info(" => calculated balance = on_chain = %s", output_utils.str_with_precision(balance)) + logging.debug(" => calculated balance = on_chain = %s", output_utils.str_with_precision(balance)) # check if there's an outstanding balance if balance != 0 or on_chain_balance != 0: if asset_type == "SHORT": # loop across all the positions owned by this wallet sum_product_of_open_share_price_and_value, sum_value = 0, 0 - for specific_trade in trades_in_position.index[trades_in_position]: - value = info.trades.loc[specific_trade, "value"] - value *= -1 if info.trades.loc[specific_trade, "from"] == address else 1 + for specific_trade in relevant_trades.index[relevant_trades]: + value = trade_history.loc[specific_trade, "value"] + value *= -1 if trade_history.loc[specific_trade, "from"] == address else 1 sum_value += value sum_product_of_open_share_price_and_value += ( - value * info.share_price[info.trades.loc[specific_trade, "block_number"]] + value * trade_history.loc[specific_trade, "share_price"] ) open_share_price = int(sum_product_of_open_share_price_and_value / sum_value) assert ( - abs(balance - sum_value) <= MAXIMUM_BALANCE_MISMATCH_IN_WEI + abs(balance - sum_value) <= tolerance ), "weighted average open share price calculation is wrong" logging.debug("calculated weighted average open share price of %s", open_share_price) previous_balance = wallet.shorts[mint_time].balance if mint_time in wallet.shorts else 0 @@ -732,9 +725,7 @@ def get_agent_deltas(txn_receipt: ReceiptAPI, trade, addresses, trade_type, pool ) }, ) - else: - if trade_type != "closeShort": - raise ValueError(f"Unknown trade type: {trade_type}") + elif trade_type == "closeShort": agent_deltas = Wallet( address=addresses.index(agent), balance=types.Quantity(amount=trade["value"], unit=types.TokenType.BASE), @@ -745,6 +736,8 @@ def get_agent_deltas(txn_receipt: ReceiptAPI, trade, addresses, trade_type, pool ) }, ) + else: + raise ValueError(f"Unknown trade type: {trade_type}") return agent_deltas @@ -977,7 +970,7 @@ def ape_trade( return get_pool_state(txn_receipt=txn_receipt, hyperdrive_contract=hyperdrive_contract), txn_receipt except TransactionError as exc: logging.error( - "Failed to execute %s: %s\n => Amount: %s\n => Agent: %s\n => Pool: %s", + "Failed to execute %s: %s\n => Amount: %s\n => Agent: %s\n => Pool: %s", trade_type, exc, output_utils.str_with_precision(amount), diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 43d97fca3b..3e398372f9 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -42,6 +42,7 @@ ape_logger.set_level(logging.ERROR) + def get_accounts(bot_config: BotConfig) -> list[KeyfileAccount]: """Generate dev accounts and turn on auto-sign.""" num = sum(bot_config.scratch[f"num_{bot}"] for bot in bot_config.scratch["bot_names"]) @@ -65,7 +66,7 @@ def create_agent( dev_accounts: list[KeyfileAccount], faucet: ContractInstance | None, base_instance: ContractInstance, - on_chain_trade_info: ape_utils.OnChainTradeInfo, + trade_history: pd.DataFrame, hyperdrive_contract: ContractInstance, bot_config: BotConfig, rng: NumpyGenerator, @@ -82,12 +83,14 @@ def create_agent( Contract for faucet that mints the testnet base token base_instance : `ape.contracts.ContractInstance `_ Contract for base token - on_chain_trade_info : ape_utils.OnChainTradeInfo - Information about on-chain trades. + trade_history : pd.DataFrame + History of previously completed trades. hyperdrive_contract : `ape.contracts.ContractInstance `_ Contract for hyperdrive bot_config : BotConfig Configuration parameters for the experiment + rng : NumpyGenerator + The random number generator. Returns ------- @@ -133,16 +136,17 @@ def create_agent( ape_utils.attempt_txn(agent.contract, faucet.mint, *txn_args) logging.info( " agent_%s is a %s with budget=%s Eth=%s Base=%s", - agent.contract.address[:8], + # agent.contract.address[:8], + bot.index, bot.name, str_with_precision(params["budget"]), str_with_precision(agent.contract.balance / 1e18), str_with_precision(base_instance.balanceOf(agent.contract.address) / 1e18), ) - agent.wallet = ape_utils.get_wallet_from_onchain_trade_info( + agent.wallet = ape_utils.get_wallet_from_trade_history( address=agent.contract.address, index=bot.index, - info=on_chain_trade_info, + trade_history=trade_history, hyperdrive_contract=hyperdrive_contract, base_contract=base_instance, ) @@ -156,30 +160,31 @@ def set_up_agents( base_instance: ContractInstance, addresses: dict[str, str], rng: NumpyGenerator, -) -> tuple[dict[str, Agent], ape_utils.OnChainTradeInfo]: + trade_history: pd.DataFrame | None = None, +) -> tuple[dict[str, Agent], pd.DataFrame]: """Set up python agents & corresponding on-chain accounts. Parameters ---------- bot_config : BotConfig Configuration parameters for the experiment - provider : ape.api.ProviderAPI - The Ape object that connects to the Ethereum network. + provider : `ape.api.ProviderAPI `_ + The Ape object that represents your connection to the Ethereum network. hyperdrive_instance : `ape.contracts.ContractInstance `_ The hyperdrive contract instance. base_instance : `ape.contracts.ContractInstance `_ The base token contract instance. addresses : dict[str, str] Addresses of deployed contracts. - deployer_account : KeyfileAccount - The deployer account. + rng : NumpyGenerator + The random number generator. Returns ------- sim_agents : dict[str, Agent] Dict of agents used in the simulation. - on_chain_trade_info : ape_utils.OnChainTradeInfo - Information about on-chain trades. + trade_history : pd.DataFrame + History of previously completed trades. """ # pylint: disable=too-many-arguments, too-many-locals dev_accounts: list[KeyfileAccount] = get_accounts(bot_config) @@ -198,28 +203,37 @@ def set_up_agents( bot_num += bot_config.scratch[f"num_{bot_name}"] sim_agents = {} start_time_ = now() - on_chain_trade_info: ape_utils.OnChainTradeInfo = ape_utils.get_on_chain_trade_info( - hyperdrive_contract=hyperdrive_instance - ) + if trade_history is None: + trade_history = ape_utils.get_trade_history(hyperdrive_contract=hyperdrive_instance) logging.debug("Getting on-chain trade info took %s seconds", str_with_precision(now() - start_time_)) for bot_name in [name for name in bot_config.scratch["bot_names"] if bot_config.scratch[f"num_{name}"] > 0]: bot_info = bot_config.scratch[bot_name] bot_info.name = bot_name for _ in range(bot_config.scratch[f"num_{bot_name}"]): # loop across number of bots of this type bot_info.index = len(sim_agents) - logging.debug("Creating %s agent %s/%s: %s", bot_name, bot_info.index + 1, bot_num, bot_info) + logging.info("Creating %s agent %s/%s: %s", bot_name, bot_info.index + 1, bot_num, bot_info) agent = create_agent( bot=bot_info, dev_accounts=dev_accounts, faucet=faucet, base_instance=base_instance, - on_chain_trade_info=on_chain_trade_info, + trade_history=trade_history, hyperdrive_contract=hyperdrive_instance, bot_config=bot_config, rng=rng, ) + log_str = "Created %s agent with wallet:" + for long in agent.wallet.longs: + print(f"{long=}") + print(f"{type(long)=}") + log_str += " long " + for short in agent.wallet.shorts: + print(f"{short=}") + print(f"{type(short)=}") + log_str += " short " + logging.info("created kek") sim_agents[f"agent_{agent.wallet.address}"] = agent - return sim_agents, on_chain_trade_info + return sim_agents, trade_history, dev_accounts def do_trade( @@ -301,8 +315,8 @@ def log_and_show_block_info( Parameters ---------- - provider : ape.api.ProviderAPI - The Ape object that connects to the Ethereum blockchain. + provider : `ape.api.ProviderAPI `_ + The Ape object that represents your connection to the Ethereum network. no_crash_streak : int The number of trades without crashing. block_number : int @@ -338,8 +352,8 @@ def set_up_devnet( The addresses of the deployed contracts. project : HyperdriveProject The Ape project that contains a Hyperdrive contract. - provider : ape.api.ProviderAPI - The Ape object that connects to the Ethereum blockchain. + provider : `ape.api.ProviderAPI `_ + The Ape object that represents your connection to the Ethereum network. bot_config : BotConfig Configuration parameters for the experiment pricing_model : HyperdrivePricingModel @@ -388,6 +402,7 @@ def set_up_ape( addresses: dict, network_choice: str, pricing_model: HyperdrivePricingModel, + rng: NumpyGenerator, ) -> tuple[ProviderAPI, ContractInstance, ContractInstance, dict, KeyfileAccount]: r"""Set up ape. @@ -403,10 +418,12 @@ def set_up_ape( The network to connect to. pricing_model : BasePricingModel The elf-simulations pricing model to use. + rng : NumpyGenerator + The random number generator. Returns ------- - provider : ProviderAPI + provider : `ape.api.ProviderAPI `_ The Ape object that represents your connection to the Ethereum network. base_instance : `ape.contracts.ContractInstance `_ The deployed base token instance. @@ -414,6 +431,10 @@ def set_up_ape( The deployed Hyperdrive instance. hyperdrive_config : dict The configuration of the deployed Hyperdrive instance + agent_addresses : list[str] | None + List of deployed agent addresses, if loading state, otherwise None. + trade_history : pd.DataFrame + History of previously completed trades. """ provider: ProviderAPI = ape.networks.parse_network_choice( network_choice=network_choice, @@ -424,6 +445,12 @@ def set_up_ape( "devnet" if bot_config.devnet else network_choice, provider.get_block("latest").number, ) + agent_addresses, trade_history = None, None + if config.load_state_id is not None: # load state from specified id + logging.info("Loading state from id: %s", config.load_state_id) + load_state_block_number, addresses, agent_addresses, trade_history = load_state(bot_config, rng) + print(f"Loaded state up to block number {load_state_block_number}") + print(f"{provider.get_block('latest').number=}") project: ape_utils.HyperdriveProject = ape_utils.HyperdriveProject( path=Path.cwd(), hyperdrive_address=addresses["hyperdrive"] if bot_config.devnet else addresses["goerli_hyperdrive"], @@ -440,7 +467,9 @@ def set_up_ape( hyperdrive_instance: ContractInstance = project.get_hyperdrive_contract() # read the hyperdrive config from the contract, and log (and print) it hyperdrive_config = ape_utils.get_hyperdrive_config(hyperdrive_instance) - return provider, base_instance, hyperdrive_instance, hyperdrive_config + # becomes provider.get_auto_mine() with this PR: https://github.com/ApeWorX/ape-foundry/pull/51 + automine = provider._make_request("anvil_getAutomine", parameters={}) # pylint: disable=protected-access + return provider, automine, base_instance, hyperdrive_instance, hyperdrive_config, agent_addresses, trade_history def do_policy( @@ -452,17 +481,43 @@ def do_policy( hyperdrive_instance: ContractInstance, base_instance: ContractInstance, bot_config: BotConfig, -): # pylint: disable=too-many-arguments - """Execute an agent's policy.""" +) -> int: + """Execute an agent's policy. + + Parameters + ---------- + agent : BasePolicy + The agent object used in elf-simulations. + elfpy_market : HyperdriveMarket + The elf-simulations object representing the Hyperdrive market. + no_crash_streak : int + Number of trades in a row without a crash. + crash_file : str + Location of the file to which we store `no_crash_streak`. + sim_agents : dict[str, Agent] + Dict of agents used in the simulation. + hyperdrive_instance : `ape.contracts.ContractInstance `_ + The hyperdrive contract instance. + base_instance : `ape.contracts.ContractInstance `_ + Contract for base token + bot_config : BotConfig + The bot configuration. + + Returns + ------- + no_crash_streak : int + Number of trades in a row without a crash. + """ + # pylint: disable=too-many-arguments trades: list[types.Trade] = agent.get_trades(market=elfpy_market) for trade_object in trades: try: logging.debug(trade_object) do_trade(trade_object, sim_agents, hyperdrive_instance, base_instance) # marginal update to wallet - agent.wallet = ape_utils.get_wallet_from_onchain_trade_info( + agent.wallet = ape_utils.get_wallet_from_trade_history( address=agent.contract.address, - info=ape_utils.get_on_chain_trade_info(hyperdrive_instance, ape.chain.blocks[-1].number), + trade_history=ape_utils.get_trade_history(hyperdrive_instance, ape.chain.blocks[-1].number), hyperdrive_contract=hyperdrive_instance, base_contract=base_instance, add_to_existing_wallet=agent.wallet, @@ -477,25 +532,77 @@ def do_policy( return no_crash_streak -def dump_state(provider, bot_config, block_number, rng, hyperdrive_instance): - """Dump relevant pieces of information: full node state from anvil, random generator state, and trade history.""" - dump_name = f"state_dump_rng_{bot_config.random_seed}_block_{block_number}" # we name our dumps in this house - node_state_hex = provider._make_request("anvil_dumpState", parameters={}) # pylint: disable=protected-access - node_state_dump_file_name = f"{dump_name}_node_state.json" - with open(bot_config.scratch["state_dump_file_path"] / node_state_dump_file_name, "w", encoding="utf-8") as file: - json.dump(node_state_hex, file) - random_state_dump_file_name = f"{dump_name}_random_state.json" - with open(bot_config.scratch["state_dump_file_path"] / random_state_dump_file_name, "w", encoding="utf-8") as file: - print(f"printing {rng.bit_generator.state=}") - json.dump(rng.bit_generator.state, file) - on_chain_trade_info = ape_utils.get_on_chain_trade_info(hyperdrive_instance) # get all trades ever - trade_history_dump_file_name = f"{dump_name}_trade_history.csv" - df_share_price = pd.DataFrame(on_chain_trade_info.share_price.items(), columns=["block_number", "share_price"]) - pd.merge(on_chain_trade_info.trades, df_share_price, on="block_number").to_csv( - bot_config.scratch["state_dump_file_path"] / trade_history_dump_file_name, index=False +def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance): + """Dump relevant pieces of information: full node state from anvil, random generator state, and trade history. + + Parameters + ---------- + bot_config : BotConfig + The bot configuration. + block_number : int + The block number to load the state from. + rng : NumpyGenerator + The random number generator. + addresses : dict + List of deployed contract addresses. + sim_agents : dict[str, Agent] + Dict of agents used in the simulation. + hyperdrive_instance : `ape.contracts.ContractInstance `_ + The hyperdrive contract instance. + """ + trade_history = ape_utils.get_trade_history(hyperdrive_instance) + if trade_history is not None: + trade_history = trade_history.to_dict(orient="records"), # make it JSON serializable + json.dump( + { + "rand_seed": bot_config.random_seed, + "rand_state": rng.bit_generator.state, + "trade_history": trade_history, + "block_number": block_number, + "addresses": addresses, + "agent_addresses": [agent.contract.address for agent in sim_agents.values()], + }, + fp=open( + bot_config.scratch["state_dump_file_path"] / f"randseed_{bot_config.random_seed}.json", + "w", + encoding="utf-8", + ), ) +def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame]: + """Load relevant pieces of information: full node state from anvil, random generator state, and trade history. + + Parameters + ---------- + bot_config : BotConfig + The bot configuration. + rng : NumpyGenerator + The random number generator. + + Returns + ------- + state["block_number"] : int + The block number of the state loaded. + state["addresses"] : list[str] + List of deployed contract addresses. + state["agent_addresses"] : list[str] + List of agent addresses. + trade_history : pd.DataFrame + History of previously completed trades. + """ + state_id = bot_config.load_state_id.replace(".json", "") + with open(bot_config.scratch["state_dump_file_path"] / f"{state_id}.json", "r", encoding="utf-8") as file: + state = json.load(file) + # se tthe random seed + bot_config.random_seed = state["rand_seed"] + # set the state of the rng + rng.bit_generator.state = state["rand_state"] + # reconstitute + trade_history = pd.DataFrame(state["trade_history"][0]) + return state["block_number"], state["addresses"], state["agent_addresses"], trade_history + + def main( bot_config: BotConfig, rng: NumpyGenerator, @@ -536,25 +643,59 @@ def main( if bot_config.devnet: addresses = ape_utils.get_devnet_addresses(bot_config, addresses) pricing_model = HyperdrivePricingModel() + ( + provider, + automine, + base_instance, + hyperdrive_instance, + hyperdrive_config, + agent_addresses, + trade_history, + ) = set_up_ape(bot_config, provider_settings, addresses, network_choice, pricing_model, rng) no_crash_streak = 0 - last_executed_block = 0 - provider, base_instance, hyperdrive_instance, hyperdrive_config = set_up_ape( - bot_config, provider_settings, addresses, network_choice, pricing_model + # set up the environment + sim_agents, trade_history, dev_accounts = set_up_agents( + bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history ) - sim_agents, _ = set_up_agents(bot_config, provider, hyperdrive_instance, base_instance, addresses, rng) + # list_of_addresses = list(set(trade_history["from"].to_list()+trade_history["to"].to_list())) + # list_of_addresses = [l for l in list_of_addresses if l != "0x0000000000000000000000000000000000000000"] + list_of_addresses = dev_accounts + for idx, address in enumerate(list_of_addresses): + print(f"querying agent_{idx} at addr={address}") + wallet = ape_utils.get_wallet_from_trade_history( + address=str(address), + index=idx, + trade_history=trade_history, + hyperdrive_contract=hyperdrive_instance, + base_contract=base_instance, + ) + print(f"{wallet=}") + log_str = f"agent_{idx} has wallet:" + for _, long in wallet.longs.items(): + print(f"{long=}") + print(f"{type(long)=}") + log_str += f" long {long.balance}" + for _, short in wallet.shorts.items(): + print(f"{short=}") + print(f"{type(short)=}") + log_str += f" short {short.balance}" + log_str += f" lp_tokens {wallet.lp_tokens}" + print(log_str) + logging.info("inspected kek") ape_utils.dump_agent_info(sim_agents, bot_config) logging.info("Constructed %s agents:", len(sim_agents)) for agent_name in sim_agents: logging.info("\t%s", agent_name) start_timestamp = ape.chain.blocks[-1].timestamp + last_executed_block = 0 while True: # hyper drive forever into the sunset latest_block = ape.chain.blocks[-1] block_number = latest_block.number block_timestamp = latest_block.timestamp if block_number > last_executed_block: log_and_show_block_info(provider, no_crash_streak, block_number, block_timestamp) - # dump state at the beginning of every new block - dump_state(provider, bot_config, block_number, rng, hyperdrive_instance) + if config.dump_state is True: # dump state at the beginning of every new block + dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance) # create market object needed for agent execution elfpy_market = ape_utils.create_elfpy_market( pricing_model, hyperdrive_instance, hyperdrive_config, block_number, block_timestamp, start_timestamp diff --git a/tests/test_transformers.py b/tests/test_transformers.py index 1f1b63aacb..3bb5af9710 100644 --- a/tests/test_transformers.py +++ b/tests/test_transformers.py @@ -123,10 +123,10 @@ def test_trans_lib_market_state() -> hyperdrive_market.HyperdriveMarketState: def test_trans_lib_wallet(): """TRANSFORMERS ROLL OUT: tx_receipt --> Wallet (issue #392).""" test = TransformerTest() - return ape_utils.get_wallet_from_onchain_trade_info( + return ape_utils.get_wallet_from_trade_history( address=test.test_account.address, index=1, # index of the agent in the list of ALL agents, assigned in set_up_agents() to len(sim_agents) - info=ape_utils.get_on_chain_trade_info(test.hyperdrive_instance), + trade_history=ape_utils.get_on_chain_trade_info(test.hyperdrive_instance), hyperdrive_contract=test.hyperdrive_instance, base_contract=test.base_instance, ) From f81c1f3be9717422e2939983aea0893f947b8ae5 Mon Sep 17 00:00:00 2001 From: Mihai Date: Mon, 26 Jun 2023 18:40:07 -0400 Subject: [PATCH 03/28] inspect dump --- elfpy/utils/apeworx_integrations.py | 67 +++++++++++++++++++++++++++++ examples/evm_bots.py | 30 +------------ 2 files changed, 69 insertions(+), 28 deletions(-) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index 44e8dfc02f..adfe0ddef8 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -129,6 +129,73 @@ class DefaultHyperdriveConfig: target_liquidity = FixedPoint(1 * 10**6) +def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance = 1e16): + """Sanity check trade history and balances. + + """ + for idx, address in enumerate(agent_addresses): + log_str = f"querying agent_{idx} at addr={address}" + + # create wallet + wallet = get_wallet_from_trade_history( + address=str(address), + index=idx, + trade_history=trade_history, + hyperdrive_contract=hyperdrive_instance, + base_contract=base_instance, + tolerance=1e16, # allow being off by $0.01 + ) + + # print what's in wallet + log_str += f"{wallet=}" + log_str = f"agent_{idx} has:" + log_str += "\n wallet:" + for _, long in wallet.longs.items(): + log_str += f"\n long {long.balance}" + for _, short in wallet.shorts.items(): + log_str += f"\n short {short.balance}" + log_str += f"\n lp_tokens {wallet.lp_tokens}" + + # print trade counts + from_agent = trade_history["from"] == address + to_agent = trade_history["to"] == address + num_trades = sum((from_agent | to_agent)) + num_longs = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LONG")) + num_shorts = sum((from_agent | to_agent) & (trade_history["trade_type"] == "SHORT")) + num_lp = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LP")) + num_withdrawal = sum((from_agent | to_agent) & (trade_history["trade_type"] == "WITHDRAWAL_SHARE")) + log_str += "\n trade counts:" + log_str += f"\n {num_trades:2.0f} total" + log_str += f"\n {num_longs:2.0f} long" + log_str += f"\n {num_shorts:2.0f} short" + log_str += f"\n {num_lp:2.0f} LP" + log_str += f"\n {num_withdrawal:2.0f} withdrawal share" + if num_trades!= (num_longs + num_shorts + num_lp + num_withdrawal): + log_str += "\n trade counts don't add up to total ❌" + else: + log_str += "\n trade counts add up to total ✅" + + # compare off-chain to on-chain balances + log_str += "tuples of balances by position by calculation source. balance = (trade_history, onchain)" + log_str += f" tolerance = {tolerance} aka ${tolerance/1e18}" + for position_id in trade_history["id"].unique(): # loop across all unique positions + from_agent = trade_history["from"] == address + to_agent = trade_history["to"] == address + relevant_trades = ((from_agent) | (to_agent)) & (trade_history["id"] == position_id) + positive_balance = int(trade_history.loc[relevant_trades & (to_agent), "value"].sum()) + negative_balance = int(trade_history.loc[relevant_trades & (from_agent), "value"].sum()) + balance = positive_balance - negative_balance + on_chain_balance = hyperdrive_instance.balanceOf(position_id, address) + first_relevant_row = relevant_trades.index[relevant_trades][0] + info = trade_history.loc[first_relevant_row, :] + if abs(balance - on_chain_balance) > tolerance: + log_str += f"{address[:8]} {info.trade_type:16} balance = ({balance/1e18:0.5f},{on_chain_balance/1e18:0.5f})" + log_str += " mismatch ❌" + else: + log_str += f"{address[:8]} {info.trade_type:16} balance = ({balance/1e18:0.0f},{on_chain_balance/1e18:0.0f})" + log_str += " match ✅" + logging.info(log_str) + def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: """Get devnet addresses from address file.""" if addresses is None: diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 3e398372f9..7a18274e04 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -149,6 +149,7 @@ def create_agent( trade_history=trade_history, hyperdrive_contract=hyperdrive_contract, base_contract=base_instance, + tolerance=1e16 if bot_config.load_state_id is not None else None ) return agent @@ -594,11 +595,8 @@ def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame state_id = bot_config.load_state_id.replace(".json", "") with open(bot_config.scratch["state_dump_file_path"] / f"{state_id}.json", "r", encoding="utf-8") as file: state = json.load(file) - # se tthe random seed bot_config.random_seed = state["rand_seed"] - # set the state of the rng rng.bit_generator.state = state["rand_state"] - # reconstitute trade_history = pd.DataFrame(state["trade_history"][0]) return state["block_number"], state["addresses"], state["agent_addresses"], trade_history @@ -657,31 +655,7 @@ def main( sim_agents, trade_history, dev_accounts = set_up_agents( bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history ) - # list_of_addresses = list(set(trade_history["from"].to_list()+trade_history["to"].to_list())) - # list_of_addresses = [l for l in list_of_addresses if l != "0x0000000000000000000000000000000000000000"] - list_of_addresses = dev_accounts - for idx, address in enumerate(list_of_addresses): - print(f"querying agent_{idx} at addr={address}") - wallet = ape_utils.get_wallet_from_trade_history( - address=str(address), - index=idx, - trade_history=trade_history, - hyperdrive_contract=hyperdrive_instance, - base_contract=base_instance, - ) - print(f"{wallet=}") - log_str = f"agent_{idx} has wallet:" - for _, long in wallet.longs.items(): - print(f"{long=}") - print(f"{type(long)=}") - log_str += f" long {long.balance}" - for _, short in wallet.shorts.items(): - print(f"{short=}") - print(f"{type(short)=}") - log_str += f" short {short.balance}" - log_str += f" lp_tokens {wallet.lp_tokens}" - print(log_str) - logging.info("inspected kek") + ape_utils.inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance) ape_utils.dump_agent_info(sim_agents, bot_config) logging.info("Constructed %s agents:", len(sim_agents)) for agent_name in sim_agents: From c3777735d4950a10a8297463d1d9b35ee943ab7c Mon Sep 17 00:00:00 2001 From: Mihai Date: Tue, 27 Jun 2023 16:48:54 -0400 Subject: [PATCH 04/28] single-file anvil state --- elfpy/utils/apeworx_integrations.py | 12 ++++++------ examples/evm_bots.py | 19 ++++++++++++++++--- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index adfe0ddef8..08166c25be 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -171,13 +171,13 @@ def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_insta log_str += f"\n {num_lp:2.0f} LP" log_str += f"\n {num_withdrawal:2.0f} withdrawal share" if num_trades!= (num_longs + num_shorts + num_lp + num_withdrawal): - log_str += "\n trade counts don't add up to total ❌" + log_str += "\n trade counts DON'T add up to total" else: - log_str += "\n trade counts add up to total ✅" + log_str += "\n trade counts add up to total\n" # compare off-chain to on-chain balances - log_str += "tuples of balances by position by calculation source. balance = (trade_history, onchain)" - log_str += f" tolerance = {tolerance} aka ${tolerance/1e18}" + log_str += "tuples of balances by position by calculation source. balance = (trade_history, onchain)\n" + log_str += f" tolerance = {tolerance} aka ${tolerance/1e18}\n" for position_id in trade_history["id"].unique(): # loop across all unique positions from_agent = trade_history["from"] == address to_agent = trade_history["to"] == address @@ -190,10 +190,10 @@ def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_insta info = trade_history.loc[first_relevant_row, :] if abs(balance - on_chain_balance) > tolerance: log_str += f"{address[:8]} {info.trade_type:16} balance = ({balance/1e18:0.5f},{on_chain_balance/1e18:0.5f})" - log_str += " mismatch ❌" + log_str += " MISMATCH\n" else: log_str += f"{address[:8]} {info.trade_type:16} balance = ({balance/1e18:0.0f},{on_chain_balance/1e18:0.0f})" - log_str += " match ✅" + log_str += " match\n" logging.info(log_str) def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 7a18274e04..743fa450e4 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -149,7 +149,7 @@ def create_agent( trade_history=trade_history, hyperdrive_contract=hyperdrive_contract, base_contract=base_instance, - tolerance=1e16 if bot_config.load_state_id is not None else None + tolerance=1e16 if bot_config.load_state_id is not None else None, ) return agent @@ -529,6 +529,11 @@ def do_policy( logging.info("Crashed with error: %s", exc) no_crash_streak = set_days_without_crashing(no_crash_streak, crash_file, reset=True) # set and save to file if bot_config.halt_on_errors: + # rename anvil_regular.json to anvil_crash.json + anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" + anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" + Path.rename(anvil_regular, anvil_crash) + logging.info("CRASHED, anvil state saved to %s", anvil_crash) raise exc return no_crash_streak @@ -553,7 +558,7 @@ def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_ """ trade_history = ape_utils.get_trade_history(hyperdrive_instance) if trade_history is not None: - trade_history = trade_history.to_dict(orient="records"), # make it JSON serializable + trade_history = (trade_history.to_dict(orient="records"),) # make it JSON serializable json.dump( { "rand_seed": bot_config.random_seed, @@ -593,11 +598,19 @@ def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame History of previously completed trades. """ state_id = bot_config.load_state_id.replace(".json", "") - with open(bot_config.scratch["state_dump_file_path"] / f"{state_id}.json", "r", encoding="utf-8") as file: + file_path = bot_config.scratch["state_dump_file_path"] / f"{state_id}.json" + with open(file_path, "r", encoding="utf-8") as file: state = json.load(file) bot_config.random_seed = state["rand_seed"] rng.bit_generator.state = state["rand_state"] trade_history = pd.DataFrame(state["trade_history"][0]) + logging.info( + "STATE loaded from %s with:\n rand_seed %s\n rand_state %s\n trades %s", + file_path, + bot_config.random_seed, + rng.bit_generator.state, + len(trade_history), + ) return state["block_number"], state["addresses"], state["agent_addresses"], trade_history From f0b4f993075e71d883ff1616dfd0345ee865ab68 Mon Sep 17 00:00:00 2001 From: Mihai Date: Tue, 27 Jun 2023 16:57:48 -0400 Subject: [PATCH 05/28] single-file elfpy state --- examples/evm_bots.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 743fa450e4..f0d86e058c 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -533,7 +533,11 @@ def do_policy( anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" Path.rename(anvil_regular, anvil_crash) - logging.info("CRASHED, anvil state saved to %s", anvil_crash) + # rename elfpy_regular.json to elfpy_crash.json + elfpy_regular = bot_config.scratch["project_dir"] / "elfpy_regular.json" + elfpy_crash = bot_config.scratch["project_dir"] / "elfpy_crash.json" + Path.rename(elfpy_regular, elfpy_crash) + logging.info(" => anvil state saved to %s, elfpy state saved to %s", anvil_crash, elfpy_crash) raise exc return no_crash_streak @@ -569,7 +573,7 @@ def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_ "agent_addresses": [agent.contract.address for agent in sim_agents.values()], }, fp=open( - bot_config.scratch["state_dump_file_path"] / f"randseed_{bot_config.random_seed}.json", + bot_config.scratch["project_dir"] / "elfpy_regular.json", "w", encoding="utf-8", ), @@ -598,7 +602,7 @@ def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame History of previously completed trades. """ state_id = bot_config.load_state_id.replace(".json", "") - file_path = bot_config.scratch["state_dump_file_path"] / f"{state_id}.json" + file_path = bot_config.scratch["project_dir"] / f"{state_id}.json" with open(file_path, "r", encoding="utf-8") as file: state = json.load(file) bot_config.random_seed = state["rand_seed"] From 1d2523c0e1de5446e4e9cfa2ad8f9305aa3c1600 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 12:22:04 -0400 Subject: [PATCH 06/28] save dataframe as dict inside json --- .gitignore | 1 - elfpy/utils/apeworx_integrations.py | 58 +++++++++++++++----------- examples/evm_bots.py | 63 +++++++++++++++-------------- 3 files changed, 67 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index 18a36fe7cb..7bfd2eb033 100644 --- a/.gitignore +++ b/.gitignore @@ -88,7 +88,6 @@ examples/pics examples/abi/ state_dumps/ config_json_generator.py -*.json # Local artifacts artifacts/ diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index 08166c25be..fe1ff72af6 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -129,10 +129,8 @@ class DefaultHyperdriveConfig: target_liquidity = FixedPoint(1 * 10**6) -def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance = 1e16): - """Sanity check trade history and balances. - - """ +def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance=1e16): + """Sanity check trade history and balances.""" for idx, address in enumerate(agent_addresses): log_str = f"querying agent_{idx} at addr={address}" @@ -170,7 +168,7 @@ def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_insta log_str += f"\n {num_shorts:2.0f} short" log_str += f"\n {num_lp:2.0f} LP" log_str += f"\n {num_withdrawal:2.0f} withdrawal share" - if num_trades!= (num_longs + num_shorts + num_lp + num_withdrawal): + if num_trades != (num_longs + num_shorts + num_lp + num_withdrawal): log_str += "\n trade counts DON'T add up to total" else: log_str += "\n trade counts add up to total\n" @@ -188,14 +186,14 @@ def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_insta on_chain_balance = hyperdrive_instance.balanceOf(position_id, address) first_relevant_row = relevant_trades.index[relevant_trades][0] info = trade_history.loc[first_relevant_row, :] + log_str += f"{address[:8]} {info.trade_type:16} balance = " if abs(balance - on_chain_balance) > tolerance: - log_str += f"{address[:8]} {info.trade_type:16} balance = ({balance/1e18:0.5f},{on_chain_balance/1e18:0.5f})" - log_str += " MISMATCH\n" + log_str += f"({balance/1e18:0.5f},{on_chain_balance/1e18:0.5f}) MISMATCH\n" else: - log_str += f"{address[:8]} {info.trade_type:16} balance = ({balance/1e18:0.0f},{on_chain_balance/1e18:0.0f})" - log_str += " match\n" + log_str += f"({balance/1e18:0.0f},{on_chain_balance/1e18:0.0f}) match\n" logging.info(log_str) + def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: """Get devnet addresses from address file.""" if addresses is None: @@ -397,24 +395,30 @@ def get_market_state_from_contract(hyperdrive_contract: ContractInstance, **kwar ) -def get_trade_history(hyperdrive_contract: ContractInstance, block_number: int | None = None) -> pd.DataFrame: +def get_trade_history( + hyperdrive_contract: ContractInstance, start_block: int = 0, stop_block: int | None = None, add_to: pd.DataFrame | None = None +) -> pd.DataFrame: r"""Get all trades from hyperdrive contract. Arguments --------- hyperdrive_contract : `ape.contracts.base.ContractInstance `_ Contract pointing to the initialized Hyperdrive (or MockHyperdriveTestnet) smart contract. - block_number : int, Optional - The block number at which to start querying. If not provided, query from the beginning. + start_block : int, Optional + The block number at which to start querying. Default is 0. + stop_block : int, Optional + The block number at which to stop querying. Returns ------- pd.DataFrame | None History of all trade events. """ - trades = hyperdrive_contract.TransferSingle.query("*", start_block=block_number or 0, stop_block=block_number) + if stop_block is not None and start_block > stop_block: + return add_to + trades = hyperdrive_contract.TransferSingle.query("*", start_block=start_block, stop_block=stop_block) if len(trades) == 0: - return None + return add_to trades = pd.concat( # flatten event_arguments [ trades.loc[:, [c for c in trades.columns if c != "event_arguments"]], @@ -430,7 +434,11 @@ def get_trade_history(hyperdrive_contract: ContractInstance, block_number: int | for block_number_ in trades["block_number"].unique() } df_share_price = pd.DataFrame(share_price_.items(), columns=["block_number", "share_price"]) - return pd.merge(trades, df_share_price, on="block_number") + trades = pd.merge(trades, df_share_price, on="block_number") + # add marginal update to previous DataFrame + if add_to is not None: + trades = pd.concat([add_to, trades], axis=0).reset_index(drop=True) + return trades def get_wallet_from_trade_history( @@ -440,7 +448,7 @@ def get_wallet_from_trade_history( base_contract: ContractInstance, index: int = 0, add_to_existing_wallet: Wallet | None = None, - tolerance = None, + tolerance=None, ) -> Wallet: # pylint: disable=too-many-arguments, too-many-branches @@ -487,20 +495,23 @@ def get_wallet_from_trade_history( logging.debug( "balance %s = positive_balance %s - negative_balance %s", balance, positive_balance, negative_balance ) - asset_prefix, maturity = hyperdrive_assets.decode_asset_id(position_id) - asset_type = AssetIdPrefix(asset_prefix).name + if "prefix" in trade_history.columns and len(trade_history.loc[relevant_trades,:])>0: + asset_prefix = trade_history.loc[relevant_trades, "prefix"].iloc[0] + maturity = trade_history.loc[relevant_trades, "maturity_timestamp"].iloc[0] + else: + asset_prefix, maturity = hyperdrive_assets.decode_asset_id(position_id) mint_time = maturity - SECONDS_IN_YEAR + asset_type = AssetIdPrefix(asset_prefix).name logging.debug(" => %s(%s) maturity=%s mint_time=%s", asset_type, asset_prefix, maturity, mint_time) on_chain_balance = 0 # verify our calculation against the onchain balance if add_to_existing_wallet is None and position_id != 0: - on_chain_balance = hyperdrive_contract.balanceOf(position_id, address) + on_chain_balance = hyperdrive_contract.balanceOf(int(position_id), address) # only do balance checks if not marignal update if abs(balance - on_chain_balance) > tolerance: raise ValueError( - f"events {balance=} and {on_chain_balance=} disagree by " - f"more than {tolerance} wei for {address}" + f"events {balance=} and {on_chain_balance=} disagree by more than {tolerance} wei for {address}" ) logging.debug(" => calculated balance = on_chain = %s", output_utils.str_with_precision(balance)) # check if there's an outstanding balance @@ -516,9 +527,7 @@ def get_wallet_from_trade_history( value * trade_history.loc[specific_trade, "share_price"] ) open_share_price = int(sum_product_of_open_share_price_and_value / sum_value) - assert ( - abs(balance - sum_value) <= tolerance - ), "weighted average open share price calculation is wrong" + assert abs(balance - sum_value) <= tolerance, "weighted average open share price calculation is wrong" logging.debug("calculated weighted average open share price of %s", open_share_price) previous_balance = wallet.shorts[mint_time].balance if mint_time in wallet.shorts else 0 new_balance = previous_balance + FixedPoint(scaled_value=balance) @@ -984,6 +993,7 @@ def create_trade( selected_abi, args = select_abi(params=params, method=info[trade_type].method) # create a transaction with the selected ABI contract_txn: ContractTransaction = ContractTransaction(abi=selected_abi, address=hyperdrive_contract.address) + args = [arg.scaled_value if isinstance(arg, FixedPoint) else arg for arg in args] return contract_txn, args, selected_abi diff --git a/examples/evm_bots.py b/examples/evm_bots.py index f0d86e058c..8ce72b1a29 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -223,16 +223,6 @@ def set_up_agents( bot_config=bot_config, rng=rng, ) - log_str = "Created %s agent with wallet:" - for long in agent.wallet.longs: - print(f"{long=}") - print(f"{type(long)=}") - log_str += " long " - for short in agent.wallet.shorts: - print(f"{short=}") - print(f"{type(short)=}") - log_str += " short " - logging.info("created kek") sim_agents[f"agent_{agent.wallet.address}"] = agent return sim_agents, trade_history, dev_accounts @@ -450,8 +440,8 @@ def set_up_ape( if config.load_state_id is not None: # load state from specified id logging.info("Loading state from id: %s", config.load_state_id) load_state_block_number, addresses, agent_addresses, trade_history = load_state(bot_config, rng) - print(f"Loaded state up to block number {load_state_block_number}") - print(f"{provider.get_block('latest').number=}") + # print(f"Loaded state up to block number {load_state_block_number}") + # print(f"{provider.get_block('latest').number=}") project: ape_utils.HyperdriveProject = ape_utils.HyperdriveProject( path=Path.cwd(), hyperdrive_address=addresses["hyperdrive"] if bot_config.devnet else addresses["goerli_hyperdrive"], @@ -482,6 +472,7 @@ def do_policy( hyperdrive_instance: ContractInstance, base_instance: ContractInstance, bot_config: BotConfig, + trade_history: pd.DataFrame | None = None, ) -> int: """Execute an agent's policy. @@ -503,6 +494,8 @@ def do_policy( Contract for base token bot_config : BotConfig The bot configuration. + trade_history : pd.DataFrame, Optional + History of previously completed trades. If not provided, it will be queried. Returns ------- @@ -518,7 +511,7 @@ def do_policy( # marginal update to wallet agent.wallet = ape_utils.get_wallet_from_trade_history( address=agent.contract.address, - trade_history=ape_utils.get_trade_history(hyperdrive_instance, ape.chain.blocks[-1].number), + trade_history=trade_history, hyperdrive_contract=hyperdrive_instance, base_contract=base_instance, add_to_existing_wallet=agent.wallet, @@ -537,12 +530,16 @@ def do_policy( elfpy_regular = bot_config.scratch["project_dir"] / "elfpy_regular.json" elfpy_crash = bot_config.scratch["project_dir"] / "elfpy_crash.json" Path.rename(elfpy_regular, elfpy_crash) - logging.info(" => anvil state saved to %s, elfpy state saved to %s", anvil_crash, elfpy_crash) + logging.info( + " => anvil state saved to %s\n => elfpy state saved to %s", + anvil_crash, + elfpy_crash, + ) raise exc return no_crash_streak -def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance): +def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history=None): """Dump relevant pieces of information: full node state from anvil, random generator state, and trade history. Parameters @@ -559,24 +556,21 @@ def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_ Dict of agents used in the simulation. hyperdrive_instance : `ape.contracts.ContractInstance `_ The hyperdrive contract instance. + trade_history : pd.DataFrame, Optional + History of previously completed trades. """ - trade_history = ape_utils.get_trade_history(hyperdrive_instance) - if trade_history is not None: - trade_history = (trade_history.to_dict(orient="records"),) # make it JSON serializable + if trade_history is None: + trade_history = ape_utils.get_trade_history(hyperdrive_instance) json.dump( { "rand_seed": bot_config.random_seed, "rand_state": rng.bit_generator.state, - "trade_history": trade_history, "block_number": block_number, "addresses": addresses, + "trade_history": trade_history.to_dict(), "agent_addresses": [agent.contract.address for agent in sim_agents.values()], }, - fp=open( - bot_config.scratch["project_dir"] / "elfpy_regular.json", - "w", - encoding="utf-8", - ), + fp=open(bot_config.scratch["project_dir"] / "elfpy_regular.json","w",encoding="utf-8"), ) @@ -607,7 +601,7 @@ def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame state = json.load(file) bot_config.random_seed = state["rand_seed"] rng.bit_generator.state = state["rand_state"] - trade_history = pd.DataFrame(state["trade_history"][0]) + trade_history = pd.DataFrame(state["trade_history"]) logging.info( "STATE loaded from %s with:\n rand_seed %s\n rand_state %s\n trades %s", file_path, @@ -629,10 +623,10 @@ def main( # pylint: disable=too-many-locals # Custom parameters for this experiment bot_config.scratch["project_dir"] = Path.cwd().parent if Path.cwd().name == "examples" else Path.cwd() - bot_config.scratch["state_dump_file_path"] = bot_config.scratch["project_dir"] / "state_dumps" + # bot_config.scratch["state_dump_file_path"] = bot_config.scratch["project_dir"] / "state_dumps" # make dir if it doesn't exist - if not bot_config.scratch["state_dump_file_path"].exists(): - bot_config.scratch["state_dump_file_path"].mkdir() + # if not bot_config.scratch["state_dump_file_path"].exists(): + # bot_config.scratch["state_dump_file_path"].mkdir() if "num_louie" not in bot_config.scratch: bot_config.scratch["num_louie"]: int = 1 if "num_sally" not in bot_config.scratch: @@ -672,7 +666,8 @@ def main( sim_agents, trade_history, dev_accounts = set_up_agents( bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history ) - ape_utils.inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance) + if bot_config.load_state_id is not None: + ape_utils.inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance) ape_utils.dump_agent_info(sim_agents, bot_config) logging.info("Constructed %s agents:", len(sim_agents)) for agent_name in sim_agents: @@ -685,8 +680,15 @@ def main( block_timestamp = latest_block.timestamp if block_number > last_executed_block: log_and_show_block_info(provider, no_crash_streak, block_number, block_timestamp) + # marginal update to trade_history + start_block = trade_history.block_number.max() + 1 + start_time = now() + trade_history = ape_utils.get_trade_history(hyperdrive_instance, start_block, block_number, trade_history) + logging.info("Trade history updated in %s seconds", now() - start_time) if config.dump_state is True: # dump state at the beginning of every new block - dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance) + start_time = now() + dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history) + logging.info("Dumped state in %s seconds", now() - start_time) # create market object needed for agent execution elfpy_market = ape_utils.create_elfpy_market( pricing_model, hyperdrive_instance, hyperdrive_config, block_number, block_timestamp, start_timestamp @@ -701,6 +703,7 @@ def main( hyperdrive_instance, base_instance, bot_config, + trade_history, ) last_executed_block = block_number if ( From 2478e6e9e865ada0f0baf2b517a6cddab408486c Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 12:32:56 -0400 Subject: [PATCH 07/28] commit profiles and helper scripts --- anvil_crash.sh | 2 ++ anvil_crash2.sh | 1 + anvil_regular.sh | 1 + profile_crash.json | 25 +++++++++++++++++++++++++ profile_regular.json | 25 +++++++++++++++++++++++++ 5 files changed, 54 insertions(+) create mode 100755 anvil_crash.sh create mode 100755 anvil_crash2.sh create mode 100755 anvil_regular.sh create mode 100644 profile_crash.json create mode 100644 profile_regular.json diff --git a/anvil_crash.sh b/anvil_crash.sh new file mode 100755 index 0000000000..c5bf35572b --- /dev/null +++ b/anvil_crash.sh @@ -0,0 +1,2 @@ +cp anvil_crash.json anvil_crash_live.json +anvil --tracing --code-size-limit=9999999999999999 --state anvil_crash_live.json --accounts 1 diff --git a/anvil_crash2.sh b/anvil_crash2.sh new file mode 100755 index 0000000000..c78d39b62d --- /dev/null +++ b/anvil_crash2.sh @@ -0,0 +1 @@ +anvil --tracing --code-size-limit=9999999999999999 --load-state anvil_crash.json --accounts 1 diff --git a/anvil_regular.sh b/anvil_regular.sh new file mode 100755 index 0000000000..2c3dfcb125 --- /dev/null +++ b/anvil_regular.sh @@ -0,0 +1 @@ +anvil --tracing --code-size-limit=9999999999999999 --state anvil_regular.json --accounts 1 diff --git a/profile_crash.json b/profile_crash.json new file mode 100644 index 0000000000..341ff41aa9 --- /dev/null +++ b/profile_crash.json @@ -0,0 +1,25 @@ +{ + "alchemy": false, + "artifacts_url": "http://localhost:80", + "delete_previous_logs": true, + "devnet": true, + "frozen": false, + "halt_on_errors": true, + "log_file_and_stdout": true, + "log_filename": "crash-test.log", + "log_formatter": "%(message)s", + "log_level": "INFO", + "max_bytes": 2000000, + "no_new_attribs": true, + "random_seed": 1234, + "risk_threshold": 0.0, + "rpc_url": "http://localhost:8545", + "load_state_id": "elfpy_crash", + "scratch": { + "num_frida": 0, + "num_random": 1, + "num_louie": 0, + "num_sally": 0 + }, + "trade_chance": 1 +} \ No newline at end of file diff --git a/profile_regular.json b/profile_regular.json new file mode 100644 index 0000000000..7e633faeca --- /dev/null +++ b/profile_regular.json @@ -0,0 +1,25 @@ +{ + "alchemy": false, + "artifacts_url": "http://localhost:80", + "delete_previous_logs": true, + "devnet": true, + "frozen": false, + "halt_on_errors": true, + "log_file_and_stdout": true, + "log_filename": "crash-test.log", + "log_formatter": "%(message)s", + "log_level": "INFO", + "max_bytes": 2000000, + "no_new_attribs": true, + "random_seed": 1234, + "risk_threshold": 0.0, + "rpc_url": "http://localhost:8545", + "dump_state": true, + "scratch": { + "num_frida": 0, + "num_random": 1, + "num_louie": 0, + "num_sally": 0 + }, + "trade_chance": 1 +} \ No newline at end of file From acb2345941e5fc20fc95f3610d15ce25e956485e Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 14:36:07 -0400 Subject: [PATCH 08/28] move inspect_dump and get_devnet_addresses into evm_bots --- elfpy/utils/apeworx_integrations.py | 121 ++-------------------------- examples/evm_bots.py | 118 ++++++++++++++++++++++++++- 2 files changed, 120 insertions(+), 119 deletions(-) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index fe1ff72af6..18b1c8612e 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -6,14 +6,12 @@ import logging import os import re -from time import sleep from collections import namedtuple from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING, Any, Callable # third party libs -import requests import ape import numpy as np import pandas as pd @@ -31,7 +29,6 @@ from elfpy import MAXIMUM_BALANCE_MISMATCH_IN_WEI, SECONDS_IN_YEAR, WEI, simulators, time, types from elfpy.markets.hyperdrive import AssetIdPrefix, HyperdriveMarketState, HyperdrivePricingModel, hyperdrive_assets from elfpy.markets.hyperdrive.hyperdrive_market import HyperdriveMarket -from elfpy.bots import BotConfig from elfpy.math import FixedPoint from elfpy.simulators.config import Config from elfpy.utils import outputs as output_utils @@ -129,117 +126,6 @@ class DefaultHyperdriveConfig: target_liquidity = FixedPoint(1 * 10**6) -def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance=1e16): - """Sanity check trade history and balances.""" - for idx, address in enumerate(agent_addresses): - log_str = f"querying agent_{idx} at addr={address}" - - # create wallet - wallet = get_wallet_from_trade_history( - address=str(address), - index=idx, - trade_history=trade_history, - hyperdrive_contract=hyperdrive_instance, - base_contract=base_instance, - tolerance=1e16, # allow being off by $0.01 - ) - - # print what's in wallet - log_str += f"{wallet=}" - log_str = f"agent_{idx} has:" - log_str += "\n wallet:" - for _, long in wallet.longs.items(): - log_str += f"\n long {long.balance}" - for _, short in wallet.shorts.items(): - log_str += f"\n short {short.balance}" - log_str += f"\n lp_tokens {wallet.lp_tokens}" - - # print trade counts - from_agent = trade_history["from"] == address - to_agent = trade_history["to"] == address - num_trades = sum((from_agent | to_agent)) - num_longs = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LONG")) - num_shorts = sum((from_agent | to_agent) & (trade_history["trade_type"] == "SHORT")) - num_lp = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LP")) - num_withdrawal = sum((from_agent | to_agent) & (trade_history["trade_type"] == "WITHDRAWAL_SHARE")) - log_str += "\n trade counts:" - log_str += f"\n {num_trades:2.0f} total" - log_str += f"\n {num_longs:2.0f} long" - log_str += f"\n {num_shorts:2.0f} short" - log_str += f"\n {num_lp:2.0f} LP" - log_str += f"\n {num_withdrawal:2.0f} withdrawal share" - if num_trades != (num_longs + num_shorts + num_lp + num_withdrawal): - log_str += "\n trade counts DON'T add up to total" - else: - log_str += "\n trade counts add up to total\n" - - # compare off-chain to on-chain balances - log_str += "tuples of balances by position by calculation source. balance = (trade_history, onchain)\n" - log_str += f" tolerance = {tolerance} aka ${tolerance/1e18}\n" - for position_id in trade_history["id"].unique(): # loop across all unique positions - from_agent = trade_history["from"] == address - to_agent = trade_history["to"] == address - relevant_trades = ((from_agent) | (to_agent)) & (trade_history["id"] == position_id) - positive_balance = int(trade_history.loc[relevant_trades & (to_agent), "value"].sum()) - negative_balance = int(trade_history.loc[relevant_trades & (from_agent), "value"].sum()) - balance = positive_balance - negative_balance - on_chain_balance = hyperdrive_instance.balanceOf(position_id, address) - first_relevant_row = relevant_trades.index[relevant_trades][0] - info = trade_history.loc[first_relevant_row, :] - log_str += f"{address[:8]} {info.trade_type:16} balance = " - if abs(balance - on_chain_balance) > tolerance: - log_str += f"({balance/1e18:0.5f},{on_chain_balance/1e18:0.5f}) MISMATCH\n" - else: - log_str += f"({balance/1e18:0.0f},{on_chain_balance/1e18:0.0f}) match\n" - logging.info(log_str) - - -def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: - """Get devnet addresses from address file.""" - if addresses is None: - addresses = {} - deployed_addresses = {} - # get deployed addresses from local file, if it exists - address_file_path = bot_config.scratch["project_dir"] / "hyperdrive_solidity/artifacts/addresses.json" - if os.path.exists(address_file_path): - logging.info("Loading addresses.json from local file. This should only be used for development.") - with open(address_file_path, "r", encoding="utf-8") as file: - deployed_addresses = json.load(file) - else: # otherwise get deployed addresses from artifacts server - logging.info( - "Attempting to load addresses.json, which requires waiting for the contract deployment to complete." - ) - num_attempts = 120 - for attempt_num in range(num_attempts): - logging.info("\tAttempt %s out of %s to %s", attempt_num + 1, num_attempts, bot_config.artifacts_url) - try: - response = requests.get(f"{bot_config.artifacts_url}/addresses.json", timeout=10) - if response.status_code == 200: - deployed_addresses = response.json() - break - except requests.exceptions.ConnectionError as exc: - logging.info("Connection error: %s", exc) - sleep(1) - logging.info("Contracts deployed; addresses loaded.") - if "baseToken" in deployed_addresses: - addresses["baseToken"] = deployed_addresses["baseToken"] - logging.info("found devnet base address: %s", addresses["baseToken"]) - else: - addresses["baseToken"] = None - if "mockHyperdrive" in deployed_addresses: - addresses["hyperdrive"] = deployed_addresses["mockHyperdrive"] - logging.info("found devnet hyperdrive address: %s", addresses["hyperdrive"]) - elif "hyperdrive" in deployed_addresses: - addresses["hyperdrive"] = deployed_addresses["hyperdrive"] - logging.info("found devnet hyperdrive address: %s", addresses["hyperdrive"]) - else: - addresses["hyperdrive"] = None - if "mockHyperdriveMath" in deployed_addresses: - addresses["hyperdriveMath"] = deployed_addresses["mockHyperdriveMath"] - logging.info("found devnet hyperdriveMath address: %s", addresses["hyperdriveMath"]) - return addresses - - def get_hyperdrive_config(hyperdrive_instance) -> dict: """Get the hyperdrive config from a deployed hyperdrive contract. @@ -396,7 +282,10 @@ def get_market_state_from_contract(hyperdrive_contract: ContractInstance, **kwar def get_trade_history( - hyperdrive_contract: ContractInstance, start_block: int = 0, stop_block: int | None = None, add_to: pd.DataFrame | None = None + hyperdrive_contract: ContractInstance, + start_block: int = 0, + stop_block: int | None = None, + add_to: pd.DataFrame | None = None, ) -> pd.DataFrame: r"""Get all trades from hyperdrive contract. @@ -495,7 +384,7 @@ def get_wallet_from_trade_history( logging.debug( "balance %s = positive_balance %s - negative_balance %s", balance, positive_balance, negative_balance ) - if "prefix" in trade_history.columns and len(trade_history.loc[relevant_trades,:])>0: + if "prefix" in trade_history.columns and len(trade_history.loc[relevant_trades, :]) > 0: asset_prefix = trade_history.loc[relevant_trades, "prefix"].iloc[0] maturity = trade_history.loc[relevant_trades, "maturity_timestamp"].iloc[0] else: diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 8ce72b1a29..d38edd9af0 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -7,6 +7,7 @@ import argparse import json import logging +import requests from datetime import datetime from pathlib import Path from time import sleep @@ -43,6 +44,117 @@ ape_logger.set_level(logging.ERROR) +def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance=1e16): + """Sanity check trade history and balances.""" + for idx, address in enumerate(agent_addresses): + log_str = f"querying agent_{idx} at addr={address}" + + # create wallet + wallet = ape_utils.get_wallet_from_trade_history( + address=str(address), + index=idx, + trade_history=trade_history, + hyperdrive_contract=hyperdrive_instance, + base_contract=base_instance, + tolerance=1e16, # allow being off by $0.01 + ) + + # print what's in wallet + log_str += f"{wallet=}" + log_str = f"agent_{idx} has:" + log_str += "\n wallet:" + for _, long in wallet.longs.items(): + log_str += f"\n long {long.balance}" + for _, short in wallet.shorts.items(): + log_str += f"\n short {short.balance}" + log_str += f"\n lp_tokens {wallet.lp_tokens}" + + # print trade counts + from_agent = trade_history["from"] == address + to_agent = trade_history["to"] == address + num_trades = sum((from_agent | to_agent)) + num_longs = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LONG")) + num_shorts = sum((from_agent | to_agent) & (trade_history["trade_type"] == "SHORT")) + num_lp = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LP")) + num_withdrawal = sum((from_agent | to_agent) & (trade_history["trade_type"] == "WITHDRAWAL_SHARE")) + log_str += "\n trade counts:" + log_str += f"\n {num_trades:2.0f} total" + log_str += f"\n {num_longs:2.0f} long" + log_str += f"\n {num_shorts:2.0f} short" + log_str += f"\n {num_lp:2.0f} LP" + log_str += f"\n {num_withdrawal:2.0f} withdrawal share" + if num_trades != (num_longs + num_shorts + num_lp + num_withdrawal): + log_str += "\n trade counts DON'T add up to total" + else: + log_str += "\n trade counts add up to total\n" + + # compare off-chain to on-chain balances + log_str += "tuples of balances by position by calculation source. balance = (trade_history, onchain)\n" + log_str += f" tolerance = {tolerance} aka ${tolerance/1e18}\n" + for position_id in trade_history["id"].unique(): # loop across all unique positions + from_agent = trade_history["from"] == address + to_agent = trade_history["to"] == address + relevant_trades = ((from_agent) | (to_agent)) & (trade_history["id"] == position_id) + positive_balance = int(trade_history.loc[relevant_trades & (to_agent), "value"].sum()) + negative_balance = int(trade_history.loc[relevant_trades & (from_agent), "value"].sum()) + balance = positive_balance - negative_balance + on_chain_balance = hyperdrive_instance.balanceOf(position_id, address) + first_relevant_row = relevant_trades.index[relevant_trades][0] + info = trade_history.loc[first_relevant_row, :] + log_str += f"{address[:8]} {info.trade_type:16} balance = " + if abs(balance - on_chain_balance) > tolerance: + log_str += f"({balance/1e18:0.5f},{on_chain_balance/1e18:0.5f}) MISMATCH\n" + else: + log_str += f"({balance/1e18:0.0f},{on_chain_balance/1e18:0.0f}) match\n" + logging.info(log_str) + + +def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: + """Get devnet addresses from address file.""" + if addresses is None: + addresses = {} + deployed_addresses = {} + # get deployed addresses from local file, if it exists + address_file_path = bot_config.scratch["project_dir"] / "hyperdrive_solidity/artifacts/addresses.json" + if os.path.exists(address_file_path): + logging.info("Loading addresses.json from local file. This should only be used for development.") + with open(address_file_path, "r", encoding="utf-8") as file: + deployed_addresses = json.load(file) + else: # otherwise get deployed addresses from artifacts server + logging.info( + "Attempting to load addresses.json, which requires waiting for the contract deployment to complete." + ) + num_attempts = 120 + for attempt_num in range(num_attempts): + logging.info("\tAttempt %s out of %s to %s", attempt_num + 1, num_attempts, bot_config.artifacts_url) + try: + response = requests.get(f"{bot_config.artifacts_url}/addresses.json", timeout=10) + if response.status_code == 200: + deployed_addresses = response.json() + break + except requests.exceptions.ConnectionError as exc: + logging.info("Connection error: %s", exc) + sleep(1) + logging.info("Contracts deployed; addresses loaded.") + if "baseToken" in deployed_addresses: + addresses["baseToken"] = deployed_addresses["baseToken"] + logging.info("found devnet base address: %s", addresses["baseToken"]) + else: + addresses["baseToken"] = None + if "mockHyperdrive" in deployed_addresses: + addresses["hyperdrive"] = deployed_addresses["mockHyperdrive"] + logging.info("found devnet hyperdrive address: %s", addresses["hyperdrive"]) + elif "hyperdrive" in deployed_addresses: + addresses["hyperdrive"] = deployed_addresses["hyperdrive"] + logging.info("found devnet hyperdrive address: %s", addresses["hyperdrive"]) + else: + addresses["hyperdrive"] = None + if "mockHyperdriveMath" in deployed_addresses: + addresses["hyperdriveMath"] = deployed_addresses["mockHyperdriveMath"] + logging.info("found devnet hyperdriveMath address: %s", addresses["hyperdriveMath"]) + return addresses + + def get_accounts(bot_config: BotConfig) -> list[KeyfileAccount]: """Generate dev accounts and turn on auto-sign.""" num = sum(bot_config.scratch[f"num_{bot}"] for bot in bot_config.scratch["bot_names"]) @@ -570,7 +682,7 @@ def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_ "trade_history": trade_history.to_dict(), "agent_addresses": [agent.contract.address for agent in sim_agents.values()], }, - fp=open(bot_config.scratch["project_dir"] / "elfpy_regular.json","w",encoding="utf-8"), + fp=open(bot_config.scratch["project_dir"] / "elfpy_regular.json", "w", encoding="utf-8"), ) @@ -650,7 +762,7 @@ def main( "goerli_hyperdrive": "0xB311B825171AF5A60d69aAD590B857B1E5ed23a2", } if bot_config.devnet: - addresses = ape_utils.get_devnet_addresses(bot_config, addresses) + addresses = get_devnet_addresses(bot_config, addresses) pricing_model = HyperdrivePricingModel() ( provider, @@ -667,7 +779,7 @@ def main( bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history ) if bot_config.load_state_id is not None: - ape_utils.inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance) + inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance) ape_utils.dump_agent_info(sim_agents, bot_config) logging.info("Constructed %s agents:", len(sim_agents)) for agent_name in sim_agents: From 055f8e6e8282c3b5c23e6b09f8b0a7cbb5dad4b3 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 14:40:12 -0400 Subject: [PATCH 09/28] change inspect_dump name --- examples/evm_bots.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/evm_bots.py b/examples/evm_bots.py index d38edd9af0..a2c86b47c6 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -44,8 +44,8 @@ ape_logger.set_level(logging.ERROR) -def inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance=1e16): - """Sanity check trade history and balances.""" +def check_state_vs_onchain_balances(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance=1e16): + """Inspect the dump we just loaded to ensure accuracy between balances from trade history on-chain queries.""" for idx, address in enumerate(agent_addresses): log_str = f"querying agent_{idx} at addr={address}" @@ -779,7 +779,7 @@ def main( bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history ) if bot_config.load_state_id is not None: - inspect_dump(trade_history, agent_addresses, hyperdrive_instance, base_instance) + check_state_vs_onchain_balances(trade_history, agent_addresses, hyperdrive_instance, base_instance) ape_utils.dump_agent_info(sim_agents, bot_config) logging.info("Constructed %s agents:", len(sim_agents)) for agent_name in sim_agents: From ba373929400fdf955b38b58b8ce41fff389c52b4 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 14:48:45 -0400 Subject: [PATCH 10/28] don't return block when loading state --- examples/evm_bots.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/examples/evm_bots.py b/examples/evm_bots.py index a2c86b47c6..fa81c83420 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -551,9 +551,7 @@ def set_up_ape( agent_addresses, trade_history = None, None if config.load_state_id is not None: # load state from specified id logging.info("Loading state from id: %s", config.load_state_id) - load_state_block_number, addresses, agent_addresses, trade_history = load_state(bot_config, rng) - # print(f"Loaded state up to block number {load_state_block_number}") - # print(f"{provider.get_block('latest').number=}") + addresses, agent_addresses, trade_history = load_state(bot_config, rng) project: ape_utils.HyperdriveProject = ape_utils.HyperdriveProject( path=Path.cwd(), hyperdrive_address=addresses["hyperdrive"] if bot_config.devnet else addresses["goerli_hyperdrive"], @@ -686,7 +684,7 @@ def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_ ) -def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame]: +def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame]: """Load relevant pieces of information: full node state from anvil, random generator state, and trade history. Parameters @@ -698,8 +696,6 @@ def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame Returns ------- - state["block_number"] : int - The block number of the state loaded. state["addresses"] : list[str] List of deployed contract addresses. state["agent_addresses"] : list[str] @@ -721,7 +717,7 @@ def load_state(bot_config, rng) -> tuple[int, list[str], list[str], pd.DataFrame rng.bit_generator.state, len(trade_history), ) - return state["block_number"], state["addresses"], state["agent_addresses"], trade_history + return state["addresses"], state["agent_addresses"], trade_history def main( From 0b9574a37710f7f7a72faaafdfa18c379c70bb54 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 15:01:48 -0400 Subject: [PATCH 11/28] dump state only once, in try/except block --- examples/evm_bots.py | 93 ++++++++++++++++++++------------------------ 1 file changed, 42 insertions(+), 51 deletions(-) diff --git a/examples/evm_bots.py b/examples/evm_bots.py index fa81c83420..67fd98040e 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -4,10 +4,11 @@ from __future__ import annotations # types will be strings by default in 3.11 # stdlib +import os import argparse import json import logging -import requests +import shutil from datetime import datetime from pathlib import Path from time import sleep @@ -16,6 +17,7 @@ # external lib import ape +import requests import numpy as np import pandas as pd from ape import accounts @@ -581,7 +583,6 @@ def do_policy( sim_agents: dict[str, Agent], hyperdrive_instance: ContractInstance, base_instance: ContractInstance, - bot_config: BotConfig, trade_history: pd.DataFrame | None = None, ) -> int: """Execute an agent's policy. @@ -615,41 +616,22 @@ def do_policy( # pylint: disable=too-many-arguments trades: list[types.Trade] = agent.get_trades(market=elfpy_market) for trade_object in trades: - try: - logging.debug(trade_object) - do_trade(trade_object, sim_agents, hyperdrive_instance, base_instance) - # marginal update to wallet - agent.wallet = ape_utils.get_wallet_from_trade_history( - address=agent.contract.address, - trade_history=trade_history, - hyperdrive_contract=hyperdrive_instance, - base_contract=base_instance, - add_to_existing_wallet=agent.wallet, - ) - logging.debug("%s", agent.wallet) - no_crash_streak = set_days_without_crashing(no_crash_streak, crash_file) # set and save to file - except Exception as exc: # we want to catch all exceptions (pylint: disable=broad-exception-caught) - logging.info("Crashed with error: %s", exc) - no_crash_streak = set_days_without_crashing(no_crash_streak, crash_file, reset=True) # set and save to file - if bot_config.halt_on_errors: - # rename anvil_regular.json to anvil_crash.json - anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" - anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" - Path.rename(anvil_regular, anvil_crash) - # rename elfpy_regular.json to elfpy_crash.json - elfpy_regular = bot_config.scratch["project_dir"] / "elfpy_regular.json" - elfpy_crash = bot_config.scratch["project_dir"] / "elfpy_crash.json" - Path.rename(elfpy_regular, elfpy_crash) - logging.info( - " => anvil state saved to %s\n => elfpy state saved to %s", - anvil_crash, - elfpy_crash, - ) - raise exc + logging.debug(trade_object) + do_trade(trade_object, sim_agents, hyperdrive_instance, base_instance) + # marginal update to wallet + agent.wallet = ape_utils.get_wallet_from_trade_history( + address=agent.contract.address, + trade_history=trade_history, + hyperdrive_contract=hyperdrive_instance, + base_contract=base_instance, + add_to_existing_wallet=agent.wallet, + ) + logging.debug("%s", agent.wallet) + no_crash_streak = set_days_without_crashing(no_crash_streak, crash_file) # set and save to file return no_crash_streak -def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history=None): +def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history=None): """Dump relevant pieces of information: full node state from anvil, random generator state, and trade history. Parameters @@ -669,8 +651,10 @@ def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_ trade_history : pd.DataFrame, Optional History of previously completed trades. """ + start_time = now() if trade_history is None: trade_history = ape_utils.get_trade_history(hyperdrive_instance) + elfpy_crash = bot_config.scratch["project_dir"] / "elfpy_crash.json" json.dump( { "rand_seed": bot_config.random_seed, @@ -680,8 +664,13 @@ def dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_ "trade_history": trade_history.to_dict(), "agent_addresses": [agent.contract.address for agent in sim_agents.values()], }, - fp=open(bot_config.scratch["project_dir"] / "elfpy_regular.json", "w", encoding="utf-8"), + fp=open(elfpy_crash, "w", encoding="utf-8"), ) + logging.info("Dumped state in %s seconds", now() - start_time) + anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" + anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" + shutil.copy(anvil_regular, anvil_crash) # save anvil's state, so we can reproduce the crash + logging.info(" => anvil state saved to %s\n => elfpy state saved to %s",anvil_crash,elfpy_crash) def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame]: @@ -793,26 +782,28 @@ def main( start_time = now() trade_history = ape_utils.get_trade_history(hyperdrive_instance, start_block, block_number, trade_history) logging.info("Trade history updated in %s seconds", now() - start_time) - if config.dump_state is True: # dump state at the beginning of every new block - start_time = now() - dump_state(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history) - logging.info("Dumped state in %s seconds", now() - start_time) # create market object needed for agent execution elfpy_market = ape_utils.create_elfpy_market( pricing_model, hyperdrive_instance, hyperdrive_config, block_number, block_timestamp, start_timestamp ) - for agent in sim_agents.values(): - no_crash_streak = do_policy( - agent, - elfpy_market, - no_crash_streak, - crash_file, - sim_agents, - hyperdrive_instance, - base_instance, - bot_config, - trade_history, - ) + try: + for agent in sim_agents.values(): + no_crash_streak = do_policy( + agent, + elfpy_market, + no_crash_streak, + crash_file, + sim_agents, + hyperdrive_instance, + base_instance, + trade_history, + ) + except Exception as exc: # we want to catch all exceptions (pylint: disable=broad-exception-caught) + logging.info("Crashed with error: %s", exc) + no_crash_streak = set_days_without_crashing(no_crash_streak, crash_file, reset=True) # set and save to file + process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history) + if bot_config.halt_on_errors: + raise exc last_executed_block = block_number if ( bot_config.devnet and provider.auto_mine From b276ea20fbd39ee2ab3879f9a9c4cf5327e74e63 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 15:16:55 -0400 Subject: [PATCH 12/28] remove profiles and anvil scripts --- anvil_crash.sh | 2 -- anvil_crash2.sh | 1 - anvil_regular.sh | 1 - profile_crash.json | 25 ------------------------- profile_regular.json | 25 ------------------------- 5 files changed, 54 deletions(-) delete mode 100755 anvil_crash.sh delete mode 100755 anvil_crash2.sh delete mode 100755 anvil_regular.sh delete mode 100644 profile_crash.json delete mode 100644 profile_regular.json diff --git a/anvil_crash.sh b/anvil_crash.sh deleted file mode 100755 index c5bf35572b..0000000000 --- a/anvil_crash.sh +++ /dev/null @@ -1,2 +0,0 @@ -cp anvil_crash.json anvil_crash_live.json -anvil --tracing --code-size-limit=9999999999999999 --state anvil_crash_live.json --accounts 1 diff --git a/anvil_crash2.sh b/anvil_crash2.sh deleted file mode 100755 index c78d39b62d..0000000000 --- a/anvil_crash2.sh +++ /dev/null @@ -1 +0,0 @@ -anvil --tracing --code-size-limit=9999999999999999 --load-state anvil_crash.json --accounts 1 diff --git a/anvil_regular.sh b/anvil_regular.sh deleted file mode 100755 index 2c3dfcb125..0000000000 --- a/anvil_regular.sh +++ /dev/null @@ -1 +0,0 @@ -anvil --tracing --code-size-limit=9999999999999999 --state anvil_regular.json --accounts 1 diff --git a/profile_crash.json b/profile_crash.json deleted file mode 100644 index 341ff41aa9..0000000000 --- a/profile_crash.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "alchemy": false, - "artifacts_url": "http://localhost:80", - "delete_previous_logs": true, - "devnet": true, - "frozen": false, - "halt_on_errors": true, - "log_file_and_stdout": true, - "log_filename": "crash-test.log", - "log_formatter": "%(message)s", - "log_level": "INFO", - "max_bytes": 2000000, - "no_new_attribs": true, - "random_seed": 1234, - "risk_threshold": 0.0, - "rpc_url": "http://localhost:8545", - "load_state_id": "elfpy_crash", - "scratch": { - "num_frida": 0, - "num_random": 1, - "num_louie": 0, - "num_sally": 0 - }, - "trade_chance": 1 -} \ No newline at end of file diff --git a/profile_regular.json b/profile_regular.json deleted file mode 100644 index 7e633faeca..0000000000 --- a/profile_regular.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "alchemy": false, - "artifacts_url": "http://localhost:80", - "delete_previous_logs": true, - "devnet": true, - "frozen": false, - "halt_on_errors": true, - "log_file_and_stdout": true, - "log_filename": "crash-test.log", - "log_formatter": "%(message)s", - "log_level": "INFO", - "max_bytes": 2000000, - "no_new_attribs": true, - "random_seed": 1234, - "risk_threshold": 0.0, - "rpc_url": "http://localhost:8545", - "dump_state": true, - "scratch": { - "num_frida": 0, - "num_random": 1, - "num_louie": 0, - "num_sally": 0 - }, - "trade_chance": 1 -} \ No newline at end of file From 1636dfcc8746aa2387f2ed8a4258d19548acbaec Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 15:21:06 -0400 Subject: [PATCH 13/28] revert changes to .gitignore --- .gitignore | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitignore b/.gitignore index 7bfd2eb033..b35a9aa5cf 100644 --- a/.gitignore +++ b/.gitignore @@ -86,8 +86,6 @@ no_crash.txt *.gif examples/pics examples/abi/ -state_dumps/ -config_json_generator.py # Local artifacts artifacts/ From 36a204a2b38179b59e2887ddd66a1409321cec7b Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 15:37:16 -0400 Subject: [PATCH 14/28] remove unnecessary return of devnet_addresses --- examples/evm_bots.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 67fd98040e..49ed3ef492 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -338,7 +338,7 @@ def set_up_agents( rng=rng, ) sim_agents[f"agent_{agent.wallet.address}"] = agent - return sim_agents, trade_history, dev_accounts + return sim_agents, trade_history def do_trade( @@ -670,7 +670,7 @@ def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdri anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" shutil.copy(anvil_regular, anvil_crash) # save anvil's state, so we can reproduce the crash - logging.info(" => anvil state saved to %s\n => elfpy state saved to %s",anvil_crash,elfpy_crash) + logging.info(" => anvil state saved to %s\n => elfpy state saved to %s", anvil_crash, elfpy_crash) def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame]: @@ -760,7 +760,7 @@ def main( ) = set_up_ape(bot_config, provider_settings, addresses, network_choice, pricing_model, rng) no_crash_streak = 0 # set up the environment - sim_agents, trade_history, dev_accounts = set_up_agents( + sim_agents, trade_history = set_up_agents( bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history ) if bot_config.load_state_id is not None: @@ -800,7 +800,9 @@ def main( ) except Exception as exc: # we want to catch all exceptions (pylint: disable=broad-exception-caught) logging.info("Crashed with error: %s", exc) - no_crash_streak = set_days_without_crashing(no_crash_streak, crash_file, reset=True) # set and save to file + no_crash_streak = set_days_without_crashing( + no_crash_streak, crash_file, reset=True + ) # set and save to file process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history) if bot_config.halt_on_errors: raise exc From 71282698f905cb2ba93cf38158ad3264cdc6d224 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 15:53:09 -0400 Subject: [PATCH 15/28] name trade_streak uniformly to distinguish from crash --- examples/evm_bots.py | 58 ++++++++++++++++++++------------------------ 1 file changed, 26 insertions(+), 32 deletions(-) diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 49ed3ef492..43ae08e2dc 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -405,24 +405,22 @@ def do_trade( return pool_state -def set_days_without_crashing(current_streak, crash_file, reset: bool = False): - """Calculate the number of days without crashing.""" +def save_trade_streak(current_streak, trade_streak_file, reset: bool = False): + """Save to file our trade streak so we can resume it on interrupt.""" streak = 0 if reset is True else current_streak + 1 - with open(crash_file, "w", encoding="utf-8") as file: + with open(trade_streak_file, "w", encoding="utf-8") as file: file.write(f"{streak}") return streak -def log_and_show_block_info( - provider: ape.api.ProjectAPI, no_crash_streak: int, block_number: int, block_timestamp: int -): +def log_and_show_block_info(provider: ape.api.ProjectAPI, trade_streak: int, block_number: int, block_timestamp: int): """Get and show the latest block number and gas fees. Parameters ---------- provider : `ape.api.ProviderAPI `_ The Ape object that represents your connection to the Ethereum network. - no_crash_streak : int + trade_streak : int The number of trades without crashing. block_number : int The number of the latest block. @@ -437,7 +435,7 @@ def log_and_show_block_info( "Block number: %s, Block time: %s, Trades without crashing: %s, base_fee: %s", str_with_precision(block_number), datetime.fromtimestamp(block_timestamp), - no_crash_streak, + trade_streak, base_fee, ) @@ -578,8 +576,8 @@ def set_up_ape( def do_policy( agent: BasePolicy, elfpy_market: HyperdriveMarket, - no_crash_streak: int, - crash_file: str, + trade_streak: int, + trade_streak_file: Path.PathLike, sim_agents: dict[str, Agent], hyperdrive_instance: ContractInstance, base_instance: ContractInstance, @@ -593,10 +591,10 @@ def do_policy( The agent object used in elf-simulations. elfpy_market : HyperdriveMarket The elf-simulations object representing the Hyperdrive market. - no_crash_streak : int + trade_streak : int Number of trades in a row without a crash. - crash_file : str - Location of the file to which we store `no_crash_streak`. + trade_streak_file : Path.PathLike + Location of the file to which we store our trade streak (continues on interrupt, resets on crash). sim_agents : dict[str, Agent] Dict of agents used in the simulation. hyperdrive_instance : `ape.contracts.ContractInstance `_ @@ -610,7 +608,7 @@ def do_policy( Returns ------- - no_crash_streak : int + trade_streak : int Number of trades in a row without a crash. """ # pylint: disable=too-many-arguments @@ -627,8 +625,8 @@ def do_policy( add_to_existing_wallet=agent.wallet, ) logging.debug("%s", agent.wallet) - no_crash_streak = set_days_without_crashing(no_crash_streak, crash_file) # set and save to file - return no_crash_streak + trade_streak = save_trade_streak(trade_streak, trade_streak_file) # set and save to file + return trade_streak def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history=None): @@ -712,7 +710,6 @@ def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame]: def main( bot_config: BotConfig, rng: NumpyGenerator, - crash_file: str, network_choice: str, provider_settings: str, ): @@ -720,10 +717,9 @@ def main( # pylint: disable=too-many-locals # Custom parameters for this experiment bot_config.scratch["project_dir"] = Path.cwd().parent if Path.cwd().name == "examples" else Path.cwd() - # bot_config.scratch["state_dump_file_path"] = bot_config.scratch["project_dir"] / "state_dumps" - # make dir if it doesn't exist - # if not bot_config.scratch["state_dump_file_path"].exists(): - # bot_config.scratch["state_dump_file_path"].mkdir() + bot_config.scratch["trade_streak"] = ( + bot_config.scratch["project_dir"] / f".logging/trade_streak{'_devnet' if config.devnet else ''}.txt" + ) if "num_louie" not in bot_config.scratch: bot_config.scratch["num_louie"]: int = 1 if "num_sally" not in bot_config.scratch: @@ -758,7 +754,6 @@ def main( agent_addresses, trade_history, ) = set_up_ape(bot_config, provider_settings, addresses, network_choice, pricing_model, rng) - no_crash_streak = 0 # set up the environment sim_agents, trade_history = set_up_agents( bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history @@ -770,29 +765,30 @@ def main( for agent_name in sim_agents: logging.info("\t%s", agent_name) start_timestamp = ape.chain.blocks[-1].timestamp + trade_streak = 0 last_executed_block = 0 while True: # hyper drive forever into the sunset latest_block = ape.chain.blocks[-1] block_number = latest_block.number block_timestamp = latest_block.timestamp if block_number > last_executed_block: - log_and_show_block_info(provider, no_crash_streak, block_number, block_timestamp) + log_and_show_block_info(provider, trade_streak, block_number, block_timestamp) # marginal update to trade_history start_block = trade_history.block_number.max() + 1 start_time = now() trade_history = ape_utils.get_trade_history(hyperdrive_instance, start_block, block_number, trade_history) - logging.info("Trade history updated in %s seconds", now() - start_time) + logging.debug("Trade history updated in %s seconds", now() - start_time) # create market object needed for agent execution elfpy_market = ape_utils.create_elfpy_market( pricing_model, hyperdrive_instance, hyperdrive_config, block_number, block_timestamp, start_timestamp ) try: for agent in sim_agents.values(): - no_crash_streak = do_policy( + trade_streak = do_policy( agent, elfpy_market, - no_crash_streak, - crash_file, + trade_streak, + bot_config.scratch["trade_streak"], sim_agents, hyperdrive_instance, base_instance, @@ -800,8 +796,8 @@ def main( ) except Exception as exc: # we want to catch all exceptions (pylint: disable=broad-exception-caught) logging.info("Crashed with error: %s", exc) - no_crash_streak = set_days_without_crashing( - no_crash_streak, crash_file, reset=True + trade_streak = save_trade_streak( + trade_streak, bot_config.scratch["trade_streak"], reset=True ) # set and save to file process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history) if bot_config.halt_on_errors: @@ -845,9 +841,7 @@ def get_argparser() -> argparse.ArgumentParser: log_file_and_stdout=config.log_file_and_stdout, log_formatter=config.log_formatter, ) - CRASH_FILE = f".logging/no_crash_streak{'_devnet' if config.devnet else ''}.txt" # inputs NETWORK_CHOICE = "ethereum:local:" + ("alchemy" if config.alchemy else "foundry") PROVIDER_SETTINGS = {"host": config.rpc_url} - # dynamically load devnet addresses from address file - main(config, np.random.default_rng(config.random_seed), CRASH_FILE, NETWORK_CHOICE, PROVIDER_SETTINGS) + main(config, np.random.default_rng(config.random_seed), NETWORK_CHOICE, PROVIDER_SETTINGS) From c54a3a287a6e65c613f74d6635cde954528caf56 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 16:03:37 -0400 Subject: [PATCH 16/28] remove comment --- examples/evm_bots.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 43ae08e2dc..54496ce606 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -250,7 +250,6 @@ def create_agent( ape_utils.attempt_txn(agent.contract, faucet.mint, *txn_args) logging.info( " agent_%s is a %s with budget=%s Eth=%s Base=%s", - # agent.contract.address[:8], bot.index, bot.name, str_with_precision(params["budget"]), From 0af8fd8a2739c5284f977c0fa547931141477673 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 17:45:32 -0400 Subject: [PATCH 17/28] lint --- elfpy/utils/apeworx_integrations.py | 4 +--- examples/evm_bots.py | 32 ++++++++++++++--------------- tests/test_transformers.py | 2 +- 3 files changed, 18 insertions(+), 20 deletions(-) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index 18b1c8612e..88a0b94e44 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -5,7 +5,6 @@ import json import logging import os -import re from collections import namedtuple from dataclasses import dataclass from pathlib import Path @@ -339,8 +338,6 @@ def get_wallet_from_trade_history( add_to_existing_wallet: Wallet | None = None, tolerance=None, ) -> Wallet: - # pylint: disable=too-many-arguments, too-many-branches - r"""Construct wallet balances from on-chain trade info. Arguments @@ -361,6 +358,7 @@ def get_wallet_from_trade_history( Wallet Wallet with Short, Long, and LP positions. """ + # pylint: disable=too-many-arguments, too-many-branches, disable=too-many-statements # TODO: remove restriction forcing Wallet index to be an int (issue #415) if tolerance is None: tolerance = MAXIMUM_BALANCE_MISMATCH_IN_WEI diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 54496ce606..604a66c474 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -48,6 +48,7 @@ def check_state_vs_onchain_balances(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance=1e16): """Inspect the dump we just loaded to ensure accuracy between balances from trade history on-chain queries.""" + # pylint: disable=too-many-locals, too-many-arguments for idx, address in enumerate(agent_addresses): log_str = f"querying agent_{idx} at addr={address}" @@ -538,6 +539,7 @@ def set_up_ape( trade_history : pd.DataFrame History of previously completed trades. """ + # pylint: disable=too-many-arguments provider: ProviderAPI = ape.networks.parse_network_choice( network_choice=network_choice, provider_settings=provider_settings, @@ -648,21 +650,21 @@ def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdri trade_history : pd.DataFrame, Optional History of previously completed trades. """ + # pylint: disable=too-many-arguments start_time = now() if trade_history is None: trade_history = ape_utils.get_trade_history(hyperdrive_instance) elfpy_crash = bot_config.scratch["project_dir"] / "elfpy_crash.json" - json.dump( - { - "rand_seed": bot_config.random_seed, - "rand_state": rng.bit_generator.state, - "block_number": block_number, - "addresses": addresses, - "trade_history": trade_history.to_dict(), - "agent_addresses": [agent.contract.address for agent in sim_agents.values()], - }, - fp=open(elfpy_crash, "w", encoding="utf-8"), - ) + dump_dict = { + "rand_seed": bot_config.random_seed, + "rand_state": rng.bit_generator.state, + "block_number": block_number, + "addresses": addresses, + "trade_history": trade_history.to_dict(), + "agent_addresses": [agent.contract.address for agent in sim_agents.values()], + } + with open(elfpy_crash, "w", encoding="utf-8") as file: + json.dump(dump_dict, fp=file) logging.info("Dumped state in %s seconds", now() - start_time) anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" @@ -713,7 +715,7 @@ def main( provider_settings: str, ): """Run the simulation.""" - # pylint: disable=too-many-locals + # pylint: disable=too-many-locals, too-many-branches, too-many-statements # Custom parameters for this experiment bot_config.scratch["project_dir"] = Path.cwd().parent if Path.cwd().name == "examples" else Path.cwd() bot_config.scratch["trade_streak"] = ( @@ -802,10 +804,8 @@ def main( if bot_config.halt_on_errors: raise exc last_executed_block = block_number - if ( - bot_config.devnet and provider.auto_mine - ): # anvil automatically mines after you send a transaction. or manually. - sleep(0.5) + if bot_config.devnet and automine: + # "automine" means anvil automatically mines a new block after you send a transaction, not time-based. ape.chain.mine() else: # either on goerli or on devnet with automine disabled (which means time-based mining is enabled) sleep(1) diff --git a/tests/test_transformers.py b/tests/test_transformers.py index 3bb5af9710..d9d38bb4c2 100644 --- a/tests/test_transformers.py +++ b/tests/test_transformers.py @@ -126,7 +126,7 @@ def test_trans_lib_wallet(): return ape_utils.get_wallet_from_trade_history( address=test.test_account.address, index=1, # index of the agent in the list of ALL agents, assigned in set_up_agents() to len(sim_agents) - trade_history=ape_utils.get_on_chain_trade_info(test.hyperdrive_instance), + trade_history=ape_utils.get_trade_history(test.hyperdrive_instance), hyperdrive_contract=test.hyperdrive_instance, base_contract=test.base_instance, ) From 49750ac7a67fc211fa0a041144a2d50e8917b7f2 Mon Sep 17 00:00:00 2001 From: Mihai Date: Thu, 29 Jun 2023 17:58:12 -0400 Subject: [PATCH 18/28] clean up imports --- elfpy/utils/apeworx_integrations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index 88a0b94e44..5364c66788 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -28,7 +28,6 @@ from elfpy import MAXIMUM_BALANCE_MISMATCH_IN_WEI, SECONDS_IN_YEAR, WEI, simulators, time, types from elfpy.markets.hyperdrive import AssetIdPrefix, HyperdriveMarketState, HyperdrivePricingModel, hyperdrive_assets from elfpy.markets.hyperdrive.hyperdrive_market import HyperdriveMarket -from elfpy.math import FixedPoint from elfpy.simulators.config import Config from elfpy.utils import outputs as output_utils from elfpy.utils import sim_utils From da7e8214044135c93688daae06c053ecfc2d9097 Mon Sep 17 00:00:00 2001 From: Mihai Date: Fri, 30 Jun 2023 15:45:29 -0400 Subject: [PATCH 19/28] pickle agents and feed them rng --- elfpy/utils/apeworx_integrations.py | 5 - examples/evm_bots.py | 158 +++++++++++++++++++++++----- 2 files changed, 132 insertions(+), 31 deletions(-) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index 5364c66788..d5c913be3d 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -274,11 +274,6 @@ def get_market_state_from_contract(hyperdrive_contract: ContractInstance, **kwar ) -OnChainTradeInfo = namedtuple( - "OnChainTradeInfo", ["trades", "unique_maturities", "unique_ids", "unique_block_numbers", "share_price"] -) - - def get_trade_history( hyperdrive_contract: ContractInstance, start_block: int = 0, diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 604a66c474..9bc46865c1 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -4,10 +4,11 @@ from __future__ import annotations # types will be strings by default in 3.11 # stdlib -import os import argparse import json import logging +import os +import pickle import shutil from datetime import datetime from pathlib import Path @@ -17,9 +18,9 @@ # external lib import ape -import requests import numpy as np import pandas as pd +import requests from ape import accounts from ape.api import ProviderAPI from ape.contracts import ContractInstance @@ -293,6 +294,8 @@ def set_up_agents( Addresses of deployed contracts. rng : NumpyGenerator The random number generator. + trade_history : pd.DataFrame, Optional + History of previously completed trades. Returns ------- @@ -499,15 +502,25 @@ def set_up_devnet( return base_instance, hyperdrive_instance, addresses -def set_up_ape( +def set_up_experiment( bot_config: BotConfig, provider_settings: dict, addresses: dict, network_choice: str, pricing_model: HyperdrivePricingModel, rng: NumpyGenerator, -) -> tuple[ProviderAPI, ContractInstance, ContractInstance, dict, KeyfileAccount]: - r"""Set up ape. +) -> tuple[ + ProviderAPI, + ContractInstance, + ContractInstance, + dict, + list[str] | None, + pd.DataFrame, + dict[str, Agent], + NumpyGenerator, + dict | None, +]: + r"""Set up Ape objects, agent addresses, trade history, and simulation agents. Parameters ---------- @@ -538,6 +551,12 @@ def set_up_ape( List of deployed agent addresses, if loading state, otherwise None. trade_history : pd.DataFrame History of previously completed trades. + sim_agents : dict[str, Agent] + Dict of agents used in the simulation. + rng : NumpyGenerator + The random number generator. + random_state : dict | None + The state of the random number generator, from the start of the latest block. """ # pylint: disable=too-many-arguments provider: ProviderAPI = ape.networks.parse_network_choice( @@ -549,10 +568,10 @@ def set_up_ape( "devnet" if bot_config.devnet else network_choice, provider.get_block("latest").number, ) - agent_addresses, trade_history = None, None + agent_addresses, trade_history, sim_agents, random_state = None, None, None, None if config.load_state_id is not None: # load state from specified id logging.info("Loading state from id: %s", config.load_state_id) - addresses, agent_addresses, trade_history = load_state(bot_config, rng) + addresses, agent_addresses, trade_history, rng, sim_agents, random_state = load_state(bot_config, rng) project: ape_utils.HyperdriveProject = ape_utils.HyperdriveProject( path=Path.cwd(), hyperdrive_address=addresses["hyperdrive"] if bot_config.devnet else addresses["goerli_hyperdrive"], @@ -567,11 +586,23 @@ def set_up_ape( provider=provider, ) hyperdrive_instance: ContractInstance = project.get_hyperdrive_contract() - # read the hyperdrive config from the contract, and log (and print) it - hyperdrive_config = ape_utils.get_hyperdrive_config(hyperdrive_instance) - # becomes provider.get_auto_mine() with this PR: https://github.com/ApeWorX/ape-foundry/pull/51 - automine = provider._make_request("anvil_getAutomine", parameters={}) # pylint: disable=protected-access - return provider, automine, base_instance, hyperdrive_instance, hyperdrive_config, agent_addresses, trade_history + + if config.load_state_id is None: # create sim_agents because they're not loaded from state + sim_agents, trade_history = set_up_agents( + bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history + ) + return ( + provider, + provider.auto_mine, + base_instance, + hyperdrive_instance, + ape_utils.get_hyperdrive_config(hyperdrive_instance), + agent_addresses, + trade_history, + sim_agents, + rng, + random_state, + ) def do_policy( @@ -630,7 +661,15 @@ def do_policy( return trade_streak -def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history=None): +def process_crash( + bot_config, + block_number, + addresses, + sim_agents, + hyperdrive_instance, + random_state_at_start_of_block, + trade_history=None, +): """Dump relevant pieces of information: full node state from anvil, random generator state, and trade history. Parameters @@ -639,14 +678,14 @@ def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdri The bot configuration. block_number : int The block number to load the state from. - rng : NumpyGenerator - The random number generator. addresses : dict List of deployed contract addresses. sim_agents : dict[str, Agent] Dict of agents used in the simulation. hyperdrive_instance : `ape.contracts.ContractInstance `_ The hyperdrive contract instance. + random_state_at_start_of_block : dict + The state of the random number generator, from the start of this block. trade_history : pd.DataFrame, Optional History of previously completed trades. """ @@ -657,14 +696,18 @@ def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdri elfpy_crash = bot_config.scratch["project_dir"] / "elfpy_crash.json" dump_dict = { "rand_seed": bot_config.random_seed, - "rand_state": rng.bit_generator.state, + "rand_state": random_state_at_start_of_block, "block_number": block_number, "addresses": addresses, "trade_history": trade_history.to_dict(), "agent_addresses": [agent.contract.address for agent in sim_agents.values()], + "agent_keys": list(sim_agents.keys()), } with open(elfpy_crash, "w", encoding="utf-8") as file: json.dump(dump_dict, fp=file) + for agent_key, agent in sim_agents.items(): + with open(bot_config.scratch["project_dir"] / f"{agent_key}.pickle", "wb") as file: + pickle.dump(agent, file) logging.info("Dumped state in %s seconds", now() - start_time) anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" @@ -672,7 +715,7 @@ def process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdri logging.info(" => anvil state saved to %s\n => elfpy state saved to %s", anvil_crash, elfpy_crash) -def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame]: +def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame, NumpyGenerator]: """Load relevant pieces of information: full node state from anvil, random generator state, and trade history. Parameters @@ -690,14 +733,25 @@ def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame]: List of agent addresses. trade_history : pd.DataFrame History of previously completed trades. + rng : NumpyGenerator + The random number generator. + state["rand_state"] : dict + The state of the random number generator, from the start of the latest block. """ state_id = bot_config.load_state_id.replace(".json", "") file_path = bot_config.scratch["project_dir"] / f"{state_id}.json" with open(file_path, "r", encoding="utf-8") as file: state = json.load(file) bot_config.random_seed = state["rand_seed"] + rng = np.random.default_rng(bot_config.random_seed) rng.bit_generator.state = state["rand_state"] trade_history = pd.DataFrame(state["trade_history"]) + sim_agents = {} + for agent_key in state["agent_keys"]: + with open(bot_config.scratch["project_dir"] / f"{agent_key}.pickle", "rb") as file: + agent = pickle.load(file) + agent.policy.rng = rng + sim_agents[agent_key] = agent logging.info( "STATE loaded from %s with:\n rand_seed %s\n rand_state %s\n trades %s", file_path, @@ -705,7 +759,47 @@ def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame]: rng.bit_generator.state, len(trade_history), ) - return state["addresses"], state["agent_addresses"], trade_history + return state["addresses"], state["agent_addresses"], trade_history, rng, sim_agents, state["rand_state"] + + +def check_rng_matches(rng, _random_state, bot_config, agent=None): + """Check that the random number generator matches the provided random state. + + Parameters + ---------- + rng : NumpyGenerator + The random number generator. + _random_state : dict + The state of the random number generator. + bot_config : BotConfig + The bot configuration. + agent : Agent, Optional + The agent to check. + """ + if _random_state is not None and bot_config.load_state_id is not None: + assert rng.bit_generator.state == _random_state, ( + "Random state doesn't match:\n" + f"rng.bit_generator.state\n{rng.bit_generator.state}\n" + f"random_state_at_start_of_block\n{_random_state}" + ) + logging.debug( + "Random state does match:\nrng.bit_generator.state\n%s\nrandom_state_at_start_of_block\n%s", + rng.bit_generator.state, + _random_state, + ) + if agent is not None: + assert agent.policy.rng.bit_generator.state == _random_state, ( + "AGENT Random state doesn't match:\n" + f"agent.policy.rng.bit_generator.state\n{agent.policy.rng.bit_generator.state}\n" + f"random_state_at_start_of_block\n{_random_state}" + ) + logging.debug( + "AGENT Random state does match:\n" + "agent.policy.rng.bit_generator.state\n%s\n" + "random_state_at_start_of_block\n%s", + agent.policy.rng.bit_generator.state, + _random_state, + ) def main( @@ -754,11 +848,11 @@ def main( hyperdrive_config, agent_addresses, trade_history, - ) = set_up_ape(bot_config, provider_settings, addresses, network_choice, pricing_model, rng) - # set up the environment - sim_agents, trade_history = set_up_agents( - bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history - ) + sim_agents, + rng, + random_state_at_start_of_block, + ) = set_up_experiment(bot_config, provider_settings, addresses, network_choice, pricing_model, rng) + assert isinstance(sim_agents, dict), "sim_agents wasn't created or loaded properly." if bot_config.load_state_id is not None: check_state_vs_onchain_balances(trade_history, agent_addresses, hyperdrive_instance, base_instance) ape_utils.dump_agent_info(sim_agents, bot_config) @@ -783,8 +877,12 @@ def main( elfpy_market = ape_utils.create_elfpy_market( pricing_model, hyperdrive_instance, hyperdrive_config, block_number, block_timestamp, start_timestamp ) + check_rng_matches(rng, random_state_at_start_of_block, bot_config) + random_state_at_start_of_block = rng.bit_generator.state try: - for agent in sim_agents.values(): + for idx, agent in enumerate(sim_agents.values()): + if idx == 0: + check_rng_matches(rng, random_state_at_start_of_block, bot_config, agent) trade_streak = do_policy( agent, elfpy_market, @@ -800,7 +898,15 @@ def main( trade_streak = save_trade_streak( trade_streak, bot_config.scratch["trade_streak"], reset=True ) # set and save to file - process_crash(bot_config, block_number, rng, addresses, sim_agents, hyperdrive_instance, trade_history) + process_crash( + bot_config, + block_number, + addresses, + sim_agents, + hyperdrive_instance, + random_state_at_start_of_block, + trade_history, + ) if bot_config.halt_on_errors: raise exc last_executed_block = block_number @@ -812,7 +918,7 @@ def main( def get_argparser() -> argparse.ArgumentParser: - """Define & parse arguments from stdin""" + """Define & parse arguments from stdin.""" parser = argparse.ArgumentParser( prog="evm_bots", description="Example execution script for running bots using Elfpy", From 97477be4fef99998e803d98184574fd5fe85bb3d Mon Sep 17 00:00:00 2001 From: Mihai Date: Fri, 30 Jun 2023 16:50:08 -0400 Subject: [PATCH 20/28] add tests --- tests/test_dumps.py | 83 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 tests/test_dumps.py diff --git a/tests/test_dumps.py b/tests/test_dumps.py new file mode 100644 index 0000000000..e41fd4bc05 --- /dev/null +++ b/tests/test_dumps.py @@ -0,0 +1,83 @@ +"""Test initialization of markets.""" +from __future__ import annotations + +import pickle + +# external +import numpy as np +import pytest +from fixedpointmath import FixedPoint + +# elfpy core repo +from elfpy import time +from elfpy.agents.agent import Agent +from elfpy.agents.policies import RandomAgent +from elfpy.markets.hyperdrive.hyperdrive_market import HyperdriveMarket, HyperdriveMarketState +from elfpy.markets.hyperdrive.hyperdrive_pricing_model import HyperdrivePricingModel + +RAND_SEED = np.random.randint(1000) + +# pylint: disable=redefined-outer-name + + +@pytest.fixture +def rng(): + """Random number generator created with constant seed.""" + return np.random.default_rng(RAND_SEED) + + +@pytest.fixture +def agent(rng): + """Agent object initialized with RandomAgent policy.""" + policy = RandomAgent(budget=FixedPoint(50_000), trade_chance=FixedPoint(0.1), rng=rng) + return Agent(wallet_address=0, policy=policy) + + +@pytest.fixture +def market(): + """HyperdriveMarket initialized with defaults, a 365 day term, and a time-stretch of 10.""" + return HyperdriveMarket( + pricing_model=HyperdrivePricingModel(), + market_state=HyperdriveMarketState(), + position_duration=time.StretchedTime( + days=FixedPoint("365.0"), time_stretch=FixedPoint("10.0"), normalizing_constant=FixedPoint("365.0") + ), + block_time=time.BlockTime(), + ) + + +def test_rng(rng): + """Test creating a new rng object that matches a saved state.""" + # warm up the rng + _ = rng.random(99) + + # save and load state + state = rng.bit_generator.state + + # get first rng results + rand_seq = rng.random(99) + + # create a new rng + rng2 = np.random.default_rng(RAND_SEED) + rng2.bit_generator.state = state + rand_seq2 = rng2.random(99) + + # compare the two random sequences + assert np.array_equal(rand_seq, rand_seq2) + + +def test_agent(rng, agent, market, tmp_path): + """Ensure pickling an agent then reloading it returns the same next trade.""" + with open(tmp_path / "agent.pkl", "wb") as file: + pickle.dump(agent, file) + + trade1 = agent.get_trades(market=market) + + agent2 = None + with open(tmp_path / "agent.pkl", "rb") as file: + agent2 = pickle.load(file) + agent2.policy.rng = rng + + trade2 = agent2.get_trades(market=market) + + assert trade1 == trade2 From 23dbd56d15b8b603d73b0b2b598f0381007563c9 Mon Sep 17 00:00:00 2001 From: Mihai Date: Fri, 30 Jun 2023 17:14:34 -0400 Subject: [PATCH 21/28] fix comment --- elfpy/bots/bot_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elfpy/bots/bot_config.py b/elfpy/bots/bot_config.py index a909df4ba0..d79b595a0b 100644 --- a/elfpy/bots/bot_config.py +++ b/elfpy/bots/bot_config.py @@ -48,7 +48,7 @@ class BotConfig(types.FrozenClass): # In general the bot will be more risk averse as it grows to infinity. # A value of 0 will usually disable it. risk_threshold: float = 0.0 - # whether or not to dump state at every block + # whether or not to dump state on a crash dump_state: bool = False # whether to initialize state from a specific point load_state_id: str | None = None From 8bd8468bc012c9b63a6dff2e184aaeab404dd68f Mon Sep 17 00:00:00 2001 From: Mihai Date: Fri, 30 Jun 2023 17:25:13 -0400 Subject: [PATCH 22/28] use constant seed --- tests/test_dumps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dumps.py b/tests/test_dumps.py index e41fd4bc05..4aa22ed173 100644 --- a/tests/test_dumps.py +++ b/tests/test_dumps.py @@ -15,7 +15,7 @@ from elfpy.markets.hyperdrive.hyperdrive_market import HyperdriveMarket, HyperdriveMarketState from elfpy.markets.hyperdrive.hyperdrive_pricing_model import HyperdrivePricingModel -RAND_SEED = np.random.randint(1000) +RAND_SEED = 123 # pylint: disable=redefined-outer-name From cca1c3047a78612d5280a1eeea62de0913a45c28 Mon Sep 17 00:00:00 2001 From: Mihai Date: Fri, 30 Jun 2023 17:54:37 -0400 Subject: [PATCH 23/28] use existing market fixture and set trade chance to 1 --- tests/test_dumps.py | 49 ++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/tests/test_dumps.py b/tests/test_dumps.py index 4aa22ed173..39ad186ad3 100644 --- a/tests/test_dumps.py +++ b/tests/test_dumps.py @@ -9,11 +9,8 @@ from fixedpointmath import FixedPoint # elfpy core repo -from elfpy import time from elfpy.agents.agent import Agent from elfpy.agents.policies import RandomAgent -from elfpy.markets.hyperdrive.hyperdrive_market import HyperdriveMarket, HyperdriveMarketState -from elfpy.markets.hyperdrive.hyperdrive_pricing_model import HyperdrivePricingModel RAND_SEED = 123 @@ -29,23 +26,11 @@ def rng(): @pytest.fixture def agent(rng): """Agent object initialized with RandomAgent policy.""" - policy = RandomAgent(budget=FixedPoint(50_000), trade_chance=FixedPoint(0.1), rng=rng) + print(f"{rng=}") + policy = RandomAgent(budget=FixedPoint(50_000), trade_chance=FixedPoint(1), rng=rng) return Agent(wallet_address=0, policy=policy) -@pytest.fixture -def market(): - """HyperdriveMarket initialized with defaults, a 365 day term, and a time-stretch of 10.""" - return HyperdriveMarket( - pricing_model=HyperdrivePricingModel(), - market_state=HyperdriveMarketState(), - position_duration=time.StretchedTime( - days=FixedPoint("365.0"), time_stretch=FixedPoint("10.0"), normalizing_constant=FixedPoint("365.0") - ), - block_time=time.BlockTime(), - ) - - def test_rng(rng): """Test creating a new rng object that matches a saved state.""" # warm up the rng @@ -66,18 +51,36 @@ def test_rng(rng): assert np.array_equal(rand_seq, rand_seq2) -def test_agent(rng, agent, market, tmp_path): +def test_agent(agent, hyperdrive_sim, tmp_path): """Ensure pickling an agent then reloading it returns the same next trade.""" - with open(tmp_path / "agent.pkl", "wb") as file: - pickle.dump(agent, file) + # roll forward 10 trades + for _ in range(10): + _ = agent.get_trades(market=hyperdrive_sim) - trade1 = agent.get_trades(market=market) + # get the state we want to save + state = agent.policy.rng.bit_generator.state + # save the agent + with open(tmp_path / "agent.pkl", "wb") as file: + pickle.dump(agent, file) + # save the state + with open(tmp_path / "state.pkl", "wb") as file: + pickle.dump(state, file) + + # get the agent's 11th trade + trade1 = agent.get_trades(market=hyperdrive_sim) + print(f"{trade1=}") + + # reload the agent and call it agent2 agent2 = None with open(tmp_path / "agent.pkl", "rb") as file: agent2 = pickle.load(file) - agent2.policy.rng = rng + agent2.policy.rng = np.random.default_rng(RAND_SEED) + agent2.policy.rng.bit_generator.state = state - trade2 = agent2.get_trades(market=market) + # get the loaded agent's next trade + trade2 = agent2.get_trades(market=hyperdrive_sim) + print(f"{trade2=}") + # compare the two trades assert trade1 == trade2 From 39f845c0de780427174b19419ce3106759035f05 Mon Sep 17 00:00:00 2001 From: Mihai Date: Fri, 30 Jun 2023 17:56:55 -0400 Subject: [PATCH 24/28] lint with black --- tests/test_dumps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_dumps.py b/tests/test_dumps.py index 39ad186ad3..8423001ac7 100644 --- a/tests/test_dumps.py +++ b/tests/test_dumps.py @@ -66,7 +66,7 @@ def test_agent(agent, hyperdrive_sim, tmp_path): # save the state with open(tmp_path / "state.pkl", "wb") as file: pickle.dump(state, file) - + # get the agent's 11th trade trade1 = agent.get_trades(market=hyperdrive_sim) print(f"{trade1=}") From ed7ed8497570a2007f27dc9dd5e4c250568f5dc0 Mon Sep 17 00:00:00 2001 From: Mihai Date: Fri, 30 Jun 2023 19:27:32 -0400 Subject: [PATCH 25/28] add failure case to test --- tests/test_dumps.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/tests/test_dumps.py b/tests/test_dumps.py index 8423001ac7..f0543995bd 100644 --- a/tests/test_dumps.py +++ b/tests/test_dumps.py @@ -60,6 +60,10 @@ def test_agent(agent, hyperdrive_sim, tmp_path): # get the state we want to save state = agent.policy.rng.bit_generator.state + # get the agent's 11th trade, last before it crashes + trade1 = agent.get_trades(market=hyperdrive_sim) + print(f"{trade1=}") + # save the agent with open(tmp_path / "agent.pkl", "wb") as file: pickle.dump(agent, file) @@ -67,18 +71,21 @@ def test_agent(agent, hyperdrive_sim, tmp_path): with open(tmp_path / "state.pkl", "wb") as file: pickle.dump(state, file) - # get the agent's 11th trade - trade1 = agent.get_trades(market=hyperdrive_sim) - print(f"{trade1=}") - # reload the agent and call it agent2 agent2 = None with open(tmp_path / "agent.pkl", "rb") as file: agent2 = pickle.load(file) - agent2.policy.rng = np.random.default_rng(RAND_SEED) - agent2.policy.rng.bit_generator.state = state - # get the loaded agent's next trade + # get the loaded agent's next trade immediately + trade2 = agent2.get_trades(market=hyperdrive_sim) + # assert that this fails + assert trade1 != trade2 + + # update the agent's rng state + agent2.policy.rng = np.random.default_rng(RAND_SEED) + agent2.policy.rng.bit_generator.state = state + + # get the loaded agent's next trade after setting its rng state trade2 = agent2.get_trades(market=hyperdrive_sim) print(f"{trade2=}") From 2185ec6f0e23c2d22a4eb0a19bc9ab0b0e50308c Mon Sep 17 00:00:00 2001 From: Mihai Date: Mon, 3 Jul 2023 17:33:08 -0400 Subject: [PATCH 26/28] remove state dump --- elfpy/bots/bot_config.py | 4 - elfpy/utils/apeworx_integrations.py | 1 - examples/evm_bots.py | 257 +--------------------------- 3 files changed, 4 insertions(+), 258 deletions(-) diff --git a/elfpy/bots/bot_config.py b/elfpy/bots/bot_config.py index d79b595a0b..18c9e7b613 100644 --- a/elfpy/bots/bot_config.py +++ b/elfpy/bots/bot_config.py @@ -48,10 +48,6 @@ class BotConfig(types.FrozenClass): # In general the bot will be more risk averse as it grows to infinity. # A value of 0 will usually disable it. risk_threshold: float = 0.0 - # whether or not to dump state on a crash - dump_state: bool = False - # whether to initialize state from a specific point - load_state_id: str | None = None # scratch space for any application-specific & extraneous parameters scratch: dict[Any, Any] = field(default_factory=dict) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index d5c913be3d..e3f7ea2b0b 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -874,7 +874,6 @@ def create_trade( selected_abi, args = select_abi(params=params, method=info[trade_type].method) # create a transaction with the selected ABI contract_txn: ContractTransaction = ContractTransaction(abi=selected_abi, address=hyperdrive_contract.address) - args = [arg.scaled_value if isinstance(arg, FixedPoint) else arg for arg in args] return contract_txn, args, selected_abi diff --git a/examples/evm_bots.py b/examples/evm_bots.py index 9bc46865c1..1b33095aef 100644 --- a/examples/evm_bots.py +++ b/examples/evm_bots.py @@ -8,8 +8,6 @@ import json import logging import os -import pickle -import shutil from datetime import datetime from pathlib import Path from time import sleep @@ -47,72 +45,6 @@ ape_logger.set_level(logging.ERROR) -def check_state_vs_onchain_balances(trade_history, agent_addresses, hyperdrive_instance, base_instance, tolerance=1e16): - """Inspect the dump we just loaded to ensure accuracy between balances from trade history on-chain queries.""" - # pylint: disable=too-many-locals, too-many-arguments - for idx, address in enumerate(agent_addresses): - log_str = f"querying agent_{idx} at addr={address}" - - # create wallet - wallet = ape_utils.get_wallet_from_trade_history( - address=str(address), - index=idx, - trade_history=trade_history, - hyperdrive_contract=hyperdrive_instance, - base_contract=base_instance, - tolerance=1e16, # allow being off by $0.01 - ) - - # print what's in wallet - log_str += f"{wallet=}" - log_str = f"agent_{idx} has:" - log_str += "\n wallet:" - for _, long in wallet.longs.items(): - log_str += f"\n long {long.balance}" - for _, short in wallet.shorts.items(): - log_str += f"\n short {short.balance}" - log_str += f"\n lp_tokens {wallet.lp_tokens}" - - # print trade counts - from_agent = trade_history["from"] == address - to_agent = trade_history["to"] == address - num_trades = sum((from_agent | to_agent)) - num_longs = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LONG")) - num_shorts = sum((from_agent | to_agent) & (trade_history["trade_type"] == "SHORT")) - num_lp = sum((from_agent | to_agent) & (trade_history["trade_type"] == "LP")) - num_withdrawal = sum((from_agent | to_agent) & (trade_history["trade_type"] == "WITHDRAWAL_SHARE")) - log_str += "\n trade counts:" - log_str += f"\n {num_trades:2.0f} total" - log_str += f"\n {num_longs:2.0f} long" - log_str += f"\n {num_shorts:2.0f} short" - log_str += f"\n {num_lp:2.0f} LP" - log_str += f"\n {num_withdrawal:2.0f} withdrawal share" - if num_trades != (num_longs + num_shorts + num_lp + num_withdrawal): - log_str += "\n trade counts DON'T add up to total" - else: - log_str += "\n trade counts add up to total\n" - - # compare off-chain to on-chain balances - log_str += "tuples of balances by position by calculation source. balance = (trade_history, onchain)\n" - log_str += f" tolerance = {tolerance} aka ${tolerance/1e18}\n" - for position_id in trade_history["id"].unique(): # loop across all unique positions - from_agent = trade_history["from"] == address - to_agent = trade_history["to"] == address - relevant_trades = ((from_agent) | (to_agent)) & (trade_history["id"] == position_id) - positive_balance = int(trade_history.loc[relevant_trades & (to_agent), "value"].sum()) - negative_balance = int(trade_history.loc[relevant_trades & (from_agent), "value"].sum()) - balance = positive_balance - negative_balance - on_chain_balance = hyperdrive_instance.balanceOf(position_id, address) - first_relevant_row = relevant_trades.index[relevant_trades][0] - info = trade_history.loc[first_relevant_row, :] - log_str += f"{address[:8]} {info.trade_type:16} balance = " - if abs(balance - on_chain_balance) > tolerance: - log_str += f"({balance/1e18:0.5f},{on_chain_balance/1e18:0.5f}) MISMATCH\n" - else: - log_str += f"({balance/1e18:0.0f},{on_chain_balance/1e18:0.0f}) match\n" - logging.info(log_str) - - def get_devnet_addresses(bot_config: BotConfig, addresses: dict[str, str] | None = None) -> tuple[dict[str, str], str]: """Get devnet addresses from address file.""" if addresses is None: @@ -264,7 +196,7 @@ def create_agent( trade_history=trade_history, hyperdrive_contract=hyperdrive_contract, base_contract=base_instance, - tolerance=1e16 if bot_config.load_state_id is not None else None, + tolerance=None, # when recovering form crash, set tolerance to 1e16 ) return agent @@ -509,17 +441,7 @@ def set_up_experiment( network_choice: str, pricing_model: HyperdrivePricingModel, rng: NumpyGenerator, -) -> tuple[ - ProviderAPI, - ContractInstance, - ContractInstance, - dict, - list[str] | None, - pd.DataFrame, - dict[str, Agent], - NumpyGenerator, - dict | None, -]: +) -> tuple[ProviderAPI, ContractInstance, ContractInstance, dict, pd.DataFrame, dict[str, Agent], NumpyGenerator,]: r"""Set up Ape objects, agent addresses, trade history, and simulation agents. Parameters @@ -547,16 +469,12 @@ def set_up_experiment( The deployed Hyperdrive instance. hyperdrive_config : dict The configuration of the deployed Hyperdrive instance - agent_addresses : list[str] | None - List of deployed agent addresses, if loading state, otherwise None. trade_history : pd.DataFrame History of previously completed trades. sim_agents : dict[str, Agent] Dict of agents used in the simulation. rng : NumpyGenerator The random number generator. - random_state : dict | None - The state of the random number generator, from the start of the latest block. """ # pylint: disable=too-many-arguments provider: ProviderAPI = ape.networks.parse_network_choice( @@ -568,10 +486,6 @@ def set_up_experiment( "devnet" if bot_config.devnet else network_choice, provider.get_block("latest").number, ) - agent_addresses, trade_history, sim_agents, random_state = None, None, None, None - if config.load_state_id is not None: # load state from specified id - logging.info("Loading state from id: %s", config.load_state_id) - addresses, agent_addresses, trade_history, rng, sim_agents, random_state = load_state(bot_config, rng) project: ape_utils.HyperdriveProject = ape_utils.HyperdriveProject( path=Path.cwd(), hyperdrive_address=addresses["hyperdrive"] if bot_config.devnet else addresses["goerli_hyperdrive"], @@ -587,21 +501,16 @@ def set_up_experiment( ) hyperdrive_instance: ContractInstance = project.get_hyperdrive_contract() - if config.load_state_id is None: # create sim_agents because they're not loaded from state - sim_agents, trade_history = set_up_agents( - bot_config, provider, hyperdrive_instance, base_instance, addresses, rng, trade_history - ) + sim_agents, trade_history = set_up_agents(bot_config, provider, hyperdrive_instance, base_instance, addresses, rng) return ( provider, provider.auto_mine, base_instance, hyperdrive_instance, ape_utils.get_hyperdrive_config(hyperdrive_instance), - agent_addresses, trade_history, sim_agents, rng, - random_state, ) @@ -661,147 +570,6 @@ def do_policy( return trade_streak -def process_crash( - bot_config, - block_number, - addresses, - sim_agents, - hyperdrive_instance, - random_state_at_start_of_block, - trade_history=None, -): - """Dump relevant pieces of information: full node state from anvil, random generator state, and trade history. - - Parameters - ---------- - bot_config : BotConfig - The bot configuration. - block_number : int - The block number to load the state from. - addresses : dict - List of deployed contract addresses. - sim_agents : dict[str, Agent] - Dict of agents used in the simulation. - hyperdrive_instance : `ape.contracts.ContractInstance `_ - The hyperdrive contract instance. - random_state_at_start_of_block : dict - The state of the random number generator, from the start of this block. - trade_history : pd.DataFrame, Optional - History of previously completed trades. - """ - # pylint: disable=too-many-arguments - start_time = now() - if trade_history is None: - trade_history = ape_utils.get_trade_history(hyperdrive_instance) - elfpy_crash = bot_config.scratch["project_dir"] / "elfpy_crash.json" - dump_dict = { - "rand_seed": bot_config.random_seed, - "rand_state": random_state_at_start_of_block, - "block_number": block_number, - "addresses": addresses, - "trade_history": trade_history.to_dict(), - "agent_addresses": [agent.contract.address for agent in sim_agents.values()], - "agent_keys": list(sim_agents.keys()), - } - with open(elfpy_crash, "w", encoding="utf-8") as file: - json.dump(dump_dict, fp=file) - for agent_key, agent in sim_agents.items(): - with open(bot_config.scratch["project_dir"] / f"{agent_key}.pickle", "wb") as file: - pickle.dump(agent, file) - logging.info("Dumped state in %s seconds", now() - start_time) - anvil_regular = bot_config.scratch["project_dir"] / "anvil_regular.json" - anvil_crash = bot_config.scratch["project_dir"] / "anvil_crash.json" - shutil.copy(anvil_regular, anvil_crash) # save anvil's state, so we can reproduce the crash - logging.info(" => anvil state saved to %s\n => elfpy state saved to %s", anvil_crash, elfpy_crash) - - -def load_state(bot_config, rng) -> tuple[list[str], list[str], pd.DataFrame, NumpyGenerator]: - """Load relevant pieces of information: full node state from anvil, random generator state, and trade history. - - Parameters - ---------- - bot_config : BotConfig - The bot configuration. - rng : NumpyGenerator - The random number generator. - - Returns - ------- - state["addresses"] : list[str] - List of deployed contract addresses. - state["agent_addresses"] : list[str] - List of agent addresses. - trade_history : pd.DataFrame - History of previously completed trades. - rng : NumpyGenerator - The random number generator. - state["rand_state"] : dict - The state of the random number generator, from the start of the latest block. - """ - state_id = bot_config.load_state_id.replace(".json", "") - file_path = bot_config.scratch["project_dir"] / f"{state_id}.json" - with open(file_path, "r", encoding="utf-8") as file: - state = json.load(file) - bot_config.random_seed = state["rand_seed"] - rng = np.random.default_rng(bot_config.random_seed) - rng.bit_generator.state = state["rand_state"] - trade_history = pd.DataFrame(state["trade_history"]) - sim_agents = {} - for agent_key in state["agent_keys"]: - with open(bot_config.scratch["project_dir"] / f"{agent_key}.pickle", "rb") as file: - agent = pickle.load(file) - agent.policy.rng = rng - sim_agents[agent_key] = agent - logging.info( - "STATE loaded from %s with:\n rand_seed %s\n rand_state %s\n trades %s", - file_path, - bot_config.random_seed, - rng.bit_generator.state, - len(trade_history), - ) - return state["addresses"], state["agent_addresses"], trade_history, rng, sim_agents, state["rand_state"] - - -def check_rng_matches(rng, _random_state, bot_config, agent=None): - """Check that the random number generator matches the provided random state. - - Parameters - ---------- - rng : NumpyGenerator - The random number generator. - _random_state : dict - The state of the random number generator. - bot_config : BotConfig - The bot configuration. - agent : Agent, Optional - The agent to check. - """ - if _random_state is not None and bot_config.load_state_id is not None: - assert rng.bit_generator.state == _random_state, ( - "Random state doesn't match:\n" - f"rng.bit_generator.state\n{rng.bit_generator.state}\n" - f"random_state_at_start_of_block\n{_random_state}" - ) - logging.debug( - "Random state does match:\nrng.bit_generator.state\n%s\nrandom_state_at_start_of_block\n%s", - rng.bit_generator.state, - _random_state, - ) - if agent is not None: - assert agent.policy.rng.bit_generator.state == _random_state, ( - "AGENT Random state doesn't match:\n" - f"agent.policy.rng.bit_generator.state\n{agent.policy.rng.bit_generator.state}\n" - f"random_state_at_start_of_block\n{_random_state}" - ) - logging.debug( - "AGENT Random state does match:\n" - "agent.policy.rng.bit_generator.state\n%s\n" - "random_state_at_start_of_block\n%s", - agent.policy.rng.bit_generator.state, - _random_state, - ) - - def main( bot_config: BotConfig, rng: NumpyGenerator, @@ -846,15 +614,11 @@ def main( base_instance, hyperdrive_instance, hyperdrive_config, - agent_addresses, trade_history, sim_agents, rng, - random_state_at_start_of_block, ) = set_up_experiment(bot_config, provider_settings, addresses, network_choice, pricing_model, rng) assert isinstance(sim_agents, dict), "sim_agents wasn't created or loaded properly." - if bot_config.load_state_id is not None: - check_state_vs_onchain_balances(trade_history, agent_addresses, hyperdrive_instance, base_instance) ape_utils.dump_agent_info(sim_agents, bot_config) logging.info("Constructed %s agents:", len(sim_agents)) for agent_name in sim_agents: @@ -877,12 +641,8 @@ def main( elfpy_market = ape_utils.create_elfpy_market( pricing_model, hyperdrive_instance, hyperdrive_config, block_number, block_timestamp, start_timestamp ) - check_rng_matches(rng, random_state_at_start_of_block, bot_config) - random_state_at_start_of_block = rng.bit_generator.state try: - for idx, agent in enumerate(sim_agents.values()): - if idx == 0: - check_rng_matches(rng, random_state_at_start_of_block, bot_config, agent) + for agent in sim_agents.values(): trade_streak = do_policy( agent, elfpy_market, @@ -898,15 +658,6 @@ def main( trade_streak = save_trade_streak( trade_streak, bot_config.scratch["trade_streak"], reset=True ) # set and save to file - process_crash( - bot_config, - block_number, - addresses, - sim_agents, - hyperdrive_instance, - random_state_at_start_of_block, - trade_history, - ) if bot_config.halt_on_errors: raise exc last_executed_block = block_number From 0fd76432cbf8f041fe612b3f60b5ffcce08792bd Mon Sep 17 00:00:00 2001 From: Mihai Date: Mon, 3 Jul 2023 17:34:40 -0400 Subject: [PATCH 27/28] remote state dump tests --- tests/test_dumps.py | 93 --------------------------------------------- 1 file changed, 93 deletions(-) delete mode 100644 tests/test_dumps.py diff --git a/tests/test_dumps.py b/tests/test_dumps.py deleted file mode 100644 index f0543995bd..0000000000 --- a/tests/test_dumps.py +++ /dev/null @@ -1,93 +0,0 @@ -"""Test initialization of markets.""" -from __future__ import annotations - -import pickle - -# external -import numpy as np -import pytest -from fixedpointmath import FixedPoint - -# elfpy core repo -from elfpy.agents.agent import Agent -from elfpy.agents.policies import RandomAgent - -RAND_SEED = 123 - -# pylint: disable=redefined-outer-name - - -@pytest.fixture -def rng(): - """Random number generator created with constant seed.""" - return np.random.default_rng(RAND_SEED) - - -@pytest.fixture -def agent(rng): - """Agent object initialized with RandomAgent policy.""" - print(f"{rng=}") - policy = RandomAgent(budget=FixedPoint(50_000), trade_chance=FixedPoint(1), rng=rng) - return Agent(wallet_address=0, policy=policy) - - -def test_rng(rng): - """Test creating a new rng object that matches a saved state.""" - # warm up the rng - _ = rng.random(99) - - # save and load state - state = rng.bit_generator.state - - # get first rng results - rand_seq = rng.random(99) - - # create a new rng - rng2 = np.random.default_rng(RAND_SEED) - rng2.bit_generator.state = state - rand_seq2 = rng2.random(99) - - # compare the two random sequences - assert np.array_equal(rand_seq, rand_seq2) - - -def test_agent(agent, hyperdrive_sim, tmp_path): - """Ensure pickling an agent then reloading it returns the same next trade.""" - # roll forward 10 trades - for _ in range(10): - _ = agent.get_trades(market=hyperdrive_sim) - - # get the state we want to save - state = agent.policy.rng.bit_generator.state - - # get the agent's 11th trade, last before it crashes - trade1 = agent.get_trades(market=hyperdrive_sim) - print(f"{trade1=}") - - # save the agent - with open(tmp_path / "agent.pkl", "wb") as file: - pickle.dump(agent, file) - # save the state - with open(tmp_path / "state.pkl", "wb") as file: - pickle.dump(state, file) - - # reload the agent and call it agent2 - agent2 = None - with open(tmp_path / "agent.pkl", "rb") as file: - agent2 = pickle.load(file) - - # get the loaded agent's next trade immediately - trade2 = agent2.get_trades(market=hyperdrive_sim) - # assert that this fails - assert trade1 != trade2 - - # update the agent's rng state - agent2.policy.rng = np.random.default_rng(RAND_SEED) - agent2.policy.rng.bit_generator.state = state - - # get the loaded agent's next trade after setting its rng state - trade2 = agent2.get_trades(market=hyperdrive_sim) - print(f"{trade2=}") - - # compare the two trades - assert trade1 == trade2 From 4888fe0be412e22894a8b76d01c1fa7bf809f270 Mon Sep 17 00:00:00 2001 From: Mihai Date: Mon, 3 Jul 2023 18:59:10 -0400 Subject: [PATCH 28/28] remove FixedPoint when creating ape trade --- elfpy/utils/apeworx_integrations.py | 1 + 1 file changed, 1 insertion(+) diff --git a/elfpy/utils/apeworx_integrations.py b/elfpy/utils/apeworx_integrations.py index e3f7ea2b0b..d5c913be3d 100644 --- a/elfpy/utils/apeworx_integrations.py +++ b/elfpy/utils/apeworx_integrations.py @@ -874,6 +874,7 @@ def create_trade( selected_abi, args = select_abi(params=params, method=info[trade_type].method) # create a transaction with the selected ABI contract_txn: ContractTransaction = ContractTransaction(abi=selected_abi, address=hyperdrive_contract.address) + args = [arg.scaled_value if isinstance(arg, FixedPoint) else arg for arg in args] return contract_txn, args, selected_abi