Lighter: benchmark pools

  • In this notebook, we examine native Lighter pools (perpetual trading pools on Lighter DEX)

  • These pools use Lighter’s ZK-rollup on Ethereum, identified by synthetic chain ID 9998

  • The main protocol pool is the LLP (Lighter Liquidity Pool) which provides market-making liquidity

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.

  • Run the Lighter daily metrics pipeline (scripts/lighter/daily-pool-metrics.py) to populate Lighter pool data

For any questions, follow and contact Trading Strategy community.

Setup

  • Set up notebook rendering output mode

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

[ ]:
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

# Fix X time axis bugs in Plotly charts
from eth_defi.monkeypatch import plotly

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

  • Lighter pool data is merged into the same files as ERC-4626 data

[ ]:
from pathlib import Path

from eth_defi.vault.vaultdb import VaultDatabase, read_default_vault_prices

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

print(f"We have {len(vault_db):,} vaults in the database and {len(prices_df):,} price rows.")

Choose Lighter chain

  • Lighter is a ZK-rollup perpetual DEX on Ethereum

  • Uses synthetic chain ID 9998 to avoid collision with real EVM chain IDs

[ ]:
from eth_defi.vault.base import VaultSpec
from eth_defi.chain import get_chain_name
from eth_defi.lighter.constants import LIGHTER_CHAIN_ID

selected_chain_id = LIGHTER_CHAIN_ID
chain_name = get_chain_name(selected_chain_id)

print(f"Examining chain {chain_name} ({selected_chain_id})")

Price data filtering

  • Filter prices for Lighter pools only

[ ]:
prices_df = prices_df[prices_df["chain"] == selected_chain_id]
print(f"Examined prices contain {len(prices_df):,} price rows across all pools on {chain_name}.")

prices_df.head(4)

Filter pools

  • Choose pools on Lighter

  • Filter out low TVL entries

[ ]:
min_tvl = 50_000

vault_db_chain = {spec: row for spec, row in vault_db.items() if spec.chain_id == selected_chain_id}
vault_db = {spec: row for spec, row in vault_db_chain.items() if (row["NAV"] or 0) >= min_tvl}
selected_vault_ids = {spec.as_string_id() for spec in vault_db.keys()}
prices_df = prices_df.loc[prices_df["id"].isin(selected_vault_ids)]

print(f"We have selected {len(vault_db)} pools out of total of {len(vault_db_chain):,} pools on chain {chain_name}, having {len(prices_df):,} price rows.")

print("An example pool metadata:")
example_vault = next(iter(vault_db.values()))
display(pd.DataFrame(list(example_vault.items()), columns=["Key", "Value"]))

Calculate pool lifetime metrics

  • Calculate the DataFrame of lifetime metrics for each pool

[ ]:
from eth_defi.research.vault_metrics import calculate_lifetime_metrics, clean_lifetime_metrics
from eth_defi.research.vault_metrics import format_lifetime_table

lifetime_data_df = calculate_lifetime_metrics(
    prices_df,
    vault_db,
)

print(f"Cleaning metrics for {len(lifetime_data_df):,} pools")
lifetime_data_df = clean_lifetime_metrics(
    lifetime_data_df,
    max_annualised_return=99.99,  # 9999% max return
)

print(f"Calculated lifetime metrics for {len(lifetime_data_df):,} pools")

lifetime_data_df = lifetime_data_df.sort_values(["one_month_cagr"], ascending=False)

display(lifetime_data_df.head(2))

Top pool list

  • List top Lighter pools, formatted for readability

[ ]:
from eth_defi.research.vault_metrics import format_lifetime_table

min_tvl = 25_000

lifetime_data_filtered_df = lifetime_data_df[lifetime_data_df["current_nav"] >= min_tvl]

lifetime_data_filtered_df = lifetime_data_filtered_df.sort_values(["one_month_cagr"], ascending=False)

print(f"Pools filtered by min TVL of ${min_tvl:,}, remaining {len(lifetime_data_filtered_df):,} pools.")
formatted_df = format_lifetime_table(
    lifetime_data_filtered_df,
    add_index=True,
    add_address=True,
)

print(f"Last update {lifetime_data_filtered_df['last_updated_at'].max()}")

cols_to_move = ["Name", "1M return ann. (net / gross)"]
other_cols = [col for col in formatted_df.columns if col not in cols_to_move]
formatted_df = formatted_df[cols_to_move + other_cols]

print(f"Formatted data for {len(formatted_df):,} pools.")

# Script output
max_address_dump = 300
head = formatted_df.head(max_address_dump)
pool_count = min(max_address_dump, len(head))
print(f"Top {pool_count} pools by 1 month annualised return are: {', '.join(head['Name'])}")
print(f"Top {pool_count} pools by 1 month annualised return are:\n{', '.join(head['Address'])}")

display(formatted_df)

Top pool equity curve comparison

  • Compare top pool equity curves

  • Compare net returns

  • Only pools with fee data included

  • Lookback 90 days

[ ]:
from eth_defi.research.vault_benchmark import visualise_vault_return_benchmark

top_count = 15

top_vaults_specs = lifetime_data_filtered_df.head(top_count)["id"].apply(VaultSpec.parse_string)

fig, net_returns_df = visualise_vault_return_benchmark(
    top_vaults_specs,
    prices_df=prices_df,
    vault_db=vault_db,
)

fig.show()

Pool charts and performance tearsheets

  • Show rolling returns performance chart for N top pools

[ ]:
from eth_defi.research.vault_metrics import display_vault_chart_and_tearsheet
from eth_defi.vault.risk import VaultTechnicalRisk

examined_vaults_df = lifetime_data_filtered_df.loc[lifetime_data_filtered_df["risk"] != VaultTechnicalRisk.blacklisted]

interest_vault_specs = []

for idx, row in examined_vaults_df.head(10).iterrows():
    vault_spec = VaultSpec.parse_string(row["id"])

    # Used later
    interest_vault_specs.append(vault_spec)

    display_vault_chart_and_tearsheet(
        vault_spec,
        vault_db=vault_db,
        prices_df=prices_df,
        render=True,
    )

Rolling returns comparison

  • Show rolling returns of all picked pools

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

rolling_returns_df = calculate_rolling_returns(
    prices_df,
    interesting_vaults=[spec.as_string_id() for spec in interest_vault_specs],
    clip_up=100,
)

assert len(rolling_returns_df) > 0, "No rolling returns calculated"

fig = visualise_rolling_returns(rolling_returns_df)

fig.show()

All pools

  • List all Lighter pools found

[ ]:
from eth_defi.research.vault_metrics import format_lifetime_table, display_lifetime_table

min_tvl = 1_000

lifetime_data_filtered_df = lifetime_data_df[lifetime_data_df["current_nav"] >= min_tvl]

formatted = format_lifetime_table(
    lifetime_data_filtered_df,
    add_index=True,
    html_links=True,
)

display_lifetime_table(formatted)