Recipe 58 of 65
Classify each Hyperliquid fill as open, add, reduce, close or flip
Every fill in the archive's fills file carries start_position, the wallet's signed position just before the fill; add the signed fill size to get the position after, and the pair of numbers names the action.
GET /v1/hyperliquid/archive/url answers it with one download, because the daily fills file of the Parquet archive
has a start_position column: the wallet's signed position in the coin
immediately before the fill (negative is short). Add the fill's signed size to get the position after it. Flat before
is an open, flat after is a close, a sign change is a flip, and otherwise the position grew (add) or shrank (reduce).
Download one day, or try it first on the free sample. The full parameter list is on
Archive download link.
Quick answer
Download one day, then classify it in Polars.
# download one UTC day of every fill: redirect=1 answers with the link and -L follows it
curl -s -L -H "X-API-Key: $VIKE_API_KEY" -o fills-2026-10-23.parquet \
"https://data.vike.io/v1/hyperliquid/archive/url?dataset=fills&date=2026-10-23&redirect=1"import polars as pl
fills = pl.read_parquet("fills-2026-10-23.parquet") # or the free fills-sample.parquet
side = pl.when(pl.col("side") == "buy").then(1).otherwise(-1)
start, end = pl.col("start_position"), pl.col("end_position")
classified = (
fills.filter(start.is_not_null() & ~pl.col("asset").str.contains("^[@#]")) # perps; NULL before 2026-09-29
.with_columns(end_position=start + side * pl.col("size_token"))
.with_columns(
action=pl.when(start.abs() < 1e-9).then(pl.lit("open"))
.when(end.abs() < 1e-9).then(pl.lit("close"))
.when(start * end < 0).then(pl.lit("flip"))
.when(end.abs() > start.abs()).then(pl.lit("add"))
.otherwise(pl.lit("reduce"))
)
)
print(classified.group_by("action").agg(pl.len().alias("fills")).sort("fills", descending=True))Example response
Computed with the Polars code on the free fills sample, captured from the live API on 2026-10-24 at 17:16 UTC (every BTC fill of 2026-10-21 00:00 to 01:00 UTC, 16,830 fills), shown as JSON: the counts, then the first 5 classified fills.
Positions are in BTC, signed: 0.34092 is a long, -0.00257 a short. The first wallet sells 0.001 BTC out of a 0.34092
BTC long, a reduce; its counterparty buys 0.001 BTC from flat, an open. A cross-check holds on this hour: closed_pnl is
non-zero only on reduce, close and flip fills, never on an open or an add.
{
"counts": [
{"action": "add", "fills": 8887},
{"action": "reduce", "fills": 6097},
{"action": "open", "fills": 843},
{"action": "close", "fills": 711},
{"action": "flip", "fills": 292}
],
"first_fills": [
{"fill_ts": "2026-10-21T00:00:00.503Z", "wallet": "0xa62b923a112d50d03e1e096bbd53422490dac104", "side": "sell", "size_token": 0.001, "start_position": 0.34092, "end_position": 0.33992, "action": "reduce"},
{"fill_ts": "2026-10-21T00:00:00.503Z", "wallet": "0xd49eaf821d8fe887e043ef1510b77f461ae7e1fa", "side": "buy", "size_token": 0.001, "start_position": 0.0, "end_position": 0.001, "action": "open"},
{"fill_ts": "2026-10-21T00:00:02.073Z", "wallet": "0xa62b923a112d50d03e1e096bbd53422490dac104", "side": "sell", "size_token": 0.00018, "start_position": 0.33992, "end_position": 0.33974, "action": "reduce"},
{"fill_ts": "2026-10-21T00:00:02.073Z", "wallet": "0xb8d7bbfa7c135239f7f850f916b0e51ca9e2a700", "side": "buy", "size_token": 0.00018, "start_position": 0.24719, "end_position": 0.24737, "action": "add"},
{"fill_ts": "2026-10-21T00:00:02.165Z", "wallet": "0x4aa5f7febdd1996f00e2910593ae7190ae3f72ec", "side": "buy", "size_token": 0.00257, "start_position": -0.00257, "end_position": 0.0, "action": "close"}
]
}Variations
Try it with no key. The free sample has the same columns as a daily file.
# one BTC hour of fills, no API key
curl -o fills-sample.parquet "https://data.vike.io/archive/hl-samples/fills-sample.parquet"One action per order. An order that fills against several resting orders makes several fills. Group by order to label the order once.
orders = classified.group_by("wallet", "order_id").agg(
pl.col("action").unique().alias("actions"),
pl.col("size_token").sum(),
)
print(orders.filter(pl.col("actions").list.len() > 1)) # orders that, for example, closed and then reopenedOn the sample, 471 of 9,741 orders carried more than one action.
Only the flips. A flip closes one side and opens the other in a single fill; its closed_pnl is the closed part.
print(classified.filter(pl.col("action") == "flip").select("fill_ts", "wallet", "side", "size_token", "start_position", "end_position", "closed_pnl"))The sample's first flip: a 0.09633 BTC sell that turned a 0.01487 BTC long into a 0.08146 BTC short, with
closed_pnl -0.92194 USD.
Before you rely on it
start_position starts on 2026-09-29
The column was added on 2026-09-29. Files before that day hold NULL in every row, so they cannot be classified this way,
and the filter drops them. A 0.0 is real: the wallet was flat before the fill.
Perps only, and compare with a tolerance
The filter keeps perp rows: @ rows are spot pairs and # rows are 0-to-1 contracts, where a "position" is not a perp
position. Sizes are floats, so test "flat" with a small tolerance (1e-9 here), not == 0. One file is one UTC day:
a position opened yesterday shows today as an add or a reduce, which is correct, because start_position carries it.
Ask your agent
No MCP tool reads the archive, and the per-wallet fills route does not return start_position: use the file.