ERC-4626: examine vault historical performance

This notebook serves both as a coding tutorial and a useful data analytics tool for ERC-4626 vaults.

  • In this notebook, we examine one ERC-4626 vault historical performance

  • We pick one vault by chain id and address and show its metrics

  • We visualise the performance with Plotly charts and quantstats portfolio performance metrics

Usage

  • Read general instructions how to run the tutorials

  • See ERC-4626 scanning all vaults onchain example in tutorials first how to build a vault database as local vault_db.pickle file.

  • See ERC-4626: scanning vaults' historical price and performance example in tutorials first how to build vault-prices.parquet file.

Setup

  • Set up notebook rendering output mode

  • Use static image charts so this notebook is readeable on Github / ReadTheDocs

[28]:
import pandas as pd
from plotly.offline import init_notebook_mode
import plotly.io as pio

from eth_defi.vault.base import VaultSpec

pd.options.display.float_format = '{:,.2f}'.format
pd.options.display.max_columns = None
pd.options.display.max_rows = None


# Set up Plotly chart output as SVG
image_format = "png"
width = 1400
height = 800

 # https://stackoverflow.com/a/52956402/315168
init_notebook_mode()

# https://stackoverflow.com/a/74609837/315168
assert hasattr(pio, "kaleido"), "Kaleido rendering backend missing. Run 'pip install kaleido' needed for this notebook"
pio.kaleido.scope.default_format = image_format

# https://plotly.com/python/renderers/#overriding-the-default-renderer
pio.renderers.default = image_format

current_renderer = pio.renderers[image_format]
# Have SVGs default pixel with
current_renderer.width = width
current_renderer.height = height

Read scanned vault price data

  • Read the Parquet file produced earlier with price scan

[29]:
import pickle
from pathlib import Path

output_folder = Path("~/.tradingstrategy/vaults").expanduser()
parquet_file = output_folder / "vault-prices.parquet"
assert parquet_file.exists(), "Run the vault scanner script first"

vault_db = output_folder / "vault-db.pickle"
assert vault_db.exists(), "Run the vault scanner script first"
vault_db = pickle.load(open(vault_db, "rb"))

try:
    prices_df = pd.read_parquet(parquet_file)
except Exception as e:
    raise RuntimeError(f"Could not read: {parquet_file}: {e}") from e

chains = prices_df["chain"].unique()

print(f"We have {len(prices_df):,} price rows and {len(vault_db)} vault metadata entries for {len(chains)} chains")

display(prices_df.head())
We have 1,433,926 price rows and 7014 vault metadata entries for 10 chains
chain address block_number timestamp share_price total_assets total_supply performance_fee management_fee
0 56 0x10c90bfcfb3d2a7ae814da1548ae3a7fc31c35a0 18172265 2022-05-27 17:25:18 1.46 6.84 10.00 NaN NaN
1 56 0x10c90bfcfb3d2a7ae814da1548ae3a7fc31c35a0 18201065 2022-05-28 17:36:35 1.46 1,032.68 1,510.00 NaN NaN
2 56 0x10c90bfcfb3d2a7ae814da1548ae3a7fc31c35a0 18345065 2022-06-02 18:23:12 1.46 23,736.03 34,713.44 NaN NaN
3 56 0x0b96dccbaa03447fd5f5fd733e0ebd10680e84c1 18345065 2022-06-02 18:23:12 0.44 194.83 84.92 NaN NaN
4 56 0x10c90bfcfb3d2a7ae814da1548ae3a7fc31c35a0 18460265 2022-06-06 19:08:56 1.46 25,803.91 37,737.68 NaN NaN

Choose an interesting vault and extract its metadata

  • We choose one vault what we have seen in the earlier notebooks

[30]:
from eth_defi.chain import get_chain_name

# SteakhouseUSDT
# https://app.morpho.org/ethereum/vault/0xbEef047a543E45807105E51A8BBEFCc5950fcfBa/steakhouse-usdt

chain_id = 1
address = "0xbEef047a543E45807105E51A8BBEFCc5950fcfBa".lower()

vault_spec = VaultSpec(chain_id, address)
vault_metadata = vault_db[vault_spec]
name = vault_metadata["Name"]

meta_df = pd.DataFrame(list(vault_metadata.values()), index=vault_metadata.keys(), columns=["Value"])

chain_name = get_chain_name(vault_spec.chain_id)
print(f"Vault metadata for selected vault: {name} on {chain_name}")
display(meta_df)
Vault metadata for selected vault: Steakhouse USDT on Ethereum
Value
Symbol steakUSDT
Name Steakhouse USDT
Address 0xbeef047a543e45807105e51a8bbefcc5950fcfba
Denomination USDT
NAV 41888502.211544
Protocol Morpho
Mgmt fee 0.00
Perf fee 0.00
Shares 38848612.71044189229694404
First seen 2024-02-06 17:27:35
_detection_data ERC4262VaultDetection(chain=1, address='0xbeef...
_denomination_token {'name': 'Tether USD', 'symbol': 'USDT', 'tota...
_share_token {'name': 'Steakhouse USDT', 'symbol': 'steakUS...

Extract price series

  • Get the historical share price for this vault for all vaults data

[31]:
vault_df = prices_df.loc[
    (prices_df["address"] == address) &
    (prices_df["chain"] == chain_id)
]

vault_df = vault_df.set_index("timestamp").sort_index()

print(f"Vault price data rows for {chain_id}: {address}, total {len(vault_df)} rows")
display(vault_df.head())
display(vault_df.tail())

assert len(vault_df), f"No rows found for address: {address}"
Vault price data rows for 1: 0xbeef047a543e45807105e51a8bbefcc5950fcfba, total 425 rows
chain address block_number share_price total_assets total_supply performance_fee management_fee
timestamp
2024-02-06 22:56:35 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 19172299 1.00 10,000.00 10,000.00 0.05 0.00
2024-02-07 23:10:11 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 19179499 1.00 10,000.00 10,000.00 0.05 0.00
2024-02-08 23:26:23 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 19186699 1.00 10,000.00 10,000.00 0.05 0.00
2024-02-09 23:39:35 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 19193899 1.00 104,000.00 104,000.40 0.05 0.00
2024-02-10 23:55:23 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 19201099 1.00 104,000.00 104,001.54 0.05 0.00
chain address block_number share_price total_assets total_supply performance_fee management_fee
timestamp
2025-04-04 14:57:47 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 22196299 1.08 40,146,804.75 43,273,497.23 0.00 0.00
2025-04-05 15:06:23 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 22203499 1.08 40,043,884.52 43,166,226.04 0.00 0.00
2025-04-06 15:15:11 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 22210699 1.08 39,549,783.07 42,637,131.40 0.00 0.00
2025-04-07 15:25:23 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 22217899 1.08 42,297,566.71 45,603,152.42 0.00 0.00
2025-04-08 15:31:59 1 0xbeef047a543e45807105e51a8bbefcc5950fcfba 22225099 1.08 40,256,837.45 43,406,321.00 0.00 0.00

Net asset value

[32]:
import plotly.express as px
nav_series = vault_df["total_assets"]

# Remove the uninteresting period when NAV is low
min_nav_threshold = 100_000
nav_series = nav_series[nav_series > min_nav_threshold]

fig = px.line(
    nav_series,
    title=f"Net asset value of {name} vault over time"
)
fig.update_layout(yaxis_title=f"NAV in {vault_metadata['Denomination']}")
fig.update_layout(showlegend=False)
fig.show()
../_images/tutorials_erc-4626-historical-price_10_0.png

Share price chart

  • Show the share price of the chosen vault

  • We can calculate returns, or APY % from the share price

[33]:
import plotly.express as px
price_series = vault_df["share_price"]

fig = px.line(
    price_series,
    title=f"Share price of {name} vault over time"
)
fig.update_layout(yaxis_title=f"Price in {vault_metadata['Denomination']}")
fig.update_layout(showlegend=False)
fig.show()
../_images/tutorials_erc-4626-historical-price_12_0.png

Daily returns

  • Chart the daily returns of the vault

  • Daily returns are related to CAGR and APY

[34]:
# Becauase the original sampling was done by block number,
# not by timestamp, we need to convert the series to daily format
daily_price_series = price_series.resample("D").last()
daily_price_series = daily_price_series.ffill()
print("Daily share token prices are")
display(daily_price_series.head())

Daily share token prices are
timestamp
2024-02-06   1.00
2024-02-07   1.00
2024-02-08   1.00
2024-02-09   1.00
2024-02-10   1.00
Freq: D, Name: share_price, dtype: float64
[35]:
# Convert to returns and plot it out
daily_returns = daily_price_series.pct_change()

# Create symmetric bins for the histogram
max_abs_return = max(abs(daily_returns.min()), abs(daily_returns.max()))  # e.g., 2.505
bin_size = 0.5  # Bin width in percentage points
nbins = int(2 * max_abs_return / bin_size)  # Ensure symmetry
symmetric_range = [-max_abs_return, max_abs_return]

fig = px.histogram(
    daily_returns,
    x=daily_returns.values,  # Use the values, not the index
    title=f"Distribution of Daily Returns of {name} vault",
    labels={"x": "Return (%)", "y": "Count"},
    nbins=nbins,
    range_x=symmetric_range,
)
fig.show()
../_images/tutorials_erc-4626-historical-price_15_0.png

Performance metrics

  • Portfolio performance metrics for this vault

[36]:
import warnings

with warnings.catch_warnings():  #  DeprecationWarning: Importing display from IPython.core.display is deprecated since IPython 7.14, please import from IPython display
    warnings.simplefilter(action='ignore', category=FutureWarning)  # yfinance: The default dtype for empty Series will be 'object' instead of 'float64' in a future version. Specify a dtype explicitly to silence this warning.
    try:
        # quantstats compatibility hack for IPython 8.x, ZMQInteractiveShell error
        # https://stackoverflow.com/a/15898875/315168
        get_ipython().magic = lambda x: x
        import quantstats
    except ImportError as e:
        quantstats = None


if quantstats:
    metrics = quantstats.reports.metrics
    performance_metrics_df = metrics(
        daily_returns,
        benchmark=None,
        as_pct=display,  # QuantStats codebase is a mess
        periods_per_year=365,
        mode="full",
        display=False,
        internal=True,
    )
    print(f"Portfolio performance metrics for {name} vault")
    performance_metrics_df.rename(columns={"Strategy": name}, inplace=True)
    display(performance_metrics_df)
else:
    print("To show the portfolio performance metrics install quantstats: pip install quantstats")
Portfolio performance metrics for Steakhouse USDT vault
/Users/moo/Library/Caches/pypoetry/virtualenvs/web3-ethereum-defi-YE4GM4ox-py3.11/lib/python3.11/site-packages/scipy/stats/_distn_infrastructure.py:2304: RuntimeWarning:

invalid value encountered in multiply

/Users/moo/Library/Caches/pypoetry/virtualenvs/web3-ethereum-defi-YE4GM4ox-py3.11/lib/python3.11/site-packages/scipy/stats/_distn_infrastructure.py:2305: RuntimeWarning:

invalid value encountered in multiply

Steakhouse USDT
Start Period 2024-02-07
End Period 2025-04-08
Risk-Free Rate 0.0%
Time in Market 100.0%
Cumulative Return 7.82%
CAGR﹪ 4.56%
Sharpe 37.64
Prob. Sharpe Ratio 100.0%
Smart Sharpe 15.87
Sortino -
Smart Sortino -
Sortino/√2 -
Smart Sortino/√2 -
Omega -
Max Drawdown %
Longest DD Days -
Volatility (ann.) 0.17%
Calmar -
Skew 1.14
Kurtosis 2.36
Expected Daily 0.02%
Expected Monthly 0.5%
Expected Yearly 3.84%
Kelly Criterion -
Risk of Ruin 0.0%
Daily Value-at-Risk -0.0%
Expected Shortfall (cVaR) -0.0%
Max Consecutive Wins 189
Max Consecutive Losses 0
Gain/Pain Ratio -
Gain/Pain (1M) -
Payoff Ratio -
Profit Factor -
Common Sense Ratio -
CPC Index -
Tail Ratio 7.93
Outlier Win Ratio 2.59
Outlier Loss Ratio -
MTD 0.07%
3M 1.43%
6M 3.42%
YTD 1.58%
1Y 6.88%
3Y (ann.) 4.56%
5Y (ann.) 4.56%
10Y (ann.) 4.56%
All-time (ann.) 4.56%
Best Day 0.06%
Worst Day 0.0%
Best Month 0.82%
Worst Month 0.06%
Best Year 6.14%
Worst Year 1.58%
Recovery Factor -
Ulcer Index 0.0
Serenity Index -
Avg. Up Month 0.5%
Avg. Down Month -
Win Days 100.0%
Win Month 100.0%
Win Quarter 100.0%
Win Year 100.0%
Avg. Drawdown Days -