HL Analytics Methodology
Describes exactly how each quantitative metric exposed under /hl/* is computed, which on-chain primitives it relies on, and where the limits sit. All formulas here match the live server behaviour; nothing is aspirational.
Version: 2026-04-21
Introduction
All HL analytics endpoints are derived from four on-chain data streams we ingest from Hyperliquid:
- Account value snapshots (
snapshots_perp) — mark-to-market equity of every address's perp account, sampled every ~5–10 minutes by the Hyperliquid node. - Accumulated PnL snapshots (
accum_pnls) — realized + unrealized PnL tape keyed on address and time. - Fills / filled orders (
fills,filled_orders) — per-trade execution records, one row per fill. - Ledger updates (
user_ledger_updates) — every balance-changing event at the account level: deposits, withdrawals, internal transfers, vault flows.
Every metric this document describes is a deterministic function of one or more of these four tables. We do not inject proprietary smoothing, scoring, or benchmark overlays. If you need a metric with different semantics (Sharpe, Sortino, benchmark-relative return, etc.), the Computing Custom Metrics section shows how to derive it from our primitives.
Conventions
| Convention | Value |
|---|---|
| Time | All timestamps returned as Unix ms (UTC). Inputs in either Unix ms or RFC 3339 are accepted where documented. |
| Currency | All monetary quantities are USD-equivalent, priced against Hyperliquid's internal mark price at the snapshot instant. No FX conversion. |
| Address format | Lowercase 0x-prefixed 20-byte hex. Requests are normalised server-side. |
| Decimal precision | Computation uses arbitrary-precision decimals (shopspring/decimal). Responses serialise with full precision; clients should not assume a fixed number of decimals. |
| Scope | scope=perp is the default and currently the only supported scope for drawdown and behaviour metrics. Spot balances are excluded. |
| Time window semantics | days=N means [now − N·24h, now]. The window is evaluated server-side at request time. |
Methodology by Metric
Max Drawdown — /hl/max-drawdown, /hl/batch-max-drawdown
Purpose. The largest peak-to-trough decline of the address's perp account value over the requested window, adjusted for net capital flows so that a withdrawal does not register as a loss and a deposit does not reset the peak.
Inputs.
snapshots_perp.account_value— the address's mark-to-market perp equity at every node snapshot.user_ledger_updates— net capital flows into/out of the perp account during the window (deposits, withdrawals, spot↔perp transfers, vault moves).
Algorithm (net-inflow-decontaminated peak-to-trough).
Given the ordered sequence of account-value snapshots s_0, s_1, …, s_n inside the window, we walk forward maintaining a candidate (peak, trough) pair. For every pair we compute the netInflow netIn observed between peak.time and trough.time. The effective peak/trough are then:
if netIn > 0:
effectivePeak = peak.value + netIn
effectiveTrough = trough.value
elif netIn < 0:
effectivePeak = peak.value
effectiveTrough = trough.value + |netIn|
else:
effectivePeak, effectiveTrough = peak.value, trough.value
if effectiveTrough >= effectivePeak or effectivePeak == 0:
drawdown = 0
else:
drawdown = (effectivePeak − effectiveTrough) / effectivePeak
The pair with the largest drawdown over the window wins.
Why the decontamination matters. Without it, a user who deposits 10k after a 5k loss would appear to have "recovered" (because raw equity rose), and a user who withdraws 10k would appear to have taken a 10k drawdown. The adjustment treats net flows as exogenous and measures pure trading PnL.
Parameters.
days∈ {1, 7, 30, 60, 90}. Enforced at the HTTP layer to the range[1, 90].scope = perp(implicit).
Response shape.
{
"high": { "time": 1714000000000, "value": "123456.78" },
"low": { "time": 1714100000000, "value": "100000.00" },
"maxDrawdown": "0.19",
"netIn": "5000.00"
}
netIn is the net capital flow between high.time and low.time (not for the whole window).
Known limit — retention truncation. snapshots_perp is retained for 30 days at the storage layer. A request with days=60 or days=90 is accepted, but the snapshot series fed into the algorithm only goes back 30 days. The returned drawdown is therefore the true drawdown over [now − min(30d, days), now]. See Data Retention and Historical Depth.
Portfolio / Equity Curve — /hl/portfolio/{address}/{window}
Purpose. A time series of mark-to-market account value suitable for plotting an equity curve.
Inputs. accum_pnls.perp_value (alias TotalValue for combined perp+spot where the table is populated). Each row is an account-value snapshot aligned with a Hyperliquid node snapshot.
Windows and sampling.
window | Lookback | Sampling |
|---|---|---|
day | Last 24h | All snapshots (~50–200 points) |
week | Last 7d | All snapshots (~500–1000 points) |
month | Last 30d | Down-sampled to one point per 12h (~60 points) |
allTime | From 2025-12-06 15:00 UTC onward | One point per day |
The 2025-12-06T15:00 anchor is hard-coded as the inception timestamp for the current dataset; requests with window=allTime never return points older than that.
Value definition. Returned value is the mark-to-market equity, which includes both realised PnL accrued to the account and the unrealised PnL on any open positions. We do not decompose the series into realised vs. unrealised, nor into deposits vs. trading PnL. If you need an ROI curve decontaminated of capital flows, combine the equity curve with /hl/ledger-updates/net-flow as described in Computing Custom Metrics.
Response shape.
{
"address": "0x…",
"window": "week",
"points": [ { "time": 1714000000000, "accountValue": "123456.78" } ]
}
Known limit — retention truncation. accum_pnls is retained for 90 days. Although window=allTime nominally starts at 2025-12-06, the earliest returned point will never be older than 90 days ago. This is intentional and we will document it in the API reference.
Net Flow — /hl/ledger-updates/net-flow/{address}, /hl/ledger-updates/batch-net-flow
Purpose. Net USD amount the address has moved into its Hyperliquid perp and spot accounts during the window. Use this to decontaminate returns and distinguish PnL from cash flow.
Inputs. user_ledger_updates, filtered by address and window.
Included event types.
- L1 deposits (on-chain bridge in).
- L1 withdrawals (bridge out, counted negative).
- Internal spot ↔ perp transfers (counted in both
netPerpInandnetSpotInwith opposite signs so totals reconcile). - Vault deposits and withdrawals.
Excluded event types.
spotTransfer(wallet-to-wallet spot moves not crossing Hyperliquid's perp/spot boundary).subAccountTransfer(bookkeeping between a master account and its sub-accounts — these do not change aggregate capital for the parent).
Computation. Sum of signed USD-equivalent amounts per category, returned as:
{
"address": "0x…",
"netPerpIn": "12345.00",
"netSpotIn": "678.90"
}
Parameters.
days∈ {1, 7, 30, 60, 90, 0}.days=0means "all time".
Known limit — retention truncation. user_ledger_updates is retained for 90 days. days=0 and days=90 therefore return the same series in practice. We do not backfill pre-retention events.
Trader Behaviour — /hl/traders/{address}/addr-stat, /hl/traders/batch-behavior-metrics
Purpose. A panel of rolling-window behavioural statistics summarising how an address trades.
Inputs. Pre-aggregated materialised tables positions_dex_1d, positions_dex_7d, positions_dex_30d (one row per address with period-scoped aggregates). For period=0 ("all-time in-retention"), computation falls back to a range scan against fills and filled_orders, bounded to the last 90 days (constant behaviorMetricsMaxLookbackDays = 90).
Fields.
| Field | Definition |
|---|---|
winRate | closedPositionsWithPnl>0 / totalClosedPositions over the window. |
maxDrawdown | Same algorithm as Max Drawdown above, pre-computed on the period aggregate. |
totalPnl | Sum of closed-position realised PnL over the window, USD. |
orderCount | Count of distinct filled orders over the window. |
closedPositionCount | Count of positions that opened and fully closed within the window. |
avgPositionDurationSec | Mean of (close_time − open_time) across closed positions, in seconds. |
profitLossRatio | `mean(profit of winning closes) / mean( |
Parameters.
period∈ {1, 7, 30, 0}. Anything outside this set is rejected.period=0is capped server-side to 90 days of lookback.
Not provided. Sharpe ratio, Sortino ratio, Calmar ratio, profit factor, and benchmark-relative alpha are deliberately out of scope. These depend on a choice of risk-free rate, sampling frequency, and benchmark which we would rather clients make explicitly. Computing Custom Metrics walks through how to assemble them from the primitives we already expose.
Data Retention and Historical Depth
We operate a rolling hot dataset and do not currently maintain a cold archive. The following retention policy is in force as of 2026-04-21:
| Table | Retention | Affected endpoints |
|---|---|---|
fills | 90 days | Behaviour metrics, custom fill queries |
filled_orders | 90 days | Behaviour metrics |
accum_pnls | 90 days | Portfolio equity curve |
user_ledger_updates | 90 days | Net flow |
positions_dex_{1,7,30}d | Rolling, regenerated daily | Behaviour metrics |
snapshots_perp | 30 days | Max drawdown |
snapshots_position | 30 days | Position-state queries |
trades | 30 days | Klines, last-trade lookups |
fundings | 30 days | Funding-rate series |
completed-trades (positions) | 90 days | Closed-position history |
Practical impact. Any request whose nominal window exceeds the underlying table's retention is served against the truncated series. The API does not error — it transparently clamps. This is explicit in the methodology so clients can reconcile their own backfills.
Roadmap. A cold archive (Parquet on S3) is on the roadmap but not yet deployed. Until it lands, data older than the retention windows above is not retrievable through this API.
Known Limitations
-
Drawdown windows beyond 30 days are clamped.
days=60anddays=90on/hl/max-drawdowncurrently return a 30-day drawdown. The response schema does not yet carry aneffectiveWindowDaysfield; this will be added in a future version. Until then, treatdays≥30as "30-day max drawdown". -
Portfolio
allTimewindow is bounded by 90-day retention, not by the hard-coded 2025-12-06 anchor. The anchor defines the earliest date we are willing to return; retention defines the earliest date we actually have. -
Behaviour metrics
period=0is a 90-day approximation. First-fill time, total order count, and scale-in summaries computed forperiod=0reflect the last 90 days only. For addresses that started trading on Hyperliquid before that window, their reported "first fill time" will be the earliest retained fill, not the true inception. -
Net flow excludes pre-retention deposits. An address whose initial deposit occurred more than 90 days before the query will appear with a smaller
netPerpInthan a naive on-chain sum would suggest. -
No sub-account aggregation. Each address is queried independently. If a trader uses multiple addresses, aggregation is the client's responsibility.
-
No slippage, no fee normalisation. PnL figures come directly from Hyperliquid's ledger and already include exchange fees paid. We do not separate gross from net PnL.
-
No real-time guarantees. Snapshots are eventually-consistent. Typical end-to-end latency from Hyperliquid node to API is ~10–30 seconds; back-pressure during high-volume periods may extend this to the low minutes.
Computing Custom Metrics from Our Primitives
The following recipes show how to build common risk-adjusted metrics on the client side. All three use only the endpoints above.
Capital-Flow-Decontaminated ROI
Raw ROI = equity[T]/equity[0] − 1 is contaminated by deposits and withdrawals. Corrected ROI:
equity = /hl/portfolio/{addr}/{window}.points # time, accountValue
flows = /hl/ledger-updates/net-flow/{addr}?days=N # netPerpIn + netSpotIn
ROI_adj = (equity[T] − equity[0] − netTotalIn) / (equity[0] + max(0, netTotalIn))
For a point-in-time series, apply the Modified Dietz or TWR (time-weighted return) formulation using flow events as sub-period boundaries.
Sharpe Ratio
Pick a sampling interval Δt (e.g. daily from the allTime portfolio window). Then:
r_i = equity[t_i] / equity[t_i-1] − 1 # per-period return
r̄ = mean(r_i)
σ = stddev(r_i)
Sharpe_annualised = (r̄ − r_f · Δt) / σ · sqrt(periodsPerYear)
r_f is the risk-free rate you choose; we do not impose one.
Sortino Ratio
Identical to Sharpe but replace σ with downside deviation:
σ_d = sqrt( mean( min(0, r_i − τ)^2 ) ) # τ = target return, typically 0
Sortino = (r̄ − τ) / σ_d · sqrt(periodsPerYear)
Profit Factor
From behaviour metrics, profitLossRatio × (winRate / (1 − winRate)) is algebraically the profit factor sum(wins)/sum(|losses|) under the assumption that win and loss trades have the reported mean sizes. For a more rigorous value, query fills directly and compute:
PF = Σ positive_realised_pnl / |Σ negative_realised_pnl|
For clarifications, discrepancies, or metric requests, please reach out to your account contact.