hyperquant 0.71__py3-none-any.whl → 0.73__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.
@@ -1,10 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- import asyncio
4
- import itertools
5
3
  import logging
6
- import time
7
- from typing import Any, Iterable, Literal
4
+ from typing import Any, Literal
8
5
 
9
6
  import pybotters
10
7
 
@@ -14,9 +11,11 @@ from .lib.util import fmt_value
14
11
  logger = logging.getLogger(__name__)
15
12
 
16
13
 
17
-
18
14
  class Bitget:
19
- """Bitget public market-data client (REST + WS)."""
15
+ """Bitget public/privileged client (REST + WS).
16
+
17
+ 默认只支持单向持仓(One-way mode)。
18
+ """
20
19
 
21
20
  def __init__(
22
21
  self,
@@ -34,74 +33,259 @@ class Bitget:
34
33
 
35
34
  self._ws_app = None
36
35
 
37
-
38
36
  async def __aenter__(self) -> "Bitget":
39
37
  await self.update("detail")
40
38
  return self
41
39
 
42
- async def __aexit__(self, exc_type, exc, tb) -> None:
40
+ async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
43
41
  pass
44
42
 
43
+ async def sub_personal(self) -> None:
44
+ sub_msg = {
45
+ "op": "subscribe",
46
+ "args": [
47
+ {"instType": "USDT-FUTURES", "channel": "orders", "instId": "default"},
48
+ {
49
+ "instType": "USDT-FUTURES",
50
+ "channel": "positions",
51
+ "instId": "default",
52
+ },
53
+ {"instType": "USDT-FUTURES", "channel": "account", "coin": "default"},
54
+ ],
55
+ }
56
+ self.client.ws_connect(
57
+ self.ws_url_private,
58
+ send_json=sub_msg,
59
+ hdlr_json=self.store.onmessage,
60
+ )
61
+
45
62
  async def update(
46
63
  self,
47
- update_type: Literal["detail", 'ticker', 'all'] = "all",
64
+ update_type: Literal["detail", "ticker", "all"] = "all",
48
65
  ) -> None:
49
- fet = []
50
- if update_type in ("detail", "all"):
51
- fet.append(
66
+ """Refresh cached REST resources."""
67
+
68
+ requests: list[Any] = []
69
+
70
+ if update_type in {"detail", "all"}:
71
+ requests.append(
52
72
  self.client.get(
53
- f"{self.rest_api}/api/v2/mix/market/contracts?productType=usdt-futures",
73
+ f"{self.rest_api}/api/v2/mix/market/contracts",
74
+ params={"productType": "usdt-futures"},
54
75
  )
55
76
  )
56
- elif update_type in ("ticker", "all"):
57
- fet.append(
77
+
78
+ if update_type in {"ticker", "all"}:
79
+ requests.append(
58
80
  self.client.get(
59
- f"{self.rest_api}/api/v2/mix/market/tickers?productType=usdt-futures",
81
+ f"{self.rest_api}/api/v2/mix/market/tickers",
82
+ params={"productType": "usdt-futures"},
60
83
  )
61
84
  )
62
-
63
- await self.store.initialize(*fet)
85
+
86
+ if not requests:
87
+ raise ValueError(f"update_type err: {update_type}")
88
+
89
+ await self.store.initialize(*requests)
64
90
 
65
91
  async def place_order(
66
92
  self,
67
93
  symbol: str,
68
94
  *,
69
- direction: Literal["buy", "sell", "0", "1"],
95
+ direction: Literal["buy", "sell", "long", "short", "0", "1"],
70
96
  volume: float,
71
97
  price: float | None = None,
72
- order_type: Literal["market", "limit_ioc", "limit_gtc"] = "market",
73
- offset_flag: Literal["open", "close", "0", "1"] = "open",
74
- exchange_id: str = "Exchange",
75
- product_group: str = "SwapU",
76
- order_proportion: str = "0.0000",
98
+ order_type: Literal[
99
+ "market",
100
+ "limit_gtc",
101
+ "limit_ioc",
102
+ "limit_fok",
103
+ "limit_post_only",
104
+ "limit",
105
+ ] = "market",
106
+ margin_mode: Literal["isolated", "crossed"] = "crossed",
107
+ product_type: str = "USDT-FUTURES",
108
+ margin_coin: str = "USDT",
109
+ reduce_only: bool | None = None,
110
+ offset_flag: Literal["open", "close", "0", "1"] | None = None,
77
111
  client_order_id: str | None = None,
112
+ extra_params: dict[str, Any] | None = None,
78
113
  ) -> dict[str, Any]:
79
- pass
114
+ """Create an order via ``POST /api/v2/mix/order/place-order``.
115
+
116
+ Parameters mirror the Bitget V2 contract REST API but are normalized to
117
+ match the broker interface (``direction``, ``order_type`` etc.).
118
+ """
119
+
120
+ side = self._normalize_direction(direction)
121
+ order_type_code, force_code = self._resolve_order_type(order_type)
122
+
123
+ if reduce_only is None:
124
+ reduce_only = self._normalize_offset(offset_flag)
125
+
126
+ detail = self._get_detail_entry(symbol)
127
+ volume_str = self._format_with_step(
128
+ volume, detail.get("step_size") or detail.get("stepSize")
129
+ )
130
+
131
+ payload: dict[str, Any] = {
132
+ "symbol": symbol,
133
+ "productType": product_type,
134
+ "marginMode": margin_mode,
135
+ "marginCoin": margin_coin,
136
+ "side": side,
137
+ "size": volume_str,
138
+ "orderType": order_type_code,
139
+ }
140
+
141
+ if force_code:
142
+ payload["force"] = force_code
143
+
144
+ if order_type_code == "limit":
145
+ if price is None:
146
+ raise ValueError("price is required for Bitget limit orders")
147
+ payload["price"] = self._format_with_step(
148
+ price,
149
+ detail.get("tick_size") or detail.get("tickSize"),
150
+ )
151
+ elif price is not None:
152
+ logger.debug("Price %.8f ignored for market order", price)
153
+
154
+ if reduce_only is True:
155
+ payload["reduceOnly"] = "YES"
156
+ elif reduce_only is False:
157
+ payload["reduceOnly"] = "NO"
158
+
159
+ if client_order_id:
160
+ payload["clientOid"] = client_order_id
161
+
162
+ if extra_params:
163
+ payload.update(extra_params)
164
+
165
+ res = await self.client.post(
166
+ f"{self.rest_api}/api/v2/mix/order/place-order",
167
+ data=payload,
168
+ )
169
+ data = await res.json()
170
+ return self._ensure_ok("place_order", data)
80
171
 
81
172
  async def cancel_order(
82
173
  self,
83
174
  order_sys_id: str,
84
175
  *,
85
- action_flag: str | int = "1",
176
+ symbol: str,
177
+ margin_mode: Literal["isolated", "crossed"],
178
+ product_type: str = "USDT-FUTURES",
179
+ margin_coin: str = "USDT",
180
+ client_order_id: str | None = None,
86
181
  ) -> dict[str, Any]:
87
- pass
182
+ """Cancel an order via ``POST /api/v2/mix/order/cancel-order``."""
88
183
 
184
+ payload = {
185
+ "symbol": symbol,
186
+ "productType": product_type,
187
+ "marginMode": margin_mode,
188
+ "marginCoin": margin_coin,
189
+ }
89
190
 
90
- async def sub_orderbook(self, symbols: list[str], channel: str = 'books1') -> None:
91
- """订阅指定交易对的订单簿(遵循 LBank 协议)。
92
- """
191
+ if client_order_id:
192
+ payload["clientOid"] = client_order_id
193
+ else:
194
+ payload["orderId"] = order_sys_id
93
195
 
94
- submsg = {
95
- "op": "subscribe",
96
- "args": []
97
- }
196
+ res = await self.client.post(
197
+ f"{self.rest_api}/api/v2/mix/order/cancel-order",
198
+ json=payload,
199
+ )
200
+ data = await res.json()
201
+ return self._ensure_ok("cancel_order", data)
202
+
203
+ async def sub_orderbook(self, symbols: list[str], channel: str = "books1") -> None:
204
+ """Subscribe to Bitget order-book snapshots/updates."""
205
+
206
+ submsg = {"op": "subscribe", "args": []}
98
207
  for symbol in symbols:
99
208
  submsg["args"].append(
100
- {"instType": "SPOT", "channel": channel, "instId": symbol}
209
+ {"instType": "USDT-FUTURES", "channel": channel, "instId": symbol}
101
210
  )
102
211
 
103
212
  self.client.ws_connect(
104
213
  self.ws_url,
105
214
  send_json=submsg,
106
- hdlr_json=self.store.onmessage
107
- )
215
+ hdlr_json=self.store.onmessage,
216
+ )
217
+
218
+ def _get_detail_entry(self, symbol: str) -> dict[str, Any]:
219
+ detail = self.store.detail.get({"symbol": symbol})
220
+ if not detail:
221
+ raise ValueError(
222
+ f"Unknown Bitget instrument: {symbol}. Call update('detail') first or provide valid symbol."
223
+ )
224
+ return detail
225
+
226
+ @staticmethod
227
+ def _format_with_step(value: float, step: Any) -> str:
228
+ if step in (None, 0, "0"):
229
+ return str(value)
230
+ try:
231
+ step_float = float(step)
232
+ except (TypeError, ValueError): # pragma: no cover - defensive guard
233
+ return str(value)
234
+ if step_float <= 0:
235
+ return str(value)
236
+ return fmt_value(value, step_float)
237
+
238
+ @staticmethod
239
+ def _normalize_direction(direction: str) -> str:
240
+ mapping = {
241
+ "buy": "buy",
242
+ "long": "buy",
243
+ "0": "buy",
244
+ "sell": "sell",
245
+ "short": "sell",
246
+ "1": "sell",
247
+ }
248
+ key = str(direction).lower()
249
+ try:
250
+ return mapping[key]
251
+ except KeyError as exc: # pragma: no cover - guard
252
+ raise ValueError(f"Unsupported direction: {direction}") from exc
253
+
254
+ @staticmethod
255
+ def _normalize_offset(
256
+ offset: Literal["open", "close", "0", "1"] | None,
257
+ ) -> bool | None:
258
+ if offset is None:
259
+ return None
260
+ mapping = {
261
+ "open": False,
262
+ "0": False,
263
+ "close": True,
264
+ "1": True,
265
+ }
266
+ key = str(offset).lower()
267
+ if key in mapping:
268
+ return mapping[key]
269
+ raise ValueError(f"Unsupported offset_flag: {offset}")
270
+
271
+ @staticmethod
272
+ def _resolve_order_type(order_type: str) -> tuple[str, str | None]:
273
+ mapping = {
274
+ "market": ("market", None),
275
+ "limit": ("limit", "gtc"),
276
+ "limit_gtc": ("limit", "gtc"),
277
+ "limit_ioc": ("limit", "ioc"),
278
+ "limit_fok": ("limit", "fok"),
279
+ "limit_post_only": ("limit", "post_only"),
280
+ }
281
+ key = str(order_type).lower()
282
+ try:
283
+ return mapping[key]
284
+ except KeyError as exc: # pragma: no cover - guard
285
+ raise ValueError(f"Unsupported order_type: {order_type}") from exc
286
+
287
+ @staticmethod
288
+ def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
289
+ if not isinstance(data, dict) or data.get("code") != "00000":
290
+ raise RuntimeError(f"{operation} failed: {data}")
291
+ return data.get("data") or {}
@@ -16,13 +16,18 @@ class Detail(DataStore):
16
16
  _KEYS = ["symbol"]
17
17
 
18
18
  def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
19
-
20
- step_size = entry.get('volume_place', 1)
21
- tick_size = entry.get('price_place', 1)
22
- step_size = place_to_step(step_size)
23
- tick_size = place_to_step(tick_size)
24
- entry['tickSize'] = tick_size
25
- entry['stepSize'] = step_size
19
+ step_place = entry.get("volume_place", 1)
20
+ tick_place = entry.get("price_place", 1)
21
+
22
+ step_size = place_to_step(step_place)
23
+ tick_size = place_to_step(tick_place)
24
+
25
+ entry["stepSize"] = step_size
26
+ entry["tickSize"] = tick_size
27
+ # expose snake_case aliases for downstream callers keeping legacy naming
28
+ entry["step_size"] = step_size
29
+ entry["tick_size"] = tick_size
30
+
26
31
  return entry
27
32
 
28
33
  def _onresponse(self, data: list[dict[str, Any]] | dict[str, Any] | None) -> None:
@@ -280,4 +285,75 @@ class BitgetDataStore(BitgetV2DataStore):
280
285
  }
281
286
  ]
282
287
  """
283
- return self._get("book")
288
+ return self._get("book")
289
+
290
+ @property
291
+ def account(self) -> DataStore:
292
+ """
293
+ _KEYS = ["instType", "marginCoin"]
294
+
295
+ Data Structure:
296
+
297
+ .. code:: json
298
+
299
+ [
300
+ {
301
+ "marginCoin": "USDT",
302
+ "frozen": "0.00000000",
303
+ "available": "13.98545761",
304
+ "maxOpenPosAvailable": "13.98545761",
305
+ "maxTransferOut": "13.98545761",
306
+ "equity": "13.98545761",
307
+ "usdtEquity": "13.985457617660",
308
+ "crossedRiskRate": "0",
309
+ "unrealizedPL": "0.000000000000",
310
+ "unionTotalMargin": "100",
311
+ "unionAvailable": "20",
312
+ "unionMm": "15",
313
+ "assetMode": "union"
314
+ }
315
+ ]
316
+ """
317
+ return self._get("account")
318
+
319
+ @property
320
+ def position(self) -> DataStore:
321
+ """
322
+ _KEYS = ["instType", "instId", "posId"]
323
+
324
+ Data Structure:
325
+
326
+ .. code:: json
327
+
328
+ [
329
+ {
330
+ "posId": "1",
331
+ "instId": "ETHUSDT",
332
+ "marginCoin": "USDT",
333
+ "marginSize": "9.5",
334
+ "marginMode": "crossed",
335
+ "holdSide": "short",
336
+ "posMode": "hedge_mode",
337
+ "total": "0.1",
338
+ "available": "0.1",
339
+ "frozen": "0",
340
+ "openPriceAvg": "1900",
341
+ "leverage": 20,
342
+ "achievedProfits": "0",
343
+ "unrealizedPL": "0",
344
+ "unrealizedPLR": "0",
345
+ "liquidationPrice": "5788.108475905242",
346
+ "keepMarginRate": "0.005",
347
+ "marginRate": "0.004416374196",
348
+ "cTime": "1695649246169",
349
+ "breakEvenPrice": "24778.97",
350
+ "totalFee": "1.45",
351
+ "deductedFee": "0.388",
352
+ "markPrice": "2500",
353
+ "assetMode": "union",
354
+ "uTime": "1695711602568",
355
+ "autoMargin": "off"
356
+ }
357
+ ]
358
+ """
359
+ return self._get("position")
@@ -184,7 +184,8 @@ class Orders(DataStore):
184
184
  "side": direction,
185
185
  "offset": offset_flag,
186
186
  "order_type": order_kind,
187
- "price": entry.get("Price"),
187
+ "price": entry.get('TradePrice') or entry.get("Price"),
188
+ "order_price": entry.get("OrderPrice") or entry.get('Price'),
188
189
  "quantity": entry.get("Volume"),
189
190
  "filled": entry.get("VolumeTraded"),
190
191
  "remaining": entry.get("VolumeRemain"),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.71
3
+ Version: 0.73
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
@@ -5,7 +5,7 @@ hyperquant/draw.py,sha256=up_lQ3pHeVLoNOyh9vPjgNwjD0M-6_IetSGviQUgjhY,54624
5
5
  hyperquant/logkit.py,sha256=nUo7nx5eONvK39GOhWwS41zNRL756P2J7-5xGzwXnTY,8462
6
6
  hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
7
7
  hyperquant/broker/auth.py,sha256=Wst7mTBuUS2BQ5hZd0a8FNNs5Uc01ac9WzJpseTuyAY,7673
8
- hyperquant/broker/bitget.py,sha256=G13WRACqxuHfxNh0nKB-11VF02XDsgb0vM1g19eOo2A,2861
8
+ hyperquant/broker/bitget.py,sha256=PEzULGJJQeQ91TKa4F56WhEpcnUHC3WpIx1pi5UXpVQ,9182
9
9
  hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
10
10
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
11
11
  hyperquant/broker/lbank.py,sha256=dZUbi0a_Vhkp4pJ1V11X6nEM7I4HhQIVRgpSMeGcAMU,11681
@@ -15,15 +15,15 @@ hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQ
15
15
  hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
16
16
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
17
17
  hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
18
- hyperquant/broker/models/bitget.py,sha256=swKYa7-jRoCSQxg0AUTZ7lPEhD1vxYIKm49eaIPwMxU,8961
18
+ hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
19
19
  hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
20
20
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
21
- hyperquant/broker/models/lbank.py,sha256=ZCD1dOUMyWPT8lKDj6C6LcHEof2d0JN384McURzLA-s,18868
21
+ hyperquant/broker/models/lbank.py,sha256=_ztqhYGDqEHpmSITfYp_cpEJiJrhfJ9dwZ52K_PhE2A,18969
22
22
  hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
23
23
  hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
24
24
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
25
25
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
26
26
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
27
- hyperquant-0.71.dist-info/METADATA,sha256=ldQcPawGZVC-9RTCaAFt4lE5SAw26fPVjbEfjk04xLE,4317
28
- hyperquant-0.71.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
- hyperquant-0.71.dist-info/RECORD,,
27
+ hyperquant-0.73.dist-info/METADATA,sha256=nAkYdNGtpPFIhgtTm1JWCQCK46kJyAv2EdLrcfTh1nU,4317
28
+ hyperquant-0.73.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
29
+ hyperquant-0.73.dist-info/RECORD,,