rwa_hyperliquid.perp_positions_hourly is the per-account position surface for RWA perpetuals. Grain: one row per (trader, coin, block_hour) — every account position open in a Hyperliquid HIP-3 RWA market at the single global funding round that clears every market and account each hour. Market classification is already joined on.
It is the RWA-scoped subset of hyperliquid.perp_positions_hourly. perp_trades tells you what an account did; this table tells you what it held.
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. To detect a close, compare the set of(trader, coin) keys between two hours rather than looking for a terminating row.
This also means positions are not reconstructible from fills: perp_trades publishes both legs of every match and so is per-account complete, but it reports fills, not what an account held at an hour boundary.
RWA history starts 2025-10-13, later than the venue-wide funding feed floor, because RWA HIP-3 markets listed after it.
Single-account reads must filter trader_prefix
The physical table this view reads is partitioned on(block_month, trader_prefix). Trino derives no partition value from trader = 0x… on its own, so a trader-only predicate reads every one of the 256 buckets in range instead of one.
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, lowercase:
lower(substr(to_hex(trader), 1, 2)) gives the same value. to_hex returns uppercase and the partition values are lowercase, so an uppercase prefix matches nothing.
coin does not prune here. Market-grain questions belong in perp_metrics_hourly, which already carries open interest and funding per market-hour.
Rolling up reproduces open interest
Both tables value a position at the same carried-forward trade price, so aggregating to(coin, block_hour) reproduces perp_metrics_hourly.open_interest_usd and open_positions exactly:
perp_metrics_hourly when the market-level figure is all you need; this table is for when you need to decompose it by account.
Because valuation_price is a trade price rather than a mark price, a position’s notional differs from the Hyperliquid UI’s mark-priced value by the trade-to-mark basis.
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.
Classification is joined live
asset_class, asset_type, and underlying_ticker come from rwa_hyperliquid.markets on every read, so reclassifying a market lands immediately with nothing to backfill. RWA scope is that classification itself, and it is default-deny: a market appears only once its underlying is classified as real-world. Crypto, crypto-dominance indices, GPU-compute indices, and any market that has listed but not been classified are all absent — a market missing here is unclassified, not pending. fx is in scope.
Entry price, unrealized PnL, and an account’s leverage and margin mode are not here —
perp_trades carries leverage and margin mode per fill, and market-level max_leverage and margin_mode are in rwa_hyperliquid.markets. Vaults are ordinary accounts here and are not flagged.