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.
@@ -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:
hyperquant/broker/ws.py CHANGED
@@ -1,9 +1,12 @@
1
1
  import asyncio
2
+ import base64
2
3
  import time
4
+ from typing import Any
3
5
 
4
6
  import pybotters
5
7
  from pybotters.ws import ClientWebSocketResponse, logger
6
8
  from pybotters.auth import Hosts
9
+ import urllib
7
10
  import yarl
8
11
 
9
12
 
@@ -59,7 +62,5 @@ class WssAuth:
59
62
  break
60
63
  else:
61
64
  logger.warning(f"WebSocket login failed: {data}")
62
-
63
-
64
-
65
+
65
66
  pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.65
3
+ Version: 0.67
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,22 +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=oA9Yw1I59-u0Tnoj2e4wUup5q8V5T2qpga5RKbiAiZI,2614
8
- hyperquant/broker/edgex.py,sha256=qQtc8jZqB5ZODoGGVcG_aIVUlrJX_pRF9EyO927LiVM,6646
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=lai9xgK4Vbw7qPhRb7PGj-qKgFP40m1rHgVguTAXqDg,10756
11
11
  hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
12
- hyperquant/broker/ws.py,sha256=4Igi0zuwGHmU46BSz24sFURkYalrE5Z68qkZEFBfbJE,2147
12
+ hyperquant/broker/ws.py,sha256=9Zu5JSLj-ylYEVmFmRwvZDDnVYKwb37cLHfZzA0AZGc,2200
13
+ hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQJKcmyk,95785
13
14
  hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
14
15
  hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
15
- hyperquant/broker/models/edgex.py,sha256=texNBwz_9r9CTl7dRNjvSm_toxV_og0TWnap3dqUk2s,15795
16
+ hyperquant/broker/lib/util.py,sha256=u02kGb-7LMCi32UNLeKoPaZBZ2LBEjx72KRkaKX0yQg,275
17
+ hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
16
18
  hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
17
- hyperquant/broker/models/lbank.py,sha256=ArYBHHF9p66rNXs9fn41W2asa5mK-mqgplajCHr51YA,7106
19
+ hyperquant/broker/models/lbank.py,sha256=ZCD1dOUMyWPT8lKDj6C6LcHEof2d0JN384McURzLA-s,18868
18
20
  hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
19
21
  hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
20
22
  hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
21
23
  hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
22
24
  hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
23
- hyperquant-0.65.dist-info/METADATA,sha256=Yr3ioVFeA70YO0DwclPrN1Wcy8KX4ZLHomerfHR8rxw,4317
24
- hyperquant-0.65.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
25
- hyperquant-0.65.dist-info/RECORD,,
25
+ hyperquant-0.67.dist-info/METADATA,sha256=5v5iJ5vnK9QCgjCjtAGZsKwlAdyG0TPc1_ppcUJtjw4,4317
26
+ hyperquant-0.67.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
27
+ hyperquant-0.67.dist-info/RECORD,,