hyperquant 0.65__py3-none-any.whl → 0.67__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.
@@ -2,11 +2,9 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import itertools
5
- import json
6
5
  import logging
7
6
  import time
8
- import zlib
9
- from typing import Iterable, Literal
7
+ from typing import Any, Iterable, Literal
10
8
 
11
9
  import pybotters
12
10
 
@@ -36,6 +34,7 @@ class Lbank:
36
34
  self.ws_url = ws_url or "wss://uuws.rerrkvifj.com/ws/v3"
37
35
  self._req_id = itertools.count(int(time.time() * 1000))
38
36
  self._ws_app = None
37
+ self._rest_headers = {"source": "4", "versionflage": "true"}
39
38
 
40
39
  async def __aenter__(self) -> "Lbank":
41
40
  await self.update("detail")
@@ -44,26 +43,245 @@ class Lbank:
44
43
  async def __aexit__(self, exc_type, exc, tb) -> None:
45
44
  pass
46
45
 
47
- async def update(self, update_type: Literal["detail", "ticker", "all"]) -> list[dict]:
48
- all_urls = [f"{self.front_api}/cfd/agg/v1/instrument"]
49
- url_map = {"detail": [all_urls[0]], "all": all_urls}
46
+ async def update(
47
+ self,
48
+ update_type: Literal["detail", "balance", "position", "orders", "orders_finish", "all"] = "all",
49
+ *,
50
+ product_group: str = "SwapU",
51
+ exchange_id: str = "Exchange",
52
+ asset: str = "USDT",
53
+ instrument_id: str | None = None,
54
+ page_index: int = 1,
55
+ page_size: int = 1000,
56
+ ) -> None:
57
+ """Refresh local caches via REST endpoints.
50
58
 
59
+ Parameters mirror the documented REST API default arguments.
60
+ """
51
61
 
52
- try:
53
- urls = url_map[update_type]
54
- except KeyError:
55
- raise ValueError(f"update_type err: {update_type}")
62
+ requests: list[Any] = []
63
+
64
+ include_detail = update_type in {"detail", "all"}
65
+ include_orders = update_type in {"orders", "all"}
66
+ include_position = update_type in {"position", "all"}
67
+ include_balance = update_type in {"balance", "all"}
68
+
69
+ if update_type == "orders_finish":
70
+ await self.update_finish_order(
71
+ product_group=product_group,
72
+ page_index=page_index,
73
+ page_size=page_size,
74
+ )
75
+ return
76
+
77
+ if include_detail:
78
+ requests.append(
79
+ self.client.post(
80
+ f"{self.front_api}/cfd/agg/v1/instrument",
81
+ json={"ProductGroup": product_group},
82
+ headers=self._rest_headers,
83
+ )
84
+ )
85
+
86
+ if include_orders:
87
+ requests.append(
88
+ self.client.get(
89
+ f"{self.front_api}/cfd/query/v1.0/Order",
90
+ params={
91
+ "ProductGroup": product_group,
92
+ "ExchangeID": exchange_id,
93
+ "pageIndex": page_index,
94
+ "pageSize": page_size,
95
+ },
96
+ headers=self._rest_headers,
97
+ )
98
+ )
99
+
100
+ if include_position:
101
+ requests.append(
102
+ self.client.get(
103
+ f"{self.front_api}/cfd/query/v1.0/Position",
104
+ params={
105
+ "ProductGroup": product_group,
106
+ "Valid": 1,
107
+ "pageIndex": page_index,
108
+ "pageSize": page_size,
109
+ },
110
+ headers=self._rest_headers,
111
+ )
112
+ )
56
113
 
57
- # await self.store.initialize(*(self.client.get(url) for url in urls))
58
- if update_type == "detail" or update_type == "all":
59
- await self.store.initialize(
114
+ if include_balance:
115
+ resolved_instrument = instrument_id or self._resolve_instrument()
116
+ if not resolved_instrument:
117
+ raise ValueError(
118
+ "instrument_id is required to query balance; call update('detail') first or provide instrument_id explicitly."
119
+ )
120
+ self.store.balance.set_asset(asset)
121
+ requests.append(
60
122
  self.client.post(
61
- all_urls[0],
62
- json={"ProductGroup": "SwapU"},
63
- headers={"source": "4", "versionflage": "true"},
123
+ f"{self.front_api}/cfd/agg/v1/sendQryAll",
124
+ json={
125
+ "productGroup": product_group,
126
+ "instrumentID": resolved_instrument,
127
+ "asset": asset,
128
+ },
129
+ headers=self._rest_headers,
64
130
  )
65
131
  )
66
132
 
133
+ if not requests:
134
+ raise ValueError(f"update_type err: {update_type}")
135
+
136
+ await self.store.initialize(*requests)
137
+
138
+ def _resolve_instrument(self) -> str | None:
139
+ detail_entries = self.store.detail.find()
140
+ if detail_entries:
141
+ return detail_entries[0].get("symbol")
142
+ return None
143
+
144
+ async def update_finish_order(
145
+ self,
146
+ *,
147
+ product_group: str = "SwapU",
148
+ page_index: int = 1,
149
+ page_size: int = 200,
150
+ start_time: int | None = None,
151
+ end_time: int | None = None,
152
+ instrument_id: str | None = None,
153
+ ) -> None:
154
+ """Fetch finished orders within the specified time window (default: last hour)."""
155
+
156
+ now_ms = int(time.time() * 1000)
157
+ if end_time is None:
158
+ end_time = now_ms
159
+ if start_time is None:
160
+ start_time = end_time - 60 * 60 * 1000
161
+ if start_time >= end_time:
162
+ raise ValueError("start_time must be earlier than end_time")
163
+
164
+ params: dict[str, Any] = {
165
+ "ProductGroup": product_group,
166
+ "pageIndex": page_index,
167
+ "pageSize": page_size,
168
+ "startTime": start_time,
169
+ "endTime": end_time,
170
+ }
171
+ if instrument_id:
172
+ params["InstrumentID"] = instrument_id
173
+
174
+ await self.store.initialize(
175
+ self.client.get(
176
+ f"{self.front_api}/cfd/cff/v1/FinishOrder",
177
+ params=params,
178
+ headers=self._rest_headers,
179
+ )
180
+ )
181
+
182
+ async def place_order(
183
+ self,
184
+ symbol: str,
185
+ *,
186
+ direction: Literal["buy", "sell", "0", "1"],
187
+ volume: float,
188
+ price: float | None = None,
189
+ order_type: Literal["market", "limit_ioc", "limit_gtc"] = "market",
190
+ offset_flag: Literal["open", "close", "0", "1"] = "open",
191
+ exchange_id: str = "Exchange",
192
+ product_group: str = "SwapU",
193
+ order_proportion: str = "0.0000",
194
+ client_order_id: str | None = None,
195
+ ) -> dict[str, Any]:
196
+ """Create an order using documented REST parameters."""
197
+
198
+ direction_code = self._normalize_direction(direction)
199
+ offset_code = self._normalize_offset(offset_flag)
200
+ price_type_code, order_type_code = self._resolve_order_type(order_type)
201
+
202
+ payload: dict[str, Any] = {
203
+ # "ProductGroup": product_group,
204
+ "InstrumentID": symbol,
205
+ "ExchangeID": exchange_id,
206
+ "Direction": direction_code,
207
+ "OffsetFlag": offset_code,
208
+ "OrderPriceType": price_type_code,
209
+ "OrderType": order_type_code,
210
+ "Volume": str(volume),
211
+ "orderProportion": order_proportion,
212
+ }
213
+
214
+ if price_type_code == "0":
215
+ if price is None:
216
+ raise ValueError("price is required for limit orders")
217
+ payload["Price"] = price
218
+ elif price is not None:
219
+ logger.warning("Price is ignored for market orders")
220
+
221
+ # if client_order_id:
222
+ # payload["LocalID"] = client_order_id
223
+ print(payload)
224
+ res = await self.client.post(
225
+ f"{self.front_api}/cfd/cff/v1/SendOrderInsert",
226
+ json=payload,
227
+ headers=self._rest_headers,
228
+ )
229
+ data = await res.json()
230
+ return self._ensure_ok("place_order", data)
231
+
232
+ async def cancel_order(
233
+ self,
234
+ order_sys_id: str,
235
+ *,
236
+ action_flag: str | int = "1",
237
+ ) -> dict[str, Any]:
238
+ """Cancel an order by OrderSysID."""
239
+
240
+ payload = {"OrderSysID": order_sys_id, "ActionFlag": str(action_flag)}
241
+ res = await self.client.post(
242
+ f"{self.front_api}/cfd/action/v1.0/SendOrderAction",
243
+ json=payload,
244
+ headers=self._rest_headers,
245
+ )
246
+ data = await res.json()
247
+ return self._ensure_ok("cancel_order", data)
248
+
249
+ @staticmethod
250
+ def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
251
+ if not isinstance(data, dict) or data.get("code") != 200:
252
+ raise RuntimeError(f"{operation} failed: {data}")
253
+ return data.get("data") or {}
254
+
255
+ @staticmethod
256
+ def _normalize_direction(direction: str) -> str:
257
+ mapping = {
258
+ "buy": "0",
259
+ "long": "0",
260
+ "sell": "1",
261
+ "short": "1",
262
+ }
263
+ return mapping.get(str(direction).lower(), str(direction))
264
+
265
+ @staticmethod
266
+ def _normalize_offset(offset: str) -> str:
267
+ mapping = {
268
+ "open": "0",
269
+ "close": "1",
270
+ }
271
+ return mapping.get(str(offset).lower(), str(offset))
272
+
273
+ @staticmethod
274
+ def _resolve_order_type(order_type: str) -> tuple[str, str]:
275
+ mapping = {
276
+ "market": ("4", "1"),
277
+ "limit_ioc": ("0", "1"),
278
+ "limit_gtc": ("0", "0"),
279
+ }
280
+ try:
281
+ return mapping[str(order_type).lower()]
282
+ except KeyError as exc: # pragma: no cover - guard
283
+ raise ValueError(f"Unsupported order_type: {order_type}") from exc
284
+
67
285
 
68
286
  async def sub_orderbook(self, symbols: list[str], limit: int | None = None) -> None:
69
287
  """订阅指定交易对的订单簿(遵循 LBank 协议)。
@@ -107,4 +325,4 @@ class Lbank:
107
325
  batch = send_jsons[i:i+5]
108
326
  await asyncio.gather(*(sub(send_json) for send_json in batch))
109
327
  if i + 5 < len(send_jsons):
110
- await asyncio.sleep(0.1)
328
+ await asyncio.sleep(0.1)