ERC-4626: analyse performance of individual vaults

  • In this notebook, we examine a single or handful of handpicked ERC-4626 vaults

    • Vaults are picked manually by (chain, address) list

    • We analyse the vault performance by its share price, as reported by ERC-4626 smart contract interface.

    • We do last three months and historical all-time analyses

    • We look quantitative finance aspects of the vaults like returns, Sharpe and Sortino numbers

Some notes - Because of how vault metrics, share price and such are collected and interpreted, the results in this notebook contain various inaccuracies. - In this notebook, we use terms Net Asset Value (NAV) and Total Value Locked (TVL) interchangeably.

Usage

This is an open source notebook based on open data - You can edit and remix this notebook yourself

To do your own data research:

  • Read general instructions how to run the tutorials

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

For any questions, follow and contact Trading Strategy community.

Setup

  • Set up notebook rendering output mode

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

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

from eth_defi.vault.base import VaultSpec
from eth_defi.research.notebook import set_large_plotly_chart_font

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://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

# Set all Plotly charts to use large font sizes for better readability,
# for sharing on mobile
set_large_plotly_chart_font(line_width=5, legend_font_size=16)
pio.templates.default = "custom"

Read and clean raw scanned vault price data

  • Read the Parquet file produced earlier with price scan

  • Clean the data if necessary

[9]:
from pathlib import Path

from eth_defi.vault.vaultdb import VaultDatabase
from eth_defi.vault.vaultdb import read_default_vault_prices
from eth_defi.vault.vaultdb import DEFAULT_RAW_PRICE_DATABASE

data_folder = Path("~/.tradingstrategy/vaults").expanduser()

vault_db = VaultDatabase.read()
prices_df = read_default_vault_prices()

print(prices_df.index.max())

print(f"Prices is {DEFAULT_RAW_PRICE_DATABASE}")
print(f"We have {len(vault_db):,} vaults in the database and {len(prices_df):,} price rows.")
2025-12-29 00:55:27
Prices is /Users/moo/.tradingstrategy/vaults/cleaned-vault-prices-1h.parquet
We have 26,576 vaults in the database and 5,953,190 price rows.

Choose vaults to examine

  • We pick vaults to examine and compare by chain and address tuples

[ ]:
from eth_defi.vault.base import VaultSpec


# Historically troublesome vaults
VAULTS = [

    # atvPTmax
    # VaultSpec(1, "0xd24e4a98b5fd90ff21a9cc5e2c1254de8084cd81"),

    # TODO
    VaultSpec(43114, "0x36e2aa296e798ca6262dc5fad5f5660e638d5402"),

    # Valamor aUSD http://localhost:5173/trading-view/avalanche/vaults/varlamore-ausd-growth
    VaultSpec(43114, "0x3d7b0c3997e48fa3fc96cd057d1fb4e5f891835b"),

    # 1337
    # VaultSpec(1, "0x94643e86aa5e38ddac6c7791c1297f4e40cd96c1"),

    # yearn-v3-USDS-Farmer-USD
    # VaultSpec(1, "0x39c0aec5738ed939876245224afc7e09c8480a52"),

    # USDC Fluid Lender
    # VaultSpec(1, "0x00c8a649c9837523ebb406ceb17a6378ab5c74cf"),

    # Harvest USDC Autopilot on IPOR on Base
    # https://app.ipor.io/fusion/base/0x0d877dc7c8fa3ad980dfdb18b48ec9f8768359c4/settings
    # VaultSpec(8453, "0x0d877Dc7C8Fa3aD980DfDb18B48eC9F8768359C4"),

    # IPOR USDC base
    # https://app.ipor.io/fusion/base/0x45aa96f0b3188d47a1dafdbefce1db6b37f58216
    # VaultSpec(8453, "0x45aa96f0b3188d47a1dafdbefce1db6b37f58216"),

    # Summer.fi lazy
    # VaultSpec(42161, "0x4f63cfea7458221cb3a0eee2f31f7424ad34bb58"),

    # gTRADE (Gains) on Polygon
    # https://app.ipor.io/fusion/base/0x45aa96f0b3188d47a1dafdbefce1db6b37f58216
    # VaultSpec(137, "0x29019fe2e72e8d4d2118e8d0318bef389ffe2c81"),

    # Gains on ARbitrum
    # 42161-0xd85e038593d7a098614721eae955ec2022b9b91b
    # VaultSpec(42161, "0xd85e038593d7a098614721eae955ec2022b9b91b"),

    # gUSD on Arbitrum
    # VaultSpec(42161, "0xd3443ee1e91af28e5fb858fbd0d72a63ba8046e0"),

    # Peapods arbitrum
    # VaultSpec(42161, "0xc2810eb57526df869049fbf4c541791a3255d24c"),

    # Degen pool USDC 42161-0x20a1012b79e8f3ca3f802533c07934ef97398da
    # VaultSpec(42161, "0x20a1012b79e8f3ca3f802533c07934ef97398da7"),

    # Fluegel DAO
    # 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c
    # https://fluegelcoin.com/dashboard
    # VaultSpec(8453, "0x277a3c57f3236a7d4548576074d7c3d7046eb26c"),

    # Plutus hedge on Arbitrum
    # VaultSpec(42161, "0x58BfC95a864e18E8F3041D2FCD3418f48393fE6A"),

    # crvUSD IBTC
    # VaultSpec(42161, "0xe296ee7f83d1d95b3f7827ff1d08fe1e4cf09d8d"),

    # D2 Hype on Arbitrum
    # VaultSpec(42161, "0x75288264FDFEA8ce68e6D852696aB1cE2f3E5004"),

    # Fluid on Ethereum
    # VaultSpec(1, "0x00c8a649c9837523ebb406ceb17a6378ab5c74cf"),

    # Upshift gamma
    # VaultSpec(1, "0x998d7b14c123c1982404562b68eddb057b0477cb"),
]


Period selection

  • Choose the period we want to examine

  • Comment out the trim operation if you want to examine the latest data

[11]:
last_sample_at = prices_df.index.max()
three_months_ago = last_sample_at - pd.DateOffset(months=3)

PERIOD = [
    three_months_ago,
    last_sample_at,
]

print(f"Trimming price data to period from {PERIOD[0]} to {PERIOD[1]}.")

mask = (prices_df.index >= PERIOD[0]) & (prices_df.index <= PERIOD[1])
prices_df = prices_df[mask]
print(f"Trimmed period contains {len(prices_df):,} price rows.")

prices_df = prices_df.sort_index()

display(prices_df.tail(2))
Trimming price data to period from 2025-09-29 00:55:27 to 2025-12-29 00:55:27.
Trimmed period contains 969,491 price rows.
id chain address block_number share_price total_assets total_supply performance_fee management_fee errors name event_count protocol raw_share_price returns_1h avg_assets_by_vault dynamic_tvl_threshold tvl_filtering_mask
timestamp
2025-12-29 00:55:27 10-0x6926b434cce9b5b7966ae1bfeef6d0a7dcf3a8bb 10 0x6926b434cce9b5b7966ae1bfeef6d0a7dcf3a8bb 145685475 1.09 1,882,561.42 1,727,893.56 NaN NaN exactly USDC 226488 <protocol not yet identified> 1.09 0.00 4,566,831.10 91,336.62 False
2025-12-29 00:55:27 10-0x035c93db04e5aaea54e6cd0261c492a3e0638b37 10 0x035c93db04e5aaea54e6cd0261c492a3e0638b37 145685475 1.21 49,705.69 40,986.76 NaN NaN Static Aave Optimism USDT 33407 Superform 1.21 0.00 40,618.50 812.37 False

Examine data

Examine metadata

[12]:
examined_vault_spec = VAULTS[0]
examined_id = f"{examined_vault_spec.chain_id}-{examined_vault_spec.vault_address}"

vault_metadata = vault_db.rows[examined_vault_spec]
display(vault_metadata)
{'Symbol': 'TIDAVAY_USDC',
 'Name': 'TiD Capital Avalanche Yield',
 'Address': '0x36e2aa296e798ca6262dc5fad5f5660e638d5402',
 'Denomination': 'USDC',
 'Share token': 'TIDAVAY_USDC',
 'NAV': Decimal('67801.754191'),
 'Protocol': '<protocol not yet identified>',
 'Mgmt fee': None,
 'Perf fee': None,
 'Deposit fee': None,
 'Withdraw fee': None,
 'Shares': Decimal('14232270910.974665'),
 'First seen': datetime.datetime(2025, 9, 27, 7, 51, 43),
 'Features': '',
 'Link': 'https://routescan.io/address/0x36E2AA296E798Ca6262DC5Fad5F5660e638d5402',
 '_detection_data': ERC4262VaultDetection(chain=43114, address='0x36e2aa296e798ca6262dc5fad5f5660e638d5402', first_seen_at_block=69371655, first_seen_at=datetime.datetime(2025, 9, 27, 7, 51, 43), features=set(), updated_at=datetime.datetime(2025, 12, 29, 0, 50, 4, 453740), deposit_count=15, redeem_count=13),
 '_denomination_token': {'name': 'USD Coin',
  'symbol': 'USDC',
  'total_supply': 569693925223775,
  'decimals': 6,
  'extra_data': {'cached': True},
  'address': '0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E',
  'chain': 43114},
 '_share_token': {'name': 'TiD Capital Avalanche Yield',
  'symbol': 'TIDAVAY_USDC',
  'total_supply': 14232270910974665,
  'decimals': 6,
  'extra_data': {'cached': True},
  'address': '0x36E2AA296E798Ca6262DC5Fad5F5660e638d5402',
  'chain': 43114},
 '_fees': FeeData(fee_mode=None, management=None, performance=None, deposit=None, withdraw=None),
 '_flags': set(),
 '_lockup': None}

Examine share price/cleaned returns rows available

[13]:
examined_vault_spec = VAULTS[0]
examined_id = f"{examined_vault_spec.chain_id}-{examined_vault_spec.vault_address}"
vault_df = prices_df[prices_df["id"] == examined_id]

vault_metadata = vault_db.get(examined_vault_spec)

print(f"Data for vault {examined_id}, {vault_metadata['Name']} ({vault_metadata['Symbol']}) has {len(vault_df):,} rows.")
vault_df = vault_df.sort_index()
display(vault_df.tail(3))
Data for vault 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402, TiD Capital Avalanche Yield (TIDAVAY_USDC) has 1,646 rows.
id chain address block_number share_price total_assets total_supply performance_fee management_fee errors name event_count protocol raw_share_price returns_1h avg_assets_by_vault dynamic_tvl_threshold tvl_filtering_mask
timestamp
2025-12-28 21:05:21 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402 43114 0x36e2aa296e798ca6262dc5fad5f5660e638d5402 74624697 0.00 67,479.76 14,232,270,910.97 NaN NaN TiD Capital Avalanche Yield #4281 28 <protocol not yet identified> 0.00 0.00 25,894.50 517.89 False
2025-12-28 22:26:32 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402 43114 0x36e2aa296e798ca6262dc5fad5f5660e638d5402 74628297 0.00 67,595.92 14,232,270,910.97 NaN NaN TiD Capital Avalanche Yield #4281 28 <protocol not yet identified> 0.00 0.00 25,894.50 517.89 False
2025-12-28 23:47:01 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402 43114 0x36e2aa296e798ca6262dc5fad5f5660e638d5402 74631897 0.00 67,711.40 14,232,270,910.97 NaN NaN TiD Capital Avalanche Yield #4281 28 <protocol not yet identified> 0.00 0.00 25,894.50 517.89 False

Vault charts and performance tearsheets

  • Examine vault metrics and charts

[14]:
from eth_defi.research.vault_metrics import analyse_vault, format_ffn_performance_stats
from eth_defi.chain import get_chain_name

from IPython.display import display, HTML

for vault_spec in VAULTS:

    vault_report = analyse_vault(
        vault_db=vault_db,
        prices_df=prices_df,
        spec=vault_spec,
        chart_frequency="daily",
    )

    chain_name = get_chain_name(vault_spec.chain_id)
    vault_name = vault_report.vault_metadata["Name"]
    print(f"Analysing vault {vault_name} ({chain_name}): {vault_spec.as_string_id()}")
    display(HTML(f"<h2>Vault {vault_name} ({chain_name}): {vault_spec.vault_address})</h2><br>"))

    # Display returns figur
    returns_chart_fig = vault_report.rolling_returns_chart
    returns_chart_fig.show()

    # Check raw montly share price numbers for each vault
    hourly_price_df = vault_report.hourly_df
    last_price_at = hourly_price_df.index[-1]
    last_price = hourly_price_df["share_price"].asof(last_price_at)
    last_block = hourly_price_df["block_number"].asof(last_price_at)
    month_ago = last_price_at - pd.DateOffset(months=1)
    month_ago_price = hourly_price_df["share_price"].asof(month_ago)
    month_ago_block = hourly_price_df["block_number"].asof(month_ago)

    if not pd.isna(month_ago_price):
        print(f"Vault {vault_spec.chain_id}-{vault_spec.vault_address}: no price data for month ago {month_ago} found, last price at {last_price_at} is {last_price}")
        continue

    data = {
        "Vault": f"{vault_name} ({chain_name})",
        "Last price at": last_price_at,
        "Last price": last_price,
        "Block last price": f"{month_ago_block:,}",
        "Month ago": month_ago,
        "Block month ago": f"{month_ago_block:,}",
        "Month ago price": month_ago_price,
        "Monthly change %": (last_price - month_ago_price) / month_ago_price * 100,
    }

    df = pd.Series(data)
    display(df)

    # Display FFN stats
    performance_stats = vault_report.performance_stats
    if performance_stats is not None:
        stats_df = format_ffn_performance_stats(performance_stats)
        # display(stats_df)
        display(HTML(stats_df.to_frame(name='Value').to_html(float_format='{:,.2f}'.format, index=True)))
    else:
        print(f"Vault {vault_spec.chain_id}-{vault_spec.vault_address}: performance metrics not available, is quantstats library installed?")
Share price movement: 0.0000 2025-09-29 15:00:00 -> 0.0000 2025-12-28 23:00:00
Analysing vault TiD Capital Avalanche Yield (Avalanche): 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402

Vault TiD Capital Avalanche Yield (Avalanche): 0x36e2aa296e798ca6262dc5fad5f5660e638d5402)


../_images/tutorials_erc-4626-single-vault_15_2.png
Vault 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402: no price data for month ago 2025-11-28 23:00:00 found, last price at 2025-12-28 23:00:00 is 4e-06
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[14], line 8
      4 from IPython.display import display, HTML
      6 for vault_spec in VAULTS:
----> 8     vault_report = analyse_vault(
      9         vault_db=vault_db,
     10         prices_df=prices_df,
     11         spec=vault_spec,
     12         chart_frequency="daily",
     13     )
     15     chain_name = get_chain_name(vault_spec.chain_id)
     16     vault_name = vault_report.vault_metadata["Name"]

File ~/code/trade-executor/deps/web3-ethereum-defi/eth_defi/research/vault_metrics.py:1268, in analyse_vault(vault_db, prices_df, spec, returns_col, logger, chart_frequency)
   1266 vault_metadata = vault_db.get(spec)
   1267 if vault_metadata is None:
-> 1268     assert vault_metadata, f"Vault with id {spec} not found in vault database"
   1270 chain_name = get_chain_name(spec.chain_id)
   1271 name = vault_metadata["Name"]

AssertionError: Vault with id VaultSpec(chain_id=43114, vault_address='0x3d7b0c3997e48fa3fc96cd057d1fb4e5f891835b1') not found in vault database

Rolling returns comparison

  • Show rolling returns of all picked vaults

[ ]:
from eth_defi.research.rolling_returns import calculate_rolling_returns, visualise_rolling_returns

interesting_vaults = [spec.as_string_id() for spec in VAULTS]

rolling_returns_df = calculate_rolling_returns(
    prices_df,
    interesting_vaults=interesting_vaults,
    clip_up=100,
)

display(rolling_returns_df.head(3))

fig = visualise_rolling_returns(rolling_returns_df)

fig.show()
id timestamp chain address block_number share_price total_assets total_supply performance_fee management_fee errors name event_count protocol raw_share_price returns_1h avg_assets_by_vault dynamic_tvl_threshold tvl_filtering_mask rolling_1m_returns
0 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402 2025-09-27 08:10:55 43114 0x36e2aa296e798ca6262dc5fad5f5660e638d5402 69372297 0.00 2,000.00 2,000,000,000.00 NaN NaN TiD Capital Avalanche Yield #4279 28 <protocol not yet identified> 0.00 NaN 23,645.82 472.92 False 0.00
1 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402 2025-09-27 09:02:48 43114 0x36e2aa296e798ca6262dc5fad5f5660e638d5402 69374097 0.00 2,000.02 2,000,000,000.00 NaN NaN TiD Capital Avalanche Yield #4279 28 <protocol not yet identified> 0.00 0.00 23,645.82 472.92 False 0.00
2 43114-0x36e2aa296e798ca6262dc5fad5f5660e638d5402 2025-09-27 09:51:12 43114 0x36e2aa296e798ca6262dc5fad5f5660e638d5402 69375897 0.00 2,000.02 2,000,000,000.00 NaN NaN TiD Capital Avalanche Yield #4279 28 <protocol not yet identified> 0.00 0.00 23,645.82 472.92 False 0.00
../_images/tutorials_erc-4626-single-vault_17_1.png

Raw vault data

  • Examine raw data of a single vault

  • Find abnormal return rows

[ ]:
# Fluegel DAO
# 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c
# https://fluegelcoin.com/dashboard
vault_spec = VaultSpec(8453, "0x277a3c57f3236a7d4548576074d7c3d7046eb26c")

df = prices_df[prices_df["id"] == vault_spec.as_string_id()]

# 10% claimed hourly returns
mask = df["returns_1h"].abs() > 0.10
expanded_mask = (mask | mask.shift(1) | mask.shift(-1)).fillna(False)

df = df[expanded_mask]  # Filter out rows with small returns

display(df.head(10))
id chain address block_number share_price total_assets total_supply performance_fee management_fee errors name event_count protocol raw_share_price returns_1h avg_assets_by_vault dynamic_tvl_threshold tvl_filtering_mask
timestamp
2025-09-24 08:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 35956810 2.04 661,538.44 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.04 -0.00 603,195.56 12,063.91 False
2025-09-24 10:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 35960410 1.82 588,709.80 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 1.82 -0.11 603,195.56 12,063.91 False
2025-09-24 11:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 35962210 2.07 669,008.32 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.07 0.14 603,195.56 12,063.91 False
2025-09-24 13:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 35965810 2.06 667,852.18 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.06 -0.00 603,195.56 12,063.91 False
2025-10-01 17:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 36275410 2.25 727,138.15 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.25 -0.01 603,195.56 12,063.91 False
2025-10-01 18:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 36277210 1.99 644,306.79 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 1.99 -0.11 603,195.56 12,063.91 False
2025-10-01 19:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 36279010 2.25 728,047.48 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.25 0.13 603,195.56 12,063.91 False
2025-10-01 20:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 36280810 2.24 725,813.95 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.24 -0.00 603,195.56 12,063.91 False
2025-10-01 22:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 36284410 2.27 733,985.44 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.27 0.00 603,195.56 12,063.91 False
2025-10-01 23:36:07 8453-0x277a3c57f3236a7d4548576074d7c3d7046eb26c 8453 0x277a3c57f3236a7d4548576074d7c3d7046eb26c 36286210 2.03 655,645.90 323,616.44 NaN NaN Fluegel DAO (Base) #248 555 <protocol not yet identified> 2.03 -0.11 603,195.56 12,063.91 False