Guides
Export Hyperliquid OHLCV Candles to CSV cover

Export Hyperliquid OHLCV Candles to CSV

Generate CSV files for one Hyperliquid market, or every discovered market for one interval, from Dwellir's OHLCV endpoint.

LanguagePython
FormatCSV
ProtocolREST

Prefer a ready-made file?

If you just want the full archive as one file without building a fetcher, use the managed Hyperliquid OHLCV Full-History Exports — download a gzipped CSV for any market and interval directly from the Dwellir dashboard. This guide is for teams that want to control the fetch loop, market selection, and storage layout themselves.

This guide builds a CSV exporter for Dwellir's Hyperliquid OHLCV API. It supports two paths:

  • export one market and one interval, such as BTC 1m
  • discover all currently listed Hyperliquid markets and export one interval for each market into one CSV

The exporter uses the paginated /v1/candles/range endpoint instead of requesting one bucket at a time. Range pages return sparse finalized candles only, so empty buckets are skipped by the API and do not need special handling in the script.

Copy-Paste Prompt for Your Coding Tool

Paste this into Claude Code, Codex, Cursor, Windsurf, or another coding agent:

Text
Build me a Python script that exports Hyperliquid OHLCV candles from Dwellir into CSV.

Requirements:
- First check whether the `dwellir` CLI is installed.
- If it is not installed, recommend installing it with one of these options:
  - `brew tap dwellir-public/homebrew-tap && brew install dwellir`
  - `curl -fsSL https://raw.githubusercontent.com/dwellir-public/cli/main/scripts/install.sh | sh`
- Ask me to authenticate with `dwellir auth login`.
- Check authentication with `dwellir auth status`.
- Get an enabled API key name with `dwellir keys list --toon`.
- Use `dwellir endpoints search hyperliquid --ecosystem hyperliquid --network mainnet --key <name> --json` to discover both:
  - `Hyperliquid Index` for OHLCV range requests
  - `Hyperliquid HyperCore Info` for market discovery
- Support `--market BTC` for one market.
- Support `--all-markets` for all markets discovered from Info endpoint calls:
  - `{"type":"meta"}` for currently listed native perp names
  - `{"type":"spotMeta"}` for spot symbols, using canonical names such as `PURR/USDC` and `@INDEX` names for non-canonical pairs
  - `{"type":"perpDexs"}` for HIP-3 `prefix:ASSET` symbols
- Support `--max-markets` for short smoke tests before a full all-markets run.
- Use `GET /v1/candles/range` with `limit=5000`, follow `nextCursor`, and keep `end` exclusive.
- Support `1s`, `1m`, and `5m`.
- Write CSV columns: s, i, t, T, o, h, l, c, v, q, n, x.
- Show me how to run a one-market export and an all-markets export for one interval.

What You Will Learn

  • discover the Dwellir Hyperliquid Index and Info endpoint URLs with the CLI
  • export a sparse candle range for one market with cursor pagination
  • discover native perp, spot, and HIP-3 market symbols from the Info endpoint
  • iterate all discovered markets for one interval and append them to one CSV
  • verify the exported CSV before loading it into downstream tooling

Prerequisites

Install Python 3.10+ and requests:

Bash
python --version
pip install requests

Install and authenticate the Dwellir CLI:

Bash
brew tap dwellir-public/homebrew-tap
brew install dwellir
dwellir auth login
dwellir auth status
dwellir keys list --toon

Pick an enabled key name from dwellir keys list --toon. The examples below use YOUR_KEY_NAME.

How the Export Works

The OHLCV range endpoint returns pages of candle objects:

Text
GET https://api-hyperliquid-index.n.dwellir.com/YOUR_API_KEY/v1/candles/range

Each request includes:

  • market, for example BTC, @142, or hyna:ETH
  • interval, one of 1s, 1m, or 5m
  • start, the inclusive lower bound
  • end, the exclusive upper bound
  • limit=5000
  • cursor, only after the first page when hasMore is true

For all-market exports, we first discover symbols from Dwellir's HyperCore Info endpoint:

Market typeInfo requestSymbol format
Native perps{"type":"meta"}listed name, for example BTC
Spot{"type":"spotMeta"}canonical name, for example PURR/USDC, otherwise @INDEX, for example @142
HIP-3 perpDexs{"type":"perpDexs"}first element of assetToStreamingOiCap, for example hyna:ETH

Important: the Info endpoint discovers currently listed markets. Historical OHLCV can include markets that are no longer listed; use the dashboard export catalog when you need Dwellir's full indexed catalog including retired symbols.

Python Export Script

Save this as export_ohlcv_csv.py:

Python
import argparse
import csv
import json
import subprocess
import sys
from typing import Any

import requests

FIELDS = ["s", "i", "t", "T", "o", "h", "l", "c", "v", "q", "n", "x"]
VALID_INTERVALS = {"1s", "1m", "5m"}


def load_endpoint_catalog(key_name: str) -> dict[str, Any]:
    try:
        raw = subprocess.check_output(
            [
                "dwellir",
                "endpoints",
                "search",
                "hyperliquid",
                "--ecosystem",
                "hyperliquid",
                "--network",
                "mainnet",
                "--key",
                key_name,
                "--json",
            ],
            text=True,
        )
    except FileNotFoundError as exc:
        raise SystemExit("Install the dwellir CLI first, then run `dwellir auth login`.") from exc
    return json.loads(raw)


def endpoint_url(catalog: dict[str, Any], endpoint_name: str) -> str:
    for item in catalog.get("data", []):
        if item.get("name") == endpoint_name:
            return item["networks"][0]["nodes"][0]["https"]
    raise RuntimeError(f"Could not find endpoint named {endpoint_name!r}")


def post_info(session: requests.Session, info_url: str, payload: dict[str, Any]) -> Any:
    response = session.post(info_url, json=payload, timeout=30)
    response.raise_for_status()
    return response.json()


def discover_markets(session: requests.Session, info_url: str) -> list[str]:
    meta = post_info(session, info_url, {"type": "meta"})
    spot_meta = post_info(session, info_url, {"type": "spotMeta"})
    perp_dexs = post_info(session, info_url, {"type": "perpDexs"})

    perps = [
        asset["name"]
        for asset in meta.get("universe", [])
        if isinstance(asset, dict) and asset.get("name") and not asset.get("isDelisted")
    ]
    spots = [
        pair["name"] if pair.get("isCanonical") else f"@{pair['index']}"
        for pair in spot_meta.get("universe", [])
        if isinstance(pair, dict) and pair.get("name") and "index" in pair
    ]

    hip3 = []
    dex_items = perp_dexs if isinstance(perp_dexs, list) else perp_dexs.get("dexs", [])
    for dex in dex_items:
        if not isinstance(dex, dict):
            continue
        for pair in dex.get("assetToStreamingOiCap", []) or []:
            if isinstance(pair, list) and pair:
                hip3.append(pair[0])

    markets = []
    seen = set()
    for market in perps + spots + hip3:
        if market not in seen:
            seen.add(market)
            markets.append(market)
    return markets


def candle_pages(
    session: requests.Session,
    index_base_url: str,
    market: str,
    interval: str,
    start: str | None,
    end: str | None,
):
    cursor = None
    while True:
        params = {"market": market, "interval": interval, "limit": "5000"}
        if cursor:
            params["cursor"] = cursor
        elif start:
            params["start"] = start
        if end:
            params["end"] = end

        response = session.get(
            f"{index_base_url}/v1/candles/range",
            params=params,
            timeout=60,
        )
        response.raise_for_status()
        page = response.json()
        rows = page.get("data", [])
        if rows:
            yield rows
        if not page.get("hasMore"):
            break
        cursor = page["nextCursor"]


def export_csv(args: argparse.Namespace) -> int:
    if args.interval not in VALID_INTERVALS:
        raise SystemExit(f"Unsupported interval: {args.interval}")
    if bool(args.market) == bool(args.all_markets):
        raise SystemExit("Pass exactly one of --market or --all-markets")

    catalog = load_endpoint_catalog(args.key_name)
    index_base_url = endpoint_url(catalog, "Hyperliquid Index")
    info_url = endpoint_url(catalog, "Hyperliquid HyperCore Info")

    session = requests.Session()
    markets = [args.market] if args.market else discover_markets(session, info_url)
    if args.max_markets is not None:
        markets = markets[: args.max_markets]
    written = 0

    with open(args.output, "w", newline="") as handle:
        writer = csv.DictWriter(handle, fieldnames=FIELDS)
        writer.writeheader()
        for index, market in enumerate(markets, start=1):
            print(f"exporting {market} ({index}/{len(markets)})", file=sys.stderr)
            for rows in candle_pages(session, index_base_url, market, args.interval, args.start, args.end):
                writer.writerows(rows)
                written += len(rows)

    print(f"wrote {written} candles for {len(markets)} market(s) to {args.output}")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser()
    parser.add_argument("--key-name", required=True, help="Dwellir API key name from `dwellir keys list --toon`")
    parser.add_argument("--market", help="One market symbol, for example BTC, @142, or hyna:ETH")
    parser.add_argument("--all-markets", action="store_true", help="Discover and export all currently listed markets")
    parser.add_argument("--max-markets", type=int, help="Limit discovered markets for smoke tests")
    parser.add_argument("--interval", required=True, choices=sorted(VALID_INTERVALS))
    parser.add_argument("--start", help="Inclusive ISO-8601 UTC start, for example 2026-03-01T00:00:00Z")
    parser.add_argument("--end", help="Exclusive ISO-8601 UTC end, for example 2026-04-01T00:00:00Z")
    parser.add_argument("--output", required=True)
    return export_csv(parser.parse_args())


if __name__ == "__main__":
    raise SystemExit(main())

Run a Single-Market Export

Export BTC 1m candles for March 2026:

Bash
python export_ohlcv_csv.py \
  --key-name YOUR_KEY_NAME \
  --market BTC \
  --interval 1m \
  --start 2026-03-01T00:00:00Z \
  --end 2026-04-01T00:00:00Z \
  --output btc-1m-march-2026.csv

Run an All-Markets Export

Start with a short validation window:

Bash
python export_ohlcv_csv.py \
  --key-name YOUR_KEY_NAME \
  --all-markets \
  --interval 1m \
  --max-markets 10 \
  --start 2025-07-27T08:50:00Z \
  --end 2025-07-27T08:51:00Z \
  --output hyperliquid-all-1m-smoke.csv

Then run the full interval export. Omit --end to read through the latest completed bucket:

Bash
python export_ohlcv_csv.py \
  --key-name YOUR_KEY_NAME \
  --all-markets \
  --interval 1m \
  --start 2025-07-27T08:00:00Z \
  --output hyperliquid-all-1m.csv

This can return tens of millions of billable candle objects for 1m, and substantially more for 1s. Use your plan limits and expected billing weight when sizing a full-universe run.

Verify the Result

Inspect the first rows:

Bash
head -5 hyperliquid-all-1m-smoke.csv

Load it with DuckDB:

sql
SELECT s, i, count(*) AS rows, min(t) AS first_open, max(t) AS last_open
FROM read_csv_auto('hyperliquid-all-1m.csv')
GROUP BY s, i
ORDER BY rows DESC
LIMIT 10;

Production Notes

  • Keep start, end, and cursor aligned to the requested interval.
  • Store progress per market if you are running a full-history all-markets export. The compact script above is intentionally simple and writes one pass.
  • Retry transient 429 and 5xx responses with exponential backoff before restarting a large job.
  • The range API bills by returned candle objects, not by empty sparse buckets or by HTTP page count.
  • If you need a typed analytics format instead of CSV, use the companion guide: Export Hyperliquid OHLCV Candles to Parquet.