Positions codec
The byte layout of Hyperliquid levels and footprint payloads, as served by the API and appended to day files.
Hyperliquid level runs and footprint minutes use one small binary codec, both on the wire (the answers of /v1/hl/levels and /v1/hl/footprint) and on disk (the recorder’s .levels and .fp day files). The reference implementation is flowscope_engine::positions::codec.
Principles
- Little-endian, no padding.
- Self-delimiting: a payload’s length follows from its counts.
- Checksummed: every payload ends with an FNV-1a 32-bit hash of its own bytes.
- Appendable: a day file is payloads written one after another. A reader decodes payloads until the first truncated or damaged one and ignores the rest, so a crash mid-write loses only the last append.
- Versioned: both payload types are at version
1.
Levels payload
"FSPL" u16 version u16 0 f64 bucket_px i64 latest i64 from i64 to
u8 coin_len coin u32 n n × (i64 start u32 dur i32 bucket u8 kind f32 notional) u32 fnvHeader
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | bytes | magic | ASCII FSPL |
| 4 | 2 | u16 | version | 1 |
| 6 | 2 | u16 | — | Reserved, 0 |
| 8 | 8 | f64 | bucket_px | Price bucket size; must be positive and finite |
| 16 | 8 | i64 | latest | Newest sample time the sender had, ms |
| 24 | 8 | i64 | from | Start of the range answered, ms |
| 32 | 8 | i64 | to | End of the range answered, ms |
| 40 | 1 | u8 | coin_len | Length of the coin name, at most 255 |
| 41 | coin_len | UTF-8 | coin | Hyperliquid coin name |
| 41 + coin_len | 4 | u32 | n | Number of runs |
In an API answer, from is the requested start or later when nothing is recorded before it. In a day file, from and to are the day’s bounds.
Run record
21 bytes each.
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 8 | i64 | start | Start, ms since the epoch |
| 8 | 4 | u32 | dur | Duration, ms. The run ends at start + dur. |
| 12 | 4 | i32 | bucket | Prices [bucket × bucket_px, (bucket + 1) × bucket_px) |
| 16 | 1 | u8 | kind | See below; values above 5 are rejected |
| 17 | 4 | f32 | notional | Total USD notional of the orders or positions in the bucket |
| Kind | Name | Meaning |
|---|---|---|
| 0 | LongTp | Take-profit of a long: a sell above the price |
| 1 | ShortTp | Take-profit of a short: a buy below the price |
| 2 | LongSl | Stop of a long: a sell below the price |
| 3 | ShortSl | Stop of a short: a buy above the price |
| 4 | LongLiq | Liquidation of a long, below the price |
| 5 | ShortLiq | Liquidation of a short, above the price |
Kinds 0–1 make up the take-profit map, 2–3 the stop-loss map and 4–5 the liquidation map.
Trailer
| Size | Type | Field |
|---|---|---|
| 4 | u32 | FNV-1a 32 of every byte of this payload before the trailer, from the magic on |
Total size: 45 + coin_len + 21 × n + 4 bytes.
Footprint payload
"FSPF" u16 version u16 0 f64 row_px i64 latest i64 from u8 coin_len coin u32 n
n × (i64 minute f32 oi mark tracked_long tracked_short volume taker_known both_known u32 cells
cells × (i32 row f32 d_oi nl ns volume)) u32 fnvHeader
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | bytes | magic | ASCII FSPF |
| 4 | 2 | u16 | version | 1 |
| 6 | 2 | u16 | — | Reserved, 0 |
| 8 | 8 | f64 | row_px | Price row size; must be positive and finite |
| 16 | 8 | i64 | latest | Newest sample time the sender had, ms |
| 24 | 8 | i64 | from | Start of the range answered, ms |
| 32 | 1 | u8 | coin_len | Length of the coin name |
| 33 | coin_len | UTF-8 | coin | Hyperliquid coin name |
| 33 + coin_len | 4 | u32 | n | Number of minutes |
There is no to: the footprint payload covers minutes from from on.
Minute record
40 bytes, followed by its cells.
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 8 | i64 | minute | Minute index, ms / 60 000 |
| 8 | 4 | f32 | oi | Hyperliquid open interest at the end of the minute, coins; NaN if unknown |
| 12 | 4 | f32 | mark | Mark price at the end of the minute; NaN if unknown |
| 16 | 4 | f32 | tracked_long | Long size of the tracked accounts, coins; NaN if unknown |
| 20 | 4 | f32 | tracked_short | Short size of the tracked accounts, coins; NaN if unknown |
| 24 | 4 | f32 | volume | Traded volume in the minute, coins |
| 28 | 4 | f32 | taker_known | Volume whose aggressor was tracked |
| 32 | 4 | f32 | both_known | Volume with both sides tracked |
| 36 | 4 | u32 | cells | Number of cell records that follow |
Cell record
20 bytes each, directly after their minute record.
| Offset | Size | Type | Field | Meaning |
|---|---|---|---|---|
| 0 | 4 | i32 | row | Prices [row × row_px, (row + 1) × row_px) |
| 4 | 4 | f32 | d_oi | Δ open interest, coins (counted where both sides are tracked) |
| 8 | 4 | f32 | nl | Δ net long by the aggressor, coins |
| 12 | 4 | f32 | ns | Δ net short by the aggressor, coins |
| 16 | 4 | f32 | volume | Traded volume in the row, coins |
Trailer
A u32 FNV-1a 32 of every byte of the payload before it, from the magic on.
Checksum
Both payload types use the same 32-bit FNV-1a as day segments: offset basis 0x811C9DC5, prime 0x01000193, applied byte by byte.
fn fnv1a(bytes: &[u8]) -> u32 {
bytes.iter().fold(0x811c_9dc5u32, |h, b| (h ^ u32::from(*b)).wrapping_mul(0x0100_0193))
}Decoding
A reader checks, per payload:
| Check | Error |
|---|---|
Magic is FSPL or FSPF as expected | Not a position payload |
Version is 1 | Unsupported version |
| Each count fits in the remaining bytes | Truncated |
| The coin name is valid UTF-8 | Corrupt: coin |
| Every run kind is 0–5 | Corrupt: kind |
| The trailer matches | Checksum mismatch |
bucket_px or row_px is positive and finite | Corrupt: bucket / row |
To read a day file, decode payloads from offset 0 until the end of the file or the first error. Everything before the error is valid; anything after is a torn tail.
In Rust
use flowscope_engine::positions::codec::{decode_levels, decode_footprint, decode_levels_file};
// One payload from the API.
let levels = decode_levels(&std::fs::read("btc.levels")?)?;
let footprint = decode_footprint(&std::fs::read("eth.fp")?)?;
// A recorder day file: every intact payload and the bytes they cover.
let (payloads, good) = decode_levels_file(&std::fs::read("flowscope-data/hl/BTC/2026-10-04.levels")?);decode_footprint_file does the same for .fp files.