hyperquant 0.7__py3-none-any.whl → 0.9__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.

Potentially problematic release.


This version of hyperquant might be problematic. Click here for more details.

hyperquant/broker/auth.py CHANGED
@@ -198,6 +198,75 @@ class Auth:
198
198
 
199
199
  return args
200
200
 
201
+ @staticmethod
202
+ def coinw(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
203
+ method: str = args[0]
204
+ url: URL = args[1]
205
+ headers: CIMultiDict = kwargs["headers"]
206
+
207
+ session = kwargs["session"]
208
+ try:
209
+ api_key, secret, _ = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name]
210
+ except (KeyError, ValueError):
211
+ raise RuntimeError("CoinW credentials (api_key, secret) are required")
212
+
213
+ timestamp = str(int(time.time() * 1000))
214
+ method_upper = method.upper()
215
+
216
+ params = kwargs.get("params")
217
+ query_string = ""
218
+ if isinstance(params, dict) and params:
219
+ query_items = [
220
+ f"{key}={value}"
221
+ for key, value in params.items()
222
+ if value is not None
223
+ ]
224
+ query_string = "&".join(query_items)
225
+ elif url.query_string:
226
+ query_string = url.query_string
227
+
228
+ body_str = ""
229
+
230
+ if method_upper == "GET":
231
+ body = None
232
+ data = None
233
+ else:
234
+ body = kwargs.get("json")
235
+ data = kwargs.get("data")
236
+ payload = body if body is not None else data
237
+ if isinstance(payload, (dict, list)):
238
+ body_str = pyjson.dumps(payload, separators=(",", ":"), ensure_ascii=False)
239
+ kwargs["data"] = body_str
240
+ kwargs.pop("json", None)
241
+ elif payload is not None:
242
+ body_str = str(payload)
243
+ kwargs["data"] = body_str
244
+ kwargs.pop("json", None)
245
+
246
+ if query_string:
247
+ path = f"{url.raw_path}?{query_string}"
248
+ else:
249
+ path = url.raw_path
250
+
251
+ message = f"{timestamp}{method_upper}{path}{body_str}"
252
+ signature = hmac.new(
253
+ secret, message.encode("utf-8"), hashlib.sha256
254
+ ).digest()
255
+ signature_b64 = base64.b64encode(signature).decode("ascii")
256
+
257
+ headers.update(
258
+ {
259
+ "sign": signature_b64,
260
+ "api_key": api_key,
261
+ "timestamp": timestamp,
262
+ }
263
+ )
264
+
265
+ if method_upper in {"POST", "PUT", "PATCH", "DELETE"} and "data" in kwargs:
266
+ headers.setdefault("Content-Type", "application/json")
267
+
268
+ return args
269
+
201
270
  pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
202
271
  "ourbit", Auth.ourbit
203
272
  )
@@ -220,4 +289,8 @@ pybotters.auth.Hosts.items["quote.edgex.exchange"] = pybotters.auth.Item(
220
289
 
221
290
  pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
222
291
  "lbank", Auth.lbank
223
- )
292
+ )
293
+
294
+ pybotters.auth.Hosts.items["api.coinw.com"] = pybotters.auth.Item(
295
+ "coinw", Auth.coinw
296
+ )
@@ -1,10 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import asyncio
4
- import itertools
5
4
  import logging
6
- import time
7
- from typing import Any, Iterable, Literal
5
+ import uuid
6
+ from typing import Any, Literal
8
7
 
9
8
  import pybotters
10
9
 
@@ -14,9 +13,11 @@ from .lib.util import fmt_value
14
13
  logger = logging.getLogger(__name__)
15
14
 
16
15
 
17
-
18
16
  class Bitget:
19
- """Bitget public market-data client (REST + WS)."""
17
+ """Bitget public/privileged client (REST + WS).
18
+
19
+ 默认只支持单向持仓(One-way mode)。
20
+ """
20
21
 
21
22
  def __init__(
22
23
  self,
@@ -32,70 +33,279 @@ class Bitget:
32
33
  self.ws_url = ws_url or "wss://ws.bitget.com/v2/ws/public"
33
34
  self.ws_url_private = ws_url or "wss://ws.bitget.com/v2/ws/private"
34
35
 
35
- self._ws_app = None
36
-
36
+ self.ws_app = None
37
+ self.has_sub_personal = False
38
+
37
39
 
38
40
  async def __aenter__(self) -> "Bitget":
39
41
  await self.update("detail")
40
42
  return self
41
43
 
42
- async def __aexit__(self, exc_type, exc, tb) -> None:
44
+ async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
43
45
  pass
44
46
 
47
+ async def sub_personal(self) -> None:
48
+ sub_msg = {
49
+ "op": "subscribe",
50
+ "args": [
51
+ {"instType": "USDT-FUTURES", "channel": "orders", "instId": "default"},
52
+ {
53
+ "instType": "USDT-FUTURES",
54
+ "channel": "positions",
55
+ "instId": "default",
56
+ },
57
+ {"instType": "USDT-FUTURES", "channel": "account", "coin": "default"},
58
+ ],
59
+ }
60
+ self.ws_app = await self._ensure_private_ws()
61
+
62
+
63
+ await self.ws_app.current_ws.send_json(sub_msg)
64
+
65
+
66
+ self.has_sub_personal = True
67
+
45
68
  async def update(
46
69
  self,
47
- update_type: Literal["detail", 'all'] = "all",
70
+ update_type: Literal["detail", "ticker", "all"] = "all",
48
71
  ) -> None:
49
- fet = []
50
- if update_type in ("detail", "all"):
51
- fet.append(
72
+ """Refresh cached REST resources."""
73
+
74
+ requests: list[Any] = []
75
+
76
+ if update_type in {"detail", "all"}:
77
+ requests.append(
52
78
  self.client.get(
53
- f"{self.rest_api}/api/v2/mix/market/contracts?productType=usdt-futures",
79
+ f"{self.rest_api}/api/v2/mix/market/contracts",
80
+ params={"productType": "usdt-futures"},
54
81
  )
55
82
  )
56
83
 
57
- await self.store.initialize(*fet)
84
+ if update_type in {"ticker", "all"}:
85
+ requests.append(
86
+ self.client.get(
87
+ f"{self.rest_api}/api/v2/mix/market/tickers",
88
+ params={"productType": "usdt-futures"},
89
+ )
90
+ )
91
+
92
+ if not requests:
93
+ raise ValueError(f"update_type err: {update_type}")
94
+
95
+ await self.store.initialize(*requests)
58
96
 
59
97
  async def place_order(
60
98
  self,
61
99
  symbol: str,
62
100
  *,
63
- direction: Literal["buy", "sell", "0", "1"],
101
+ direction: Literal["buy", "sell", "long", "short", "0", "1"],
64
102
  volume: float,
65
103
  price: float | None = None,
66
- order_type: Literal["market", "limit_ioc", "limit_gtc"] = "market",
67
- offset_flag: Literal["open", "close", "0", "1"] = "open",
68
- exchange_id: str = "Exchange",
69
- product_group: str = "SwapU",
70
- order_proportion: str = "0.0000",
71
- client_order_id: str | None = None,
72
- ) -> dict[str, Any]:
73
- pass
104
+ order_type: Literal[
105
+ "market",
106
+ "limit_gtc",
107
+ "limit_ioc",
108
+ "limit_fok",
109
+ "limit_post_only",
110
+ "limit",
111
+ ] = "market",
112
+ margin_mode: Literal["isolated", "crossed"] = "crossed",
113
+ product_type: str = "USDT-FUTURES",
114
+ margin_coin: str = "USDT",
115
+ reduce_only: bool | None = None,
116
+ offset_flag: Literal["open", "close", "0", "1"] | None = None,
117
+ client_order_id: str | None = None
118
+ ) -> dict[str, Any] | None:
119
+ """
120
+ 请求成功返回示例:
121
+
122
+ .. code:: json
123
+
124
+ {
125
+ "clientOid": "121211212122",
126
+ "orderId": "121211212122"
127
+ }
128
+ """
129
+
130
+ side = self._normalize_direction(direction)
131
+ order_type_code, force_code = self._resolve_order_type(order_type)
132
+
133
+ if reduce_only is None:
134
+ reduce_only = self._normalize_offset(offset_flag)
135
+
136
+ detail = self._get_detail_entry(symbol)
137
+ volume_str = self._format_with_step(
138
+ volume, detail.get("step_size") or detail.get("stepSize")
139
+ )
140
+
141
+ payload: dict[str, Any] = {
142
+ "symbol": symbol,
143
+ "productType": product_type,
144
+ "marginMode": margin_mode,
145
+ "marginCoin": margin_coin,
146
+ "side": side,
147
+ "size": volume_str,
148
+ "orderType": order_type_code,
149
+ }
150
+
151
+ if force_code:
152
+ payload["force"] = force_code
153
+
154
+ if order_type_code == "limit":
155
+ if price is None:
156
+ raise ValueError("price is required for Bitget limit orders")
157
+ payload["price"] = self._format_with_step(
158
+ price,
159
+ detail.get("tick_size") or detail.get("tickSize"),
160
+ )
161
+ elif price is not None:
162
+ logger.debug("Price %.8f ignored for market order", price)
163
+
164
+ if reduce_only is True:
165
+ payload["reduceOnly"] = "YES"
166
+ elif reduce_only is False:
167
+ payload["reduceOnly"] = "NO"
168
+
169
+ if client_order_id:
170
+ payload["clientOid"] = client_order_id
171
+
172
+ res = await self.client.post(
173
+ f"{self.rest_api}/api/v2/mix/order/place-order",
174
+ data=payload,
175
+ )
176
+ data = await res.json()
177
+ return self._ensure_ok("place_order", data)
74
178
 
75
179
  async def cancel_order(
76
180
  self,
77
181
  order_sys_id: str,
78
182
  *,
79
- action_flag: str | int = "1",
183
+ symbol: str,
184
+ margin_mode: Literal["isolated", "crossed"],
185
+ product_type: str = "USDT-FUTURES",
186
+ margin_coin: str = "USDT",
187
+ client_order_id: str | None = None,
80
188
  ) -> dict[str, Any]:
81
- pass
189
+ """Cancel an order via ``POST /api/v2/mix/order/cancel-order``."""
82
190
 
191
+ payload = {
192
+ "symbol": symbol,
193
+ "productType": product_type,
194
+ "marginMode": margin_mode,
195
+ "marginCoin": margin_coin,
196
+ }
83
197
 
84
- async def sub_orderbook(self, symbols: list[str], channel: str = 'books1') -> None:
85
- """订阅指定交易对的订单簿(遵循 LBank 协议)。
86
- """
198
+ if client_order_id:
199
+ payload["clientOid"] = client_order_id
200
+ else:
201
+ payload["orderId"] = order_sys_id
87
202
 
88
- submsg = {
89
- "op": "subscribe",
90
- "args": []
91
- }
203
+ res = await self.client.post(
204
+ f"{self.rest_api}/api/v2/mix/order/cancel-order",
205
+ data=payload,
206
+ )
207
+ data = await res.json()
208
+ return self._ensure_ok("cancel_order", data)
209
+
210
+ async def sub_orderbook(self, symbols: list[str], channel: str = "books1") -> None:
211
+ """Subscribe to Bitget order-book snapshots/updates."""
212
+
213
+ submsg = {"op": "subscribe", "args": []}
92
214
  for symbol in symbols:
93
215
  submsg["args"].append(
94
- {"instType": "SPOT", "channel": channel, "instId": symbol}
216
+ {"instType": "USDT-FUTURES", "channel": channel, "instId": symbol}
95
217
  )
96
218
 
97
219
  self.client.ws_connect(
98
220
  self.ws_url,
99
221
  send_json=submsg,
100
- hdlr_json=self.store.onmessage
101
- )
222
+ hdlr_json=self.store.onmessage,
223
+ )
224
+
225
+ def _get_detail_entry(self, symbol: str) -> dict[str, Any]:
226
+ detail = self.store.detail.get({"symbol": symbol})
227
+ if not detail:
228
+ raise ValueError(
229
+ f"Unknown Bitget instrument: {symbol}. Call update('detail') first or provide valid symbol."
230
+ )
231
+ return detail
232
+
233
+ async def _ensure_private_ws(self):
234
+ wsqueue = pybotters.WebSocketQueue()
235
+ ws_app = self.client.ws_connect(
236
+ self.ws_url_private,
237
+ hdlr_json=self.store.onmessage,
238
+ )
239
+ # async for msg in wsqueue:
240
+ # print(msg)
241
+
242
+ await ws_app._event.wait()
243
+ await ws_app.current_ws._wait_authtask()
244
+ return ws_app
245
+
246
+ @staticmethod
247
+ def _format_with_step(value: float, step: Any) -> str:
248
+ if step in (None, 0, "0"):
249
+ return str(value)
250
+ try:
251
+ step_float = float(step)
252
+ except (TypeError, ValueError): # pragma: no cover - defensive guard
253
+ return str(value)
254
+ if step_float <= 0:
255
+ return str(value)
256
+ return fmt_value(value, step_float)
257
+
258
+ @staticmethod
259
+ def _normalize_direction(direction: str) -> str:
260
+ mapping = {
261
+ "buy": "buy",
262
+ "long": "buy",
263
+ "0": "buy",
264
+ "sell": "sell",
265
+ "short": "sell",
266
+ "1": "sell",
267
+ }
268
+ key = str(direction).lower()
269
+ try:
270
+ return mapping[key]
271
+ except KeyError as exc: # pragma: no cover - guard
272
+ raise ValueError(f"Unsupported direction: {direction}") from exc
273
+
274
+ @staticmethod
275
+ def _normalize_offset(
276
+ offset: Literal["open", "close", "0", "1"] | None,
277
+ ) -> bool | None:
278
+ if offset is None:
279
+ return None
280
+ mapping = {
281
+ "open": False,
282
+ "0": False,
283
+ "close": True,
284
+ "1": True,
285
+ }
286
+ key = str(offset).lower()
287
+ if key in mapping:
288
+ return mapping[key]
289
+ raise ValueError(f"Unsupported offset_flag: {offset}")
290
+
291
+ @staticmethod
292
+ def _resolve_order_type(order_type: str) -> tuple[str, str | None]:
293
+ mapping = {
294
+ "market": ("market", None),
295
+ "limit": ("limit", "gtc"),
296
+ "limit_gtc": ("limit", "gtc"),
297
+ "limit_ioc": ("limit", "ioc"),
298
+ "limit_fok": ("limit", "fok"),
299
+ "limit_post_only": ("limit", "post_only"),
300
+ }
301
+ key = str(order_type).lower()
302
+ try:
303
+ return mapping[key]
304
+ except KeyError as exc: # pragma: no cover - guard
305
+ raise ValueError(f"Unsupported order_type: {order_type}") from exc
306
+
307
+ @staticmethod
308
+ def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
309
+ if not isinstance(data, dict) or data.get("code") != "00000":
310
+ raise RuntimeError(f"{operation} failed: {data}")
311
+ return data.get("data") or {}