hyperquant 0.66__py3-none-any.whl → 0.68__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/broker/auth.py CHANGED
@@ -170,6 +170,33 @@ class Auth:
170
170
  headers.update({"cookie": cookie})
171
171
  return args
172
172
 
173
+ @staticmethod
174
+ def lbank(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
175
+ method: str = args[0]
176
+ url: URL = args[1]
177
+ data = kwargs.get("data") or {}
178
+ headers: CIMultiDict = kwargs["headers"]
179
+
180
+ # 从 session 里取 api_key & secret
181
+ session = kwargs["session"]
182
+ token = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name][0]
183
+
184
+
185
+ # 设置 headers
186
+ headers.update(
187
+ {
188
+ "ex-language": 'zh-TW',
189
+ "ex-token": token,
190
+ "source": "4",
191
+ "versionflage": "true",
192
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0"
193
+ }
194
+ )
195
+
196
+ # 更新 kwargs.body,保证发出去的与签名一致
197
+ # kwargs.update({"data": raw_body_for_sign})
198
+
199
+ return args
173
200
 
174
201
  pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
175
202
  "ourbit", Auth.ourbit
@@ -178,6 +205,10 @@ pybotters.auth.Hosts.items["www.ourbit.com"] = pybotters.auth.Item(
178
205
  "ourbit", Auth.ourbit_spot
179
206
  )
180
207
 
208
+ pybotters.auth.Hosts.items["www.ourbit.com"] = pybotters.auth.Item(
209
+ "ourbit", Auth.ourbit_spot
210
+ )
211
+
181
212
  pybotters.auth.Hosts.items["pro.edgex.exchange"] = pybotters.auth.Item(
182
213
  "edgex", Auth.edgex
183
214
  )
@@ -185,4 +216,8 @@ pybotters.auth.Hosts.items["pro.edgex.exchange"] = pybotters.auth.Item(
185
216
 
186
217
  pybotters.auth.Hosts.items["quote.edgex.exchange"] = pybotters.auth.Item(
187
218
  "edgex", Auth.edgex
219
+ )
220
+
221
+ pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
222
+ "lbank", Auth.lbank
188
223
  )
@@ -126,29 +126,41 @@ class Edgex:
126
126
 
127
127
  async def update(
128
128
  self,
129
- update_type: Literal["balance", "position", "orders", "all"] = "all",
130
- ):
131
- """使用 REST 刷新本地缓存的账户资产、持仓及活动订单。"""
129
+ update_type: Literal["balance", "position", "orders", "ticker", "all"] = "all",
130
+ *,
131
+ contract_id: str | None = None,
132
+ ) -> None:
133
+ """使用 REST 刷新本地缓存的账户资产、持仓、活动订单与 24h 行情。"""
132
134
 
133
- if not getattr(self, "accountid", None):
135
+ requires_account = {"balance", "position", "orders", "all"}
136
+ if update_type in requires_account and not getattr(self, "accountid", None):
134
137
  raise ValueError("accountid not set; call sync_user() before update().")
135
138
 
136
- account_asset_url = (
137
- f"{self.api_url}/api/v1/private/account/getAccountAsset"
138
- f"?accountId={self.accountid}"
139
- )
140
- active_orders_url = (
141
- f"{self.api_url}/api/v1/private/order/getActiveOrderPage"
142
- f"?accountId={self.accountid}&size=200"
143
- )
144
-
145
- all_urls = [account_asset_url, active_orders_url]
146
-
147
- url_map = {
148
- "balance": [account_asset_url],
149
- "position": [account_asset_url],
150
- "orders": [active_orders_url],
151
- "all": all_urls,
139
+ account_asset_url = None
140
+ active_orders_url = None
141
+ if update_type in requires_account:
142
+ account_asset_url = (
143
+ f"{self.api_url}/api/v1/private/account/getAccountAsset"
144
+ f"?accountId={self.accountid}"
145
+ )
146
+ active_orders_url = (
147
+ f"{self.api_url}/api/v1/private/order/getActiveOrderPage"
148
+ f"?accountId={self.accountid}&size=200"
149
+ )
150
+
151
+ ticker_url = f"{self.api_url}/api/v1/public/quote/getTicker"
152
+ if contract_id:
153
+ ticker_url = f"{ticker_url}?contractId={contract_id}"
154
+
155
+ url_map: dict[str, list[str]] = {
156
+ "balance": [account_asset_url] if account_asset_url else [],
157
+ "position": [account_asset_url] if account_asset_url else [],
158
+ "orders": [active_orders_url] if active_orders_url else [],
159
+ "ticker": [ticker_url],
160
+ "all": [
161
+ *(url for url in (account_asset_url, active_orders_url) if url),
162
+ ticker_url,
163
+ ],
152
164
  }
153
165
 
154
166
  try:
@@ -261,7 +273,7 @@ class Edgex:
261
273
 
262
274
  url = ws_url or f"{self.ws_url}/api/v1/public/ws?timestamp=" + str(int(time.time() * 1000))
263
275
  payload = [{"type": "subscribe", "channel": ch} for ch in channels]
264
-
276
+ print(payload)
265
277
  wsapp = self.client.ws_connect(url, send_json=payload, hdlr_json=self.store.onmessage)
266
278
  await wsapp._event.wait()
267
279
 
@@ -448,8 +460,8 @@ class Edgex:
448
460
  if data.get("code") != "SUCCESS": # pragma: no cover - defensive guard
449
461
  raise RuntimeError(f"Failed to place Edgex order: {data}")
450
462
 
451
- # latency = int(data.get("responseTime",0)) - int(data.get("requestTime",0))
452
-
463
+ latency = int(data.get("responseTime",0)) - int(data.get("requestTime",0))
464
+ print(latency)
453
465
  order_id = data.get("data", {}).get("orderId")
454
466
  return order_id
455
467
 
@@ -2,15 +2,14 @@ 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
 
13
11
  from .models.lbank import LbankDataStore
12
+ from .lib.util import fmt_value
14
13
 
15
14
  logger = logging.getLogger(__name__)
16
15
 
@@ -36,6 +35,7 @@ class Lbank:
36
35
  self.ws_url = ws_url or "wss://uuws.rerrkvifj.com/ws/v3"
37
36
  self._req_id = itertools.count(int(time.time() * 1000))
38
37
  self._ws_app = None
38
+ self._rest_headers = {"source": "4", "versionflage": "true"}
39
39
 
40
40
  async def __aenter__(self) -> "Lbank":
41
41
  await self.update("detail")
@@ -44,26 +44,269 @@ class Lbank:
44
44
  async def __aexit__(self, exc_type, exc, tb) -> None:
45
45
  pass
46
46
 
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}
47
+ async def update(
48
+ self,
49
+ update_type: Literal["detail", "balance", "position", "orders", "orders_finish", "all"] = "all",
50
+ *,
51
+ product_group: str = "SwapU",
52
+ exchange_id: str = "Exchange",
53
+ asset: str = "USDT",
54
+ instrument_id: str | None = None,
55
+ page_index: int = 1,
56
+ page_size: int = 1000,
57
+ ) -> None:
58
+ """Refresh local caches via REST endpoints.
50
59
 
60
+ Parameters mirror the documented REST API default arguments.
61
+ """
51
62
 
52
- try:
53
- urls = url_map[update_type]
54
- except KeyError:
55
- raise ValueError(f"update_type err: {update_type}")
63
+ requests: list[Any] = []
64
+
65
+ include_detail = update_type in {"detail", "all"}
66
+ include_orders = update_type in {"orders", "all"}
67
+ include_position = update_type in {"position", "all"}
68
+ include_balance = update_type in {"balance", "all"}
56
69
 
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(
70
+ if update_type == "orders_finish":
71
+ await self.update_finish_order(
72
+ product_group=product_group,
73
+ page_index=page_index,
74
+ page_size=page_size,
75
+ )
76
+ return
77
+
78
+ if include_detail:
79
+ requests.append(
60
80
  self.client.post(
61
- all_urls[0],
62
- json={"ProductGroup": "SwapU"},
63
- headers={"source": "4", "versionflage": "true"},
81
+ f"{self.front_api}/cfd/agg/v1/instrument",
82
+ json={"ProductGroup": product_group},
83
+ headers=self._rest_headers,
84
+ )
85
+ )
86
+
87
+ if include_orders:
88
+ requests.append(
89
+ self.client.get(
90
+ f"{self.front_api}/cfd/query/v1.0/Order",
91
+ params={
92
+ "ProductGroup": product_group,
93
+ "ExchangeID": exchange_id,
94
+ "pageIndex": page_index,
95
+ "pageSize": page_size,
96
+ },
97
+ headers=self._rest_headers,
98
+ )
99
+ )
100
+
101
+ if include_position:
102
+ requests.append(
103
+ self.client.get(
104
+ f"{self.front_api}/cfd/query/v1.0/Position",
105
+ params={
106
+ "ProductGroup": product_group,
107
+ "Valid": 1,
108
+ "pageIndex": page_index,
109
+ "pageSize": page_size,
110
+ },
111
+ headers=self._rest_headers,
64
112
  )
65
113
  )
66
114
 
115
+ if include_balance:
116
+ resolved_instrument = instrument_id or self._resolve_instrument()
117
+ if not resolved_instrument:
118
+ raise ValueError(
119
+ "instrument_id is required to query balance; call update('detail') first or provide instrument_id explicitly."
120
+ )
121
+ self.store.balance.set_asset(asset)
122
+ requests.append(
123
+ self.client.post(
124
+ f"{self.front_api}/cfd/agg/v1/sendQryAll",
125
+ json={
126
+ "productGroup": product_group,
127
+ "instrumentID": resolved_instrument,
128
+ "asset": asset,
129
+ },
130
+ headers=self._rest_headers,
131
+ )
132
+ )
133
+
134
+ if not requests:
135
+ raise ValueError(f"update_type err: {update_type}")
136
+
137
+ await self.store.initialize(*requests)
138
+
139
+ def _resolve_instrument(self) -> str | None:
140
+ detail_entries = self.store.detail.find()
141
+ if detail_entries:
142
+ return detail_entries[0].get("symbol")
143
+ return None
144
+
145
+ def _get_detail_entry(self, symbol: str) -> dict[str, Any]:
146
+ detail = self.store.detail.get({"symbol": symbol})
147
+ if not detail:
148
+ raise ValueError(f"Unknown LBank instrument: {symbol}")
149
+ return detail
150
+
151
+ @staticmethod
152
+ def _format_with_step(value: float, step: Any) -> str:
153
+ try:
154
+ step_float = float(step)
155
+ except (TypeError, ValueError): # pragma: no cover - defensive guard
156
+ step_float = 0.0
157
+
158
+ if step_float <= 0:
159
+ return str(value)
160
+
161
+ return fmt_value(value, step_float)
162
+
163
+ async def update_finish_order(
164
+ self,
165
+ *,
166
+ product_group: str = "SwapU",
167
+ page_index: int = 1,
168
+ page_size: int = 200,
169
+ start_time: int | None = None,
170
+ end_time: int | None = None,
171
+ instrument_id: str | None = None,
172
+ ) -> None:
173
+ """Fetch finished orders within the specified time window (default: last hour)."""
174
+
175
+ now_ms = int(time.time() * 1000)
176
+ if end_time is None:
177
+ end_time = now_ms
178
+ if start_time is None:
179
+ start_time = end_time - 60 * 60 * 1000
180
+ if start_time >= end_time:
181
+ raise ValueError("start_time must be earlier than end_time")
182
+
183
+ params: dict[str, Any] = {
184
+ "ProductGroup": product_group,
185
+ "pageIndex": page_index,
186
+ "pageSize": page_size,
187
+ "startTime": start_time,
188
+ "endTime": end_time,
189
+ }
190
+ if instrument_id:
191
+ params["InstrumentID"] = instrument_id
192
+
193
+ await self.store.initialize(
194
+ self.client.get(
195
+ f"{self.front_api}/cfd/cff/v1/FinishOrder",
196
+ params=params,
197
+ headers=self._rest_headers,
198
+ )
199
+ )
200
+
201
+ async def place_order(
202
+ self,
203
+ symbol: str,
204
+ *,
205
+ direction: Literal["buy", "sell", "0", "1"],
206
+ volume: float,
207
+ price: float | None = None,
208
+ order_type: Literal["market", "limit_ioc", "limit_gtc"] = "market",
209
+ offset_flag: Literal["open", "close", "0", "1"] = "open",
210
+ exchange_id: str = "Exchange",
211
+ product_group: str = "SwapU",
212
+ order_proportion: str = "0.0000",
213
+ client_order_id: str | None = None,
214
+ ) -> dict[str, Any]:
215
+ """Create an order using documented REST parameters."""
216
+
217
+ direction_code = self._normalize_direction(direction)
218
+ offset_code = self._normalize_offset(offset_flag)
219
+ price_type_code, order_type_code = self._resolve_order_type(order_type)
220
+
221
+ detail_entry = self._get_detail_entry(symbol)
222
+ volume_str = self._format_with_step(volume, detail_entry.get("step_size"))
223
+ price_str: str | None = None
224
+ if price_type_code == "0":
225
+ if price is None:
226
+ raise ValueError("price is required for limit orders")
227
+ price_str = self._format_with_step(price, detail_entry.get("tick_size"))
228
+
229
+ payload: dict[str, Any] = {
230
+ # "ProductGroup": product_group,
231
+ "InstrumentID": symbol,
232
+ "ExchangeID": exchange_id,
233
+ "Direction": direction_code,
234
+ "OffsetFlag": offset_code,
235
+ "OrderPriceType": price_type_code,
236
+ "OrderType": order_type_code,
237
+ "Volume": volume_str,
238
+ "orderProportion": order_proportion,
239
+ }
240
+
241
+ if price_type_code == "0":
242
+ payload["Price"] = price_str
243
+ elif price is not None:
244
+ logger.warning("Price is ignored for market orders")
245
+
246
+ # if client_order_id:
247
+ # payload["LocalID"] = client_order_id
248
+ print(payload)
249
+ res = await self.client.post(
250
+ f"{self.front_api}/cfd/cff/v1/SendOrderInsert",
251
+ json=payload,
252
+ headers=self._rest_headers,
253
+ )
254
+ data = await res.json()
255
+ return self._ensure_ok("place_order", data)
256
+
257
+ async def cancel_order(
258
+ self,
259
+ order_sys_id: str,
260
+ *,
261
+ action_flag: str | int = "1",
262
+ ) -> dict[str, Any]:
263
+ """Cancel an order by OrderSysID."""
264
+
265
+ payload = {"OrderSysID": order_sys_id, "ActionFlag": str(action_flag)}
266
+ res = await self.client.post(
267
+ f"{self.front_api}/cfd/action/v1.0/SendOrderAction",
268
+ json=payload,
269
+ headers=self._rest_headers,
270
+ )
271
+ data = await res.json()
272
+ return self._ensure_ok("cancel_order", data)
273
+
274
+ @staticmethod
275
+ def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
276
+ if not isinstance(data, dict) or data.get("code") != 200:
277
+ raise RuntimeError(f"{operation} failed: {data}")
278
+ return data.get("data") or {}
279
+
280
+ @staticmethod
281
+ def _normalize_direction(direction: str) -> str:
282
+ mapping = {
283
+ "buy": "0",
284
+ "long": "0",
285
+ "sell": "1",
286
+ "short": "1",
287
+ }
288
+ return mapping.get(str(direction).lower(), str(direction))
289
+
290
+ @staticmethod
291
+ def _normalize_offset(offset: str) -> str:
292
+ mapping = {
293
+ "open": "0",
294
+ "close": "1",
295
+ }
296
+ return mapping.get(str(offset).lower(), str(offset))
297
+
298
+ @staticmethod
299
+ def _resolve_order_type(order_type: str) -> tuple[str, str]:
300
+ mapping = {
301
+ "market": ("4", "1"),
302
+ "limit_ioc": ("0", "1"),
303
+ "limit_gtc": ("0", "0"),
304
+ }
305
+ try:
306
+ return mapping[str(order_type).lower()]
307
+ except KeyError as exc: # pragma: no cover - guard
308
+ raise ValueError(f"Unsupported order_type: {order_type}") from exc
309
+
67
310
 
68
311
  async def sub_orderbook(self, symbols: list[str], limit: int | None = None) -> None:
69
312
  """订阅指定交易对的订单簿(遵循 LBank 协议)。
@@ -107,4 +350,4 @@ class Lbank:
107
350
  batch = send_jsons[i:i+5]
108
351
  await asyncio.gather(*(sub(send_json) for send_json in batch))
109
352
  if i + 5 < len(send_jsons):
110
- await asyncio.sleep(0.1)
353
+ await asyncio.sleep(0.1)
@@ -235,6 +235,22 @@ class Ticker(DataStore):
235
235
  else:
236
236
  self._update([item])
237
237
 
238
+ def _onresponse(self, data: dict[str, Any]) -> None:
239
+ entries = data.get("data") or []
240
+
241
+ if not isinstance(entries, list):
242
+ entries = [entries]
243
+
244
+ items = []
245
+ for entry in entries:
246
+ item = self._format(entry)
247
+ if item:
248
+ items.append(item)
249
+
250
+ self._clear()
251
+ if items:
252
+ self._insert(items)
253
+
238
254
  def _format(self, entry: dict[str, Any]) -> dict[str, Any] | None:
239
255
  contract_id = entry.get("contractId")
240
256
  if contract_id is None:
@@ -966,6 +982,8 @@ class EdgexDataStore(DataStoreCollection):
966
982
  self.position._onresponse(data)
967
983
  elif res.url.path == "/api/v1/private/order/getActiveOrderPage":
968
984
  self.orders._onresponse(data)
985
+ elif res.url.path == "/api/v1/public/quote/getTicker":
986
+ self.ticker._onresponse(data)
969
987
 
970
988
  def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
971
989
  # print(msg)
@@ -121,12 +121,247 @@ class Detail(DataStore):
121
121
  self._insert(items)
122
122
 
123
123
 
124
+ class Orders(DataStore):
125
+ """Active order snapshots fetched via the REST order query."""
126
+
127
+ _KEYS = ["order_id"]
128
+
129
+ _ORDER_STATUS_MAP = {
130
+ "1": "filled",
131
+ "2": "filled",
132
+ "4": "open",
133
+ "5": "partially_filled",
134
+ "6": "canceled",
135
+ }
136
+
137
+ _DIRECTION_MAP = {
138
+ "0": "buy",
139
+ "1": "sell",
140
+ }
141
+
142
+ _OFFSET_FLAG_MAP = {
143
+ "0": "open",
144
+ "1": "close",
145
+ }
146
+
147
+ def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
148
+ if not entry:
149
+ return None
150
+
151
+ order_id = entry.get("OrderSysID") or entry.get("orderSysID")
152
+ if not order_id:
153
+ return None
154
+
155
+ direction = self._DIRECTION_MAP.get(str(entry.get("Direction")), str(entry.get("Direction")))
156
+ offset_flag = self._OFFSET_FLAG_MAP.get(
157
+ str(entry.get("OffsetFlag")), str(entry.get("OffsetFlag"))
158
+ )
159
+
160
+ order_price_type = str(entry.get("OrderPriceType")) if entry.get("OrderPriceType") is not None else None
161
+ order_type = str(entry.get("OrderType")) if entry.get("OrderType") is not None else None
162
+
163
+ if order_price_type == "4":
164
+ order_kind = "market"
165
+ elif order_type == "1":
166
+ order_kind = "limit_fak"
167
+ else:
168
+ order_kind = "limit"
169
+
170
+ status_code = str(entry.get("OrderStatus")) if entry.get("OrderStatus") is not None else None
171
+ status = self._ORDER_STATUS_MAP.get(status_code, status_code)
172
+
173
+ client_order_id = (
174
+ entry.get("LocalID")
175
+ or entry.get("localID")
176
+ or entry.get("LocalId")
177
+ or entry.get("localId")
178
+ )
179
+
180
+ return {
181
+ "order_id": order_id,
182
+ "client_order_id": client_order_id,
183
+ "symbol": entry.get("InstrumentID"),
184
+ "side": direction,
185
+ "offset": offset_flag,
186
+ "order_type": order_kind,
187
+ "price": entry.get("Price"),
188
+ "quantity": entry.get("Volume"),
189
+ "filled": entry.get("VolumeTraded"),
190
+ "remaining": entry.get("VolumeRemain"),
191
+ "status": status,
192
+ "status_code": status_code,
193
+ "position_id": entry.get("PositionID"),
194
+ "leverage": entry.get("Leverage"),
195
+ "frozen_margin": entry.get("FrozenMargin"),
196
+ "frozen_fee": entry.get("FrozenFee"),
197
+ "insert_time": entry.get("InsertTime"),
198
+ "update_time": entry.get("UpdateTime"),
199
+ }
200
+
201
+ @staticmethod
202
+ def _extract_rows(data: dict[str, Any] | None) -> list[dict[str, Any]]:
203
+ if not data:
204
+ return []
205
+ payload = data.get("data") if isinstance(data, dict) else None
206
+ if isinstance(payload, dict):
207
+ rows = payload.get("data")
208
+ if isinstance(rows, list):
209
+ return rows
210
+ if isinstance(payload, list): # pragma: no cover - defensive path
211
+ return payload
212
+ return []
213
+
214
+ def _onresponse(self, data: dict[str, Any] | None) -> None:
215
+ rows = self._extract_rows(data)
216
+ if not rows:
217
+ self._clear()
218
+ return
219
+
220
+ items: list[dict[str, Any]] = []
221
+ for row in rows:
222
+ transformed = self._transform(row)
223
+ if transformed:
224
+ items.append(transformed)
225
+
226
+ self._clear()
227
+ if items:
228
+ self._insert(items)
229
+
230
+
231
+ class OrderFinish(Orders):
232
+ """Finished order snapshots fetched from the historical REST endpoint."""
233
+
234
+ def _onresponse(self, data: dict[str, Any] | None) -> None:
235
+ rows: list[dict[str, Any]] = []
236
+ if isinstance(data, dict):
237
+ payload = data.get("data") or {}
238
+ if isinstance(payload, dict):
239
+ list_payload = payload.get("list") or {}
240
+ if isinstance(list_payload, dict):
241
+ rows = list_payload.get("resultList") or []
242
+
243
+ if not rows:
244
+ self._clear()
245
+ return
246
+
247
+ items: list[dict[str, Any]] = []
248
+ for row in rows:
249
+ transformed = self._transform(row)
250
+ if transformed:
251
+ items.append(transformed)
252
+
253
+ self._clear()
254
+ if items:
255
+ self._insert(items)
256
+
257
+
258
+ class Position(DataStore):
259
+ """Open position snapshots fetched from the REST position endpoint."""
260
+
261
+ _KEYS = ["position_id"]
262
+
263
+ _POS_DIRECTION_MAP = {
264
+ "1": "net",
265
+ "2": "long",
266
+ "3": "short",
267
+ }
268
+
269
+ def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
270
+ if not entry:
271
+ return None
272
+ position_id = entry.get("PositionID")
273
+ if not position_id:
274
+ return None
275
+
276
+ direction_code = str(entry.get("PosiDirection")) if entry.get("PosiDirection") is not None else None
277
+ side = self._POS_DIRECTION_MAP.get(direction_code, direction_code)
278
+
279
+ return {
280
+ "position_id": position_id,
281
+ "symbol": entry.get("InstrumentID"),
282
+ "side": side,
283
+ "quantity": entry.get("Position"),
284
+ "available": entry.get("AvailableUse"),
285
+ "avg_price": entry.get("OpenPrice"),
286
+ "entry_price": entry.get("OpenPrice"),
287
+ "leverage": entry.get("Leverage"),
288
+ "liquidation_price": entry.get("estimateLiquidationPrice") or entry.get("FORCECLOSEPRICE"),
289
+ "margin_used": entry.get("UseMargin"),
290
+ "unrealized_pnl": entry.get("PositionFee"),
291
+ "realized_pnl": entry.get("CloseProfit"),
292
+ "update_time": entry.get("UpdateTime"),
293
+ "insert_time": entry.get("InsertTime"),
294
+ }
295
+
296
+ def _onresponse(self, data: dict[str, Any] | None) -> None:
297
+ rows = Orders._extract_rows(data) # reuse helper for nested payload
298
+ if not rows:
299
+ self._clear()
300
+ return
301
+
302
+ items: list[dict[str, Any]] = []
303
+ for row in rows:
304
+ transformed = self._transform(row)
305
+ if transformed:
306
+ items.append(transformed)
307
+
308
+ self._clear()
309
+ if items:
310
+ self._insert(items)
311
+
312
+
313
+ class Balance(DataStore):
314
+ """Account balance snapshot derived from sendQryAll endpoint."""
315
+
316
+ _KEYS = ["asset"]
317
+
318
+ def _init(self) -> None:
319
+ self._asset: str | None = None
320
+
321
+ def set_asset(self, asset: str | None) -> None:
322
+ self._asset = asset
323
+
324
+ def _transform(self, payload: dict[str, Any]) -> dict[str, Any] | None:
325
+ if not payload:
326
+ return None
327
+ asset_balance = payload.get("assetBalance") or {}
328
+ if not asset_balance:
329
+ return None
330
+
331
+ asset = payload.get("asset") or asset_balance.get("currency") or self._asset or "USDT"
332
+
333
+ return {
334
+ "asset": asset,
335
+ "balance": asset_balance.get("balance"),
336
+ "available": asset_balance.get("available"),
337
+ "real_available": asset_balance.get("realAvailable"),
338
+ "frozen_margin": asset_balance.get("frozenMargin"),
339
+ "frozen_fee": asset_balance.get("frozenFee"),
340
+ "total_close_profit": asset_balance.get("totalCloseProfit"),
341
+ "cross_margin": asset_balance.get("crossMargin"),
342
+ }
343
+
344
+ def _onresponse(self, data: dict[str, Any] | None) -> None:
345
+ payload: dict[str, Any] = {}
346
+ if isinstance(data, dict):
347
+ payload = data.get("data") or {}
348
+
349
+ item = self._transform(payload)
350
+ self._clear()
351
+ if item:
352
+ self._insert([item])
353
+
354
+
124
355
  class LbankDataStore(DataStoreCollection):
125
356
  """Aggregates book/detail stores for the LBank public feed."""
126
357
 
127
358
  def _init(self) -> None:
128
359
  self._create("book", datastore_class=Book)
129
360
  self._create("detail", datastore_class=Detail)
361
+ self._create("orders", datastore_class=Orders)
362
+ self._create("order_finish", datastore_class=OrderFinish)
363
+ self._create("position", datastore_class=Position)
364
+ self._create("balance", datastore_class=Balance)
130
365
  self._channel_to_symbol: dict[str, str] = {}
131
366
 
132
367
  @property
@@ -185,6 +420,104 @@ class LbankDataStore(DataStoreCollection):
185
420
  """
186
421
  return self._get("detail")
187
422
 
423
+ @property
424
+ def orders(self) -> Orders:
425
+ """
426
+ 活跃订单数据流。
427
+
428
+ 此属性表示通过 REST 接口获取的当前活跃订单快照,包括已开仓订单、部分成交订单等状态。
429
+
430
+ Data structure:
431
+ [
432
+ {
433
+ "order_id": <系统订单ID>,
434
+ "client_order_id": <用户自定义订单ID>,
435
+ "symbol": <合约ID>,
436
+ "side": "buy" 或 "sell",
437
+ "offset": "open" 或 "close",
438
+ "order_type": "limit" / "market" / "limit_fak",
439
+ "price": <下单价格>,
440
+ "quantity": <下单数量>,
441
+ "filled": <已成交数量>,
442
+ "remaining": <剩余数量>,
443
+ "status": <订单状态>,
444
+ "status_code": <原始状态码>,
445
+ "position_id": <关联仓位ID>,
446
+ "leverage": <杠杆倍数>,
447
+ "frozen_margin": <冻结保证金>,
448
+ "frozen_fee": <冻结手续费>,
449
+ "insert_time": <下单时间>,
450
+ "update_time": <更新时间>
451
+ },
452
+ ...
453
+ ]
454
+
455
+ 通过本属性可以跟踪当前活跃订单状态,便于订单管理和风控。
456
+ """
457
+ return self._get("orders")
458
+
459
+ @property
460
+ def order_finish(self) -> OrderFinish:
461
+ """历史已完成订单数据流,与 ``orders`` 字段保持兼容。"""
462
+ return self._get("order_finish")
463
+
464
+ @property
465
+ def position(self) -> Position:
466
+ """
467
+ 持仓数据流。
468
+
469
+ 此属性表示通过 REST 接口获取的当前持仓快照,包括多头、空头或净持仓等方向信息。
470
+
471
+ Data structure:
472
+ [
473
+ {
474
+ "position_id": <仓位ID>,
475
+ "symbol": <合约ID>,
476
+ "side": "long" / "short" / "net",
477
+ "quantity": <持仓数量>,
478
+ "available": <可用数量>,
479
+ "avg_price": <持仓均价>,
480
+ "entry_price": <开仓均价>,
481
+ "leverage": <杠杆倍数>,
482
+ "liquidation_price": <预估强平价>,
483
+ "margin_used": <已用保证金>,
484
+ "unrealized_pnl": <未实现盈亏>,
485
+ "realized_pnl": <已实现盈亏>,
486
+ "update_time": <更新时间>,
487
+ "insert_time": <插入时间>
488
+ },
489
+ ...
490
+ ]
491
+
492
+ 通过本属性可以跟踪账户当前仓位状态,便于盈亏分析和风控。
493
+ """
494
+ return self._get("position")
495
+
496
+ @property
497
+ def balance(self) -> Balance:
498
+ """
499
+ 账户余额数据流。
500
+
501
+ 此属性表示通过 REST 接口获取的账户资产快照,包括余额、可用余额、保证金等信息。
502
+
503
+ Data structure:
504
+ [
505
+ {
506
+ "asset": <资产币种>,
507
+ "balance": <总余额>,
508
+ "available": <可用余额>,
509
+ "real_available": <实际可用余额>,
510
+ "frozen_margin": <冻结保证金>,
511
+ "frozen_fee": <冻结手续费>,
512
+ "total_close_profit": <累计平仓收益>,
513
+ "cross_margin": <全仓保证金>
514
+ }
515
+ ]
516
+
517
+ 通过本属性可以跟踪账户余额与资金情况,便于资金管理和风险控制。
518
+ """
519
+ return self._get("balance")
520
+
188
521
 
189
522
  def register_book_channel(self, channel_id: str, symbol: str, *, raw_symbol: str | None = None) -> None:
190
523
  if channel_id is not None:
@@ -197,8 +530,17 @@ class LbankDataStore(DataStoreCollection):
197
530
  for fut in asyncio.as_completed(aws):
198
531
  res = await fut
199
532
  data = await res.json()
533
+
200
534
  if res.url.path == "/cfd/agg/v1/instrument":
201
535
  self.detail._onresponse(data)
536
+ if res.url.path == "/cfd/query/v1.0/Order":
537
+ self.orders._onresponse(data)
538
+ if res.url.path == "/cfd/query/v1.0/Position":
539
+ self.position._onresponse(data)
540
+ if res.url.path == "/cfd/agg/v1/sendQryAll":
541
+ self.balance._onresponse(data)
542
+ if res.url.path == "/cfd/cff/v1/FinishOrder":
543
+ self.order_finish._onresponse(data)
202
544
 
203
545
 
204
546
  def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.66
3
+ Version: 0.68
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
@@ -4,24 +4,24 @@ hyperquant/db.py,sha256=i2TjkCbmH4Uxo7UTDvOYBfy973gLcGexdzuT_YcSeIE,6678
4
4
  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
- hyperquant/broker/auth.py,sha256=PgWw6eFGQtVbbA_JcJkK71L4JckinO8h2B0FrEV5G-U,6516
8
- hyperquant/broker/edgex.py,sha256=7oUY3HJUR87eSnkRFMEh1ttZCZkeAB_CkQF4Rodoevs,17645
7
+ hyperquant/broker/auth.py,sha256=Wst7mTBuUS2BQ5hZd0a8FNNs5Uc01ac9WzJpseTuyAY,7673
8
+ hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
9
9
  hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
10
- hyperquant/broker/lbank.py,sha256=kuxRfZylGZK3LRMzQJcB3w_nOXWM9-si65EwDsj0DnY,3325
10
+ hyperquant/broker/lbank.py,sha256=7o0xaCuUuIGDUX93-1tnWBH39QGohHpqpL-KFa2OiJc,11662
11
11
  hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
12
12
  hyperquant/broker/ws.py,sha256=9Zu5JSLj-ylYEVmFmRwvZDDnVYKwb37cLHfZzA0AZGc,2200
13
13
  hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQJKcmyk,95785
14
14
  hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
15
15
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
16
16
  hyperquant/broker/lib/util.py,sha256=u02kGb-7LMCi32UNLeKoPaZBZ2LBEjx72KRkaKX0yQg,275
17
- hyperquant/broker/models/edgex.py,sha256=ba9ogBprp3uGPgYprjBJoVcVWUBbmZ_l2hYCIGQCRpA,33449
17
+ hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
18
18
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
19
- hyperquant/broker/models/lbank.py,sha256=ArYBHHF9p66rNXs9fn41W2asa5mK-mqgplajCHr51YA,7106
19
+ hyperquant/broker/models/lbank.py,sha256=ZCD1dOUMyWPT8lKDj6C6LcHEof2d0JN384McURzLA-s,18868
20
20
  hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
21
21
  hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
22
22
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
23
23
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
24
24
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
25
- hyperquant-0.66.dist-info/METADATA,sha256=GVJWSwmMpQU_6dpHiOreOSExsvq4e3Pg34_QUHh_Y9U,4317
26
- hyperquant-0.66.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
- hyperquant-0.66.dist-info/RECORD,,
25
+ hyperquant-0.68.dist-info/METADATA,sha256=-5HFGYBVdzJALnWhh4AFh62i_3cPqw_CGs5wF3XXjEk,4317
26
+ hyperquant-0.68.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ hyperquant-0.68.dist-info/RECORD,,