ironflow-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ironflow_sdk/__init__.py +340 -0
- ironflow_sdk/client.py +134 -0
- ironflow_sdk/errors.py +53 -0
- ironflow_sdk/py.typed +0 -0
- ironflow_sdk/stream.py +104 -0
- ironflow_sdk/types.py +247 -0
- ironflow_sdk-0.1.0.dist-info/METADATA +254 -0
- ironflow_sdk-0.1.0.dist-info/RECORD +10 -0
- ironflow_sdk-0.1.0.dist-info/WHEEL +4 -0
- ironflow_sdk-0.1.0.dist-info/licenses/LICENSE +21 -0
ironflow_sdk/__init__.py
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
# Ironflow Python SDK — market data for on-chain derivatives.
|
|
2
|
+
#
|
|
3
|
+
# Usage:
|
|
4
|
+
# from ironflow_sdk import Ironflow
|
|
5
|
+
# client = Ironflow("if_your_api_key")
|
|
6
|
+
# trades = await client.trades("BTC-PERP", limit=10)
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .client import HttpClient, Page
|
|
13
|
+
from .errors import AuthError, IronflowError, QueryWindowError, QueryTimeoutError, QueryTooExpensiveError, RateLimitError, TierError
|
|
14
|
+
from .stream import StreamClient
|
|
15
|
+
from .types import (
|
|
16
|
+
BookSnapshot, BookLevel, Candle, Deposit, FundingRate, Liquidation,
|
|
17
|
+
MarkPrice, OpenInterest, OrderStatus, Trade, Withdrawal,
|
|
18
|
+
VaultOperation, Trigger, CohortInfo, CohortAddressEntry, CohortAddresses,
|
|
19
|
+
NetFlow, LiquidationLevel, OrderFlow, VaultLeaderboardEntry, FundingStats,
|
|
20
|
+
StatusResponse, StreamStatus, VenueStatus,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"Ironflow",
|
|
25
|
+
"IronflowError", "RateLimitError", "AuthError", "TierError",
|
|
26
|
+
"QueryWindowError", "QueryTimeoutError", "QueryTooExpensiveError",
|
|
27
|
+
"Trade", "BookLevel", "BookSnapshot", "Candle", "FundingRate",
|
|
28
|
+
"Liquidation", "OpenInterest", "MarkPrice", "Deposit", "Withdrawal", "OrderStatus",
|
|
29
|
+
"VaultOperation", "Trigger", "CohortInfo", "CohortAddressEntry", "CohortAddresses",
|
|
30
|
+
"NetFlow", "LiquidationLevel", "OrderFlow", "VaultLeaderboardEntry", "FundingStats",
|
|
31
|
+
"StatusResponse", "StreamStatus", "VenueStatus",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _parse_trade(d: dict[str, Any]) -> Trade:
|
|
36
|
+
return Trade(**d)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_book(d: dict[str, Any]) -> BookSnapshot:
|
|
40
|
+
return BookSnapshot(
|
|
41
|
+
source=d["source"],
|
|
42
|
+
market=d["market"],
|
|
43
|
+
timestamp=d["timestamp"],
|
|
44
|
+
bids=[BookLevel(**b) for b in d["bids"]],
|
|
45
|
+
asks=[BookLevel(**a) for a in d["asks"]],
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _parse_candle(d: dict[str, Any]) -> Candle:
|
|
50
|
+
return Candle(**d)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_funding(d: dict[str, Any]) -> FundingRate:
|
|
54
|
+
return FundingRate(**d)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _parse_liquidation(d: dict[str, Any]) -> Liquidation:
|
|
58
|
+
return Liquidation(**d)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_open_interest(d: dict[str, Any]) -> OpenInterest:
|
|
62
|
+
return OpenInterest(**d)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _parse_mark_price(d: dict[str, Any]) -> MarkPrice:
|
|
66
|
+
return MarkPrice(**d)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _parse_deposit(d: dict[str, Any]) -> Deposit:
|
|
70
|
+
return Deposit(**d)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse_withdrawal(d: dict[str, Any]) -> Withdrawal:
|
|
74
|
+
return Withdrawal(**d)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parse_order_status(d: dict[str, Any]) -> OrderStatus:
|
|
78
|
+
return OrderStatus(**d)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _parse_vault_operation(d: dict[str, Any]) -> VaultOperation:
|
|
82
|
+
return VaultOperation(**d)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _parse_trigger(d: dict[str, Any]) -> Trigger:
|
|
86
|
+
return Trigger(**{k: v for k, v in d.items() if k in Trigger.__dataclass_fields__})
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _parse_cohort_info(d: dict[str, Any]) -> CohortInfo:
|
|
90
|
+
return CohortInfo(**d)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_net_flow(d: dict[str, Any]) -> NetFlow:
|
|
94
|
+
return NetFlow(**d)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _parse_liquidation_level(d: dict[str, Any]) -> LiquidationLevel:
|
|
98
|
+
return LiquidationLevel(**d)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _parse_order_flow(d: dict[str, Any]) -> OrderFlow:
|
|
102
|
+
return OrderFlow(**d)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _parse_vault_leaderboard(d: dict[str, Any]) -> VaultLeaderboardEntry:
|
|
106
|
+
return VaultLeaderboardEntry(**d)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parse_funding_stats(d: dict[str, Any]) -> FundingStats:
|
|
110
|
+
return FundingStats(**d)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Ironflow:
|
|
114
|
+
"""Ironflow API client."""
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
api_key: str,
|
|
119
|
+
*,
|
|
120
|
+
base_url: str = "https://api.ironflow.sh",
|
|
121
|
+
source: str = "hyperliquid",
|
|
122
|
+
timeout: float = 30.0,
|
|
123
|
+
) -> None:
|
|
124
|
+
self._http = HttpClient(api_key, base_url=base_url, source=source, timeout=timeout)
|
|
125
|
+
self.stream = StreamClient(self._http.ws_url)
|
|
126
|
+
|
|
127
|
+
# ─── Market Data ────────────────────────────────────────────────────
|
|
128
|
+
|
|
129
|
+
async def trades(self, market: str, **options: Any) -> Page[Trade]:
|
|
130
|
+
"""Get recent trades for a market."""
|
|
131
|
+
return await self._paginated("/v1/trades", {"market": market, **options}, _parse_trade)
|
|
132
|
+
|
|
133
|
+
async def fills(self, address: str, **options: Any) -> Page[Trade]:
|
|
134
|
+
"""Get fills for an address."""
|
|
135
|
+
return await self._paginated("/v1/fills", {"address": address, **options}, _parse_trade)
|
|
136
|
+
|
|
137
|
+
async def book(self, market: str, **options: Any) -> BookSnapshot:
|
|
138
|
+
"""Get the current order book snapshot."""
|
|
139
|
+
raw = await self._http.get("/v1/book", {"market": market, **options})
|
|
140
|
+
return _parse_book(raw)
|
|
141
|
+
|
|
142
|
+
async def candles(self, market: str, interval: str | None = None, **options: Any) -> Page[Candle]:
|
|
143
|
+
"""Get OHLCV candlestick data."""
|
|
144
|
+
params: dict[str, Any] = {"market": market, **options}
|
|
145
|
+
if interval is not None:
|
|
146
|
+
params["interval"] = interval
|
|
147
|
+
return await self._paginated("/v1/candles", params, _parse_candle)
|
|
148
|
+
|
|
149
|
+
async def liquidations(self, market: str | None = None, **options: Any) -> Page[Liquidation]:
|
|
150
|
+
"""Get liquidation events."""
|
|
151
|
+
params: dict[str, Any] = {**options}
|
|
152
|
+
if market is not None:
|
|
153
|
+
params["market"] = market
|
|
154
|
+
return await self._paginated("/v1/liquidations", params, _parse_liquidation)
|
|
155
|
+
|
|
156
|
+
async def funding(self, market: str, **options: Any) -> Page[FundingRate]:
|
|
157
|
+
"""Get funding rate history."""
|
|
158
|
+
return await self._paginated("/v1/funding", {"market": market, **options}, _parse_funding)
|
|
159
|
+
|
|
160
|
+
async def open_interest(self, market: str, **options: Any) -> Page[OpenInterest]:
|
|
161
|
+
"""Get open interest snapshots."""
|
|
162
|
+
return await self._paginated("/v1/open-interest", {"market": market, **options}, _parse_open_interest)
|
|
163
|
+
|
|
164
|
+
async def mark_prices(self, market: str, **options: Any) -> Page[MarkPrice]:
|
|
165
|
+
"""Get mark and oracle prices."""
|
|
166
|
+
return await self._paginated("/v1/mark-prices", {"market": market, **options}, _parse_mark_price)
|
|
167
|
+
|
|
168
|
+
async def deposits(self, **options: Any) -> Page[Deposit]:
|
|
169
|
+
"""Get deposit events."""
|
|
170
|
+
return await self._paginated("/v1/deposits", options, _parse_deposit)
|
|
171
|
+
|
|
172
|
+
async def withdrawals(self, **options: Any) -> Page[Withdrawal]:
|
|
173
|
+
"""Get withdrawal events."""
|
|
174
|
+
return await self._paginated("/v1/withdrawals", options, _parse_withdrawal)
|
|
175
|
+
|
|
176
|
+
async def order_statuses(self, **options: Any) -> Page[OrderStatus]:
|
|
177
|
+
"""Get order status events."""
|
|
178
|
+
return await self._paginated("/v1/order-statuses", options, _parse_order_status)
|
|
179
|
+
|
|
180
|
+
# ─── Vault Operations ────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
async def vault_operations(self, **options: Any) -> Page[VaultOperation]:
|
|
183
|
+
"""Get vault deposit/withdraw events."""
|
|
184
|
+
return await self._paginated("/v1/vault-operations", options, _parse_vault_operation)
|
|
185
|
+
|
|
186
|
+
# ─── Triggers ───────────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
async def create_trigger(
|
|
189
|
+
self, name: str, channel: str, rule: dict[str, Any], webhook_url: str,
|
|
190
|
+
) -> Trigger:
|
|
191
|
+
"""Create a rule-based webhook trigger."""
|
|
192
|
+
raw = await self._http.post("/v1/triggers", {
|
|
193
|
+
"name": name, "channel": channel, "rule": rule, "webhook_url": webhook_url,
|
|
194
|
+
})
|
|
195
|
+
return _parse_trigger(raw)
|
|
196
|
+
|
|
197
|
+
async def list_triggers(self) -> list[Trigger]:
|
|
198
|
+
"""List all triggers for the authenticated user."""
|
|
199
|
+
raw = await self._http.get("/v1/triggers")
|
|
200
|
+
return [_parse_trigger(t) for t in raw["data"]]
|
|
201
|
+
|
|
202
|
+
async def toggle_trigger(self, trigger_id: int, *, is_active: bool) -> Trigger:
|
|
203
|
+
"""Toggle a trigger on or off."""
|
|
204
|
+
raw = await self._http.patch(f"/v1/triggers/{trigger_id}", {"is_active": is_active})
|
|
205
|
+
return _parse_trigger(raw)
|
|
206
|
+
|
|
207
|
+
async def delete_trigger(self, trigger_id: int) -> None:
|
|
208
|
+
"""Delete a trigger."""
|
|
209
|
+
await self._http.delete(f"/v1/triggers/{trigger_id}")
|
|
210
|
+
|
|
211
|
+
async def test_trigger(self, rule: dict[str, Any], event: dict[str, Any]) -> bool:
|
|
212
|
+
"""Test a trigger rule against a sample event. Returns whether it matched."""
|
|
213
|
+
raw = await self._http.post("/v1/triggers/test", {"rule": rule, "event": event})
|
|
214
|
+
return raw["matched"]
|
|
215
|
+
|
|
216
|
+
# ─── Cohorts ────────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
async def list_cohorts(self) -> list[CohortInfo]:
|
|
219
|
+
"""List predefined and custom cohorts."""
|
|
220
|
+
raw = await self._http.get("/v1/cohorts")
|
|
221
|
+
return [_parse_cohort_info(c) for c in raw["data"]]
|
|
222
|
+
|
|
223
|
+
async def create_cohort(self, name: str, addresses: list[str]) -> CohortInfo:
|
|
224
|
+
"""Create a custom address cohort."""
|
|
225
|
+
raw = await self._http.post("/v1/cohorts", {"name": name, "addresses": addresses})
|
|
226
|
+
return _parse_cohort_info(raw)
|
|
227
|
+
|
|
228
|
+
async def cohort_addresses(self, name: str) -> CohortAddresses:
|
|
229
|
+
"""Get addresses in a cohort."""
|
|
230
|
+
raw = await self._http.get(f"/v1/cohorts/{name}")
|
|
231
|
+
return CohortAddresses(
|
|
232
|
+
addresses=[CohortAddressEntry(**a) for a in raw["addresses"]],
|
|
233
|
+
fetched_at=raw["fetched_at"],
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
async def delete_cohort(self, name: str) -> None:
|
|
237
|
+
"""Delete a custom cohort."""
|
|
238
|
+
await self._http.delete(f"/v1/cohorts/{name}")
|
|
239
|
+
|
|
240
|
+
# ─── Analytics (Builder+ only) ─────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
async def net_flows(self, **options: Any) -> list[NetFlow]:
|
|
243
|
+
"""Get net deposit/withdrawal flows over time."""
|
|
244
|
+
raw = await self._http.get("/v1/analytics/net-flows", options)
|
|
245
|
+
return [_parse_net_flow(f) for f in raw["data"]]
|
|
246
|
+
|
|
247
|
+
async def liquidation_levels(self, market: str, **options: Any) -> list[LiquidationLevel]:
|
|
248
|
+
"""Get liquidations bucketed by price level (heatmap data)."""
|
|
249
|
+
raw = await self._http.get("/v1/analytics/liquidation-levels", {"market": market, **options})
|
|
250
|
+
return [_parse_liquidation_level(l) for l in raw["data"]]
|
|
251
|
+
|
|
252
|
+
async def order_flow(self, **options: Any) -> list[OrderFlow]:
|
|
253
|
+
"""Get fill and cancel rates per market."""
|
|
254
|
+
raw = await self._http.get("/v1/analytics/order-flow", options)
|
|
255
|
+
return [_parse_order_flow(f) for f in raw["data"]]
|
|
256
|
+
|
|
257
|
+
async def vault_leaderboard(self, **options: Any) -> list[VaultLeaderboardEntry]:
|
|
258
|
+
"""Get vaults ranked by net deposits."""
|
|
259
|
+
raw = await self._http.get("/v1/analytics/vault-leaderboard", options)
|
|
260
|
+
return [_parse_vault_leaderboard(v) for v in raw["data"]]
|
|
261
|
+
|
|
262
|
+
async def funding_stats(self, market: str, **options: Any) -> list[FundingStats]:
|
|
263
|
+
"""Get funding rate statistics over time."""
|
|
264
|
+
raw = await self._http.get("/v1/analytics/funding-stats", {"market": market, **options})
|
|
265
|
+
return [_parse_funding_stats(f) for f in raw["data"]]
|
|
266
|
+
|
|
267
|
+
# ─── Status ────────────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
async def status(self) -> StatusResponse:
|
|
270
|
+
"""Get system status and data freshness (unauthenticated)."""
|
|
271
|
+
raw = await self._http.get("/v1/status")
|
|
272
|
+
venues: dict[str, VenueStatus] = {}
|
|
273
|
+
for vname, vdata in raw.get("venues", {}).items():
|
|
274
|
+
streams = {
|
|
275
|
+
sname: StreamStatus(**sdata)
|
|
276
|
+
for sname, sdata in vdata.get("streams", {}).items()
|
|
277
|
+
}
|
|
278
|
+
venues[vname] = VenueStatus(streams=streams)
|
|
279
|
+
return StatusResponse(status=raw["status"], venues=venues)
|
|
280
|
+
|
|
281
|
+
# ─── Info API ───────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
async def info(self, request: dict[str, Any]) -> Any:
|
|
284
|
+
"""Proxy request to the Hyperliquid Info API."""
|
|
285
|
+
return await self._http.post("/v1/info", request)
|
|
286
|
+
|
|
287
|
+
# ─── Export ─────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
async def export_data(
|
|
290
|
+
self,
|
|
291
|
+
event_type: str,
|
|
292
|
+
*,
|
|
293
|
+
market: str | None = None,
|
|
294
|
+
from_ts: int | None = None,
|
|
295
|
+
to_ts: int | None = None,
|
|
296
|
+
format: str = "csv",
|
|
297
|
+
) -> bytes:
|
|
298
|
+
"""Export data as CSV or Parquet (Builder+ only). Returns raw bytes."""
|
|
299
|
+
params: dict[str, Any] = {"event_type": event_type, "format": format}
|
|
300
|
+
if market is not None:
|
|
301
|
+
params["market"] = market
|
|
302
|
+
if from_ts is not None:
|
|
303
|
+
params["from"] = from_ts
|
|
304
|
+
if to_ts is not None:
|
|
305
|
+
params["to"] = to_ts
|
|
306
|
+
return await self._http.get_raw("/v1/export", params)
|
|
307
|
+
|
|
308
|
+
# ─── Lifecycle ──────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
async def close(self) -> None:
|
|
311
|
+
"""Close the underlying HTTP client."""
|
|
312
|
+
await self._http.aclose()
|
|
313
|
+
|
|
314
|
+
async def __aenter__(self) -> "Ironflow":
|
|
315
|
+
return self
|
|
316
|
+
|
|
317
|
+
async def __aexit__(self, *_: Any) -> None:
|
|
318
|
+
await self.close()
|
|
319
|
+
|
|
320
|
+
# ─── Helpers ────────────────────────────────────────────────────────
|
|
321
|
+
|
|
322
|
+
async def _paginated(
|
|
323
|
+
self,
|
|
324
|
+
path: str,
|
|
325
|
+
params: dict[str, Any],
|
|
326
|
+
parser: Any,
|
|
327
|
+
) -> Page[Any]:
|
|
328
|
+
raw = await self._http.get(path, params)
|
|
329
|
+
data = [parser(item) for item in raw["data"]]
|
|
330
|
+
pagination = raw["pagination"]
|
|
331
|
+
|
|
332
|
+
async def fetch_next(cursor: str) -> Page[Any]:
|
|
333
|
+
return await self._paginated(path, {**params, "cursor": cursor}, parser)
|
|
334
|
+
|
|
335
|
+
return Page(
|
|
336
|
+
data=data,
|
|
337
|
+
has_more=pagination["has_more"],
|
|
338
|
+
next_cursor=pagination.get("next_cursor"),
|
|
339
|
+
next_fn=fetch_next,
|
|
340
|
+
)
|
ironflow_sdk/client.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# HTTP client for the Ironflow REST API. Uses httpx.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable, Generic, Never, TypeVar
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
|
|
9
|
+
from .errors import AuthError, IronflowError, QueryWindowError, QueryTimeoutError, QueryTooExpensiveError, RateLimitError, TierError
|
|
10
|
+
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.ironflow.sh"
|
|
12
|
+
DEFAULT_SOURCE = "hyperliquid"
|
|
13
|
+
SDK_VERSION = "0.1.0"
|
|
14
|
+
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class HttpClient:
|
|
19
|
+
"""Thin httpx wrapper that handles auth, error mapping, and source injection."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
api_key: str,
|
|
24
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
25
|
+
source: str = DEFAULT_SOURCE,
|
|
26
|
+
timeout: float = 30.0,
|
|
27
|
+
) -> None:
|
|
28
|
+
self._api_key = api_key
|
|
29
|
+
self.base_url = base_url.rstrip("/")
|
|
30
|
+
self.source = source
|
|
31
|
+
self._client = httpx.AsyncClient(
|
|
32
|
+
base_url=self.base_url,
|
|
33
|
+
headers={
|
|
34
|
+
"Authorization": f"Bearer {api_key}",
|
|
35
|
+
"User-Agent": f"ironflow-sdk/{SDK_VERSION}",
|
|
36
|
+
},
|
|
37
|
+
timeout=timeout,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def ws_url(self) -> str:
|
|
42
|
+
url = self.base_url.replace("https://", "wss://").replace("http://", "ws://")
|
|
43
|
+
return f"{url}/ws?api_key={self._api_key}"
|
|
44
|
+
|
|
45
|
+
async def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
46
|
+
"""Send a GET request; inject source if absent."""
|
|
47
|
+
p = dict(params or {})
|
|
48
|
+
p.setdefault("source", self.source)
|
|
49
|
+
# Remove None values.
|
|
50
|
+
p = {k: v for k, v in p.items() if v is not None}
|
|
51
|
+
res = await self._client.get(path, params=p)
|
|
52
|
+
return self._handle(res)
|
|
53
|
+
|
|
54
|
+
async def post(self, path: str, body: Any) -> Any:
|
|
55
|
+
"""Send a POST request with a JSON body."""
|
|
56
|
+
res = await self._client.post(path, json=body)
|
|
57
|
+
return self._handle(res)
|
|
58
|
+
|
|
59
|
+
async def patch(self, path: str, body: Any) -> Any:
|
|
60
|
+
"""Send a PATCH request with a JSON body."""
|
|
61
|
+
res = await self._client.patch(path, json=body)
|
|
62
|
+
return self._handle(res)
|
|
63
|
+
|
|
64
|
+
async def delete(self, path: str) -> None:
|
|
65
|
+
"""Send a DELETE request."""
|
|
66
|
+
res = await self._client.request("DELETE", path)
|
|
67
|
+
if not res.is_success:
|
|
68
|
+
self._raise_from(res)
|
|
69
|
+
|
|
70
|
+
async def aclose(self) -> None:
|
|
71
|
+
await self._client.aclose()
|
|
72
|
+
|
|
73
|
+
async def get_raw(self, path: str, params: dict[str, Any] | None = None) -> bytes:
|
|
74
|
+
"""Send a GET request and return raw bytes (for binary exports)."""
|
|
75
|
+
p = dict(params or {})
|
|
76
|
+
p.setdefault("source", self.source)
|
|
77
|
+
p = {k: v for k, v in p.items() if v is not None}
|
|
78
|
+
res = await self._client.get(path, params=p)
|
|
79
|
+
if not res.is_success:
|
|
80
|
+
self._raise_from(res)
|
|
81
|
+
return res.content
|
|
82
|
+
|
|
83
|
+
def _handle(self, res: httpx.Response) -> Any:
|
|
84
|
+
if res.is_success:
|
|
85
|
+
return res.json()
|
|
86
|
+
self._raise_from(res)
|
|
87
|
+
|
|
88
|
+
def _raise_from(self, res: httpx.Response) -> "Never":
|
|
89
|
+
"""Parse an error response and raise the appropriate typed exception."""
|
|
90
|
+
try:
|
|
91
|
+
err = res.json()
|
|
92
|
+
code = err.get("error", {}).get("code", "UNKNOWN")
|
|
93
|
+
message = err.get("error", {}).get("message", res.text)
|
|
94
|
+
except Exception:
|
|
95
|
+
code = "UNKNOWN"
|
|
96
|
+
message = res.text
|
|
97
|
+
|
|
98
|
+
if res.status_code == 400:
|
|
99
|
+
if code == "QUERY_WINDOW_EXCEEDED":
|
|
100
|
+
raise QueryWindowError(message)
|
|
101
|
+
if code == "QUERY_TOO_EXPENSIVE":
|
|
102
|
+
raise QueryTooExpensiveError(message)
|
|
103
|
+
if res.status_code == 401:
|
|
104
|
+
raise AuthError(message)
|
|
105
|
+
if res.status_code == 403:
|
|
106
|
+
raise TierError(message, code)
|
|
107
|
+
if res.status_code == 429:
|
|
108
|
+
retry_after = int(res.headers.get("Retry-After", "60"))
|
|
109
|
+
raise RateLimitError(message, retry_after * 1000)
|
|
110
|
+
if res.status_code == 504:
|
|
111
|
+
raise QueryTimeoutError(message)
|
|
112
|
+
raise IronflowError(message, code, res.status_code)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class Page(Generic[T]):
|
|
116
|
+
"""Paginated result with async next() support."""
|
|
117
|
+
|
|
118
|
+
def __init__(
|
|
119
|
+
self,
|
|
120
|
+
data: list[T],
|
|
121
|
+
has_more: bool,
|
|
122
|
+
next_cursor: str | None,
|
|
123
|
+
next_fn: Callable[[str], "Page[T]"] | None,
|
|
124
|
+
) -> None:
|
|
125
|
+
self.data = data
|
|
126
|
+
self.has_more = has_more
|
|
127
|
+
self._next_cursor = next_cursor
|
|
128
|
+
self._next_fn = next_fn if has_more and next_cursor else None
|
|
129
|
+
|
|
130
|
+
async def next(self) -> "Page[T]":
|
|
131
|
+
"""Fetch the next page. Raises if there are no more pages."""
|
|
132
|
+
if not self._next_fn or not self._next_cursor:
|
|
133
|
+
raise StopAsyncIteration("No more pages")
|
|
134
|
+
return await self._next_fn(self._next_cursor)
|
ironflow_sdk/errors.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Typed exceptions for the Ironflow API.
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class IronflowError(Exception):
|
|
5
|
+
"""Base class for all Ironflow API errors."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, message: str, code: str, status: int) -> None:
|
|
8
|
+
super().__init__(message)
|
|
9
|
+
self.code = code
|
|
10
|
+
self.status = status
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class RateLimitError(IronflowError):
|
|
14
|
+
"""Raised when the API returns 429 Too Many Requests."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, message: str, retry_after_ms: int) -> None:
|
|
17
|
+
super().__init__(message, "RATE_LIMIT_EXCEEDED", 429)
|
|
18
|
+
self.retry_after_ms = retry_after_ms
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AuthError(IronflowError):
|
|
22
|
+
"""Raised when the API returns 401 Unauthorized."""
|
|
23
|
+
|
|
24
|
+
def __init__(self, message: str) -> None:
|
|
25
|
+
super().__init__(message, "UNAUTHORIZED", 401)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TierError(IronflowError):
|
|
29
|
+
"""Raised when the API returns 403 Forbidden (tier insufficient)."""
|
|
30
|
+
|
|
31
|
+
def __init__(self, message: str, code: str = "TIER_FORBIDDEN") -> None:
|
|
32
|
+
super().__init__(message, code, 403)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class QueryWindowError(IronflowError):
|
|
36
|
+
"""Raised when the query time range exceeds the per-request maximum."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, message: str) -> None:
|
|
39
|
+
super().__init__(message, "QUERY_WINDOW_EXCEEDED", 400)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class QueryTimeoutError(IronflowError):
|
|
43
|
+
"""Raised when a query exceeds the server execution time limit (15s)."""
|
|
44
|
+
|
|
45
|
+
def __init__(self, message: str) -> None:
|
|
46
|
+
super().__init__(message, "QUERY_TIMEOUT", 504)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class QueryTooExpensiveError(IronflowError):
|
|
50
|
+
"""Raised when a query exceeds server resource limits (row/byte scan caps)."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, message: str) -> None:
|
|
53
|
+
super().__init__(message, "QUERY_TOO_EXPENSIVE", 400)
|
ironflow_sdk/py.typed
ADDED
|
File without changes
|
ironflow_sdk/stream.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# WebSocket streaming for the Ironflow API.
|
|
2
|
+
# Returns async generators for each channel.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
8
|
+
from typing import Any, Callable, TypeVar
|
|
9
|
+
|
|
10
|
+
from websockets.asyncio.client import connect
|
|
11
|
+
|
|
12
|
+
T = TypeVar("T")
|
|
13
|
+
|
|
14
|
+
from .types import (
|
|
15
|
+
BookLevel, BookSnapshot, Deposit, FundingRate, Liquidation,
|
|
16
|
+
OrderStatus, Trade, VaultOperation, Withdrawal,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _parse_trade(d: dict[str, Any]) -> Trade:
|
|
21
|
+
return Trade(**d)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_book(d: dict[str, Any]) -> BookSnapshot:
|
|
25
|
+
return BookSnapshot(
|
|
26
|
+
source=d["source"],
|
|
27
|
+
market=d["market"],
|
|
28
|
+
timestamp=d["timestamp"],
|
|
29
|
+
bids=[BookLevel(**b) for b in d["bids"]],
|
|
30
|
+
asks=[BookLevel(**a) for a in d["asks"]],
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class StreamClient:
|
|
35
|
+
"""WebSocket streaming client. Each method returns an AsyncGenerator."""
|
|
36
|
+
|
|
37
|
+
def __init__(self, ws_url: str) -> None:
|
|
38
|
+
self._ws_url = ws_url
|
|
39
|
+
|
|
40
|
+
def trades(self, market: str, *, min_size: str | None = None) -> AsyncGenerator[Trade, None]:
|
|
41
|
+
opts: dict[str, Any] = {"channel": "trades", "market": market}
|
|
42
|
+
if min_size is not None:
|
|
43
|
+
opts["min_size"] = min_size
|
|
44
|
+
return self._subscribe(opts, _parse_trade)
|
|
45
|
+
|
|
46
|
+
def fills(self, address: str, *, market: str | None = None) -> AsyncGenerator[Trade, None]:
|
|
47
|
+
opts: dict[str, Any] = {"channel": "fills", "address": address}
|
|
48
|
+
if market is not None:
|
|
49
|
+
opts["market"] = market
|
|
50
|
+
return self._subscribe(opts, _parse_trade)
|
|
51
|
+
|
|
52
|
+
def book(self, market: str) -> AsyncGenerator[BookSnapshot, None]:
|
|
53
|
+
return self._subscribe({"channel": "book", "market": market}, _parse_book)
|
|
54
|
+
|
|
55
|
+
def book_l4(self, market: str, *, depth: int | None = None) -> AsyncGenerator[BookSnapshot, None]:
|
|
56
|
+
opts: dict[str, Any] = {"channel": "book_l4", "market": market}
|
|
57
|
+
if depth is not None:
|
|
58
|
+
opts["depth"] = depth
|
|
59
|
+
return self._subscribe(opts, _parse_book)
|
|
60
|
+
|
|
61
|
+
def orders(self, address: str) -> AsyncGenerator[OrderStatus, None]:
|
|
62
|
+
return self._subscribe({"channel": "orders", "address": address}, lambda d: OrderStatus(**d))
|
|
63
|
+
|
|
64
|
+
def liquidations(self, market: str) -> AsyncGenerator[Liquidation, None]:
|
|
65
|
+
return self._subscribe({"channel": "liquidations", "market": market}, lambda d: Liquidation(**d))
|
|
66
|
+
|
|
67
|
+
def funding_rates(self, market: str) -> AsyncGenerator[FundingRate, None]:
|
|
68
|
+
return self._subscribe({"channel": "funding_rate", "market": market}, lambda d: FundingRate(**d))
|
|
69
|
+
|
|
70
|
+
def deposits(self, *, address: str | None = None) -> AsyncGenerator[Deposit, None]:
|
|
71
|
+
opts: dict[str, Any] = {"channel": "deposits"}
|
|
72
|
+
if address is not None:
|
|
73
|
+
opts["address"] = address
|
|
74
|
+
return self._subscribe(opts, lambda d: Deposit(**d))
|
|
75
|
+
|
|
76
|
+
def withdrawals(self, *, address: str | None = None) -> AsyncGenerator[Withdrawal, None]:
|
|
77
|
+
opts: dict[str, Any] = {"channel": "withdrawals"}
|
|
78
|
+
if address is not None:
|
|
79
|
+
opts["address"] = address
|
|
80
|
+
return self._subscribe(opts, lambda d: Withdrawal(**d))
|
|
81
|
+
|
|
82
|
+
def vault_operations(self, *, address: str | None = None, vault: str | None = None) -> AsyncGenerator[VaultOperation, None]:
|
|
83
|
+
opts: dict[str, Any] = {"channel": "vault_operations"}
|
|
84
|
+
if address is not None:
|
|
85
|
+
opts["address"] = address
|
|
86
|
+
if vault is not None:
|
|
87
|
+
opts["vault"] = vault
|
|
88
|
+
return self._subscribe(opts, lambda d: VaultOperation(**d))
|
|
89
|
+
|
|
90
|
+
async def _subscribe(self, options: dict[str, Any], parser: Callable[[dict[str, Any]], Any]) -> AsyncGenerator[Any, None]:
|
|
91
|
+
sub_msg = {"op": "subscribe", **options}
|
|
92
|
+
async with connect(self._ws_url) as ws:
|
|
93
|
+
await ws.send(json.dumps(sub_msg))
|
|
94
|
+
async for raw in ws:
|
|
95
|
+
try:
|
|
96
|
+
msg = json.loads(raw)
|
|
97
|
+
if msg.get("op") == "ping":
|
|
98
|
+
await ws.send(json.dumps({"op": "pong"}))
|
|
99
|
+
continue
|
|
100
|
+
if msg.get("op") != "data":
|
|
101
|
+
continue
|
|
102
|
+
yield parser(msg["data"])
|
|
103
|
+
except (json.JSONDecodeError, KeyError):
|
|
104
|
+
continue
|
ironflow_sdk/types.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# Response dataclasses for the Ironflow API.
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class Trade:
|
|
11
|
+
source: str
|
|
12
|
+
market: str
|
|
13
|
+
market_type: str
|
|
14
|
+
timestamp: int
|
|
15
|
+
block_number: int
|
|
16
|
+
sequence: int
|
|
17
|
+
price: str
|
|
18
|
+
size: str
|
|
19
|
+
side: str
|
|
20
|
+
address: str
|
|
21
|
+
fee: str
|
|
22
|
+
realized_pnl: str
|
|
23
|
+
hash: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class BookLevel:
|
|
28
|
+
price: str
|
|
29
|
+
size: str
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class BookSnapshot:
|
|
34
|
+
source: str
|
|
35
|
+
market: str
|
|
36
|
+
timestamp: int
|
|
37
|
+
bids: list[BookLevel]
|
|
38
|
+
asks: list[BookLevel]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@dataclass
|
|
42
|
+
class Candle:
|
|
43
|
+
source: str
|
|
44
|
+
market: str
|
|
45
|
+
interval: str
|
|
46
|
+
timestamp: int
|
|
47
|
+
open: str
|
|
48
|
+
high: str
|
|
49
|
+
low: str
|
|
50
|
+
close: str
|
|
51
|
+
volume: str
|
|
52
|
+
trade_count: int
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class FundingRate:
|
|
57
|
+
source: str
|
|
58
|
+
market: str
|
|
59
|
+
timestamp: int
|
|
60
|
+
block_number: int
|
|
61
|
+
rate: str
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class Liquidation:
|
|
66
|
+
source: str
|
|
67
|
+
market: str
|
|
68
|
+
timestamp: int
|
|
69
|
+
block_number: int
|
|
70
|
+
address: str
|
|
71
|
+
size: str
|
|
72
|
+
price: str
|
|
73
|
+
side: str
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class OpenInterest:
|
|
78
|
+
source: str
|
|
79
|
+
market: str
|
|
80
|
+
timestamp: int
|
|
81
|
+
open_interest: str
|
|
82
|
+
volume_24h: str
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass
|
|
86
|
+
class MarkPrice:
|
|
87
|
+
source: str
|
|
88
|
+
market: str
|
|
89
|
+
timestamp: int
|
|
90
|
+
mark_price: str
|
|
91
|
+
oracle_price: str
|
|
92
|
+
funding_rate: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
@dataclass
|
|
96
|
+
class Deposit:
|
|
97
|
+
source: str
|
|
98
|
+
timestamp: int
|
|
99
|
+
block_number: int
|
|
100
|
+
address: str
|
|
101
|
+
token: str
|
|
102
|
+
amount: str
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass
|
|
106
|
+
class Withdrawal:
|
|
107
|
+
source: str
|
|
108
|
+
timestamp: int
|
|
109
|
+
block_number: int
|
|
110
|
+
address: str
|
|
111
|
+
token: str
|
|
112
|
+
amount: str
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class OrderStatus:
|
|
117
|
+
source: str
|
|
118
|
+
market: str
|
|
119
|
+
timestamp: int
|
|
120
|
+
address: str
|
|
121
|
+
order_id: str
|
|
122
|
+
status: str
|
|
123
|
+
side: str
|
|
124
|
+
price: str
|
|
125
|
+
size: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# ─── Vault Operations ─────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass
|
|
132
|
+
class VaultOperation:
|
|
133
|
+
source: str
|
|
134
|
+
timestamp: int
|
|
135
|
+
block_number: int
|
|
136
|
+
vault: str
|
|
137
|
+
address: str
|
|
138
|
+
operation: str
|
|
139
|
+
amount: str
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ─── Triggers ─────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@dataclass
|
|
146
|
+
class Trigger:
|
|
147
|
+
id: int
|
|
148
|
+
user_id: int
|
|
149
|
+
name: str
|
|
150
|
+
channel: str
|
|
151
|
+
rule: dict[str, Any]
|
|
152
|
+
webhook_url: str
|
|
153
|
+
is_active: bool
|
|
154
|
+
created_at: str = ""
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ─── Cohorts ──────────────────────────────────────────────────────────
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@dataclass
|
|
161
|
+
class CohortInfo:
|
|
162
|
+
name: str
|
|
163
|
+
type: str
|
|
164
|
+
address_count: int
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@dataclass
|
|
168
|
+
class CohortAddressEntry:
|
|
169
|
+
address: str
|
|
170
|
+
label: str | None = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass
|
|
174
|
+
class CohortAddresses:
|
|
175
|
+
addresses: list[CohortAddressEntry]
|
|
176
|
+
fetched_at: int
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ─── Analytics ────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@dataclass
|
|
183
|
+
class NetFlow:
|
|
184
|
+
source: str
|
|
185
|
+
address: str
|
|
186
|
+
bucket: int
|
|
187
|
+
deposits: str
|
|
188
|
+
withdrawals: str
|
|
189
|
+
net_flow: str
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass
|
|
193
|
+
class LiquidationLevel:
|
|
194
|
+
source: str
|
|
195
|
+
market: str
|
|
196
|
+
price_bucket: str
|
|
197
|
+
side: str
|
|
198
|
+
count: int
|
|
199
|
+
total_size: str
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@dataclass
|
|
203
|
+
class OrderFlow:
|
|
204
|
+
source: str
|
|
205
|
+
market: str
|
|
206
|
+
total_orders: int
|
|
207
|
+
filled_orders: int
|
|
208
|
+
cancelled: int
|
|
209
|
+
fill_rate: str
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
@dataclass
|
|
213
|
+
class VaultLeaderboardEntry:
|
|
214
|
+
source: str
|
|
215
|
+
vault: str
|
|
216
|
+
total_deposits: str
|
|
217
|
+
total_withdrawals: str
|
|
218
|
+
net_flow: str
|
|
219
|
+
unique_users: int
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@dataclass
|
|
223
|
+
class FundingStats:
|
|
224
|
+
source: str
|
|
225
|
+
market: str
|
|
226
|
+
bucket: int
|
|
227
|
+
avg_rate: str
|
|
228
|
+
min_rate: str
|
|
229
|
+
max_rate: str
|
|
230
|
+
cumulative_rate: str
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class StreamStatus:
|
|
235
|
+
latest_timestamp: int
|
|
236
|
+
age_seconds: float
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@dataclass
|
|
240
|
+
class VenueStatus:
|
|
241
|
+
streams: dict[str, StreamStatus]
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
@dataclass
|
|
245
|
+
class StatusResponse:
|
|
246
|
+
status: str
|
|
247
|
+
venues: dict[str, VenueStatus]
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ironflow-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Ironflow market data API — on-chain derivatives data
|
|
5
|
+
Project-URL: Homepage, https://ironflow.sh
|
|
6
|
+
Project-URL: Documentation, https://docs.ironflow.sh
|
|
7
|
+
Project-URL: Support, https://docs.ironflow.sh
|
|
8
|
+
Author-email: Ironflow <hello@ironflow.sh>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: crypto,defi,hyperliquid,ironflow,market-data,perpetuals,sdk,trading,websocket
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Framework :: AsyncIO
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: httpx>=0.27
|
|
25
|
+
Requires-Dist: websockets>=12
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
29
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# ironflow
|
|
33
|
+
|
|
34
|
+
Python SDK for the [Ironflow](https://ironflow.sh) market data API — on-chain derivatives data.
|
|
35
|
+
|
|
36
|
+
- Async-first with `httpx` and `websockets`
|
|
37
|
+
- Typed dataclass responses for all endpoints
|
|
38
|
+
- Automatic pagination with `await page.next()`
|
|
39
|
+
- Typed WebSocket streams with async generators
|
|
40
|
+
- Full type annotations (`py.typed`)
|
|
41
|
+
|
|
42
|
+
## Install
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install ironflow-sdk
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires Python 3.10+.
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from ironflow_sdk import Ironflow
|
|
54
|
+
|
|
55
|
+
async with Ironflow("if_your_api_key") as client:
|
|
56
|
+
# Get recent trades
|
|
57
|
+
page = await client.trades("BTC-PERP", limit=10)
|
|
58
|
+
for trade in page.data:
|
|
59
|
+
print(trade.price, trade.size, trade.side)
|
|
60
|
+
|
|
61
|
+
# Paginate
|
|
62
|
+
if page.has_more:
|
|
63
|
+
next_page = await page.next()
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Configuration
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
client = Ironflow(
|
|
70
|
+
"if_your_api_key",
|
|
71
|
+
base_url="https://api.ironflow.sh", # default
|
|
72
|
+
source="hyperliquid", # default
|
|
73
|
+
timeout=30.0, # request timeout in seconds (default: 30)
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## REST Endpoints
|
|
78
|
+
|
|
79
|
+
### Market Data
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
async with Ironflow("if_your_api_key") as client:
|
|
83
|
+
# Trades
|
|
84
|
+
trades = await client.trades("BTC-PERP", limit=100)
|
|
85
|
+
|
|
86
|
+
# Fills (requires address)
|
|
87
|
+
fills = await client.fills("0xabc...", market="ETH-PERP")
|
|
88
|
+
|
|
89
|
+
# Order book snapshot
|
|
90
|
+
book = await client.book("BTC-PERP")
|
|
91
|
+
# book.bids[0].price, book.bids[0].size
|
|
92
|
+
|
|
93
|
+
# OHLCV candles
|
|
94
|
+
candles = await client.candles("BTC-PERP", "1h", limit=24)
|
|
95
|
+
|
|
96
|
+
# Liquidations
|
|
97
|
+
liqs = await client.liquidations("BTC-PERP")
|
|
98
|
+
|
|
99
|
+
# Funding rates
|
|
100
|
+
rates = await client.funding("BTC-PERP")
|
|
101
|
+
|
|
102
|
+
# Open interest
|
|
103
|
+
oi = await client.open_interest("BTC-PERP")
|
|
104
|
+
|
|
105
|
+
# Mark prices
|
|
106
|
+
prices = await client.mark_prices("BTC-PERP")
|
|
107
|
+
|
|
108
|
+
# Deposits & withdrawals
|
|
109
|
+
deps = await client.deposits(address="0xabc...")
|
|
110
|
+
wds = await client.withdrawals(address="0xabc...")
|
|
111
|
+
|
|
112
|
+
# Order statuses
|
|
113
|
+
orders = await client.order_statuses(address="0xabc...")
|
|
114
|
+
|
|
115
|
+
# Vault operations
|
|
116
|
+
vaults = await client.vault_operations(vault="HLP")
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Analytics (Builder+)
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
flows = await client.net_flows(interval="1d", limit=7)
|
|
123
|
+
# flows[0].deposits, flows[0].withdrawals, flows[0].net_flow
|
|
124
|
+
|
|
125
|
+
levels = await client.liquidation_levels("BTC-PERP", bucket_size=100)
|
|
126
|
+
# levels[0].price_bucket, levels[0].count, levels[0].total_size
|
|
127
|
+
|
|
128
|
+
order_flow = await client.order_flow()
|
|
129
|
+
# order_flow[0].total_orders, order_flow[0].filled_orders, order_flow[0].fill_rate
|
|
130
|
+
|
|
131
|
+
leaderboard = await client.vault_leaderboard()
|
|
132
|
+
# leaderboard[0].vault, leaderboard[0].total_deposits, leaderboard[0].net_flow
|
|
133
|
+
|
|
134
|
+
funding = await client.funding_stats("BTC-PERP", interval="8h")
|
|
135
|
+
# funding[0].avg_rate, funding[0].min_rate, funding[0].max_rate
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Triggers
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# Create a webhook trigger
|
|
142
|
+
trigger = await client.create_trigger(
|
|
143
|
+
name="whale-alert",
|
|
144
|
+
channel="trades",
|
|
145
|
+
webhook_url="https://your-server.com/hook",
|
|
146
|
+
rule={"condition": {"field": "data.size", "op": "gt", "value": "100"}},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# List, toggle, delete
|
|
150
|
+
triggers = await client.list_triggers()
|
|
151
|
+
await client.toggle_trigger(trigger.id, is_active=False)
|
|
152
|
+
await client.delete_trigger(trigger.id)
|
|
153
|
+
|
|
154
|
+
# Test a rule without creating
|
|
155
|
+
matched = await client.test_trigger(
|
|
156
|
+
rule={"condition": {"field": "data.size", "op": "gt", "value": "10"}},
|
|
157
|
+
event={"data": {"size": "50", "market": "BTC-PERP"}},
|
|
158
|
+
)
|
|
159
|
+
print(matched) # True
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Cohorts (Enterprise)
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
cohorts = await client.list_cohorts()
|
|
166
|
+
addrs = await client.cohort_addresses("top_pnl_30d")
|
|
167
|
+
await client.create_cohort("my_whales", ["0xabc...", "0xdef..."])
|
|
168
|
+
await client.delete_cohort("my_whales")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Export (Builder+)
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
data = await client.export_data(
|
|
175
|
+
"fills",
|
|
176
|
+
market="BTC-PERP",
|
|
177
|
+
format="csv",
|
|
178
|
+
)
|
|
179
|
+
# data is bytes — write to file or parse with pandas
|
|
180
|
+
import pandas as pd
|
|
181
|
+
df = pd.read_csv(io.BytesIO(data))
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Status
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
status = await client.status()
|
|
188
|
+
print(status.status) # "ok"
|
|
189
|
+
print(status.venues["hyperliquid"].streams["fills"].age_seconds)
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Info API Proxy
|
|
193
|
+
|
|
194
|
+
```python
|
|
195
|
+
meta = await client.info({"type": "metaAndAssetCtxs"})
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## WebSocket Streaming
|
|
199
|
+
|
|
200
|
+
All stream methods return typed async generators:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
# Real-time trades
|
|
204
|
+
async for trade in client.stream.trades("BTC-PERP"):
|
|
205
|
+
print(trade.price, trade.size, trade.side)
|
|
206
|
+
|
|
207
|
+
# Wallet fills (copy trading)
|
|
208
|
+
async for fill in client.stream.fills("0xleader"):
|
|
209
|
+
print(fill.side, fill.size, fill.market)
|
|
210
|
+
|
|
211
|
+
# Order book updates
|
|
212
|
+
async for snap in client.stream.book("ETH-PERP"):
|
|
213
|
+
print(snap.bids[0].price, snap.asks[0].price)
|
|
214
|
+
|
|
215
|
+
# Filter by minimum size
|
|
216
|
+
async for trade in client.stream.trades("BTC-PERP", min_size="10"):
|
|
217
|
+
pass # Only trades >= 10 BTC
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Available streams: `trades`, `fills`, `book`, `book_l4`, `orders`, `liquidations`, `funding_rates`, `deposits`, `withdrawals`, `vault_operations`.
|
|
221
|
+
|
|
222
|
+
## Error Handling
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from ironflow_sdk import Ironflow, RateLimitError, AuthError, TierError, QueryWindowError
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
trades = await client.trades("BTC-PERP")
|
|
229
|
+
except RateLimitError as e:
|
|
230
|
+
print(f"Rate limited, retry in {e.retry_after_ms}ms")
|
|
231
|
+
except AuthError:
|
|
232
|
+
print("Invalid API key")
|
|
233
|
+
except TierError:
|
|
234
|
+
print("Upgrade your plan for this endpoint")
|
|
235
|
+
except QueryWindowError:
|
|
236
|
+
print("Time range too wide for your tier")
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Pagination
|
|
240
|
+
|
|
241
|
+
All list endpoints return a `Page[T]` with async pagination:
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
page = await client.trades("BTC-PERP", limit=1000)
|
|
245
|
+
all_trades = list(page.data)
|
|
246
|
+
|
|
247
|
+
while page.has_more:
|
|
248
|
+
page = await page.next()
|
|
249
|
+
all_trades.extend(page.data)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## License
|
|
253
|
+
|
|
254
|
+
MIT
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ironflow_sdk/__init__.py,sha256=PHptmK6i0cHiL6BdssUw_mIA8q8MU0eMdRHXr1NYXOs,14184
|
|
2
|
+
ironflow_sdk/client.py,sha256=KC9c4pWKVKzjIHXVpLkSKYW7SerHFqAmZlp_vqsZH8I,4704
|
|
3
|
+
ironflow_sdk/errors.py,sha256=2PftqamRBmYG3-Q9W3wuIDRB2RHl5rp1JmtiJhbAXR4,1669
|
|
4
|
+
ironflow_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
ironflow_sdk/stream.py,sha256=y8FYdZ2MplwZIRqZs7MAKKllpxCNpgWJMbDHvAZ1UuU,4206
|
|
6
|
+
ironflow_sdk/types.py,sha256=UUtyySCDuSO3OKfXbkDh1mWpKzldwwlJD2zZaT4-qKE,4117
|
|
7
|
+
ironflow_sdk-0.1.0.dist-info/METADATA,sha256=y2wkQBaI9GuYAcXMP_dOAhW5aTvxEPqMS6UZnr1su1c,6778
|
|
8
|
+
ironflow_sdk-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
9
|
+
ironflow_sdk-0.1.0.dist-info/licenses/LICENSE,sha256=KpKyQ8NIAi5ndZ8RVkSkdOUrs6UnEehu7fOvF3_MhPE,1065
|
|
10
|
+
ironflow_sdk-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ironflow
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|