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:

  1. Account value snapshots (snapshots_perp) — mark-to-market equity of every address's perp account, sampled every ~5–10 minutes by the Hyperliquid node.
  2. Accumulated PnL snapshots (accum_pnls) — realized + unrealized PnL tape keyed on address and time.
  3. Fills / filled orders (fills, filled_orders) — per-trade execution records, one row per fill.
  4. 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

ConventionValue
TimeAll timestamps returned as Unix ms (UTC). Inputs in either Unix ms or RFC 3339 are accepted where documented.
CurrencyAll monetary quantities are USD-equivalent, priced against Hyperliquid's internal mark price at the snapshot instant. No FX conversion.
Address formatLowercase 0x-prefixed 20-byte hex. Requests are normalised server-side.
Decimal precisionComputation uses arbitrary-precision decimals (shopspring/decimal). Responses serialise with full precision; clients should not assume a fixed number of decimals.
Scopescope=perp is the default and currently the only supported scope for drawdown and behaviour metrics. Spot balances are excluded.
Time window semanticsdays=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.

windowLookbackSampling
dayLast 24hAll snapshots (~50–200 points)
weekLast 7dAll snapshots (~500–1000 points)
monthLast 30dDown-sampled to one point per 12h (~60 points)
allTimeFrom 2025-12-06 15:00 UTC onwardOne 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 netPerpIn and netSpotIn with 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=0 means "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.

FieldDefinition
winRateclosedPositionsWithPnl>0 / totalClosedPositions over the window.
maxDrawdownSame algorithm as Max Drawdown above, pre-computed on the period aggregate.
totalPnlSum of closed-position realised PnL over the window, USD.
orderCountCount of distinct filled orders over the window.
closedPositionCountCount of positions that opened and fully closed within the window.
avgPositionDurationSecMean 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=0 is 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:

TableRetentionAffected endpoints
fills90 daysBehaviour metrics, custom fill queries
filled_orders90 daysBehaviour metrics
accum_pnls90 daysPortfolio equity curve
user_ledger_updates90 daysNet flow
positions_dex_{1,7,30}dRolling, regenerated dailyBehaviour metrics
snapshots_perp30 daysMax drawdown
snapshots_position30 daysPosition-state queries
trades30 daysKlines, last-trade lookups
fundings30 daysFunding-rate series
completed-trades (positions)90 daysClosed-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

  1. Drawdown windows beyond 30 days are clamped. days=60 and days=90 on /hl/max-drawdown currently return a 30-day drawdown. The response schema does not yet carry an effectiveWindowDays field; this will be added in a future version. Until then, treat days≥30 as "30-day max drawdown".

  2. Portfolio allTime window 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.

  3. Behaviour metrics period=0 is a 90-day approximation. First-fill time, total order count, and scale-in summaries computed for period=0 reflect 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.

  4. Net flow excludes pre-retention deposits. An address whose initial deposit occurred more than 90 days before the query will appear with a smaller netPerpIn than a naive on-chain sum would suggest.

  5. No sub-account aggregation. Each address is queried independently. If a trader uses multiple addresses, aggregation is the client's responsibility.

  6. 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.

  7. 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.

Was this page helpful?