vike

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.

cURL
# 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"
Polars
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.

Example response
{
  "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.

cURL — free sample
# 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.

Polars — per order
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 reopened

On 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.

Polars — flips
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.

Example response
{  "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"}  ]}