Hyperliquid Vault PnL Analysis
In this notebook, we demonstrate how to analyse the historical performance of a Hyperliquid vault by reconstructing its position history and visualising the equity curve (cumulative PnL).
What we’ll cover:
Fetching trade fills from Hyperliquid API for a specific vault
Reconstructing position events (opens, closes, increases, decreases) from fill data
Creating an analysis DataFrame with exposure and PnL tracking per market
Visualising the vault’s equity curve using Plotly
Analysing vault deposits and withdrawals (capital flows)
Combining PnL and deposits to visualise overall account value
Calculating internal share price to measure true investment performance
About Hyperliquid Vaults:
Hyperliquid vaults are managed trading accounts where depositors delegate capital to vault managers who execute perpetual futures trading strategies. Unlike ERC-4626 vaults, Hyperliquid vaults operate natively on Hyperliquid Core, a specialised chain that is not EVM compatible.
Note: The Hyperliquid API has pagination limits (max 10,000 fills), so historical analysis is limited to recent trading activity.
For questions or feedback, contact Trading Strategy community.
Setup
Configure notebook display settings
Set up Plotly for static image output (for documentation rendering)
[29]:
import datetime
import pandas as pd
from plotly.offline import init_notebook_mode
import plotly.io as pio
import plotly.graph_objects as go
pd.options.display.float_format = "{:,.2f}".format
pd.options.display.max_columns = None
pd.options.display.max_rows = None
# Set up Plotly chart output
image_format = "png"
width = 1200
height = 600
init_notebook_mode()
pio.renderers.default = image_format
current_renderer = pio.renderers[image_format]
current_renderer.width = width
current_renderer.height = height
Vault Configuration
We’ll analyze the Trading Strategy - IchiV3 LS vault, which executes long/short perpetual futures strategies.
You can view this vault on the Hyperliquid app: https://app.hyperliquid.xyz/vaults/0x3df9769bbbb335340872f01d8157c779d73c6ed0
[30]:
# Vault address to analyze
VAULT_ADDRESS = "0x3df9769bbbb335340872f01d8157c779d73c6ed0"
# Time range for analysis (last 30 days by default)
END_TIME = datetime.datetime.now()
START_TIME = END_TIME - datetime.timedelta(days=30)
# Display configuration as a table
config_df = pd.DataFrame({
"Setting": ["Vault Address", "Start Date", "End Date"],
"Value": [VAULT_ADDRESS, START_TIME.strftime('%Y-%m-%d'), END_TIME.strftime('%Y-%m-%d')]
})
display(config_df.set_index("Setting"))
| Value | |
|---|---|
| Setting | |
| Vault Address | 0x3df9769bbbb335340872f01d8157c779d73c6ed0 |
| Start Date | 2025-11-30 |
| End Date | 2025-12-30 |
Fetch Trade Fills
First, we create an HTTP session configured with retry logic for the Hyperliquid API, then fetch all trade fills for the vault within our time range.
The API returns fills in reverse chronological order, but our fetch function automatically sorts them chronologically for position reconstruction.
[31]:
from eth_defi.hyperliquid.session import create_hyperliquid_session
from eth_defi.hyperliquid.position import fetch_vault_fills
# Create session with automatic retry logic
session = create_hyperliquid_session()
# Fetch fills for the vault
fills = list(fetch_vault_fills(
session,
VAULT_ADDRESS,
start_time=START_TIME,
end_time=END_TIME,
))
print(f"Fetched {len(fills)} fills")
# Show sample fills as a table
if fills:
sample_data = []
for fill in fills[:5]: # Show first 5 fills
sample_data.append({
"Coin": fill.coin,
"Side": fill.side,
"Size": float(fill.size),
"Price": float(fill.price),
"Time": fill.timestamp,
})
display(pd.DataFrame(sample_data))
Fetched 128 fills
| Coin | Side | Size | Price | Time | |
|---|---|---|---|---|---|
| 0 | ENA | B | 107.00 | 0.24 | 2025-12-01 06:07:19.756 |
| 1 | WLFI | B | 205.00 | 0.15 | 2025-12-02 11:01:08.209 |
| 2 | XRP | B | 23.00 | 2.19 | 2025-12-03 22:05:03.025 |
| 3 | ADA | A | 92.00 | 0.45 | 2025-12-04 01:01:07.787 |
| 4 | WLFI | A | 205.00 | 0.16 | 2025-12-04 18:01:21.837 |
Reconstruct Position History
The raw fills don’t directly tell us about position state. We need to process them chronologically to reconstruct position events:
Open: New position from flat
Close: Position closed to flat (realised PnL)
Increase: Position size increased
Decrease: Partial position reduction (realised PnL)
[32]:
from eth_defi.hyperliquid.position import reconstruct_position_history, get_position_summary
# Reconstruct position events from fills
events = list(reconstruct_position_history(fills))
print(f"Reconstructed {len(events)} position events")
# Show position summary per market as a table
summary = get_position_summary(events)
summary_data = []
for coin, stats in sorted(summary.items()):
summary_data.append({
"Market": coin,
"Total Trades": stats['total_trades'],
"Opens": stats['opens'],
"Closes": stats['closes'],
"Realised PnL": float(stats['total_realized_pnl']),
"Total Fees": float(stats['total_fees']),
})
summary_df = pd.DataFrame(summary_data).set_index("Market")
display(summary_df)
Reconstructed 129 position events
| Total Trades | Opens | Closes | Realised PnL | Total Fees | |
|---|---|---|---|---|---|
| Market | |||||
| 0G | 7 | 1 | 1 | 0.76 | 0.59 |
| AAVE | 4 | 1 | 1 | 96.61 | 0.66 |
| ACE | 13 | 5 | 5 | -125.76 | 2.42 |
| ADA | 2 | 1 | 1 | -1.87 | 0.04 |
| APEX | 2 | 1 | 1 | -3.02 | 0.04 |
| APT | 2 | 1 | 1 | -30.65 | 0.64 |
| ARB | 2 | 1 | 1 | -0.98 | 0.02 |
| AVNT | 6 | 1 | 1 | -75.53 | 0.69 |
| BTC | 3 | 2 | 1 | -0.40 | 0.37 |
| CC | 3 | 1 | 1 | -16.09 | 0.25 |
| CRV | 2 | 1 | 1 | 0.49 | 0.02 |
| DOGE | 2 | 1 | 1 | 1.07 | 0.02 |
| ENA | 1 | 1 | 0 | 0.00 | 0.01 |
| HBAR | 2 | 1 | 1 | 2.63 | 0.01 |
| HYPE | 5 | 2 | 1 | 0.00 | 0.28 |
| LDO | 4 | 1 | 1 | -5.77 | 0.23 |
| LTC | 3 | 1 | 0 | 0.00 | 0.41 |
| MERL | 5 | 1 | 1 | -33.83 | 0.64 |
| MON | 7 | 2 | 1 | -48.90 | 0.96 |
| SEI | 2 | 1 | 1 | -0.52 | 0.02 |
| SUI | 2 | 1 | 1 | 0.86 | 0.03 |
| TAO | 2 | 1 | 1 | -0.87 | 0.02 |
| TRX | 4 | 2 | 2 | -2.91 | 0.23 |
| USTC | 2 | 1 | 1 | -1.43 | 0.02 |
| VVV | 8 | 2 | 1 | 32.01 | 0.96 |
| WIF | 1 | 1 | 0 | 0.00 | 0.05 |
| WLD | 2 | 1 | 1 | 0.58 | 0.02 |
| WLFI | 8 | 3 | 3 | -10.09 | 0.31 |
| XPL | 4 | 1 | 1 | -13.63 | 0.24 |
| XRP | 4 | 1 | 0 | -2.51 | 0.07 |
| ZEC | 13 | 4 | 4 | 249.09 | 2.18 |
| ZORA | 2 | 1 | 1 | 1.53 | 0.02 |
Create Analysis DataFrame
Now we convert the position events into a pandas DataFrame suitable for analysis. The DataFrame tracks:
Exposure: Notional value (size × price) for each position direction per market
PnL: Cumulative realised profit/loss for each direction per market
Column naming convention: - {coin}_long_exposure / {coin}_long_pnl - {coin}_short_exposure / {coin}_short_pnl
[33]:
from eth_defi.hyperliquid.position_analysis import create_account_dataframe
# Create the analysis DataFrame
df = create_account_dataframe(events)
# Display DataFrame info as a table
info_df = pd.DataFrame({
"Metric": ["Rows", "Columns", "Start Time", "End Time"],
"Value": [df.shape[0], df.shape[1], str(df.index.min()), str(df.index.max())]
})
display(info_df.set_index("Metric"))
# Show the last few rows
display(df.tail())
| Value | |
|---|---|
| Metric | |
| Rows | 129 |
| Columns | 128 |
| Start Time | 2025-12-01 06:07:19.756000 |
| End Time | 2025-12-29 20:07:28.020000 |
| 0G_long_exposure | 0G_long_pnl | 0G_short_exposure | 0G_short_pnl | AAVE_long_exposure | AAVE_long_pnl | AAVE_short_exposure | AAVE_short_pnl | ACE_long_exposure | ACE_long_pnl | ACE_short_exposure | ACE_short_pnl | ADA_long_exposure | ADA_long_pnl | ADA_short_exposure | ADA_short_pnl | APEX_long_exposure | APEX_long_pnl | APEX_short_exposure | APEX_short_pnl | APT_long_exposure | APT_long_pnl | APT_short_exposure | APT_short_pnl | ARB_long_exposure | ARB_long_pnl | ARB_short_exposure | ARB_short_pnl | AVNT_long_exposure | AVNT_long_pnl | AVNT_short_exposure | AVNT_short_pnl | BTC_long_exposure | BTC_long_pnl | BTC_short_exposure | BTC_short_pnl | CC_long_exposure | CC_long_pnl | CC_short_exposure | CC_short_pnl | CRV_long_exposure | CRV_long_pnl | CRV_short_exposure | CRV_short_pnl | DOGE_long_exposure | DOGE_long_pnl | DOGE_short_exposure | DOGE_short_pnl | ENA_long_exposure | ENA_long_pnl | ENA_short_exposure | ENA_short_pnl | HBAR_long_exposure | HBAR_long_pnl | HBAR_short_exposure | HBAR_short_pnl | HYPE_long_exposure | HYPE_long_pnl | HYPE_short_exposure | HYPE_short_pnl | LDO_long_exposure | LDO_long_pnl | LDO_short_exposure | LDO_short_pnl | LTC_long_exposure | LTC_long_pnl | LTC_short_exposure | LTC_short_pnl | MERL_long_exposure | MERL_long_pnl | MERL_short_exposure | MERL_short_pnl | MON_long_exposure | MON_long_pnl | MON_short_exposure | MON_short_pnl | SEI_long_exposure | SEI_long_pnl | SEI_short_exposure | SEI_short_pnl | SUI_long_exposure | SUI_long_pnl | SUI_short_exposure | SUI_short_pnl | TAO_long_exposure | TAO_long_pnl | TAO_short_exposure | TAO_short_pnl | TRX_long_exposure | TRX_long_pnl | TRX_short_exposure | TRX_short_pnl | USTC_long_exposure | USTC_long_pnl | USTC_short_exposure | USTC_short_pnl | VVV_long_exposure | VVV_long_pnl | VVV_short_exposure | VVV_short_pnl | WIF_long_exposure | WIF_long_pnl | WIF_short_exposure | WIF_short_pnl | WLD_long_exposure | WLD_long_pnl | WLD_short_exposure | WLD_short_pnl | WLFI_long_exposure | WLFI_long_pnl | WLFI_short_exposure | WLFI_short_pnl | XPL_long_exposure | XPL_long_pnl | XPL_short_exposure | XPL_short_pnl | XRP_long_exposure | XRP_long_pnl | XRP_short_exposure | XRP_short_pnl | ZEC_long_exposure | ZEC_long_pnl | ZEC_short_exposure | ZEC_short_pnl | ZORA_long_exposure | ZORA_long_pnl | ZORA_short_exposure | ZORA_short_pnl | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| timestamp | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 2025-12-29 13:05:31.428 | 0.00 | 0.76 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 96.61 | 0.00 | -18.65 | 0.00 | -107.11 | 0.00 | 0.00 | 0.00 | -1.87 | 0.00 | 0.00 | 0.00 | -3.02 | 0.00 | 0.00 | 0.00 | -30.65 | 0.00 | 0.00 | 0.00 | -0.98 | 0.00 | 0.00 | 0.00 | -75.53 | 0.00 | -0.40 | 773.20 | 0.00 | 0.00 | -16.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.49 | 0.00 | 0.00 | 0.00 | 1.07 | 26.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 2.63 | 18.81 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | -5.77 | 0.00 | 0.00 | 908.15 | 0.00 | 0.00 | -33.83 | 0.00 | 0.00 | 0.00 | 0.00 | 712.96 | -48.90 | 0.00 | -0.52 | 0.00 | 0.00 | 0.00 | 0.86 | 0.00 | 0.00 | 0.00 | -0.87 | 0.00 | 0.00 | 0.00 | -0.07 | 0.00 | -2.84 | 0.00 | -1.43 | 0.00 | 0.00 | 689.13 | 32.01 | 0.00 | 0.00 | 0.00 | 0.00 | 116.52 | 0.00 | 0.00 | 0.00 | 0.00 | 0.58 | 0.00 | -0.17 | 0.00 | -9.92 | 0.00 | 0.00 | 0.00 | -13.63 | 47.55 | -2.51 | 0.00 | 0.00 | 0.00 | 226.89 | 0.00 | 22.20 | 0.00 | 0.00 | 0.00 | 1.53 |
| 2025-12-29 20:07:28.020 | 0.00 | 0.76 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 96.61 | 0.00 | -18.65 | 0.00 | -107.11 | 0.00 | 0.00 | 0.00 | -1.87 | 0.00 | 0.00 | 0.00 | -3.02 | 0.00 | 0.00 | 0.00 | -30.65 | 0.00 | 0.00 | 0.00 | -0.98 | 0.00 | 0.00 | 0.00 | -75.53 | 0.00 | -0.40 | 773.20 | 0.00 | 0.00 | -16.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.49 | 0.00 | 0.00 | 0.00 | 1.07 | 26.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 2.63 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | -5.77 | 0.00 | 0.00 | 908.15 | 0.00 | 0.00 | -33.83 | 0.00 | 0.00 | 0.00 | 0.00 | 712.96 | -48.90 | 0.00 | -0.52 | 0.00 | 0.00 | 0.00 | 0.86 | 0.00 | 0.00 | 0.00 | -0.87 | 0.00 | 0.00 | 0.00 | -0.07 | 0.00 | -2.84 | 0.00 | -1.43 | 0.00 | 0.00 | 689.13 | 32.01 | 0.00 | 0.00 | 0.00 | 0.00 | 116.52 | 0.00 | 0.00 | 0.00 | 0.00 | 0.58 | 0.00 | -0.17 | 0.00 | -9.92 | 0.00 | 0.00 | 0.00 | -13.63 | 47.55 | -2.51 | 0.00 | 0.00 | 0.00 | 226.89 | 0.00 | 22.20 | 0.00 | 0.00 | 0.00 | 1.53 |
| 2025-12-29 20:07:28.020 | 0.00 | 0.76 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 96.61 | 0.00 | -18.65 | 0.00 | -107.11 | 0.00 | 0.00 | 0.00 | -1.87 | 0.00 | 0.00 | 0.00 | -3.02 | 0.00 | 0.00 | 0.00 | -30.65 | 0.00 | 0.00 | 0.00 | -0.98 | 0.00 | 0.00 | 0.00 | -75.53 | 0.00 | -0.40 | 773.20 | 0.00 | 0.00 | -16.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.49 | 0.00 | 0.00 | 0.00 | 1.07 | 26.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 2.63 | 0.00 | 0.00 | 27.13 | 0.00 | 0.00 | 0.00 | 0.00 | -5.77 | 0.00 | 0.00 | 908.15 | 0.00 | 0.00 | -33.83 | 0.00 | 0.00 | 0.00 | 0.00 | 712.96 | -48.90 | 0.00 | -0.52 | 0.00 | 0.00 | 0.00 | 0.86 | 0.00 | 0.00 | 0.00 | -0.87 | 0.00 | 0.00 | 0.00 | -0.07 | 0.00 | -2.84 | 0.00 | -1.43 | 0.00 | 0.00 | 689.13 | 32.01 | 0.00 | 0.00 | 0.00 | 0.00 | 116.52 | 0.00 | 0.00 | 0.00 | 0.00 | 0.58 | 0.00 | -0.17 | 0.00 | -9.92 | 0.00 | 0.00 | 0.00 | -13.63 | 47.55 | -2.51 | 0.00 | 0.00 | 0.00 | 226.89 | 0.00 | 22.20 | 0.00 | 0.00 | 0.00 | 1.53 |
| 2025-12-29 20:07:28.020 | 0.00 | 0.76 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 96.61 | 0.00 | -18.65 | 0.00 | -107.11 | 0.00 | 0.00 | 0.00 | -1.87 | 0.00 | 0.00 | 0.00 | -3.02 | 0.00 | 0.00 | 0.00 | -30.65 | 0.00 | 0.00 | 0.00 | -0.98 | 0.00 | 0.00 | 0.00 | -75.53 | 0.00 | -0.40 | 773.20 | 0.00 | 0.00 | -16.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.49 | 0.00 | 0.00 | 0.00 | 1.07 | 26.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 2.63 | 0.00 | 0.00 | 73.30 | 0.00 | 0.00 | 0.00 | 0.00 | -5.77 | 0.00 | 0.00 | 908.15 | 0.00 | 0.00 | -33.83 | 0.00 | 0.00 | 0.00 | 0.00 | 712.96 | -48.90 | 0.00 | -0.52 | 0.00 | 0.00 | 0.00 | 0.86 | 0.00 | 0.00 | 0.00 | -0.87 | 0.00 | 0.00 | 0.00 | -0.07 | 0.00 | -2.84 | 0.00 | -1.43 | 0.00 | 0.00 | 689.13 | 32.01 | 0.00 | 0.00 | 0.00 | 0.00 | 116.52 | 0.00 | 0.00 | 0.00 | 0.00 | 0.58 | 0.00 | -0.17 | 0.00 | -9.92 | 0.00 | 0.00 | 0.00 | -13.63 | 47.55 | -2.51 | 0.00 | 0.00 | 0.00 | 226.89 | 0.00 | 22.20 | 0.00 | 0.00 | 0.00 | 1.53 |
| 2025-12-29 20:07:28.020 | 0.00 | 0.76 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 96.61 | 0.00 | -18.65 | 0.00 | -107.11 | 0.00 | 0.00 | 0.00 | -1.87 | 0.00 | 0.00 | 0.00 | -3.02 | 0.00 | 0.00 | 0.00 | -30.65 | 0.00 | 0.00 | 0.00 | -0.98 | 0.00 | 0.00 | 0.00 | -75.53 | 0.00 | -0.40 | 773.20 | 0.00 | 0.00 | -16.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.49 | 0.00 | 0.00 | 0.00 | 1.07 | 26.09 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 0.00 | 2.63 | 0.00 | 0.00 | 535.20 | 0.00 | 0.00 | 0.00 | 0.00 | -5.77 | 0.00 | 0.00 | 908.15 | 0.00 | 0.00 | -33.83 | 0.00 | 0.00 | 0.00 | 0.00 | 712.96 | -48.90 | 0.00 | -0.52 | 0.00 | 0.00 | 0.00 | 0.86 | 0.00 | 0.00 | 0.00 | -0.87 | 0.00 | 0.00 | 0.00 | -0.07 | 0.00 | -2.84 | 0.00 | -1.43 | 0.00 | 0.00 | 689.13 | 32.01 | 0.00 | 0.00 | 0.00 | 0.00 | 116.52 | 0.00 | 0.00 | 0.00 | 0.00 | 0.58 | 0.00 | -0.17 | 0.00 | -9.92 | 0.00 | 0.00 | 0.00 | -13.63 | 47.55 | -2.51 | 0.00 | 0.00 | 0.00 | 226.89 | 0.00 | 22.20 | 0.00 | 0.00 | 0.00 | 1.53 |
Calculate Total Account PnL
The total account PnL at any point in time is the sum of all *_pnl columns. This represents the cumulative realised profit/loss across all markets and directions.
[34]:
# Find all PnL columns
pnl_columns = [col for col in df.columns if col.endswith('_pnl')]
# Calculate total PnL
df['total_pnl'] = df[pnl_columns].sum(axis=1)
# Show PnL statistics as a table
final_pnl = df['total_pnl'].iloc[-1]
max_pnl = df['total_pnl'].max()
min_pnl = df['total_pnl'].min()
stats_df = pd.DataFrame({
"Metric": ["Final PnL", "Max PnL", "Min PnL", "PnL Columns"],
"Value": [f"${final_pnl:,.2f}", f"${max_pnl:,.2f}", f"${min_pnl:,.2f}", len(pnl_columns)]
})
display(stats_df.set_index("Metric"))
| Value | |
|---|---|
| Metric | |
| Final PnL | $10.85 |
| Max PnL | $160.26 |
| Min PnL | $-171.32 |
| PnL Columns | 64 |
Visualise profit and loss
The equity curve shows how the vault’s cumulative realised PnL evolves over time. This is the key metric for evaluating trading performance.
We use Plotly for interactive visualisation (rendered as static image in documentation).
Note: This profit and loss does not account for vault deposits.
[35]:
# Create equity curve chart
fig = go.Figure()
fig.add_trace(go.Scatter(
x=df.index,
y=df['total_pnl'],
mode='lines',
name='Cumulative PnL',
line=dict(color='#2ecc71', width=2),
fill='tozeroy',
fillcolor='rgba(46, 204, 113, 0.2)',
))
# Add zero line
fig.add_hline(y=0, line_dash="dash", line_color="grey", opacity=0.5)
fig.update_layout(
title=f"Hyperliquid Vault Equity Curve<br><sub>Vault: {VAULT_ADDRESS[:10]}...{VAULT_ADDRESS[-8:]}</sub>",
xaxis_title="Date",
yaxis_title="Cumulative Realised PnL (USD)",
template="plotly_white",
hovermode="x unified",
yaxis=dict(tickformat="$,.0f"),
)
fig.show()
Per-Market PnL Breakdown
Let’s visualise the PnL contribution from each market to understand which assets drove performance.
[36]:
# Calculate total PnL per market (combining long and short)
markets = set(col.rsplit('_', 2)[0] for col in pnl_columns)
market_pnl = {}
for market in markets:
long_col = f"{market}_long_pnl"
short_col = f"{market}_short_pnl"
total = 0
if long_col in df.columns:
total += df[long_col].iloc[-1]
if short_col in df.columns:
total += df[short_col].iloc[-1]
market_pnl[market] = total
# Sort by absolute PnL
sorted_markets = sorted(market_pnl.items(), key=lambda x: abs(x[1]), reverse=True)
# Create bar chart
fig = go.Figure()
colours = ['#2ecc71' if pnl >= 0 else '#e74c3c' for _, pnl in sorted_markets]
fig.add_trace(go.Bar(
x=[m[0] for m in sorted_markets],
y=[m[1] for m in sorted_markets],
marker_color=colours,
text=[f"${pnl:,.0f}" for _, pnl in sorted_markets],
textposition='outside',
))
fig.update_layout(
title="Realised PnL by Market",
xaxis_title="Market",
yaxis_title="Realised PnL (USD)",
template="plotly_white",
yaxis=dict(tickformat="$,.0f"),
)
fig.show()
Vault deposits and withdrawals
Hyperliquid vaults track capital flows through deposits and withdrawals. Understanding these flows is crucial for analysing vault performance in context:
Deposits: Capital added by followers/investors
Withdrawals: Capital removed by followers (subject to lockup periods)
Net flow: Deposits minus withdrawals indicates investor sentiment
We use the userNonFundingLedgerUpdates API endpoint to fetch these events.
[37]:
from eth_defi.hyperliquid.deposit import fetch_vault_deposits, create_deposit_dataframe, get_deposit_summary
# Fetch deposit/withdrawal events for the vault
deposit_events = list(fetch_vault_deposits(
session,
VAULT_ADDRESS,
start_time=START_TIME,
end_time=END_TIME,
))
print(f"Fetched {len(deposit_events)} deposit/withdrawal events")
# Create DataFrame for analysis
deposits_df = create_deposit_dataframe(deposit_events)
# Show summary statistics
summary = get_deposit_summary(deposit_events)
summary_table = pd.DataFrame({
"Metric": [
"Total Events",
"Deposits",
"Withdrawals",
"Total Deposited (USD)",
"Total Withdrawn (USD)",
"Net Flow (USD)",
],
"Value": [
summary["total_events"],
summary["deposits"],
summary["withdrawals"],
f"${float(summary['total_deposited']):,.2f}",
f"${float(summary['total_withdrawn']):,.2f}",
f"${float(summary['net_flow']):,.2f}",
]
})
display(summary_table.set_index("Metric"))
# Show individual events
if not deposits_df.empty:
display(deposits_df[["event_type", "usdc"]].head(10))
Fetched 9 deposit/withdrawal events
| Value | |
|---|---|
| Metric | |
| Total Events | 9 |
| Deposits | 9 |
| Withdrawals | 0 |
| Total Deposited (USD) | $4,650.00 |
| Total Withdrawn (USD) | $0.00 |
| Net Flow (USD) | $4,650.00 |
| event_type | usdc | |
|---|---|---|
| timestamp | ||
| 2025-12-12 00:49:31.304 | vault_deposit | 5.00 |
| 2025-12-12 11:22:01.764 | vault_deposit | 5.00 |
| 2025-12-14 05:03:02.056 | vault_deposit | 500.00 |
| 2025-12-15 14:51:27.419 | vault_deposit | 1,000.00 |
| 2025-12-18 16:25:59.419 | vault_deposit | 100.00 |
| 2025-12-18 16:29:23.102 | vault_deposit | 1,000.00 |
| 2025-12-22 15:27:58.562 | vault_deposit | 1,000.00 |
| 2025-12-23 12:33:40.758 | vault_deposit | 40.00 |
| 2025-12-29 20:28:58.122 | vault_deposit | 1,000.00 |
Visualise cumulative capital flows
This chart shows how capital has flowed into and out of the vault over time. A rising line indicates net inflows (more deposits than withdrawals), while a falling line indicates net outflows.
[38]:
if not deposits_df.empty:
# Calculate cumulative flows
deposits_df["cumulative_flow"] = deposits_df["usdc"].cumsum()
# Create the chart
fig = go.Figure()
# Add cumulative flow line
fig.add_trace(go.Scatter(
x=deposits_df.index,
y=deposits_df["cumulative_flow"],
mode="lines+markers",
name="Cumulative Net Flow",
line=dict(color="#3498db", width=2),
fill="tozeroy",
fillcolor="rgba(52, 152, 219, 0.2)",
))
# Add individual events as markers
deposit_mask = deposits_df["event_type"] == "vault_deposit"
withdraw_mask = deposits_df["event_type"] == "vault_withdraw"
if deposit_mask.any():
fig.add_trace(go.Scatter(
x=deposits_df[deposit_mask].index,
y=deposits_df[deposit_mask]["cumulative_flow"],
mode="markers",
name="Deposits",
marker=dict(color="#2ecc71", size=10, symbol="triangle-up"),
text=[f"${v:,.0f}" for v in deposits_df[deposit_mask]["usdc"]],
hovertemplate="%{text}<extra>Deposit</extra>",
))
if withdraw_mask.any():
fig.add_trace(go.Scatter(
x=deposits_df[withdraw_mask].index,
y=deposits_df[withdraw_mask]["cumulative_flow"],
mode="markers",
name="Withdrawals",
marker=dict(color="#e74c3c", size=10, symbol="triangle-down"),
text=[f"${abs(v):,.0f}" for v in deposits_df[withdraw_mask]["usdc"]],
hovertemplate="%{text}<extra>Withdrawal</extra>",
))
fig.add_hline(y=0, line_dash="dash", line_color="grey", opacity=0.5)
fig.update_layout(
title=f"Vault Capital Flows<br><sub>Vault: {VAULT_ADDRESS[:10]}...{VAULT_ADDRESS[-8:]}</sub>",
xaxis_title="Date",
yaxis_title="Cumulative Net Flow (USD)",
template="plotly_white",
hovermode="x unified",
yaxis=dict(tickformat="$,.0f"),
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
)
fig.show()
else:
print("No deposit/withdrawal events in the selected time period")
Daily deposit and withdrawal bar chart
This chart shows the daily breakdown of deposits (green, positive) and withdrawals (red, negative) to identify patterns in investor behaviour.
[39]:
if not deposits_df.empty:
# Aggregate by date
deposits_df["date"] = deposits_df.index.date
# Separate deposits and withdrawals
daily_deposits = deposits_df[deposits_df["usdc"] > 0].groupby("date")["usdc"].sum()
daily_withdrawals = deposits_df[deposits_df["usdc"] < 0].groupby("date")["usdc"].sum()
# Create figure with bars
fig = go.Figure()
if not daily_deposits.empty:
fig.add_trace(go.Bar(
x=daily_deposits.index,
y=daily_deposits.values,
name="Deposits",
marker_color="#2ecc71",
text=[f"${v:,.0f}" for v in daily_deposits.values],
textposition="outside",
))
if not daily_withdrawals.empty:
fig.add_trace(go.Bar(
x=daily_withdrawals.index,
y=daily_withdrawals.values,
name="Withdrawals",
marker_color="#e74c3c",
text=[f"${abs(v):,.0f}" for v in daily_withdrawals.values],
textposition="outside",
))
fig.add_hline(y=0, line_color="grey", opacity=0.5)
fig.update_layout(
title="Daily Deposits and Withdrawals",
xaxis_title="Date",
yaxis_title="Amount (USD)",
template="plotly_white",
barmode="relative",
yaxis=dict(tickformat="$,.0f"),
legend=dict(yanchor="top", y=0.99, xanchor="right", x=0.99),
)
fig.show()
else:
print("No deposit/withdrawal events in the selected time period")
Account value
The previous sections analysed trading PnL and capital flows separately. To understand the true vault performance, we need to combine both into a single account value metric that tracks:
Cumulative PnL: Realised profit/loss from trading activity
Cumulative Net Flow: Total capital deposited minus withdrawn
Account Value: The sum of initial balance, net flows, and trading PnL
This combined view shows how the vault’s total value evolves over time, accounting for both trading performance and capital movements.
We use the analyse_positions_and_deposits() function to merge the position and deposit timelines into a unified DataFrame.
[40]:
from eth_defi.hyperliquid.combined_analysis import analyse_positions_and_deposits, get_combined_summary
# Combine position and deposit data into unified timeline
combined_df = analyse_positions_and_deposits(df, deposits_df)
# Show summary statistics
summary = get_combined_summary(combined_df)
summary_table = pd.DataFrame({
"Metric": [
"Total Events",
"Trading PnL",
"Net Capital Flow",
"Final Account Value",
"Max Account Value",
"Min Account Value",
"Max Drawdown",
],
"Value": [
summary["total_events"],
f"${summary['total_pnl']:,.2f}",
f"${summary['total_netflow']:,.2f}",
f"${summary['final_account_value']:,.2f}",
f"${summary['max_account_value']:,.2f}",
f"${summary['min_account_value']:,.2f}",
f"${summary['max_drawdown']:,.2f}",
]
})
display(summary_table.set_index("Metric"))
# Show the combined DataFrame
display(combined_df.tail())
| Value | |
|---|---|
| Metric | |
| Total Events | 138 |
| Trading PnL | $21.70 |
| Net Capital Flow | $4,650.00 |
| Final Account Value | $4,671.70 |
| Max Account Value | $4,671.70 |
| Min Account Value | $-9.94 |
| Max Drawdown | $-623.16 |
| pnl_update | netflow_update | cumulative_pnl | cumulative_netflow | cumulative_account_value | total_assets | total_supply | share_price | |
|---|---|---|---|---|---|---|---|---|
| timestamp | ||||||||
| 2025-12-29 20:07:28.020 | 0.00 | 0.00 | 21.70 | 3,650.00 | 3,671.70 | 3,671.70 | 3,468.21 | 1.06 |
| 2025-12-29 20:07:28.020 | 0.00 | 0.00 | 21.70 | 3,650.00 | 3,671.70 | 3,671.70 | 3,468.21 | 1.06 |
| 2025-12-29 20:07:28.020 | 0.00 | 0.00 | 21.70 | 3,650.00 | 3,671.70 | 3,671.70 | 3,468.21 | 1.06 |
| 2025-12-29 20:07:28.020 | 0.00 | 0.00 | 21.70 | 3,650.00 | 3,671.70 | 3,671.70 | 3,468.21 | 1.06 |
| 2025-12-29 20:28:58.122 | 0.00 | 1,000.00 | 21.70 | 4,650.00 | 4,671.70 | 4,671.70 | 4,412.79 | 1.06 |
Visualise account value over time
This chart shows the vault’s total account value evolution, combining both trading PnL and capital flows. The account value line represents what the vault would be worth at each point in time.
[41]:
if not combined_df.empty:
fig = go.Figure()
# Add account value line
fig.add_trace(go.Scatter(
x=combined_df.index,
y=combined_df["cumulative_account_value"],
mode="lines",
name="Account Value",
line=dict(color="#9b59b6", width=3),
fill="tozeroy",
fillcolor="rgba(155, 89, 182, 0.2)",
))
# Add cumulative net flow line for reference
fig.add_trace(go.Scatter(
x=combined_df.index,
y=combined_df["cumulative_netflow"],
mode="lines",
name="Cumulative Deposits",
line=dict(color="#3498db", width=2, dash="dot"),
))
# Add cumulative PnL line for reference
fig.add_trace(go.Scatter(
x=combined_df.index,
y=combined_df["cumulative_pnl"],
mode="lines",
name="Cumulative PnL",
line=dict(color="#2ecc71", width=2, dash="dash"),
))
fig.add_hline(y=0, line_dash="dash", line_color="grey", opacity=0.5)
fig.update_layout(
title=f"Vault Account Value Over Time<br><sub>Vault: {VAULT_ADDRESS[:10]}...{VAULT_ADDRESS[-8:]}</sub>",
xaxis_title="Date",
yaxis_title="Value (USD)",
template="plotly_white",
hovermode="x unified",
yaxis=dict(tickformat="$,.0f"),
legend=dict(yanchor="top", y=0.99, xanchor="left", x=0.01),
)
fig.show()
else:
print("No data available for account value visualisation")