hyperliquid.perp_positions_hourly is the per-account position surface for Hyperliquid perpetual futures. Grain: one row per (trader, coin, block_hour), taken at the single global funding round that clears every market and account each hour. It covers first-party markets (coin unprefixed, e.g. BTC) and HIP-3 builder-deployed markets (coin = dex:SYMBOL, e.g. xyz:TSLA); spot and HIP-4 outcome markets are out of scope.
Table schema
A snapshot series, not a position history
Rows exist only for positions open at the top of the hour. A close is the absence of a row in the next hour, never a zero-size row, and a position opened and closed inside the same hour never appears at all. Do not read consecutive rows as events: to detect a close, compare the set of(trader, coin) keys between two hours rather than looking for a terminating row.
History starts 2025-09-27, the floor of the funding feed. hyperliquid.perp_trades reaches further back (2025-07-27), so a position held in the trades era has no snapshot before that date.
A second gap comes from pricing, and it can reach further back than the funding lag. valuation_price is joined from perp_market_metrics_hourly with an inner join, and the funding feed runs ahead of that build, so an hour whose price is not built yet withholds its position rows entirely instead of emitting them with a null notional. The next run merges them in.
valuation_price and open interest
valuation_price is open_interest_usd / open_interest_units from perp_market_metrics_hourly — the market’s last close at or before the hour, carried forward. Both tables therefore price positions with one rule, and rolling this table up to (coin, block_hour) reproduces that table’s open interest exactly:
hyperliquid.perp_oracle_prices.
Funding
funding_amount_usd is the payment the venue actually settled on that position in that round, not a rate applied to a notional you compute yourself — use it directly for per-account funding cost. A long pays (negative) when funding_rate is positive and receives when it is negative; a short is the mirror. funding_amount_usd = 0 means the payment rounded to zero on a live position, not a disabled market.
funding_rate repeats on every row of a (coin, block_hour), so average it per market-hour rather than per position. It is the rate as settled, not the forward-looking rate shown beside the Hyperliquid funding countdown.
Partition keys: block_month and trader_prefix
The table is partitioned on(block_month, trader_prefix). block_month bounds the read in time; trader_prefix splits each month into 256 buckets by the first byte of the account address.
The bucket is a readable hex prefix rather than a hash precisely so you can state it yourself: it is the address’s first two hex characters. Where the address is not a literal, lower(substr(to_hex(trader), 1, 2)) gives the same value.
coin does not prune: filtering it here still reads the full month across all 256 buckets. A market-grain question belongs in perp_market_metrics_hourly, which already carries open interest and funding per market-hour.
Entry price, unrealized PnL, and an account’s leverage and margin mode are not here: the venue-wide perp models deliberately do not reconstruct leverage. Market-level
max_leverage, margin_mode and margin tiers are in perp_market_details.