Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ 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]

### Added
- `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
`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.
Expand Down
19 changes: 10 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -1009,12 +1009,13 @@ result = client.hyperliquid.orders.history(
order_type="limit"
)

# Get order flow aggregation
# Get order flow aggregation: time buckets, oldest first
flow = client.hyperliquid.orders.flow(
"BTC",
start="2024-01-01",
end="2024-01-02",
interval="1h"
start="2026-07-13T00:00:00Z",
end="2026-07-14T00:00:00Z",
interval="15m", # 1m (default), 5m, 15m, 1h
limit=1000, # buckets (default 1000, max 10000)
)

# Get TP/SL history
Expand Down
4 changes: 2 additions & 2 deletions oxarchive/resources/breadth.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion oxarchive/resources/funding.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion oxarchive/resources/openinterest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
22 changes: 16 additions & 6 deletions oxarchive/resources/orders.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,21 +137,28 @@ 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 in time buckets.

Buckets are labelled by their open time in UTC, and buckets with no
events are omitted. A response holds the oldest ``limit`` buckets of
the window.

Args:
symbol: The symbol (e.g., 'BTC', 'ETH')
start: Start timestamp (required)
end: End timestamp (required)
interval: Aggregation interval (e.g., '1h', '4h', '1d')
limit: Maximum number of results
start: Start timestamp, inclusive (required)
end: End timestamp, exclusive (required)
interval: Bucket width: '1m' (default), '5m', '15m' or '1h'
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 data and next_cursor for pagination
CursorResponse with order flow buckets
"""
symbol = self._resolve_symbol(symbol, kwargs)
data = self._http.get(
Expand All @@ -160,6 +167,7 @@ def flow(
"start": self._convert_timestamp(start),
"end": self._convert_timestamp(end),
"interval": interval,
"cursor": cursor,
"limit": limit,
},
)
Expand All @@ -175,6 +183,7 @@ async def aflow(
start: Timestamp,
end: Timestamp,
interval: Optional[str] = None,
cursor: Optional[str] = None,
limit: Optional[int] = None,
**kwargs,
) -> CursorResponse:
Expand All @@ -186,6 +195,7 @@ async def aflow(
"start": self._convert_timestamp(start),
"end": self._convert_timestamp(end),
"interval": interval,
"cursor": cursor,
"limit": limit,
},
)
Expand Down
60 changes: 57 additions & 3 deletions tests/test_coverage_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -580,11 +586,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()],
Expand Down Expand Up @@ -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"
Loading