From 4430b46a4e3cd0dc892d4ffda17ae897c9fa00b9 Mon Sep 17 00:00:00 2001 From: 0xFantomMenace <93462858+0xFantomMenace@users.noreply.github.com> Date: Sun, 27 Sep 2026 06:11:46 +0000 Subject: [PATCH 1/3] Accept 1-minute breadth buckets and list order flow's real buckets The API now serves 1-minute buckets on breadth, open interest, funding, price and liquidation-volume history. Breadth is the one method here that checks interval before sending, so it refused 1m with a ValueError. - BreadthInterval and BREADTH_INTERVALS include 1m. The test that used 1m as its refused example now uses 2h, and the accepted list covers 1m. - Funding and open interest docstrings, and the README interval lists, include 1m. - The order-flow docstring suggested 4h and 1d, which the API refuses. It now lists 1m (the default), 5m, 15m and 1h. --- CHANGELOG.md | 11 +++++++++++ README.md | 12 ++++++------ oxarchive/resources/breadth.py | 4 ++-- oxarchive/resources/funding.py | 2 +- oxarchive/resources/openinterest.py | 2 +- oxarchive/resources/orders.py | 2 +- tests/test_coverage_contract.py | 4 ++-- 7 files changed, 24 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2985b3f..426166c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,17 @@ All notable changes to the `oxarchive` Python SDK are documented here. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Changed +- `client.hyperliquid.hip3.breadth.history()` and `ahistory()` accept + `interval="1m"`. The API now serves 1-minute buckets on breadth, open + interest, funding, price and liquidation-volume history for every venue. + The other methods already pass `interval` through unchanged. +- The order-flow docstring and README list the buckets the API serves: `1m` + (the default), `5m`, `15m` and `1h`. The docstring used to suggest `4h` and + `1d`, which the API refuses. + ## [1.11.0] - 2026-09-25 Versions 1.7.1, 1.8.0, 1.9.0, 1.9.1 and 1.10.0 were not published to PyPI. diff --git a/README.md b/README.md index 2e679ba..fb3a8a7 100644 --- a/README.md +++ b/README.md @@ -414,7 +414,7 @@ print(f"{current.value_pct}% above VWAP ({current.counts.eligible} eligible)") history = client.hyperliquid.hip3.breadth.history( start="2026-08-28T00:00:00Z", end="2026-08-29T00:00:00Z", - interval="5m", # 5m, 15m, 30m, 1h, 4h, 1d + interval="5m", # 1m, 5m, 15m, 30m, 1h, 4h, 1d limit=1000, ) while history.next_cursor: @@ -569,7 +569,7 @@ hip3_current = await client.hyperliquid.hip3.funding.acurrent("km:US500") | `end` | `Timestamp` | Yes | End timestamp | | `cursor` | `Timestamp` | No | Cursor from previous response for pagination | | `limit` | `int` | No | Max results (default: 100, max: 1000) | -| `interval` | `str` | No | Aggregation interval: `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. Omit for raw rows: Hyperliquid core funding is ~1 min; HIP-3 and Lighter funding are ~10s. HIP-4 has no funding. | +| `interval` | `str` | No | Aggregation interval: `'1m'`, `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. Omit for raw rows: Hyperliquid core funding is ~1 min; HIP-3 and Lighter funding are ~10s. HIP-4 has no funding. | ### Open Interest @@ -611,7 +611,7 @@ hip3_current = await client.hyperliquid.hip3.open_interest.acurrent("km:US500") | `end` | `Timestamp` | Yes | End timestamp | | `cursor` | `str` | No | Numeric timestamp string returned as `next_cursor`; pass it back unchanged | | `limit` | `int` | No | Max results (default: 100, max: 1000) | -| `interval` | `str` | No | Aggregation interval: `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. Omit for raw rows: HIP-3, HIP-4 outcome-side OI, and Lighter update at ~10s. | +| `interval` | `str` | No | Aggregation interval: `'1m'`, `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. Omit for raw rows: HIP-3, HIP-4 outcome-side OI, and Lighter update at ~10s. | ### Liquidations @@ -679,7 +679,7 @@ volume = client.hyperliquid.liquidations.volume( "BTC", start="2026-01-01", end="2026-01-08", - interval="1h" # 5m, 15m, 30m, 1h, 4h, 1d + interval="1h" # 1m, 5m, 15m, 30m, 1h, 4h, 1d ) for bucket in volume.data: @@ -764,7 +764,7 @@ prices = client.hyperliquid.get_price_history( "BTC", start="2026-01-01", end="2026-01-02", - interval="1h" # 5m, 15m, 30m, 1h, 4h, 1d + interval="1h" # 1m, 5m, 15m, 30m, 1h, 4h, 1d ) for snapshot in prices.data: @@ -1014,7 +1014,7 @@ flow = client.hyperliquid.orders.flow( "BTC", start="2024-01-01", end="2024-01-02", - interval="1h" + interval="1h" # 1m (default), 5m, 15m, 1h ) # Get TP/SL history diff --git a/oxarchive/resources/breadth.py b/oxarchive/resources/breadth.py index 7e6c903..be97554 100644 --- a/oxarchive/resources/breadth.py +++ b/oxarchive/resources/breadth.py @@ -8,8 +8,8 @@ from ..http import HttpClient from ..types import BreadthSnapshot, CursorResponse, Timestamp -BreadthInterval = Literal["5m", "15m", "30m", "1h", "4h", "1d"] -BREADTH_INTERVALS = frozenset({"5m", "15m", "30m", "1h", "4h", "1d"}) +BreadthInterval = Literal["1m", "5m", "15m", "30m", "1h", "4h", "1d"] +BREADTH_INTERVALS = frozenset({"1m", "5m", "15m", "30m", "1h", "4h", "1d"}) class BreadthResource: diff --git a/oxarchive/resources/funding.py b/oxarchive/resources/funding.py index 0660077..a1b9b76 100644 --- a/oxarchive/resources/funding.py +++ b/oxarchive/resources/funding.py @@ -62,7 +62,7 @@ def history( end: End timestamp (required) cursor: Cursor from previous response's next_cursor (timestamp) limit: Maximum number of results (default: 100, max: 1000) - interval: Aggregation interval (e.g., '5m', '15m', '30m', '1h', '4h', '1d'). + interval: Aggregation interval (e.g., '1m', '5m', '15m', '30m', '1h', '4h', '1d'). Raw cadence is route-specific: Hyperliquid core is roughly one minute; HIP-3 and Lighter are roughly 10 seconds. HIP-4 has no funding. diff --git a/oxarchive/resources/openinterest.py b/oxarchive/resources/openinterest.py index bc53e3e..e798483 100644 --- a/oxarchive/resources/openinterest.py +++ b/oxarchive/resources/openinterest.py @@ -74,7 +74,7 @@ def history( cursor: Numeric timestamp string returned as the previous response's next_cursor; pass it back unchanged limit: Maximum number of results (default: 100, max: 1000) - interval: Aggregation interval (e.g., '5m', '15m', '30m', '1h', '4h', '1d'). + interval: Aggregation interval (e.g., '1m', '5m', '15m', '30m', '1h', '4h', '1d'). Raw cadence is route-specific. HIP-3, HIP-4 outcome-side OI, and Lighter are roughly 10 seconds. diff --git a/oxarchive/resources/orders.py b/oxarchive/resources/orders.py index c68edc4..fe105fc 100644 --- a/oxarchive/resources/orders.py +++ b/oxarchive/resources/orders.py @@ -147,7 +147,7 @@ def flow( symbol: The symbol (e.g., 'BTC', 'ETH') start: Start timestamp (required) end: End timestamp (required) - interval: Aggregation interval (e.g., '1h', '4h', '1d') + interval: Bucket width: '1m' (default), '5m', '15m' or '1h' limit: Maximum number of results Returns: diff --git a/tests/test_coverage_contract.py b/tests/test_coverage_contract.py index 47bae78..8d6ecaf 100644 --- a/tests/test_coverage_contract.py +++ b/tests/test_coverage_contract.py @@ -580,11 +580,11 @@ def test_hip3_breadth_exposes_typed_current_and_history_contract() -> None: with pytest.raises(ValueError, match="1000"): client.breadth.history(limit=1001) with pytest.raises(ValueError, match="interval"): - client.breadth.history(interval=cast(Any, "1m")) + client.breadth.history(interval=cast(Any, "2h")) assert len(http.calls) == 2 -@pytest.mark.parametrize("interval", ["5m", "15m", "30m", "1h", "4h", "1d"]) +@pytest.mark.parametrize("interval", ["1m", "5m", "15m", "30m", "1h", "4h", "1d"]) def test_hip3_breadth_accepts_every_served_history_interval(interval: str) -> None: response = { "data": [_breadth_snapshot()], From deaec46568ee6c90f517d95590c0c865d8b1e893 Mon Sep 17 00:00:00 2001 From: 0xFantomMenace <93462858+0xFantomMenace@users.noreply.github.com> Date: Sun, 27 Sep 2026 08:03:09 +0000 Subject: [PATCH 2/3] Page order flow with a cursor The API now pages order flow on Hyperliquid, HIP-3 and HIP-4: a page holds the oldest `limit` buckets of the window, and `next_cursor` is set while more may follow. `orders.flow()` and `aflow()` had no `cursor` parameter, and a `cursor` keyword fell into `**kwargs` and was dropped, so every call returned the first page. - `flow()` and `aflow()` take `cursor` and send it. Requests without it are unchanged. - The docstring says how buckets are labelled and how to page, and that `limit` counts buckets (default 1000, max 10000). - The README order-flow example pages a day at 1m until `next_cursor` is None. - Tests: the cursor is sent on core, HIP-3 and HIP-4, sync and async, and `next_cursor` comes back. Both fail without the change. --- CHANGELOG.md | 8 +++++ README.md | 20 +++++++++--- oxarchive/resources/orders.py | 21 ++++++++++--- tests/test_coverage_contract.py | 56 ++++++++++++++++++++++++++++++++- 4 files changed, 95 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 426166c..de328df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] +### Added +- `orders.flow()` and `aflow()` take `cursor` (Hyperliquid, HIP-3 and HIP-4). + The API now pages order flow: a page holds the oldest `limit` buckets of + the window, and `next_cursor` is set while more may follow. Pass it back as + `cursor` with the same `start`, `end` and `interval` until it is `None`. + Before this, a `cursor` keyword was accepted and silently dropped, so every + call returned the first page. + ### Changed - `client.hyperliquid.hip3.breadth.history()` and `ahistory()` accept `interval="1m"`. The API now serves 1-minute buckets on breadth, open diff --git a/README.md b/README.md index fb3a8a7..5c90814 100644 --- a/README.md +++ b/README.md @@ -1009,13 +1009,25 @@ result = client.hyperliquid.orders.history( order_type="limit" ) -# Get order flow aggregation +# Get order flow aggregation: one page of time buckets flow = client.hyperliquid.orders.flow( "BTC", - start="2024-01-01", - end="2024-01-02", - interval="1h" # 1m (default), 5m, 15m, 1h + start="2026-07-13T00:00:00Z", + end="2026-07-14T00:00:00Z", + interval="1m", # 1m (default), 5m, 15m, 1h ) +buckets = list(flow.data) +# A page holds up to `limit` buckets (default 1000, max 10000); follow the +# cursor with the same start, end and interval until it is None +while flow.next_cursor: + flow = client.hyperliquid.orders.flow( + "BTC", + start="2026-07-13T00:00:00Z", + end="2026-07-14T00:00:00Z", + interval="1m", + cursor=flow.next_cursor, + ) + buckets.extend(flow.data) # Get TP/SL history tpsl = client.hyperliquid.orders.tpsl( diff --git a/oxarchive/resources/orders.py b/oxarchive/resources/orders.py index fe105fc..06d915a 100644 --- a/oxarchive/resources/orders.py +++ b/oxarchive/resources/orders.py @@ -137,21 +137,29 @@ def flow( start: Timestamp, end: Timestamp, interval: Optional[str] = None, + cursor: Optional[str] = None, limit: Optional[int] = None, **kwargs, ) -> CursorResponse: """ - Get order flow aggregation. + Get order flow aggregation, one page of time buckets. + + Buckets are labelled by their open time in UTC, and buckets with no + events are omitted. A page holds the oldest ``limit`` buckets of the + window. While ``next_cursor`` is set, pass it back as ``cursor`` with + the same ``start``, ``end`` and ``interval``; stop when it is None. Args: symbol: The symbol (e.g., 'BTC', 'ETH') - start: Start timestamp (required) - end: End timestamp (required) + start: Start timestamp, inclusive (required) + end: End timestamp, exclusive (required) interval: Bucket width: '1m' (default), '5m', '15m' or '1h' - limit: Maximum number of results + cursor: The previous response's next_cursor (a numeric + millisecond timestamp string); pass it back unchanged + limit: Buckets per page (default: 1000, max: 10000) Returns: - CursorResponse with order flow data and next_cursor for pagination + CursorResponse with order flow buckets and next_cursor for pagination """ symbol = self._resolve_symbol(symbol, kwargs) data = self._http.get( @@ -160,6 +168,7 @@ def flow( "start": self._convert_timestamp(start), "end": self._convert_timestamp(end), "interval": interval, + "cursor": cursor, "limit": limit, }, ) @@ -175,6 +184,7 @@ async def aflow( start: Timestamp, end: Timestamp, interval: Optional[str] = None, + cursor: Optional[str] = None, limit: Optional[int] = None, **kwargs, ) -> CursorResponse: @@ -186,6 +196,7 @@ async def aflow( "start": self._convert_timestamp(start), "end": self._convert_timestamp(end), "interval": interval, + "cursor": cursor, "limit": limit, }, ) diff --git a/tests/test_coverage_contract.py b/tests/test_coverage_contract.py index 8d6ecaf..694d26d 100644 --- a/tests/test_coverage_contract.py +++ b/tests/test_coverage_contract.py @@ -4,7 +4,13 @@ import pytest -from oxarchive.exchanges import Hip3Client, Hip4Client, LighterClient, SpotClient +from oxarchive.exchanges import ( + Hip3Client, + Hip4Client, + HyperliquidClient, + LighterClient, + SpotClient, +) from oxarchive.http import HttpClient from oxarchive.resources.l3_orderbook import L3OrderBookResource from oxarchive.types import ( @@ -640,3 +646,51 @@ def test_hip3_breadth_supports_async_methods_and_null_is_not_zero() -> None: assert history.data[0].value_pct is None assert http.calls[-1][1] is not None assert http.calls[-1][1]["cursor"] == "1788036840000" + + +def _flow_page(next_cursor: str | None) -> dict[str, Any]: + return { + "data": [{"timestamp": "2026-07-13T16:39:00Z", "limit_orders_placed": 3}], + "meta": {"count": 1, "request_id": "flow", "next_cursor": next_cursor}, + } + + +def test_order_flow_forwards_its_cursor_and_returns_next_cursor() -> None: + http = FakeHttp(_flow_page("1783960740000")) + client = HyperliquidClient(cast(HttpClient, http)) + window = {"start": 1783900800000, "end": 1783987200000, "interval": "1m"} + + first = client.orders.flow("BTC", **window) + assert first.next_cursor == "1783960740000" + assert http.calls[-1][0] == "/v1/hyperliquid/orders/BTC/flow" + first_params = http.calls[-1][1] + assert first_params is not None + assert "cursor" not in first_params or first_params["cursor"] is None + + http.response = _flow_page(None) + last = client.orders.flow("BTC", cursor=first.next_cursor, **window) + assert last.next_cursor is None + params = http.calls[-1][1] + assert params is not None + assert params["cursor"] == "1783960740000" + assert params["start"] == 1783900800000 + assert params["end"] == 1783987200000 + assert params["interval"] == "1m" + + +def test_order_flow_cursor_on_hip3_hip4_and_async() -> None: + http = FakeHttp(_flow_page(None)) + window = {"start": 1783900800000, "end": 1783987200000, "cursor": "1783960740000"} + + Hip3Client(cast(HttpClient, http)).orders.flow("km:US500", **window) + asyncio.run(Hip4Client(cast(HttpClient, http)).orders.aflow("0", **window)) + asyncio.run(HyperliquidClient(cast(HttpClient, http)).orders.aflow("BTC", **window)) + + assert [path for path, _ in http.calls] == [ + "/v1/hyperliquid/hip3/orders/km:US500/flow", + "/v1/hyperliquid/hip4/orders/0/flow", + "/v1/hyperliquid/orders/BTC/flow", + ] + for _, params in http.calls: + assert params is not None + assert params["cursor"] == "1783960740000" From 28f6ae0428139e4c39e1be4c8ce035a2167484aa Mon Sep 17 00:00:00 2001 From: 0xFantomMenace <93462858+0xFantomMenace@users.noreply.github.com> Date: Sun, 27 Sep 2026 09:22:16 +0000 Subject: [PATCH 3/3] Stop promising next_cursor on order flow The API reads a sent order-flow cursor, but it does not return next_cursor on order flow yet. That arrives with an API switch. Until then the docs must not tell callers to page by it. - `flow()` keeps its `cursor` parameter. The docstring describes it as a resume point (a Unix millisecond timestamp) and no longer says how to follow next_cursor; `limit` is the maximum number of buckets. - The README order-flow example is a single request again. - CHANGELOG: order-flow next_cursor arrives with an API switch. --- CHANGELOG.md | 12 ++++++------ README.md | 17 +++-------------- oxarchive/resources/orders.py | 15 +++++++-------- 3 files changed, 16 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de328df..999bca0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,12 @@ this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ## [Unreleased] ### Added -- `orders.flow()` and `aflow()` take `cursor` (Hyperliquid, HIP-3 and HIP-4). - The API now pages order flow: a page holds the oldest `limit` buckets of - the window, and `next_cursor` is set while more may follow. Pass it back as - `cursor` with the same `start`, `end` and `interval` until it is `None`. - Before this, a `cursor` keyword was accepted and silently dropped, so every - call returned the first page. +- `orders.flow()` and `aflow()` take `cursor` (Hyperliquid, HIP-3 and HIP-4) + and send it; the API starts the response at the first bucket that opens + after it. Before this, a `cursor` keyword was accepted and silently dropped. + The API does not return `next_cursor` on order flow yet: it arrives with an + API switch, and until then `next_cursor` on an order-flow response is + `None`. ### Changed - `client.hyperliquid.hip3.breadth.history()` and `ahistory()` accept diff --git a/README.md b/README.md index 5c90814..ab5442b 100644 --- a/README.md +++ b/README.md @@ -1009,25 +1009,14 @@ result = client.hyperliquid.orders.history( order_type="limit" ) -# Get order flow aggregation: one page of time buckets +# Get order flow aggregation: time buckets, oldest first flow = client.hyperliquid.orders.flow( "BTC", start="2026-07-13T00:00:00Z", end="2026-07-14T00:00:00Z", - interval="1m", # 1m (default), 5m, 15m, 1h + interval="15m", # 1m (default), 5m, 15m, 1h + limit=1000, # buckets (default 1000, max 10000) ) -buckets = list(flow.data) -# A page holds up to `limit` buckets (default 1000, max 10000); follow the -# cursor with the same start, end and interval until it is None -while flow.next_cursor: - flow = client.hyperliquid.orders.flow( - "BTC", - start="2026-07-13T00:00:00Z", - end="2026-07-14T00:00:00Z", - interval="1m", - cursor=flow.next_cursor, - ) - buckets.extend(flow.data) # Get TP/SL history tpsl = client.hyperliquid.orders.tpsl( diff --git a/oxarchive/resources/orders.py b/oxarchive/resources/orders.py index 06d915a..671dd2c 100644 --- a/oxarchive/resources/orders.py +++ b/oxarchive/resources/orders.py @@ -142,24 +142,23 @@ def flow( **kwargs, ) -> CursorResponse: """ - Get order flow aggregation, one page of time buckets. + Get order flow aggregation in time buckets. Buckets are labelled by their open time in UTC, and buckets with no - events are omitted. A page holds the oldest ``limit`` buckets of the - window. While ``next_cursor`` is set, pass it back as ``cursor`` with - the same ``start``, ``end`` and ``interval``; stop when it is None. + events are omitted. A response holds the oldest ``limit`` buckets of + the window. Args: symbol: The symbol (e.g., 'BTC', 'ETH') start: Start timestamp, inclusive (required) end: End timestamp, exclusive (required) interval: Bucket width: '1m' (default), '5m', '15m' or '1h' - cursor: The previous response's next_cursor (a numeric - millisecond timestamp string); pass it back unchanged - limit: Buckets per page (default: 1000, max: 10000) + cursor: Optional resume point, a Unix millisecond timestamp; the + response starts at the first bucket that opens after it + limit: Maximum number of buckets (default: 1000, max: 10000) Returns: - CursorResponse with order flow buckets and next_cursor for pagination + CursorResponse with order flow buckets """ symbol = self._resolve_symbol(symbol, kwargs) data = self._http.get(