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.
@@ -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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.