Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
787518d
init
wakamex Jun 22, 2023
331fdff
make trade history work
wakamex Jun 26, 2023
f81c1f3
inspect dump
wakamex Jun 26, 2023
c377773
single-file anvil state
wakamex Jun 27, 2023
f0b4f99
single-file elfpy state
wakamex Jun 27, 2023
1d2523c
save dataframe as dict inside json
wakamex Jun 29, 2023
2478e6e
commit profiles and helper scripts
wakamex Jun 29, 2023
acb2345
move inspect_dump and get_devnet_addresses into evm_bots
wakamex Jun 29, 2023
055f8e6
change inspect_dump name
wakamex Jun 29, 2023
ba37392
don't return block when loading state
wakamex Jun 29, 2023
0b9574a
dump state only once, in try/except block
wakamex Jun 29, 2023
b276ea2
remove profiles and anvil scripts
wakamex Jun 29, 2023
1636dfc
revert changes to .gitignore
wakamex Jun 29, 2023
36a204a
remove unnecessary return of devnet_addresses
wakamex Jun 29, 2023
7128269
name trade_streak uniformly to distinguish from crash
wakamex Jun 29, 2023
c54a3a2
remove comment
wakamex Jun 29, 2023
0af8fd8
lint
wakamex Jun 29, 2023
49750ac
clean up imports
wakamex Jun 29, 2023
da7e821
pickle agents and feed them rng
wakamex Jun 30, 2023
97477be
add tests
wakamex Jun 30, 2023
23dbd56
fix comment
wakamex Jun 30, 2023
8bd8468
use constant seed
wakamex Jun 30, 2023
cca1c30
use existing market fixture and set trade chance to 1
wakamex Jun 30, 2023
39f845c
lint with black
wakamex Jun 30, 2023
ed7ed84
add failure case to test
wakamex Jun 30, 2023
2185ec6
remove state dump
wakamex Jul 3, 2023
0fd7643
remote state dump tests
wakamex Jul 3, 2023
4888fe0
remove FixedPoint when creating ape trade
wakamex Jul 3, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
125 changes: 64 additions & 61 deletions elfpy/utils/apeworx_integrations.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Helper functions for integrating the sim repo with solidity contracts via Apeworx."""
from __future__ import annotations

# std libs
import json
import logging
import os
Expand All @@ -9,6 +10,7 @@
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable

# third party libs
import ape
import numpy as np
import pandas as pd
Expand All @@ -22,6 +24,7 @@
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
Expand Down Expand Up @@ -271,30 +274,33 @@ 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:
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 <https://docs.apeworx.io/ape/stable/methoddocs/contracts.html#ape.contracts.base.ContractInstance>`_
Contract pointing to the initialized Hyperdrive (or MockHyperdriveTestnet) smart contract.
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
-------
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 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 add_to
trades = pd.concat( # flatten event_arguments
[
trades.loc[:, [c for c in trades.columns if c != "event_arguments"]],
Expand All @@ -305,43 +311,35 @@ 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"])
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_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

r"""Construct wallet balances from on-chain trade info.

Arguments
---------
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 <https://docs.apeworx.io/ape/stable/methoddocs/contracts.html#ape.contracts.base.ContractInstance>`_
Contract pointing to the initialized Hyperdrive (or MockHyperdriveTestnet) smart contract.
base_contract : `ape.contracts.base.ContractInstance <https://docs.apeworx.io/ape/stable/methoddocs/contracts.html#ape.contracts.base.ContractInstance>`_
Expand All @@ -354,7 +352,10 @@ def get_wallet_from_onchain_trade_info(
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
if add_to_existing_wallet is None:
wallet = Wallet(
address=index,
Expand All @@ -364,49 +365,50 @@ 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
Comment thread
wakamex marked this conversation as resolved.
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
)
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
logging.info(" => %s(%s) maturity=%s mint_time=%s", asset_type, asset_prefix, maturity, mint_time)
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:
on_chain_balance = hyperdrive_contract.balanceOf(position_id, address)
if add_to_existing_wallet is None and position_id != 0:
on_chain_balance = hyperdrive_contract.balanceOf(int(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"events {balance=} and {on_chain_balance=} disagree by 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
), "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)
Expand Down Expand Up @@ -680,9 +682,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),
Expand All @@ -693,6 +693,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


Expand Down Expand Up @@ -872,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


Expand Down Expand Up @@ -925,7 +928,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),
Expand Down
Loading