# Hyperliquid trade history CSV: export user fills

> Export Hyperliquid trades from userFillsByTime to a CSV, preserve fill IDs and UTC times, and interpret closed PnL without treating fills as a tax ledger.

Canonical HTML: [Hyperliquid trade history CSV: export user fills](https://hyperliquidreferralcodes.com/hyperliquid-trade-history-csv)
Markdown URL: [Hyperliquid trade history CSV: export user fills as Markdown](https://hyperliquidreferralcodes.com/markdown/hyperliquid-trade-history-csv.md)
Reviewed: 11 Aug 2026 (2026-08-11).
Published: 2026-08-11.

Bottom line: POST the wallet address to the mainnet Info endpoint with userFillsByTime. Each response holds at most 2,000 fills, and only the 10,000 most recent fills are available. A fills CSV is not a complete tax or accounting ledger.

## Choose a no-code or API export

The official Historical Data page links to the Enigma trade exporter for a no-code download. Hyperliquid states that Enigma is an independently maintained third-party integration. Review the destination domain, privacy terms, requested data, and output before you use it.

Use the Info API and this page’s Python script when you need a repeatable export. The export includes explicit time windows, fields, deduplication and retention checks.

Links:
- [Open the third-party Enigma trade exporter](https://trade-export.hypedexer.com/?v=1): Related resource for this section.

| Method | Use | Main boundary |
| --- | --- | --- |
| Enigma trade exporter | No-code user fill download | Independent third party; verify privacy and output |
| Info API and Python | Repeatable bounded export | 2,000 fills per response and 10,000-fill retention |

## Choose the user fills endpoint

Send a public POST request to https://api.hyperliquid.xyz/info. Put the actual 42-character master-account or subaccount address in the user field. The request does not need an API key or wallet signature. An agent or API-wallet address can return an empty result because it is not the trading account address.

Keep aggregateByTime false for a row-level export. If aggregateByTime is true, the API can combine partial fills from one crossing order. The endpoint-specific limits below apply even when you divide a request into smaller date windows.

| Request type | Time input | Current limit | Use |
| --- | --- | --- | --- |
| userFills | None | At most 2,000 most recent fills | Quick recent snapshot |
| userFillsByTime | Inclusive startTime and endTime in milliseconds | At most 2,000 fills per response; only 10,000 most recent fills available | Bounded CSV export |

Note: The 10,000-fill retention limit is an availability limit. Smaller requests cannot recover fills that have fallen outside the retained set.

## Export Hyperliquid trades with Python

Save the script as export_hyperliquid_fills.py. The script accepts one wallet address, an inclusive UTC start and end time, and a new CSV path. It requests one-day windows and divides any window that reaches the 2,000-row response cap. A one-millisecond window that still reaches the cap stops with an error instead of writing a silently truncated file.

The script preserves the API numeric strings and adds an ISO 8601 UTC column. It checks the current required fill fields and deduplicates unaggregated fills with wallet, time, coin, and tid. It writes through a temporary file in the output directory and refuses to replace an existing file.

Steps:
1. Copy the script into export_hyperliquid_fills.py. The script uses only the Python standard library.
2. Replace the example address with the master-account or subaccount address that owns the fills.
3. Run: python export_hyperliquid_fills.py 0x0000000000000000000000000000000000000000 2026-08-01T00:00:00Z 2026-08-08T23:59:59.999Z hyperliquid-fills.csv
4. Check the first and last time values, the row count, and the requested wallet before you use the file.
5. Import hash, oid, and tid as text in spreadsheet software so long identifiers do not lose digits.

Code:

~~~~text
#!/usr/bin/env python3
import argparse
import csv
import json
import os
import re
import tempfile
import time
from datetime import datetime, timedelta, timezone
from pathlib import Path
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen

INFO_URL = "https://api.hyperliquid.xyz/info"
RESPONSE_CAP = 2_000
WINDOW_MS = 24 * 60 * 60 * 1_000
EPOCH = datetime(1970, 1, 1, tzinfo=timezone.utc)
ADDRESS_RE = re.compile(r"^0x[0-9a-fA-F]{40}$")
REQUIRED_FIELDS = {
    "closedPnl", "coin", "crossed", "dir", "fee", "feeToken", "hash",
    "oid", "px", "side", "startPosition", "sz", "tid", "time",
}
CSV_FIELDS = [
    "wallet", "time", "time_utc", "coin", "side", "dir", "px", "sz",
    "startPosition", "closedPnl", "fee", "feeToken", "builderFee",
    "crossed", "hash", "oid", "tid", "liquidatedUser",
    "liquidationMarkPx", "liquidationMethod",
]


def parse_utc(value):
    text = value[:-1] + "+00:00" if value.endswith("Z") else value
    parsed = datetime.fromisoformat(text)
    if parsed.tzinfo is None:
        raise ValueError("Times must include Z or a UTC offset.")
    delta = parsed.astimezone(timezone.utc) - EPOCH
    return (delta.days * 86_400 + delta.seconds) * 1_000 + delta.microseconds // 1_000


def format_utc(milliseconds):
    return (
        (EPOCH + timedelta(milliseconds=milliseconds))
        .isoformat(timespec="milliseconds")
        .replace("+00:00", "Z")
    )


def fetch_fills(wallet, start_ms, end_ms):
    payload = {
        "type": "userFillsByTime",
        "user": wallet,
        "startTime": start_ms,
        "endTime": end_ms,
        "aggregateByTime": False,
    }
    request = Request(
        INFO_URL,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    for attempt in range(5):
        try:
            with urlopen(request, timeout=30) as response:
                rows = json.load(response)
            if not isinstance(rows, list):
                raise RuntimeError(f"Unexpected API response: {rows!r}")
            for row in rows:
                missing = REQUIRED_FIELDS.difference(row)
                if missing:
                    names = ", ".join(sorted(missing))
                    raise RuntimeError(f"Fill response is missing fields: {names}")
            return rows
        except HTTPError as error:
            if error.code not in {429, 500, 502, 503, 504} or attempt == 4:
                raise
        except URLError:
            if attempt == 4:
                raise
        time.sleep(2**attempt)
    raise RuntimeError("Request retry limit reached.")


def fetch_complete_window(wallet, start_ms, end_ms):
    rows = fetch_fills(wallet, start_ms, end_ms)
    if len(rows) &lt; RESPONSE_CAP:
        return rows
    if start_ms == end_ms:
        raise RuntimeError(
            "The API returned 2,000 fills for one millisecond; the export could be truncated."
        )
    midpoint = (start_ms + end_ms) // 2
    return fetch_complete_window(wallet, start_ms, midpoint) + fetch_complete_window(
        wallet, midpoint + 1, end_ms
    )


def csv_row(wallet, fill):
    liquidation = fill.get("liquidation") or {}
    if not isinstance(liquidation, dict):
        raise RuntimeError("Unexpected liquidation field in fill response.")
    return {
        "wallet": wallet.lower(),
        "time": fill["time"],
        "time_utc": format_utc(int(fill["time"])),
        "coin": fill["coin"],
        "side": fill["side"],
        "dir": fill["dir"],
        "px": fill["px"],
        "sz": fill["sz"],
        "startPosition": fill["startPosition"],
        "closedPnl": fill["closedPnl"],
        "fee": fill["fee"],
        "feeToken": fill["feeToken"],
        "builderFee": fill.get("builderFee", ""),
        "crossed": fill["crossed"],
        "hash": fill["hash"],
        "oid": fill["oid"],
        "tid": fill["tid"],
        "liquidatedUser": liquidation.get("liquidatedUser", ""),
        "liquidationMarkPx": liquidation.get("markPx", ""),
        "liquidationMethod": liquidation.get("method", ""),
    }


def write_new_csv(path, rows):
    path.parent.mkdir(parents=True, exist_ok=True)
    descriptor, temporary_name = tempfile.mkstemp(
        prefix=f".{path.name}.", suffix=".tmp", dir=path.parent
    )
    try:
        with os.fdopen(descriptor, "w", encoding="utf-8", newline="") as handle:
            writer = csv.DictWriter(handle, fieldnames=CSV_FIELDS, quoting=csv.QUOTE_ALL)
            writer.writeheader()
            writer.writerows(rows)
            handle.flush()
            os.fsync(handle.fileno())
        os.link(temporary_name, path)
    finally:
        if os.path.exists(temporary_name):
            os.unlink(temporary_name)


def main():
    parser = argparse.ArgumentParser(description="Export Hyperliquid user fills to CSV.")
    parser.add_argument("wallet", help="Master-account or subaccount address")
    parser.add_argument("start", help="Inclusive ISO 8601 time with timezone")
    parser.add_argument("end", help="Inclusive ISO 8601 time with timezone")
    parser.add_argument("output", type=Path, help="New CSV path")
    args = parser.parse_args()

    if not ADDRESS_RE.fullmatch(args.wallet):
        raise SystemExit("Wallet must be a 42-character hexadecimal address.")
    start_ms = parse_utc(args.start)
    end_ms = parse_utc(args.end)
    if end_ms &lt; start_ms:
        raise SystemExit("End time must be at or after start time.")

    fills = []
    cursor = start_ms
    while cursor &lt;= end_ms:
        window_end = min(cursor + WINDOW_MS - 1, end_ms)
        fills.extend(fetch_complete_window(args.wallet, cursor, window_end))
        cursor = window_end + 1

    unique = {}
    for fill in fills:
        key = (args.wallet.lower(), int(fill["time"]), str(fill["coin"]), int(fill["tid"]))
        previous = unique.get(key)
        if previous is not None and previous != fill:
            raise RuntimeError(f"Conflicting fills share identity {key!r}")
        unique[key] = fill

    ordered = sorted(
        unique.values(), key=lambda fill: (int(fill["time"]), str(fill["coin"]), int(fill["tid"]))
    )
    write_new_csv(args.output, [csv_row(args.wallet, fill) for fill in ordered])
    print(f"Wrote {len(ordered)} fills to {args.output}")


if __name__ == "__main__":
    main()
~~~~

## Read the current user fills fields

The Info endpoint returns numbers such as price, size, PnL, and fee as decimal strings. Preserve the strings in the raw export. Convert a value to Decimal only when you calculate with it.

The coin field uses the HyperCore asset name. A perpetual on the first perp DEX can appear as AVAX. A HIP-3 perpetual can use a DEX prefix such as xyz:XYZ100. Most spot pairs use an @index identifier. Resolve a spot identifier from the current spot metadata before you replace it with a display symbol.

| CSV field | Meaning |
| --- | --- |
| time, time_utc | Unix milliseconds from the API and the same instant converted to UTC |
| coin | HyperCore perpetual or spot market identifier |
| side, dir | B means buy or bid; A means sell or ask. dir is the API display direction, such as Open Long or Sell |
| px, sz | Execution price and base-coin size, preserved as decimal strings |
| startPosition | Position size before the fill |
| closedPnl | Realized closed PnL assigned to this fill; opening fills commonly have zero |
| fee, feeToken | Signed fee amount and its token. A negative fee is a rebate |
| builderFee | Optional builder fee. The fee field already includes this amount |
| crossed | True when the order crossed the spread as taker liquidity |
| hash, oid, tid | L1 transaction hash, order ID, and trade ID |
| liquidation fields | Optional liquidated user, mark price, and market or backstop method |

## Handle subaccounts and duplicate fills

A master-account query does not include each subaccount automatically. Query the actual address of every account that traded. The subAccounts Info request can list subaccount addresses for a master account. Keep the wallet column when you combine the files because subaccounts have separate state.

Do not deduplicate on hash or oid alone. One transaction or order can produce more than one fill. The official WebSocket schema says that the global trade identity combines block time, coin, and tid. For the REST fill schema, use time, coin, and tid, and include the wallet when one dataset contains several accounts.

Note: Keep aggregateByTime false when you use the fill identity in this guide. Aggregation changes what one row represents.

## Use closedPnl as one part of Hyperliquid PnL history

closedPnl is the realized closed PnL attached to a fill. The field does not include unrealized position PnL. Fees are separate fields, and a fee can use a different feeToken. Funding, deposits, withdrawals, internal transfers, subaccount transfers, vault events, rewards, and other ledger updates are also outside the fills response.

The portfolio Info request returns account-value and PnL series for documented periods, but those series are chart histories rather than transaction rows. Reconcile fills with user funding, non-funding ledger updates, position state, and source records that apply to the account and reporting period.

Note: Direct warning: fills alone are not a complete tax or accounting ledger. The CSV does not calculate cost basis, classify taxable events, apply local tax rules, or produce a tax report.

## Plan for caps and retention

userFills returns only the 2,000 most recent fills. userFillsByTime returns at most 2,000 fills in one response and exposes only the 10,000 most recent fills. The Info API also applies a shared IP limit of 1,200 weighted REST units per minute. Each userFills and userFillsByTime request has weight 20 plus additional rate-limit weight for each 20 response items.

Run a scheduled capture before an active account exceeds the 10,000-fill history. Use overlapping UTC windows and the documented fill identity to reconcile repeats. For older research, inspect the official historical-data archives and their missing-data warning. Do not assume that an archive supplies a ledger for one account or includes every retained field.

## Frequently asked questions

### Can I export Hyperliquid trades to CSV?

Yes. POST userFillsByTime to https://api.hyperliquid.xyz/info with the actual trading-account address and an inclusive millisecond range. The Python example writes the returned unaggregated fills to a new CSV file.

### Why does my Hyperliquid trade history stop?

userFills stops at the 2,000 most recent fills. userFillsByTime returns at most 2,000 fills per response and makes only the 10,000 most recent fills available. Smaller date windows prevent per-response truncation, but they do not extend retention.

### Does closedPnl equal my Hyperliquid net PnL?

No. closedPnl is the realized closed PnL assigned to each fill. Fees, funding, unrealized PnL, transfers, rewards, and other ledger events need separate records and reconciliation.

### How do I export Hyperliquid subaccount fills?

Run the same request for the actual subaccount address. A master-account request does not combine its subaccounts. Keep the address in each CSV row when you merge account files.

### Is a Hyperliquid user fills CSV a tax report?

No. A user fills CSV omits funding and non-funding ledger events, does not calculate cost basis, and does not apply the tax rules of a jurisdiction. Use the CSV as one source record and reconcile it with the other records required for the account.

## Official sources

- [Hyperliquid Docs: Info endpoint](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/info-endpoint): Official source used for the product claims on this page.
- [Hyperliquid Docs: Rate limits](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/rate-limits-and-user-limits): Official source used for the product claims on this page.
- [Hyperliquid Docs: WebSocket](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket): Official source used for the product claims on this page.
- [Hyperliquid Docs: Sub-accounts](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/sub-accounts): Official source used for the product claims on this page.
- [Hyperliquid Docs: Historical data](https://hyperliquid.gitbook.io/hyperliquid-docs/historical-data): Official source used for the product claims on this page.

## Related pages

- [Hyperliquid API, WebSocket and Python SDK guide](https://hyperliquidreferralcodes.com/api-websocket-python): Related guide.
- [Hyperliquid historical data: API, S3 archives and exports](https://hyperliquidreferralcodes.com/hyperliquid-historical-data): Related guide.
- [Hyperliquid subaccounts: limits, fees and API wallets](https://hyperliquidreferralcodes.com/hyperliquid-subaccounts): Related guide.
- [Hyperliquid fees: maker, taker, spot and staking tiers](https://hyperliquidreferralcodes.com/hyperliquid-fees): Related guide.
- [Hyperliquid trading-cost calculator](https://hyperliquidreferralcodes.com/calculator): Related tool or index.

## Publication details

- Publisher: Hyperliquid Field Guide.
- Status: Independent and unaffiliated with Hyperliquid Labs and the Hyperliquid Foundation.
- Reviewed: 11 Aug 2026 (2026-08-11).
- Affiliate disclosure: The site operator may earn protocol referral rewards when eligible users trade with code AWD. Hyperliquid controls eligibility, attribution and rewards.
- Contact: research@hyperliquidreferralcodes.com.
- Product terms: Check the linked official documentation and the official Hyperliquid app before you act.
