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
21 changes: 21 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,27 @@ All notable changes to `@0xarchive/sdk` are documented in this file.
The format is loosely based on Keep a Changelog and the project follows
semver in spirit.

## Unreleased

### Added

- `OrderFlowParams.cursor` (Hyperliquid, HIP-3 and HIP-4): a resume point
in Unix milliseconds; the API starts the response at the first bucket that
opens after it. `orders.flow()` and `hyperliquid.hip4.getOrderFlow()`
already sent any params they were given, so this adds the type and the
docs. The API does not return `nextCursor` on order flow yet: it arrives
with an API switch, and until then `nextCursor` on an order-flow response
is undefined.

### Changed

- `OiFundingInterval` includes `'1m'`. The API now serves 1-minute buckets
on funding, open interest, price, liquidation-volume and breadth history
for every venue, and every params type that uses `OiFundingInterval`
accepts it.
- `OrderFlowParams.interval` documents the buckets the API serves: `'1m'`
(the default), `'5m'`, `'15m'` and `'1h'`.

## 1.11.0 (2026-09-25)

Versions 1.9.0, 1.9.1 and 1.10.0 were not published to npm, so upgrading
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,7 @@ const hourly = await client.hyperliquid.funding.history('BTC', {
| `end` | `number \| string` | Yes | End timestamp (Unix ms or ISO string) |
| `cursor` | `number \| string` | No | Cursor from previous response for pagination |
| `limit` | `number` | No | Max results (default: 100, max: 1000) |
| `interval` | `OiFundingInterval` | No | Aggregation interval: `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. When omitted, the route's native/default cadence is returned. |
| `interval` | `OiFundingInterval` | No | Aggregation interval: `'1m'`, `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. When omitted, the route's native/default cadence is returned. |

Funding cadence is family-specific: Hyperliquid core is approximately 1 minute,
HIP-3 is approximately 10 seconds, and Lighter is approximately 10 seconds.
Expand Down Expand Up @@ -550,7 +550,7 @@ const hourly = await client.hyperliquid.openInterest.history('BTC', {
| `end` | `number \| string` | Yes | End timestamp (Unix ms or ISO string) |
| `cursor` | `number \| string` | No | Cursor from previous response for pagination |
| `limit` | `number` | No | Max results (default: 100, max: 1000) |
| `interval` | `OiFundingInterval` | No | Aggregation interval: `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. When omitted, the route's native/default cadence is returned. |
| `interval` | `OiFundingInterval` | No | Aggregation interval: `'1m'`, `'5m'`, `'15m'`, `'30m'`, `'1h'`, `'4h'`, `'1d'`. When omitted, the route's native/default cadence is returned. |

Open-interest cadence is family-specific. HIP-4 outcome-side OI is served from
2026-05-02 at ~10s, and Lighter OI updates at approximately
Expand Down Expand Up @@ -604,7 +604,7 @@ Get pre-aggregated liquidation volume in time-bucketed intervals. Returns total,
const volume = await client.hyperliquid.liquidations.volume('BTC', {
start: Date.now() - 86400000 * 7,
end: Date.now(),
interval: '1h' // 5m, 15m, 30m, 1h, 4h, 1d
interval: '1h' // 1m, 5m, 15m, 30m, 1h, 4h, 1d
});

for (const bucket of volume.data) {
Expand Down Expand Up @@ -646,12 +646,12 @@ while (orders.nextCursor) {
allOrders.push(...next.data);
}

// Get order flow (aggregated order activity over time)
// Get order flow (aggregated order activity over time), in time buckets, oldest first
const flow = await client.hyperliquid.orders.flow('BTC', {
start: Date.now() - 86400000,
end: Date.now(),
interval: '1h', // optional aggregation interval
limit: 100
interval: '15m', // optional: 1m (default), 5m, 15m, 1h
limit: 100 // buckets (default 1000, max 10000)
});

// Get TP/SL orders
Expand Down Expand Up @@ -855,7 +855,7 @@ Get mark, oracle, and mid price history over time. Supports aggregation interval
const prices = await client.hyperliquid.priceHistory('BTC', {
start: Date.now() - 86400000,
end: Date.now(),
interval: '1h' // 5m, 15m, 30m, 1h, 4h, 1d
interval: '1h' // 1m, 5m, 15m, 30m, 1h, 4h, 1d
});

for (const snapshot of prices.data) {
Expand Down
17 changes: 14 additions & 3 deletions src/resources/orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ export interface OrderHistoryParams extends CursorPaginationParams {
export interface OrderFlowParams {
start: number | string;
end: number | string;
/** Bucket width: '1m' (default), '5m', '15m' or '1h'. */
interval?: string;
/**
* Optional resume point, a Unix millisecond timestamp. The response
* starts at the first bucket that opens after it.
*/
cursor?: number | string;
/** Maximum number of buckets, oldest first (default 1000, max 10000). */
limit?: number;
}

Expand Down Expand Up @@ -77,11 +84,15 @@ export class OrdersResource {
}

/**
* Get order flow for a symbol
* Get order flow for a symbol, 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.
*
* @param symbol - The symbol (e.g., 'BTC', 'ETH')
* @param params - Time range and interval parameters
* @returns CursorResponse with order flow records
* @param params - Time range, interval, cursor, and limit parameters
* @returns CursorResponse with order flow buckets
*/
async flow(symbol: string, params: OrderFlowParams): Promise<CursorResponse<any[]>> {
const response = await this.http.get<ApiResponse<any[]>>(
Expand Down
4 changes: 2 additions & 2 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,7 @@ export interface FundingRate {
* venue- and family-specific; callers should not infer a universal raw
* one-minute interval from this shared type.
*/
export type OiFundingInterval = '5m' | '15m' | '30m' | '1h' | '4h' | '1d';
export type OiFundingInterval = '1m' | '5m' | '15m' | '30m' | '1h' | '4h' | '1d';

/**
* Parameters for getting funding rate history
Expand Down Expand Up @@ -1009,7 +1009,7 @@ export interface LiquidationVolume {

/** Parameters for getting aggregated liquidation volume */
export interface LiquidationVolumeParams extends CursorPaginationParams {
/** Aggregation interval (default: 1h). Valid: 5m, 15m, 30m, 1h, 4h, 1d */
/** Aggregation interval (default: 1h). Valid: 1m, 5m, 15m, 30m, 1h, 4h, 1d */
interval?: OiFundingInterval;
}

Expand Down
82 changes: 82 additions & 0 deletions tests/coverage-contract.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { readFileSync } from 'node:fs';
import { OxArchive } from '../src';
import type { OiFundingInterval } from '../src';
import type { OrderFlowParams } from '../src/resources/orders';

const readRepoFile = (path: string): string =>
readFileSync(new URL(`../${path}`, import.meta.url), 'utf8');
Expand Down Expand Up @@ -451,3 +453,83 @@ describe('HIP-4 candles and coverage contract', () => {
expect(`${readme}\n${changelog}\n${types}`).not.toMatch(/45 minutes/i);
});
});

describe('aggregation interval contract', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it('sends 1-minute buckets on every OiFundingInterval route', async () => {
const fetchMock = vi.fn().mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
success: true,
data: [],
meta: { count: 0, request_id: 'request-one-minute' },
}),
});
vi.stubGlobal('fetch', fetchMock);
const client = new OxArchive({ apiKey: 'fake-key', baseUrl: 'https://api.example.test' });
const oneMinute: OiFundingInterval = '1m';
const params = { start: 1790380800000, end: 1790384400000, interval: oneMinute };

await client.hyperliquid.funding.history('BTC', params);
await client.hyperliquid.openInterest.history('BTC', params);
await client.hyperliquid.liquidations.volume('BTC', params);
await client.hyperliquid.hip3.breadth.history(params);

const sent = fetchMock.mock.calls.map((call) => new URL(String(call[0])));
expect(sent.map((url) => url.pathname)).toEqual([
'/v1/hyperliquid/funding/BTC',
'/v1/hyperliquid/openinterest/BTC',
'/v1/hyperliquid/liquidations/BTC/volume',
'/v1/hyperliquid/hip3/breadth/above-vwap',
]);
for (const url of sent) expect(url.searchParams.get('interval')).toBe('1m');
});
});

describe('order flow cursor contract', () => {
afterEach(() => {
vi.unstubAllGlobals();
});

it('sends the cursor on core, HIP-3 and HIP-4 order flow and returns nextCursor', async () => {
const page = (nextCursor?: string) => ({
ok: true,
status: 200,
json: async () => ({
success: true,
data: [{ timestamp: '2026-07-13T16:39:00Z', limit_orders_placed: 3 }],
meta: { count: 1, request_id: 'order-flow', ...(nextCursor ? { next_cursor: nextCursor } : {}) },
}),
});
const fetchMock = vi.fn().mockResolvedValueOnce(page('1783960740000')).mockResolvedValue(page());
vi.stubGlobal('fetch', fetchMock);
const client = new OxArchive({ apiKey: 'fake-key', baseUrl: 'https://api.example.test' });
const window: OrderFlowParams = { start: 1783900800000, end: 1783987200000, interval: '1m' };

const first = await client.hyperliquid.orders.flow('BTC', window);
expect(first.nextCursor).toBe('1783960740000');
const last = await client.hyperliquid.orders.flow('BTC', { ...window, cursor: first.nextCursor });
expect(last.nextCursor).toBeUndefined();
await client.hyperliquid.hip3.orders.flow('km:US500', { ...window, cursor: '1783960740000' });
await client.hyperliquid.hip4.getOrderFlow('0', { ...window, cursor: '1783960740000' });

const sent = fetchMock.mock.calls.map((call) => new URL(String(call[0])));
expect(sent.map((url) => url.pathname)).toEqual([
'/v1/hyperliquid/orders/BTC/flow',
'/v1/hyperliquid/orders/BTC/flow',
'/v1/hyperliquid/hip3/orders/km:US500/flow',
'/v1/hyperliquid/hip4/orders/0/flow',
]);
expect(sent[0].searchParams.has('cursor')).toBe(false);
for (const url of sent.slice(1)) {
expect(url.searchParams.get('cursor')).toBe('1783960740000');
expect(url.searchParams.get('start')).toBe('1783900800000');
expect(url.searchParams.get('end')).toBe('1783987200000');
expect(url.searchParams.get('interval')).toBe('1m');
}
});
});
Loading