hyperquant 0.25__py3-none-any.whl → 0.31__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.
- hyperquant/__init__.py +3 -1
- hyperquant/broker/auth.py +51 -0
- hyperquant/broker/hyperliquid.py +67 -35
- hyperquant/broker/models/hyperliquid.py +284 -0
- hyperquant/broker/models/ourbit.py +502 -0
- hyperquant/broker/ourbit.py +247 -0
- hyperquant/broker/ws.py +12 -0
- hyperquant/core.py +77 -26
- {hyperquant-0.25.dist-info → hyperquant-0.31.dist-info}/METADATA +2 -2
- hyperquant-0.31.dist-info/RECORD +21 -0
- hyperquant-0.25.dist-info/RECORD +0 -16
- {hyperquant-0.25.dist-info → hyperquant-0.31.dist-info}/WHEEL +0 -0
hyperquant/__init__.py
CHANGED
@@ -0,0 +1,51 @@
|
|
1
|
+
import time
|
2
|
+
import hashlib
|
3
|
+
from typing import Any
|
4
|
+
from multidict import CIMultiDict
|
5
|
+
from yarl import URL
|
6
|
+
import pybotters
|
7
|
+
import json as pyjson
|
8
|
+
from urllib.parse import urlencode
|
9
|
+
|
10
|
+
|
11
|
+
def md5_hex(s: str) -> str:
|
12
|
+
return hashlib.md5(s.encode("utf-8")).hexdigest()
|
13
|
+
|
14
|
+
|
15
|
+
# 🔑 Ourbit 的鉴权函数
|
16
|
+
class Auth:
|
17
|
+
@staticmethod
|
18
|
+
def ourbit(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
|
19
|
+
method: str = args[0]
|
20
|
+
url: URL = args[1]
|
21
|
+
data = kwargs.get("data") or {}
|
22
|
+
headers: CIMultiDict = kwargs["headers"]
|
23
|
+
|
24
|
+
# 从 session 里取 token
|
25
|
+
session = kwargs["session"]
|
26
|
+
token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
|
27
|
+
|
28
|
+
# 时间戳 & body
|
29
|
+
now_ms = int(time.time() * 1000)
|
30
|
+
raw_body_for_sign = data if isinstance(data, str) else pyjson.dumps(data, separators=(",", ":"), ensure_ascii=False)
|
31
|
+
|
32
|
+
# 签名
|
33
|
+
mid_hash = md5_hex(f"{token}{now_ms}")[7:]
|
34
|
+
final_hash = md5_hex(f"{now_ms}{raw_body_for_sign}{mid_hash}")
|
35
|
+
|
36
|
+
# 设置 headers
|
37
|
+
headers.update({
|
38
|
+
"Authorization": token,
|
39
|
+
"Language": "Chinese",
|
40
|
+
"language": "Chinese",
|
41
|
+
"Content-Type": "application/json",
|
42
|
+
"x-ourbit-sign": final_hash,
|
43
|
+
"x-ourbit-nonce": str(now_ms),
|
44
|
+
})
|
45
|
+
|
46
|
+
# 更新 kwargs.body,保证发出去的与签名一致
|
47
|
+
kwargs.update({"data": raw_body_for_sign})
|
48
|
+
|
49
|
+
return args
|
50
|
+
|
51
|
+
pybotters.auth.Hosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", Auth.ourbit)
|
hyperquant/broker/hyperliquid.py
CHANGED
@@ -26,10 +26,34 @@ from typing import Any, Dict, Optional
|
|
26
26
|
import pybotters
|
27
27
|
from yarl import URL
|
28
28
|
|
29
|
-
from .
|
30
|
-
from .lib.hyper_types import AccountBalance
|
29
|
+
from .models.hyperliquid import MyHyperStore
|
31
30
|
|
32
|
-
|
31
|
+
def to_cloid(s: str) -> str:
|
32
|
+
"""
|
33
|
+
可逆地将字符串转为 Hyperliquid cloid 格式(要求字符串最多16字节,超出报错)。
|
34
|
+
:param s: 原始字符串
|
35
|
+
:return: 形如0x...的cloid字符串
|
36
|
+
"""
|
37
|
+
b = s.encode('utf-8')
|
38
|
+
if len(b) > 16:
|
39
|
+
raise ValueError("String too long for reversible cloid (max 16 bytes)")
|
40
|
+
# 补齐到16字节
|
41
|
+
b = b.ljust(16, b'\0')
|
42
|
+
return "0x" + b.hex()
|
43
|
+
|
44
|
+
def cloid_to_str(cloid: str) -> str:
|
45
|
+
"""
|
46
|
+
从cloid还原回原始字符串
|
47
|
+
:param cloid: 形如0x...的cloid字符串
|
48
|
+
:return: 原始字符串
|
49
|
+
"""
|
50
|
+
try:
|
51
|
+
if not (cloid.startswith("0x") and len(cloid) == 34):
|
52
|
+
raise ValueError("Invalid cloid format for reversal")
|
53
|
+
b = bytes.fromhex(cloid[2:])
|
54
|
+
return b.rstrip(b'\0').decode('utf-8')
|
55
|
+
except Exception as e:
|
56
|
+
return ''
|
33
57
|
|
34
58
|
|
35
59
|
__all__ = [
|
@@ -56,6 +80,7 @@ class AssetMeta:
|
|
56
80
|
asset_id: int
|
57
81
|
name: str
|
58
82
|
sz_decimals: int
|
83
|
+
tick_size: float = None
|
59
84
|
|
60
85
|
@dataclass(frozen=True, slots=True)
|
61
86
|
class SpotAssetMeta:
|
@@ -80,31 +105,27 @@ class OrderData():
|
|
80
105
|
|
81
106
|
_DECIMAL_CTX_5 = decimal.Context(prec=5)
|
82
107
|
|
108
|
+
def normalize_number(n):
|
109
|
+
# 能去掉小数点后多余的零
|
110
|
+
return format(decimal.Decimal(str(n)).normalize(), "f")
|
83
111
|
|
84
|
-
def
|
85
|
-
"""Normalize a number.
|
86
|
-
|
87
|
-
e.g. "3300.0" -> "3300"
|
88
|
-
|
89
|
-
Hyperliquid API expects normalized numbers. Otherwise, `L1 error` will occur.
|
112
|
+
def _fmt_price(price: float, sz_decimals: int, *, max_decimals: int = 6) -> float:
|
90
113
|
"""
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
"""Format *price* according to Hyperliquid rules.
|
95
|
-
|
96
|
-
* For `price >= 1` keep five significant digits.
|
97
|
-
* For small prices keep *max_decimals - sz_decimals* after the dot.
|
114
|
+
格式化价格:
|
115
|
+
- 大于100000直接取整数
|
116
|
+
- 其它情况保留5个有效数字,然后再按max_decimals-sz_decimals截断小数位
|
98
117
|
"""
|
99
|
-
if price
|
100
|
-
return
|
118
|
+
if price > 100_000:
|
119
|
+
return str(round(price))
|
120
|
+
# 先保留5个有效数字,再截断小数位
|
121
|
+
price_5sf = float(f"{price:.5g}")
|
122
|
+
return normalize_number(round(price_5sf, max_decimals - sz_decimals))
|
101
123
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
return _normalize_number(format(decimal.Decimal(size).quantize(decimal.Decimal(f"1e-{sz_decimals}")), "f"))
|
124
|
+
def _fmt_size(sz: float, sz_decimals: int) -> float:
|
125
|
+
"""
|
126
|
+
格式化数量:直接按 sz_decimals 小数位截断
|
127
|
+
"""
|
128
|
+
return normalize_number(round(sz, sz_decimals))
|
108
129
|
|
109
130
|
|
110
131
|
# ╭─────────────────────────────────────────────────────────────────────────╮
|
@@ -144,6 +165,7 @@ class HyperliquidTrader:
|
|
144
165
|
|
145
166
|
self._ws_app: Optional[pybotters.ws.WebSocketConnection] = None
|
146
167
|
self.store: MyHyperStore = MyHyperStore()
|
168
|
+
|
147
169
|
|
148
170
|
|
149
171
|
# ──────────────────────────────────────────────────────────────────────
|
@@ -165,6 +187,7 @@ class HyperliquidTrader:
|
|
165
187
|
if self._user:
|
166
188
|
await self.store.initialize(
|
167
189
|
("orders", self._client.post(_INFO, data={"type": "openOrders", "user": self._user})),
|
190
|
+
("positions", self._client.post(_INFO, data={"type": "clearinghouseState", "user": self._user})),
|
168
191
|
)
|
169
192
|
self._client.ws_connect(
|
170
193
|
self._wss_url,
|
@@ -198,6 +221,12 @@ class HyperliquidTrader:
|
|
198
221
|
async def __aexit__(self, exc_type, exc, tb): # noqa: D401
|
199
222
|
if not self._external_client and self._client is not None:
|
200
223
|
await self._client.__aexit__(exc_type, exc, tb)
|
224
|
+
|
225
|
+
async def sync_orders(self):
|
226
|
+
await self.store.orders._clear()
|
227
|
+
await self.store.initialize(
|
228
|
+
("orders", self._client.post(_INFO, data={"type": "openOrders", "user": self._user})),
|
229
|
+
)
|
201
230
|
|
202
231
|
# ──────────────────────────────────────────────────────────────────────
|
203
232
|
# Internal – metadata & formatting helpers
|
@@ -209,7 +238,7 @@ class HyperliquidTrader:
|
|
209
238
|
raise RuntimeError(f"Failed to fetch meta: {resp.error}")
|
210
239
|
|
211
240
|
self._assets = {
|
212
|
-
d["name"]: AssetMeta(asset_id=i, name=d["name"], sz_decimals=d["szDecimals"])
|
241
|
+
d["name"]: AssetMeta(asset_id=i, name=d["name"], sz_decimals=d["szDecimals"], tick_size=10**(d["szDecimals"] - 6))
|
213
242
|
for i, d in enumerate(resp.data["universe"])
|
214
243
|
}
|
215
244
|
|
@@ -313,7 +342,7 @@ class HyperliquidTrader:
|
|
313
342
|
# ──────────────────────────────────────────────────────────────────────
|
314
343
|
# Public API – account
|
315
344
|
# ──────────────────────────────────────────────────────────────────────
|
316
|
-
async def balances(self, user: Optional[str] = None)
|
345
|
+
async def balances(self, user: Optional[str] = None): # todo support spot
|
317
346
|
try:
|
318
347
|
user = user or self._user
|
319
348
|
if user is None:
|
@@ -437,6 +466,8 @@ class HyperliquidTrader:
|
|
437
466
|
price: Optional[float] = None, # required for limit orders
|
438
467
|
use_ws: bool = False,
|
439
468
|
is_spot: bool = False,
|
469
|
+
reduce_only: bool = False, # whether to place a reduce‑only order
|
470
|
+
cloid: Optional[str] = None,
|
440
471
|
) -> OrderData:
|
441
472
|
"""`place_order` 下订单
|
442
473
|
|
@@ -462,6 +493,7 @@ class HyperliquidTrader:
|
|
462
493
|
tif = "Ioc"
|
463
494
|
|
464
495
|
size_str = _fmt_size(size, meta.sz_decimals)
|
496
|
+
# print(f'下单 @ size_str: {size_str}, price_str: {price_str}, asset: {asset}, is_spot: {is_spot}')
|
465
497
|
order_payload = {
|
466
498
|
"action": {
|
467
499
|
"type": "order",
|
@@ -471,26 +503,26 @@ class HyperliquidTrader:
|
|
471
503
|
"b": is_buy,
|
472
504
|
"p": price_str,
|
473
505
|
"s": size_str,
|
474
|
-
"r":
|
506
|
+
"r": reduce_only,
|
475
507
|
"t": {"limit": {"tif": tif}},
|
476
508
|
}
|
477
509
|
],
|
478
510
|
"grouping": "na",
|
479
511
|
}
|
480
512
|
}
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
order_payload["action"]["orders"][0]["c"] = cloid
|
513
|
+
if cloid is not None:
|
514
|
+
if not cloid.startswith("0x"):
|
515
|
+
cloid = to_cloid(cloid)
|
516
|
+
|
517
|
+
order_payload["action"]["orders"][0]["c"] = cloid
|
487
518
|
|
488
519
|
# print(f"Placing order: {order_payload}")
|
489
|
-
print(order_payload)
|
520
|
+
# print(order_payload)
|
490
521
|
|
491
522
|
if not use_ws:
|
492
523
|
ret = await self._post(_EXCHANGE, order_payload)
|
493
|
-
|
524
|
+
print(ret)
|
525
|
+
if 'error' in str(ret) or 'err' in str(ret):
|
494
526
|
raise RuntimeError(f"Failed to place order: {ret}")
|
495
527
|
elif 'filled' in str(ret):
|
496
528
|
return OrderData(
|
@@ -0,0 +1,284 @@
|
|
1
|
+
from __future__ import annotations
|
2
|
+
|
3
|
+
import time
|
4
|
+
from aiohttp import ClientWebSocketResponse
|
5
|
+
import aiohttp
|
6
|
+
|
7
|
+
def callback(msg, ws: ClientWebSocketResponse = None):
|
8
|
+
print("Received message:", msg)
|
9
|
+
|
10
|
+
|
11
|
+
import asyncio
|
12
|
+
import pybotters
|
13
|
+
from pybotters.store import DataStore, DataStoreCollection
|
14
|
+
from pybotters.models.hyperliquid import HyperliquidDataStore
|
15
|
+
from typing import TYPE_CHECKING, Awaitable
|
16
|
+
|
17
|
+
if TYPE_CHECKING:
|
18
|
+
from pybotters.typedefs import Item
|
19
|
+
from pybotters.ws import ClientWebSocketResponse
|
20
|
+
|
21
|
+
|
22
|
+
|
23
|
+
# {'channel': 'orderUpdates', 'data': [{'order': {'coin': 'HYPE', 'side': 'A', 'limitPx': '22.887', 'sz': '1.12', 'oid': 29641480516, 'timestamp': 1746766108031, 'origSz': '1.12', 'reduceOnly': True}, 'status': 'rejected', 'statusTimestamp': 1746766108031}]}
|
24
|
+
class OrderStore(DataStore):
|
25
|
+
_KEYS = ["oid"]
|
26
|
+
|
27
|
+
def _onmessage(self, msg: Item) -> None:
|
28
|
+
|
29
|
+
for rec in msg:
|
30
|
+
order = rec["order"]
|
31
|
+
item = {
|
32
|
+
**order,
|
33
|
+
"status": rec.get("status"),
|
34
|
+
"px": None,
|
35
|
+
'fee': None,
|
36
|
+
"statusTimestamp": rec.get("statusTimestamp"),
|
37
|
+
}
|
38
|
+
|
39
|
+
if item["status"] == "open":
|
40
|
+
self._update([item])
|
41
|
+
else:
|
42
|
+
self._delete([item])
|
43
|
+
|
44
|
+
class FillStore(DataStore):
|
45
|
+
_KEYS = ["oid"]
|
46
|
+
|
47
|
+
def _onmessage(self, msg: Item) -> None:
|
48
|
+
for fill in msg:
|
49
|
+
self._update([fill])
|
50
|
+
|
51
|
+
|
52
|
+
|
53
|
+
class Account(DataStore):
|
54
|
+
_KEYS = ["marginCoin", "value"]
|
55
|
+
|
56
|
+
def _onmessage(self, data: list[Item]) -> None:
|
57
|
+
self._update(
|
58
|
+
[
|
59
|
+
{
|
60
|
+
"marginCoin": 'USDC',
|
61
|
+
'value': float(item['accountValue']),
|
62
|
+
'frozen': float(item['totalMarginUsed']),
|
63
|
+
'available': float(item['accountValue']) - float(item['totalMarginUsed']),
|
64
|
+
}
|
65
|
+
for item in data
|
66
|
+
]
|
67
|
+
)
|
68
|
+
|
69
|
+
class SpotAccount(DataStore):
|
70
|
+
|
71
|
+
_KEYS = ["coin"]
|
72
|
+
|
73
|
+
def _onmessage(self, data: list[Item]) -> None:
|
74
|
+
self._update(
|
75
|
+
[
|
76
|
+
{
|
77
|
+
"coin": item['coin'],
|
78
|
+
"total": float(item['total']),
|
79
|
+
"frozen": float(item['hold']),
|
80
|
+
"available": float(item['total']) - float(item['hold']),
|
81
|
+
"entryNtl": float(item['entryNtl']),
|
82
|
+
}
|
83
|
+
for item in data
|
84
|
+
]
|
85
|
+
)
|
86
|
+
|
87
|
+
class PositionStore(DataStore):
|
88
|
+
_KEYS = ["coin"]
|
89
|
+
|
90
|
+
def _onmessage(self, data: list[Item]) -> None:
|
91
|
+
|
92
|
+
self._clear()
|
93
|
+
self._update([
|
94
|
+
|
95
|
+
{
|
96
|
+
"coin": item['position']['coin'],
|
97
|
+
"sz": float(item['position']['szi']),
|
98
|
+
"px": float(item['position']['entryPx']),
|
99
|
+
'unpnl': float(item['position']['unrealizedPnl']),
|
100
|
+
'rt': float(item['position']['returnOnEquity']),
|
101
|
+
'lv': int(item['position']['leverage']['value']),
|
102
|
+
'update_time': int(time.time() * 1000)
|
103
|
+
}
|
104
|
+
for item in data
|
105
|
+
])
|
106
|
+
|
107
|
+
|
108
|
+
|
109
|
+
class MyHyperStore(HyperliquidDataStore):
|
110
|
+
ORDER_TYPE = 'orderUpdates'
|
111
|
+
WEBDATA2_TYPE = 'webData2'
|
112
|
+
ORDER_FILL_TYPE = 'userFills'
|
113
|
+
|
114
|
+
def _init(self) -> None:
|
115
|
+
self._create("orders", datastore_class=OrderStore)
|
116
|
+
self._create("account", datastore_class=Account)
|
117
|
+
self._create("positions", datastore_class=PositionStore)
|
118
|
+
self._create("spot_account", datastore_class=SpotAccount)
|
119
|
+
self._create("fills", datastore_class=FillStore)
|
120
|
+
super()._init()
|
121
|
+
|
122
|
+
def _onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
123
|
+
|
124
|
+
if msg.get("channel") == self.ORDER_TYPE:
|
125
|
+
self.orders._onmessage(msg.get('data', []))
|
126
|
+
elif msg.get("channel") == self.WEBDATA2_TYPE:
|
127
|
+
# print(msg.get('data', {}).get('clearinghouseState', {}))
|
128
|
+
act_data = msg.get('data', {}).get('clearinghouseState', {}).get('crossMarginSummary', [])
|
129
|
+
if act_data:
|
130
|
+
self.account._onmessage([act_data])
|
131
|
+
|
132
|
+
pos_data = msg.get('data', {}).get('clearinghouseState', {}).get('assetPositions', [])
|
133
|
+
self.positions._onmessage(pos_data)
|
134
|
+
|
135
|
+
spot_act_data = msg.get('data', {}).get('spotState', {}).get('balances', [])
|
136
|
+
self.spot_account._onmessage(spot_act_data)
|
137
|
+
|
138
|
+
elif msg.get("channel") == self.ORDER_FILL_TYPE:
|
139
|
+
fills = msg.get('data', {}).get('fills', [])
|
140
|
+
is_snap = msg.get('data', {}).get('isSnapshot', False)
|
141
|
+
if not is_snap:
|
142
|
+
self.fills._onmessage(fills)
|
143
|
+
|
144
|
+
super()._onmessage(msg, ws)
|
145
|
+
|
146
|
+
|
147
|
+
async def initialize(self, *aws: tuple[str, Awaitable[aiohttp.ClientResponse]]) -> None:
|
148
|
+
for a in aws:
|
149
|
+
method, f = a
|
150
|
+
resp = await f
|
151
|
+
data = await resp.json()
|
152
|
+
|
153
|
+
if method == "orders":
|
154
|
+
self.orders._onmessage(
|
155
|
+
[
|
156
|
+
{
|
157
|
+
'order': o,
|
158
|
+
'status': "open",
|
159
|
+
'statusTimestamp': int(time.time() * 1000)
|
160
|
+
} for o in data
|
161
|
+
]
|
162
|
+
)
|
163
|
+
elif method == "positions":
|
164
|
+
# 处理持仓数据初始化
|
165
|
+
pos_data = data.get('assetPositions', [])
|
166
|
+
if pos_data:
|
167
|
+
self.positions._onmessage(pos_data)
|
168
|
+
|
169
|
+
# 处理账户数据初始化
|
170
|
+
# act_data = data.get('crossMarginSummary', {})
|
171
|
+
# if act_data:
|
172
|
+
# self.account._onmessage([act_data])
|
173
|
+
pass
|
174
|
+
|
175
|
+
@property
|
176
|
+
def orders(self) -> OrderStore:
|
177
|
+
"""``orders`` data stream.
|
178
|
+
|
179
|
+
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
|
180
|
+
|
181
|
+
Data structure:
|
182
|
+
|
183
|
+
.. code:: python
|
184
|
+
[
|
185
|
+
{
|
186
|
+
"coin": "HYPE",
|
187
|
+
"side": "A",
|
188
|
+
"limitPx": "22.887",
|
189
|
+
"sz": "1.12",
|
190
|
+
"oid": 29641480516,
|
191
|
+
"timestamp": 1746766108031,
|
192
|
+
"origSz": "1.12",
|
193
|
+
"reduceOnly": True
|
194
|
+
"status": "open",
|
195
|
+
"statusTimestamp": 1746766108031
|
196
|
+
}...
|
197
|
+
]
|
198
|
+
"""
|
199
|
+
return self._get("orders", OrderStore)
|
200
|
+
@property
|
201
|
+
def account(self) -> Account:
|
202
|
+
"""``account`` data stream.
|
203
|
+
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
|
204
|
+
Data structure:
|
205
|
+
.. code:: python
|
206
|
+
[
|
207
|
+
{
|
208
|
+
"marginCoin": 'USDC',
|
209
|
+
'value': float(item['accountValue']),
|
210
|
+
'frozen': float(item['totalMarginUsed']),
|
211
|
+
'available': float(item['accountValue']) - float(item['totalMarginUsed']),
|
212
|
+
}...
|
213
|
+
]
|
214
|
+
"""
|
215
|
+
return self._get("account", Account)
|
216
|
+
|
217
|
+
@property
|
218
|
+
def positions(self) -> PositionStore:
|
219
|
+
"""
|
220
|
+
获取当前的持仓信息。
|
221
|
+
Data structure:
|
222
|
+
.. code:: python
|
223
|
+
[
|
224
|
+
{
|
225
|
+
"coin": item['position']['coin'],
|
226
|
+
"sz": float(item['position']['szi']),
|
227
|
+
"px": float(item['position']['entryPx']),
|
228
|
+
'unpnl': float(item['position']['unrealizedPnl']),
|
229
|
+
'rt': float(item['position']['returnOnEquity']),
|
230
|
+
'lv': int(item['position']['leverage']['value']),
|
231
|
+
'update_time': int(time.time() * 1000)
|
232
|
+
}...
|
233
|
+
]
|
234
|
+
"""
|
235
|
+
|
236
|
+
return self._get("positions", PositionStore)
|
237
|
+
|
238
|
+
@property
|
239
|
+
def spot_account(self) -> SpotAccount:
|
240
|
+
"""``spot_account`` data stream.
|
241
|
+
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
|
242
|
+
Data structure:
|
243
|
+
.. code:: python
|
244
|
+
[
|
245
|
+
{
|
246
|
+
"coin": 'FEUSD',
|
247
|
+
"sz": "21.0",
|
248
|
+
"px": "0.9719",
|
249
|
+
"unpnl": "0.0",
|
250
|
+
"rt": "0.0",
|
251
|
+
"lv": 1,
|
252
|
+
}...
|
253
|
+
]
|
254
|
+
"""
|
255
|
+
return self._get("spot_account", SpotAccount)
|
256
|
+
|
257
|
+
@property
|
258
|
+
def fills(self) -> FillStore:
|
259
|
+
"""``fills`` data stream.
|
260
|
+
https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/websocket/subscriptions
|
261
|
+
Data structure:
|
262
|
+
.. code:: python
|
263
|
+
[
|
264
|
+
{
|
265
|
+
"coin": 'FEUSD',
|
266
|
+
"px": "0.9719",
|
267
|
+
"sz": "21.0",
|
268
|
+
"side": 'buy',
|
269
|
+
"time": 1679999999999,
|
270
|
+
"startPosition": '0.0',
|
271
|
+
"dir": 'buy',
|
272
|
+
"closedPnl": '0.0',
|
273
|
+
"hash": '0x123456789abcdef',
|
274
|
+
"oid": 123456789,
|
275
|
+
"crossed": True,
|
276
|
+
"fee": '-0.0001',
|
277
|
+
"tid": 987654321,
|
278
|
+
"liquidation": None,
|
279
|
+
"feeToken": 'USDC',
|
280
|
+
}...
|
281
|
+
]
|
282
|
+
"""
|
283
|
+
return self._get("fills", FillStore)
|
284
|
+
|