FlowscopeDocs

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

Text
"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 fnv
OffsetSizeTypeFieldMeaning
04bytesmagicASCII FSPL
42u16version1
62u16—Reserved, 0
88f64bucket_pxPrice bucket size; must be positive and finite
168i64latestNewest sample time the sender had, ms
248i64fromStart of the range answered, ms
328i64toEnd of the range answered, ms
401u8coin_lenLength of the coin name, at most 255
41coin_lenUTF-8coinHyperliquid coin name
41 + coin_len4u32nNumber 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.

OffsetSizeTypeFieldMeaning
08i64startStart, ms since the epoch
84u32durDuration, ms. The run ends at start + dur.
124i32bucketPrices [bucket × bucket_px, (bucket + 1) × bucket_px)
161u8kindSee below; values above 5 are rejected
174f32notionalTotal USD notional of the orders or positions in the bucket
KindNameMeaning
0LongTpTake-profit of a long: a sell above the price
1ShortTpTake-profit of a short: a buy below the price
2LongSlStop of a long: a sell below the price
3ShortSlStop of a short: a buy above the price
4LongLiqLiquidation of a long, below the price
5ShortLiqLiquidation 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

SizeTypeField
4u32FNV-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

Text
"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 fnv

Header

OffsetSizeTypeFieldMeaning
04bytesmagicASCII FSPF
42u16version1
62u16—Reserved, 0
88f64row_pxPrice row size; must be positive and finite
168i64latestNewest sample time the sender had, ms
248i64fromStart of the range answered, ms
321u8coin_lenLength of the coin name
33coin_lenUTF-8coinHyperliquid coin name
33 + coin_len4u32nNumber of minutes

There is no to: the footprint payload covers minutes from from on.

Minute record

40 bytes, followed by its cells.

OffsetSizeTypeFieldMeaning
08i64minuteMinute index, ms / 60 000
84f32oiHyperliquid open interest at the end of the minute, coins; NaN if unknown
124f32markMark price at the end of the minute; NaN if unknown
164f32tracked_longLong size of the tracked accounts, coins; NaN if unknown
204f32tracked_shortShort size of the tracked accounts, coins; NaN if unknown
244f32volumeTraded volume in the minute, coins
284f32taker_knownVolume whose aggressor was tracked
324f32both_knownVolume with both sides tracked
364u32cellsNumber of cell records that follow

Cell record

20 bytes each, directly after their minute record.

OffsetSizeTypeFieldMeaning
04i32rowPrices [row × row_px, (row + 1) × row_px)
44f32d_oiΔ open interest, coins (counted where both sides are tracked)
84f32nlΔ net long by the aggressor, coins
124f32nsΔ net short by the aggressor, coins
164f32volumeTraded 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.

Rust
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:

CheckError
Magic is FSPL or FSPF as expectedNot a position payload
Version is 1Unsupported version
Each count fits in the remaining bytesTruncated
The coin name is valid UTF-8Corrupt: coin
Every run kind is 0–5Corrupt: kind
The trailer matchesChecksum mismatch
bucket_px or row_px is positive and finiteCorrupt: 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

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.