hyperquant 0.26__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 CHANGED
@@ -3,4 +3,6 @@ from .draw import *
3
3
  from .logkit import *
4
4
  from .datavison import *
5
5
  from .notikit import *
6
- __version__ = "0.1.0"
6
+ __version__ = "0.1.0"
7
+
8
+ from .broker import auth, ws
@@ -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)
@@ -26,10 +26,34 @@ from typing import Any, Dict, Optional
26
26
  import pybotters
27
27
  from yarl import URL
28
28
 
29
- from .lib.hpstore import MyHyperStore
30
- from .lib.hyper_types import AccountBalance
29
+ from .models.hyperliquid import MyHyperStore
31
30
 
32
- import uuid
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 _normalize_number(number: str) -> str:
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
- return format(decimal.Decimal(number).normalize(), "f")
92
-
93
- def _fmt_price(price: float, sz_decimals: int, *, max_decimals: int = 6) -> str:
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 >= 1:
100
- return format(_DECIMAL_CTX_5.create_decimal(price).normalize(), "f")
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
- decimal_places = max_decimals - sz_decimals
103
- return _normalize_number(format(decimal.Decimal(price).quantize(decimal.Decimal(f"1e-{decimal_places}")), "f"))
104
-
105
-
106
- def _fmt_size(size: float, sz_decimals: int) -> str:
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) -> AccountBalance | None: # todo support spot
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": False,
506
+ "r": reduce_only,
475
507
  "t": {"limit": {"tif": tif}},
476
508
  }
477
509
  ],
478
510
  "grouping": "na",
479
511
  }
480
512
  }
481
- # 创建时间戳cid
482
- # Generate a 128-bit client order ID as a hex string (0x-prefixed, 32 hex chars)
483
- cloid = uuid.uuid4().hex
484
- cloid = "0x" + cloid
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
- if 'error' in str(ret):
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
+
@@ -0,0 +1,502 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import TYPE_CHECKING, Any, Awaitable
6
+
7
+ import aiohttp
8
+ from pybotters.store import DataStore, DataStoreCollection
9
+
10
+ if TYPE_CHECKING:
11
+ from pybotters.typedefs import Item
12
+ from pybotters.ws import ClientWebSocketResponse
13
+
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class Book(DataStore):
19
+ """深度数据存储类,用于处理订单簿深度信息
20
+
21
+ Channel: push.depth.step
22
+
23
+ 用于存储和管理订单簿深度数据,包含买卖盘的价格和数量信息
24
+ Keys: ["symbol", "side", "px"]
25
+ - symbol: 交易对符号
26
+ - side: 买卖方向 (A: ask卖出, B: bid买入)
27
+ - px: 价格
28
+
29
+
30
+ """
31
+
32
+ _KEYS = ["symbol", "side", "px"]
33
+
34
+ def _init(self) -> None:
35
+ # super().__init__()
36
+ self._time: int | None = None
37
+
38
+ def _on_message(self, msg: dict[str, Any]) -> None:
39
+
40
+ symbol = msg.get("symbol")
41
+ data = msg.get("data", {})
42
+ asks = data.get("asks", [])
43
+ bids = data.get("bids", [])
44
+ timestamp = data.get("ct") # 使用服务器时间
45
+
46
+ data_to_insert: list[Item] = []
47
+
48
+ # 先删除旧的订单簿数据
49
+ self._find_and_delete({"symbol": symbol})
50
+
51
+ # 处理买卖盘数据
52
+ for side_id, levels in (("B", bids), ("A", asks)):
53
+ for level in levels:
54
+ # level格式: [price, size, count]
55
+ if len(level) >= 3:
56
+ price, size, count = level[0:3]
57
+ data_to_insert.append(
58
+ {
59
+ "symbol": symbol,
60
+ "side": side_id,
61
+ "px": str(price),
62
+ "sz": str(size),
63
+ "count": count,
64
+ }
65
+ )
66
+
67
+ # 插入新的订单簿数据
68
+ self._insert(data_to_insert)
69
+ self._time = timestamp
70
+
71
+ @property
72
+ def time(self) -> int | None:
73
+ """返回最后更新时间"""
74
+ return self._time
75
+
76
+ @property
77
+ def sorted(self) -> dict[str, list[Item]]:
78
+ """获取排序后的订单簿数据
79
+
80
+ Returns:
81
+ 返回按价格排序的买卖盘数据,卖盘升序,买盘降序
82
+
83
+ .. code-block:: python
84
+
85
+ {
86
+ "asks": [
87
+ {"symbol": "BTC_USDT", "side": "A", "px": "110152.5", "sz": "53539", "count": 1},
88
+ {"symbol": "BTC_USDT", "side": "A", "px": "110152.6", "sz": "95513", "count": 2}
89
+ ],
90
+ "bids": [
91
+ {"symbol": "BTC_USDT", "side": "B", "px": "110152.4", "sz": "76311", "count": 1},
92
+ {"symbol": "BTC_USDT", "side": "B", "px": "110152.3", "sz": "104688", "count": 2}
93
+ ]
94
+ }
95
+ """
96
+ return self._sorted(
97
+ item_key="side",
98
+ item_asc_key="A", # asks 升序
99
+ item_desc_key="B", # bids 降序
100
+ sort_key="px",
101
+ )
102
+
103
+
104
+ class Ticker(DataStore):
105
+ _KEYS = ["symbol"]
106
+
107
+ def _on_message(self, data: dict[str, Any]):
108
+ self._onresponse(data)
109
+
110
+ def _onresponse(self, data: dict[str, Any]):
111
+ tickers = data.get("data", [])
112
+ if tickers:
113
+ data_to_insert: list[Item] = []
114
+ for ticker in tickers:
115
+ ticker: dict[str, Any] = ticker
116
+ for ticker in tickers:
117
+ data_to_insert.append(
118
+ {
119
+ "amount24": ticker.get("amount24"),
120
+ "fair_price": ticker.get("fairPrice"),
121
+ "high24_price": ticker.get("high24Price"),
122
+ "index_price": ticker.get("indexPrice"),
123
+ "last_price": ticker.get("lastPrice"),
124
+ "lower24_price": ticker.get("lower24Price"),
125
+ "max_bid_price": ticker.get("maxBidPrice"),
126
+ "min_ask_price": ticker.get("minAskPrice"),
127
+ "rise_fall_rate": ticker.get("riseFallRate"),
128
+ "symbol": ticker.get("symbol"),
129
+ "timestamp": ticker.get("timestamp"),
130
+ "volume24": ticker.get("volume24"),
131
+ }
132
+ )
133
+ # self._clear()
134
+ self._insert(data_to_insert)
135
+
136
+
137
+ class Orders(DataStore):
138
+ _KEYS = ["order_id"]
139
+
140
+ # {'success': True, 'code': 0, 'data': [{'orderId': '219108574599630976', 'symbol': 'SOL_USDT', 'positionId': 0, 'price': 190, 'priceStr': '190', 'vol': 1, 'leverage': 20, 'side': 1, 'category': 1, 'orderType': 1, 'dealAvgPrice': 0, 'dealAvgPriceStr': '0', 'dealVol': 0, 'orderMargin': 0.09652, 'takerFee': 0, 'makerFee': 0, 'profit': 0, 'feeCurrency': 'USDT', 'openType': 1, 'state': 2, 'externalOid': '_m_2228b23a75204e1982b301e44d439cbb', 'errorCode': 0, 'usedMargin': 0, 'createTime': 1756277955008, 'updateTime': 1756277955037, 'positionMode': 1, 'version': 1, 'showCancelReason': 0, 'showProfitRateShare': 0, 'voucher': False}]}
141
+ def _onresponse(self, data: dict[str, Any]):
142
+ orders = data.get("data", [])
143
+ if orders:
144
+ data_to_insert: list[Item] = []
145
+ for order in orders:
146
+ order: dict[str, Any] = order
147
+
148
+ data_to_insert.append(
149
+ {
150
+ "order_id": order.get("orderId"),
151
+ "symbol": order.get("symbol"),
152
+ "px": order.get("priceStr"),
153
+ "vol": order.get("vol"),
154
+ "lev": order.get("leverage"),
155
+ "side": "buy" if order.get("side") == 1 else "sell",
156
+ "deal_vol": order.get("dealVol"),
157
+ "deal_avg_px": order.get("dealAvgPriceStr"),
158
+ "create_ts": order.get("createTime"),
159
+ "update_ts": order.get("updateTime"),
160
+ }
161
+ )
162
+
163
+ self._clear()
164
+ self._update(data_to_insert)
165
+
166
+
167
+ class Detail(DataStore):
168
+ _KEYS = ["symbol"]
169
+
170
+ def _on_message(self, data: dict[str, Any]):
171
+ self._onresponse(data)
172
+
173
+ def _onresponse(self, data: dict[str, Any]):
174
+ details: dict = data.get("data", {})
175
+ data_to_insert: list[Item] = []
176
+ if details:
177
+ for detail in details:
178
+ data_to_insert.append(
179
+ {
180
+ "symbol": detail.get("symbol"),
181
+ "ft": detail.get("ft"),
182
+ "max_lev": detail.get("maxL"),
183
+ "tick_size": detail.get("pu"),
184
+ "vol_unit": detail.get("vu"),
185
+ "io": detail.get("io"),
186
+ "contract_sz": detail.get("cs"),
187
+ "minv": detail.get("minV"),
188
+ "maxv": detail.get("maxV")
189
+ }
190
+ )
191
+ self._update(data_to_insert)
192
+
193
+ class Position(DataStore):
194
+ _KEYS = ["position_id"]
195
+ # {"success":true,"code":0,"data":[{"positionId":5355366,"symbol":"SOL_USDT","positionType":1,"openType":1,"state":1,"holdVol":1,"frozenVol":0,"closeVol":0,"holdAvgPrice":203.44,"holdAvgPriceFullyScale":"203.44","openAvgPrice":203.44,"openAvgPriceFullyScale":"203.44","closeAvgPrice":0,"liquidatePrice":194.07,"oim":0.10253376,"im":0.10253376,"holdFee":0,"realised":-0.0008,"leverage":20,"marginRatio":0.0998,"createTime":1756275984696,"updateTime":1756275984696,"autoAddIm":false,"version":1,"profitRatio":0,"newOpenAvgPrice":203.44,"newCloseAvgPrice":0,"closeProfitLoss":0,"fee":0.00081376}]}
196
+ def _onresponse(self, data: dict[str, Any]):
197
+ positions = data.get("data", [])
198
+ if positions:
199
+ data_to_insert: list[Item] = []
200
+ for position in positions:
201
+ position: dict[str, Any] = position
202
+
203
+ data_to_insert.append(
204
+ {
205
+ "position_id": position.get("positionId"),
206
+ "symbol": position.get("symbol"),
207
+ "side": "short" if position.get("positionType") == 2 else "long",
208
+ "open_type": position.get("openType"),
209
+ "state": position.get("state"),
210
+ "hold_vol": position.get("holdVol"),
211
+ "frozen_vol": position.get("frozenVol"),
212
+ "close_vol": position.get("closeVol"),
213
+ "hold_avg_price": position.get("holdAvgPriceFullyScale"),
214
+ "open_avg_price": position.get("openAvgPriceFullyScale"),
215
+ "close_avg_price": str(position.get("closeAvgPrice")),
216
+ "liquidate_price": str(position.get("liquidatePrice")),
217
+ "oim": position.get("oim"),
218
+ "im": position.get("im"),
219
+ "hold_fee": position.get("holdFee"),
220
+ "realised": position.get("realised"),
221
+ "leverage": position.get("leverage"),
222
+ "margin_ratio": position.get("marginRatio"),
223
+ "create_ts": position.get("createTime"),
224
+ "update_ts": position.get("updateTime"),
225
+ }
226
+ )
227
+
228
+ self._clear()
229
+ self._insert(data_to_insert)
230
+
231
+ class Balance(DataStore):
232
+ _KEYS = ["currency"]
233
+
234
+ def _onresponse(self, data: dict[str, Any]):
235
+ balances = data.get("data", [])
236
+ if balances:
237
+ data_to_insert: list[Item] = []
238
+ for balance in balances:
239
+ balance: dict[str, Any] = balance
240
+ data_to_insert.append({
241
+ "currency": balance.get("currency"),
242
+ "position_margin": balance.get("positionMargin"),
243
+ "available_balance": balance.get("availableBalance"),
244
+ "cash_balance": balance.get("cashBalance"),
245
+ "frozen_balance": balance.get("frozenBalance"),
246
+ "equity": balance.get("equity"),
247
+ "unrealized": balance.get("unrealized"),
248
+ "bonus": balance.get("bonus"),
249
+ "last_bonus": balance.get("lastBonus"),
250
+ "wallet_balance": balance.get("walletBalance"),
251
+ "voucher": balance.get("voucher"),
252
+ "voucher_using": balance.get("voucherUsing"),
253
+ })
254
+ self._clear()
255
+ self._insert(data_to_insert)
256
+
257
+ class OurbitSwapDataStore(DataStoreCollection):
258
+ """
259
+ Ourbit DataStoreCollection
260
+
261
+ REST API:
262
+ - 地址: https://futures.ourbit.com
263
+ - 合约详情
264
+ GET /api/v1/contract/detailV2?client=web
265
+ - ticker
266
+ GET /api/v1/contract/ticker
267
+ - open_orders
268
+ GET /api/v1/private/order/list/open_orders?page_size=200
269
+ - open_positions
270
+ GET /api/v1/private/position/open_positions
271
+
272
+ WebSocket API:
273
+ - 地址: wss://futures.ourbit.com/edge or /ws
274
+ - 支持频道:
275
+ * 深度数据(Book): push.depth.step
276
+ * 行情数据(Ticker): push.tickers
277
+
278
+ 示例订阅 JSON:
279
+
280
+ .. code:: json
281
+
282
+ {
283
+ "method": "sub.depth.step",
284
+ "param": {
285
+ "symbol": "BTC_USDT",
286
+ "step": "0.1"
287
+ }
288
+ }
289
+
290
+ .. code:: json
291
+
292
+ {
293
+ "method": "sub.tickers",
294
+ "param": {
295
+ "timezone": "UTC+8"
296
+ }
297
+ }
298
+
299
+ TODO:
300
+ - 添加 trades、ticker、candle 等其他数据流
301
+ """
302
+
303
+ def _init(self) -> None:
304
+ self._create("book", datastore_class=Book)
305
+ self._create("detail", datastore_class=Detail)
306
+ self._create("ticker", datastore_class=Ticker)
307
+ self._create("orders", datastore_class=Orders)
308
+ self._create("position", datastore_class=Position)
309
+ self._create("balance", datastore_class=Balance)
310
+ # TODO: 添加其他数据流,如 trades, ticker, candle 等
311
+
312
+ def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
313
+ channel = msg.get("channel")
314
+
315
+ if channel == "push.depth.step":
316
+ self.book._on_message(msg)
317
+ if channel == "push.tickers":
318
+ self.ticker._on_message(msg)
319
+ else:
320
+ logger.debug(f"未知的channel: {channel}")
321
+
322
+ async def initialize(self, *aws: Awaitable[aiohttp.ClientResponse]) -> None:
323
+ """Initialize DataStore from HTTP response data."""
324
+ for f in asyncio.as_completed(aws):
325
+ res = await f
326
+ data = await res.json()
327
+ if res.url.path == "/api/v1/contract/detailV2":
328
+ self.detail._onresponse(data)
329
+ if res.url.path == "/api/v1/contract/ticker":
330
+ self.ticker._onresponse(data)
331
+ if res.url.path == "/api/v1/private/order/list/open_orders":
332
+ self.orders._onresponse(data)
333
+ if res.url.path == "/api/v1/private/position/open_positions":
334
+ self.position._onresponse(data)
335
+ if res.url.path == "/api/v1/private/account/assets":
336
+ self.balance._onresponse(data)
337
+
338
+ @property
339
+ def detail(self) -> Detail:
340
+ """合约详情
341
+ Data structure:
342
+ .. code:: python
343
+ [
344
+ {
345
+ "symbol": "BTC_USDT", # 交易对
346
+ "ft": 100, # 合约面值
347
+ "max_lev": 100, # 最大杠杆
348
+ "tick_size": 0.1, # 最小变动价位
349
+ "vol_unit": 1, # 合约单位
350
+ "io": ["binance", "mexc"], # 交易所列表
351
+ "contract_sz": 1,
352
+ "minv": 1,
353
+ "maxv": 10000
354
+
355
+ }
356
+ ]
357
+ """
358
+ return self._get("detail", Detail)
359
+
360
+ @property
361
+ def book(self) -> Book:
362
+ """订单簿深度数据流
363
+
364
+ Data type: Mutable
365
+
366
+ Keys: ("symbol", "side", "px")
367
+
368
+ Data structure:
369
+
370
+ .. code:: python
371
+
372
+ [
373
+ {
374
+ "symbol": "BTC_USDT", # 交易对
375
+ "side": "A", # 卖出方向
376
+ "px": "110152.5", # 价格
377
+ "sz": "53539", # 数量
378
+ "count": 1 # 订单数量
379
+ },
380
+ {
381
+ "symbol": "BTC_USDT", # 交易对
382
+ "side": "B", # 买入方向
383
+ "px": "110152.4", # 价格
384
+ "sz": "76311", # 数量
385
+ "count": 1 # 订单数量
386
+ }
387
+ ]
388
+ """
389
+ return self._get("book", Book)
390
+
391
+ @property
392
+ def ticker(self) -> Ticker:
393
+ """市场行情数据流
394
+
395
+ Data type: Mutable
396
+
397
+ Keys: ("symbol",)
398
+
399
+ Data structure:
400
+
401
+ .. code:: python
402
+
403
+ [
404
+ {
405
+ "symbol": "BTC_USDT", # 交易对
406
+ "last_price": "110152.5", # 最新价格
407
+ "index_price": "110000.0", # 指数价格
408
+ "fair_price": "110100.0", # 公允价格
409
+ "high24_price": "115000.0", # 24小时最高价
410
+ "lower24_price": "105000.0", # 24小时最低价
411
+ "volume24": "1500", # 24小时交易量
412
+ "amount24": "165000000", # 24小时交易额
413
+ "rise_fall_rate": "0.05", # 涨跌幅
414
+ "max_bid_price": "110150.0", # 买一价
415
+ "min_ask_price": "110155.0", # 卖一价
416
+ "timestamp": 1625247600000 # 时间戳
417
+ }
418
+ ]
419
+ """
420
+ return self._get("ticker", Ticker)
421
+
422
+ @property
423
+ def orders(self) -> Orders:
424
+ """
425
+ 订单数据
426
+ Data structure:
427
+
428
+ .. code:: json
429
+
430
+ [
431
+ {
432
+ "id": "123456",
433
+ "symbol": "BTC_USDT",
434
+ "side": "buy",
435
+ "price": "110152.5",
436
+ "size": "0.1",
437
+ "status": "open",
438
+ "create_ts": 1625247600000,
439
+ "update_ts": 1625247600000
440
+ }
441
+ ]
442
+ """
443
+ return self._get("orders", Orders)
444
+
445
+ @property
446
+ def position(self) -> Position:
447
+ """
448
+ 持仓数据
449
+
450
+ Data structure:
451
+ .. code:: python
452
+ [
453
+ {
454
+ "position_id": "123456",
455
+ "symbol": "BTC_USDT",
456
+ "side": "long",
457
+ "open_type": "limit",
458
+ "state": "open",
459
+ "hold_vol": "0.1",
460
+ "frozen_vol": "0.0",
461
+ "close_vol": "0.0",
462
+ "hold_avg_price": "110152.5",
463
+ "open_avg_price": "110152.5",
464
+ "close_avg_price": "0.0",
465
+ "liquidate_price": "100000.0",
466
+ "oim": "0.0",
467
+ "im": "0.0",
468
+ "hold_fee": "0.0",
469
+ "realised": "0.0",
470
+ "leverage": "10",
471
+ "margin_ratio": "0.1",
472
+ "create_ts": 1625247600000,
473
+ "update_ts": 1625247600000
474
+ }
475
+ ]
476
+ """
477
+ return self._get("position", Position)
478
+
479
+ @property
480
+ def balance(self) -> Balance:
481
+ """账户余额数据
482
+
483
+ Data structure:
484
+ .. code:: python
485
+ [
486
+ {
487
+ "currency": "USDT", # 币种
488
+ "position_margin": 0.3052, # 持仓保证金
489
+ "available_balance": 19.7284, # 可用余额
490
+ "cash_balance": 19.7284, # 现金余额
491
+ "frozen_balance": 0, # 冻结余额
492
+ "equity": 19.9442, # 权益
493
+ "unrealized": -0.0895, # 未实现盈亏
494
+ "bonus": 0, # 奖励
495
+ "last_bonus": 0, # 最后奖励
496
+ "wallet_balance": 20.0337, # 钱包余额
497
+ "voucher": 0, # 代金券
498
+ "voucher_using": 0 # 使用中的代金券
499
+ }
500
+ ]
501
+ """
502
+ return self._get("balance", Balance)
@@ -0,0 +1,247 @@
1
+ from typing import Literal, Optional
2
+ import pybotters
3
+ from .models.ourbit import OurbitSwapDataStore
4
+
5
+
6
+ class OurbitSwap:
7
+
8
+ def __init__(self, client: pybotters.Client):
9
+ """
10
+ ✅ 完成:
11
+ 下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
12
+
13
+ """
14
+ self.client = client
15
+ self.store = OurbitSwapDataStore()
16
+ self.api_url = "https://futures.ourbit.com"
17
+ self.ws_url = "wss://futures.ourbit.com/edge"
18
+
19
+ async def __aenter__(self) -> "OurbitSwap":
20
+ client = self.client
21
+ await self.store.initialize(
22
+ client.get(f"{self.api_url}/api/v1/contract/detailV2?client=web")
23
+ )
24
+ return self
25
+
26
+ async def update(
27
+ self, update_type: Literal["position", "orders", "balance", "ticker", "all"] = "all"
28
+ ):
29
+ """由于交易所很多不支持ws推送,这里使用Rest"""
30
+ all_urls = [
31
+ f"{self.api_url}/api/v1/private/position/open_positions",
32
+ f"{self.api_url}/api/v1/private/order/list/open_orders?page_size=200",
33
+ f"{self.api_url}/api/v1/private/account/assets",
34
+ f"{self.api_url}/api/v1/contract/ticker",
35
+ ]
36
+
37
+ url_map = {
38
+ "position": [all_urls[0]],
39
+ "orders": [all_urls[1]],
40
+ "balance": [all_urls[2]],
41
+ "ticker": [all_urls[3]],
42
+ "all": all_urls,
43
+ }
44
+
45
+ try:
46
+ urls = url_map[update_type]
47
+ except KeyError:
48
+ raise ValueError(f"update_type err: {update_type}")
49
+
50
+ # 直接传协程进去,initialize 会自己 await
51
+ await self.store.initialize(*(self.client.get(url) for url in urls))
52
+
53
+ async def sub_tickers(self):
54
+ self.client.ws_connect(
55
+ self.ws_url,
56
+ send_json={
57
+ "method": "sub.tickers",
58
+ "param": {
59
+ "timezone": "UTC+8"
60
+ }
61
+ },
62
+ hdlr_json=self.store.onmessage
63
+ )
64
+
65
+ async def sub_order_book(self, symbols: str | list[str]):
66
+ if isinstance(symbols, str):
67
+ symbols = [symbols]
68
+
69
+ send_jsons = []
70
+ # send_json={"method":"sub.depth.step","param":{"symbol":"BTC_USDT","step":"0.1"}},
71
+
72
+ for symbol in symbols:
73
+ step = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
74
+
75
+ send_jsons.append({
76
+ "method": "sub.depth.step",
77
+ "param": {
78
+ "symbol": symbol,
79
+ "step": str(step)
80
+ }
81
+ })
82
+
83
+ await self.client.ws_connect(
84
+ self.ws_url,
85
+ send_json=send_jsons,
86
+ hdlr_json=self.store.onmessage
87
+ )
88
+
89
+ def ret_content(self, res: pybotters.FetchResult):
90
+ match res.data:
91
+ case {"success": True}:
92
+ return res.data["data"]
93
+ case _:
94
+ raise Exception(f"Failed api {res.response.url}: {res.data}")
95
+
96
+
97
+ async def place_order(
98
+ self,
99
+ symbol: str,
100
+ side: Literal["buy", "sell", "close_buy", "close_sell"],
101
+ size: float = None,
102
+ price: float = None,
103
+ order_type: Literal["market", "limit_GTC", "limit_IOC"] = "market",
104
+ usdt_amount: Optional[float] = None,
105
+ leverage: Optional[int] = 20,
106
+ position_id: Optional[int] = None,
107
+ ):
108
+ """
109
+ size为合约张数
110
+
111
+ .. code ::
112
+ {
113
+ "orderId": "219602019841167810",
114
+ "ts": 1756395601543
115
+ }
116
+
117
+ """
118
+ if (size is None) == (usdt_amount is None):
119
+ raise ValueError("params err")
120
+
121
+ max_lev = self.store.detail.find({"symbol": symbol})[0].get("max_lev")
122
+
123
+ if usdt_amount is not None:
124
+ cs = self.store.detail.find({"symbol": symbol})[0].get("contract_sz")
125
+ size = max(int(usdt_amount / cs / price), 1)
126
+
127
+
128
+ leverage = max(max_lev, leverage)
129
+
130
+ data = {
131
+ "symbol": symbol,
132
+ "side": 1 if side == "buy" else 3,
133
+ "openType": 1,
134
+ "type": "5",
135
+ "vol": size,
136
+ "leverage": leverage,
137
+ "marketCeiling": False,
138
+ "priceProtect": "0",
139
+ }
140
+
141
+ if order_type == "limit_IOC":
142
+ data["type"] = 3
143
+ data["price"] = str(price)
144
+ if order_type == "limit_GTC":
145
+ data["type"] = "1"
146
+ data["price"] = str(price)
147
+
148
+ if "close" in side:
149
+ if side == 'close_buy':
150
+ data["side"] = 4
151
+ elif side == 'close_sell':
152
+ data["side"] = 2
153
+ data["type"] = 5
154
+ if position_id is None:
155
+ raise ValueError("position_id is required for closing position")
156
+ data["positionId"] = position_id
157
+ import time
158
+ print(time.time(), '下单')
159
+ res = await self.client.fetch(
160
+ "POST", f"{self.api_url}/api/v1/private/order/create", data=data
161
+ )
162
+ return self.ret_content(res)
163
+
164
+ async def cancel_orders(self, order_ids: list[str]):
165
+ res = await self.client.fetch(
166
+ "POST",
167
+ f"{self.api_url}/api/v1/private/order/cancel",
168
+ data=order_ids,
169
+ )
170
+ return self.ret_content(res)
171
+
172
+ async def query_orders(
173
+ self,
174
+ symbol: str,
175
+ states: list[Literal["filled", "canceled"]], # filled:已成交, canceled:已撤销
176
+ start_time: Optional[int] = None,
177
+ end_time: Optional[int] = None,
178
+ page_size: int = 200,
179
+ page_num: int = 1,
180
+ ):
181
+ """查询历史订单
182
+
183
+ Args:
184
+ symbol: 交易对
185
+ states: 订单状态列表 ["filled":已成交, "canceled":已撤销]
186
+ start_time: 开始时间戳(毫秒), 可选
187
+ end_time: 结束时间戳(毫秒), 可选
188
+ page_size: 每页数量, 默认200
189
+ page_num: 页码, 默认1
190
+ """
191
+ state_map = {"filled": 3, "canceled": 4}
192
+
193
+ params = {
194
+ "symbol": symbol,
195
+ "states": ",".join(str(state_map[state]) for state in states),
196
+ "page_size": page_size,
197
+ "page_num": page_num,
198
+ "category": 1,
199
+ }
200
+
201
+ if start_time:
202
+ params["start_time"] = start_time
203
+ if end_time:
204
+ params["end_time"] = end_time
205
+
206
+ res = await self.client.fetch(
207
+ "GET",
208
+ f"{self.api_url}/api/v1/private/order/list/history_orders",
209
+ params=params,
210
+ )
211
+
212
+ return self.ret_content(res)
213
+
214
+ async def query_order(self, order_id: str):
215
+ """查询单个订单的详细信息
216
+
217
+ Args:
218
+ order_id: 订单ID
219
+
220
+ Returns:
221
+ ..code:python
222
+
223
+ 订单详情数据,例如:
224
+ [
225
+ {
226
+ "id": "38600506", # 成交ID
227
+ "symbol": "SOL_USDT", # 交易对
228
+ "side": 4, # 方向(1:买入, 3:卖出, 4:平仓)
229
+ "vol": 1, # 成交数量
230
+ "price": 204.11, # 成交价格
231
+ "fee": 0.00081644, # 手续费
232
+ "feeCurrency": "USDT", # 手续费币种
233
+ "profit": -0.0034, # 盈亏
234
+ "category": 1, # 品类
235
+ "orderId": "219079365441409152", # 订单ID
236
+ "timestamp": 1756270991000, # 时间戳
237
+ "positionMode": 1, # 持仓模式
238
+ "voucher": false, # 是否使用代金券
239
+ "taker": true # 是否是taker
240
+ }
241
+ ]
242
+ """
243
+ res = await self.client.fetch(
244
+ "GET",
245
+ f"{self.api_url}/api/v1/private/order/deal_details/{order_id}",
246
+ )
247
+ return self.ret_content(res)
@@ -0,0 +1,12 @@
1
+ import asyncio
2
+ import pybotters
3
+
4
+
5
+ class Heartbeat:
6
+ @staticmethod
7
+ async def ourbit(ws: pybotters.ws.ClientWebSocketResponse):
8
+ while not ws.closed:
9
+ await ws.send_str('{"method":"ping"}')
10
+ await asyncio.sleep(10.0)
11
+
12
+ pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.26
3
+ Version: 0.31
4
4
  Summary: A minimal yet hyper-efficient backtesting framework for quantitative trading
5
5
  Project-URL: Homepage, https://github.com/yourusername/hyperquant
6
6
  Project-URL: Issues, https://github.com/yourusername/hyperquant/issues
@@ -1,16 +1,21 @@
1
- hyperquant/__init__.py,sha256=gUuAPVpg5k8X_dpda5OpqmMyZ-ZXNQq-xwx-6JR5Jr4,131
1
+ hyperquant/__init__.py,sha256=UpjiX4LS5jmrBc2kE8RiLR02eCfD8JDQrR1q8zkLNcQ,161
2
2
  hyperquant/core.py,sha256=7XrpuHvccWl9lNyVihqaptupqUMsG3xYmQr8eEDrwS4,20610
3
3
  hyperquant/db.py,sha256=i2TjkCbmH4Uxo7UTDvOYBfy973gLcGexdzuT_YcSeIE,6678
4
4
  hyperquant/draw.py,sha256=up_lQ3pHeVLoNOyh9vPjgNwjD0M-6_IetSGviQUgjhY,54624
5
5
  hyperquant/logkit.py,sha256=WALpXpIA3Ywr5DxKKK3k5EKubZ2h-ISGfc5dUReQUBQ,7795
6
6
  hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
7
- hyperquant/broker/hyperliquid.py,sha256=mSBVfmjcv6ciI1vWrmHYwBOTHrg-NQrwcyVFUXYEgVw,21998
7
+ hyperquant/broker/auth.py,sha256=hrdVuBTZwD3xSy5GI5JTFJhKiHhvIjZHA_7G5IUFznQ,1580
8
+ hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
9
+ hyperquant/broker/ourbit.py,sha256=VE1eLvQ1hakOxWbXHmW8KXwYrP_Apsv0_FOPtmU3MKs,8033
10
+ hyperquant/broker/ws.py,sha256=98Djt5n5sHUJKVbQ8Ql1t-G-Wiwu__4MYcUr5P6SDL0,326
8
11
  hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
9
12
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
13
+ hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
14
+ hyperquant/broker/models/ourbit.py,sha256=zvjtx6fmOuOPAJ8jxD2RK9_mao2XuE2EXkxR74nmXKM,18525
10
15
  hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
11
16
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
12
17
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
13
18
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
14
- hyperquant-0.26.dist-info/METADATA,sha256=YAc40ZCIrhQE-5pyXnxPRz3bVSToOyJKiB2XHFspzZs,4317
15
- hyperquant-0.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
16
- hyperquant-0.26.dist-info/RECORD,,
19
+ hyperquant-0.31.dist-info/METADATA,sha256=ZfMLDTVVmQdtj-qCg1kwN1fl1TJfRpzHki-6AoUnhEI,4317
20
+ hyperquant-0.31.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
21
+ hyperquant-0.31.dist-info/RECORD,,