hyperquant 0.72__tar.gz → 0.74__tar.gz

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.
Files changed (48) hide show
  1. {hyperquant-0.72 → hyperquant-0.74}/PKG-INFO +1 -1
  2. {hyperquant-0.72 → hyperquant-0.74}/apis.json +4 -2
  3. {hyperquant-0.72 → hyperquant-0.74}/pyproject.toml +1 -1
  4. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/bitget.py +1 -1
  5. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lbank.py +197 -1
  6. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/bitget.py +71 -0
  7. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/lbank.py +3 -0
  8. {hyperquant-0.72 → hyperquant-0.74}/tests/test_lbank.py +125 -66
  9. {hyperquant-0.72 → hyperquant-0.74}/uv.lock +1 -1
  10. {hyperquant-0.72 → hyperquant-0.74}/.gitignore +0 -0
  11. {hyperquant-0.72 → hyperquant-0.74}/.python-version +0 -0
  12. {hyperquant-0.72 → hyperquant-0.74}/README.md +0 -0
  13. {hyperquant-0.72 → hyperquant-0.74}/data/alpine_smoke.log +0 -0
  14. {hyperquant-0.72 → hyperquant-0.74}/data/logs/notikit.log +0 -0
  15. {hyperquant-0.72 → hyperquant-0.74}/data/logs/test_order_sync.log +0 -0
  16. {hyperquant-0.72 → hyperquant-0.74}/data/records_swap.csv +0 -0
  17. {hyperquant-0.72 → hyperquant-0.74}/data/records_swapc.csv +0 -0
  18. {hyperquant-0.72 → hyperquant-0.74}/doc/lbank.md +0 -0
  19. {hyperquant-0.72 → hyperquant-0.74}/pub.sh +0 -0
  20. {hyperquant-0.72 → hyperquant-0.74}/requirements-dev.lock +0 -0
  21. {hyperquant-0.72 → hyperquant-0.74}/requirements.lock +0 -0
  22. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/__init__.py +0 -0
  23. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/auth.py +0 -0
  24. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/edgex.py +0 -0
  25. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/hyperliquid.py +0 -0
  26. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  27. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/hpstore.py +0 -0
  28. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  29. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/util.py +0 -0
  30. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/edgex.py +0 -0
  31. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  32. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/ourbit.py +0 -0
  33. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/ourbit.py +0 -0
  34. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/ws.py +0 -0
  35. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/core.py +0 -0
  36. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/_util.py +0 -0
  37. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/binance.py +0 -0
  38. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/coinglass.py +0 -0
  39. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/okx.py +0 -0
  40. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/db.py +0 -0
  41. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/draw.py +0 -0
  42. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/logkit.py +0 -0
  43. {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/notikit.py +0 -0
  44. {hyperquant-0.72 → hyperquant-0.74}/tests/test_bitget.py +0 -0
  45. {hyperquant-0.72 → hyperquant-0.74}/tests/test_draw.py +0 -0
  46. {hyperquant-0.72 → hyperquant-0.74}/tests/test_edgex.py +0 -0
  47. {hyperquant-0.72 → hyperquant-0.74}/tests/test_ourbit.py +0 -0
  48. {hyperquant-0.72 → hyperquant-0.74}/tests/tmp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.72
3
+ Version: 0.74
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
@@ -7,8 +7,10 @@
7
7
  "78Uke-bPB57YoRzAwY7SkWyPQFeKkPuWVQakC0k9rSI",
8
8
  "3MB7nAnnNPTxAnwdbjDPew-02b746d6a832346a46a97faf054b2909c1a0b58a35e04c3504923a99a5503c1c"
9
9
  ],
10
- "lbank": ["617a2d77b4cd4dfbbaa2094c17017bdb"],
11
- "bitget": [
10
+ "lbank": [
11
+ "925a645e03c84c8e96b96008e98b75f7"
12
+ ],
13
+ "bitget":[
12
14
  "bg_03e0445d9282f248d22842cfe6f30192",
13
15
  "67ec894753d75fec12332881278420863a960ec39c8f5acf1de88aa1da926854",
14
16
  "huainian0408"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "0.72"
3
+ version = "0.74"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -206,7 +206,7 @@ class Bitget:
206
206
  submsg = {"op": "subscribe", "args": []}
207
207
  for symbol in symbols:
208
208
  submsg["args"].append(
209
- {"instType": "SPOT", "channel": channel, "instId": symbol}
209
+ {"instType": "USDT-FUTURES", "channel": channel, "instId": symbol}
210
210
  )
211
211
 
212
212
  self.client.ws_connect(
@@ -136,6 +136,138 @@ class Lbank:
136
136
 
137
137
  await self.store.initialize(*requests)
138
138
 
139
+ async def query_trade(
140
+ self,
141
+ order_id: str | None = None,
142
+ *,
143
+ product_group: str = "SwapU",
144
+ page_index: int = 1,
145
+ page_size: int = 20,
146
+ ) -> list[dict[str, Any]]:
147
+ """Fetch trade executions linked to a given OrderSysID.
148
+
149
+ Example response payload::
150
+
151
+ [
152
+ {
153
+ "TradeUnitID": "e1b03fb1-6849-464f-a",
154
+ "ProductGroup": "SwapU",
155
+ "CloseProfit": 0,
156
+ "BusinessNo": 1001770339345505,
157
+ "TradeID": "1000162046503720",
158
+ "PositionID": "1000632926272299",
159
+ "DeriveSource": "0",
160
+ "OrderID": "",
161
+ "Direction": "0",
162
+ "InstrumentID": "SOLUSDT",
163
+ "OffsetFlag": "0",
164
+ "Remark": "def",
165
+ "DdlnTime": "0",
166
+ "UseMargin": 0.054213,
167
+ "Currency": "USDT",
168
+ "Turnover": 5.4213,
169
+ "SettlementGroup": "SwapU",
170
+ "Leverage": 100,
171
+ "OrderSysID": "1000632948114584",
172
+ "ExchangeID": "Exchange",
173
+ "AccountID": "e1b03fb1-6849-464f-a986-94b9a6e625e6",
174
+ "TradeTime": 1760161085,
175
+ "Fee": 0.00325278,
176
+ "OrderPrice": 180.89,
177
+ "InsertTime": 1760161085,
178
+ "MemberID": "e1b03fb1-6849-464f-a986-94b9a6e625e6",
179
+ "MatchRole": "1",
180
+ "ClearCurrency": "USDT",
181
+ "Price": 180.71,
182
+ "Volume": 0.03,
183
+ "OpenPrice": 182.94,
184
+ "MasterAccountID": "",
185
+ "PriceCurrency": "USDT",
186
+ "FeeCurrency": "USDT"
187
+ }
188
+ ]
189
+ """
190
+
191
+ if not order_id:
192
+ raise ValueError("order_id is required to query order executions")
193
+
194
+ params = {
195
+ "ProductGroup": product_group,
196
+ "OrderSysID": order_id,
197
+ "pageIndex": page_index,
198
+ "pageSize": page_size,
199
+ }
200
+
201
+ res = await self.client.get(
202
+ f"{self.front_api}/cfd/query/v1.0/Trade",
203
+ params=params,
204
+ headers=self._rest_headers,
205
+ )
206
+ data = await res.json()
207
+ payload = self._ensure_ok("query_trade", data)
208
+
209
+ if isinstance(payload, dict):
210
+ rows = payload.get("data")
211
+ if isinstance(rows, list):
212
+ return rows
213
+ elif isinstance(payload, list): # pragma: no cover - defensive fallback
214
+ return payload
215
+
216
+ return []
217
+
218
+ async def query_order(
219
+ self,
220
+ order_id: str | None = None,
221
+ *,
222
+ product_group: str = "SwapU",
223
+ page_index: int = 1,
224
+ page_size: int = 20,
225
+ ) -> dict[str, Any]:
226
+ """Aggregate trade executions to summarize overall order statistics."""
227
+
228
+ if not order_id:
229
+ raise ValueError("order_id is required to query order statistics")
230
+
231
+ trades = await self.query_trade(
232
+ order_id,
233
+ product_group=product_group,
234
+ page_index=page_index,
235
+ page_size=page_size,
236
+ )
237
+
238
+ if not trades:
239
+ return {
240
+ "order_id": order_id,
241
+ "trade_count": 0,
242
+ }
243
+
244
+ def _to_float(value: Any) -> float:
245
+ try:
246
+ return float(value)
247
+ except (TypeError, ValueError):
248
+ return 0.0
249
+
250
+ total_volume = sum(_to_float(trade.get("Volume")) for trade in trades)
251
+ total_turnover = sum(_to_float(trade.get("Turnover")) for trade in trades)
252
+ total_fee = sum(_to_float(trade.get("Fee")) for trade in trades)
253
+
254
+ avg_price = total_turnover / total_volume if total_volume else None
255
+ last_trade = trades[-1]
256
+
257
+ return {
258
+ "order_id": order_id,
259
+ "instrument_id": last_trade.get("InstrumentID"),
260
+ "position_id": last_trade.get("PositionID"),
261
+ "direction": last_trade.get("Direction"),
262
+ "offset_flag": last_trade.get("OffsetFlag"),
263
+ "trade_time": last_trade.get("TradeTime"),
264
+ "avg_price": avg_price,
265
+ "volume": total_volume,
266
+ "turnover": total_turnover,
267
+ "fee": total_fee,
268
+ "trade_count": len(trades),
269
+ }
270
+
139
271
  def _resolve_instrument(self) -> str | None:
140
272
  detail_entries = self.store.detail.find()
141
273
  if detail_entries:
@@ -212,7 +344,71 @@ class Lbank:
212
344
  order_proportion: str = "0.0000",
213
345
  client_order_id: str | None = None,
214
346
  ) -> dict[str, Any]:
215
- """Create an order using documented REST parameters."""
347
+ """Create an order using documented REST parameters.
348
+
349
+ 返回示例:
350
+
351
+ .. code:: json
352
+
353
+ {
354
+ "offsetFlag": "5",
355
+ "orderType": "1",
356
+ "reserveMode": "0",
357
+ "fee": "0.0066042",
358
+ "frozenFee": "0",
359
+ "ddlnTime": "0",
360
+ "userID": "lbank_exchange_user",
361
+ "masterAccountID": "",
362
+ "exchangeID": "Exchange",
363
+ "accountID": "e1b03fb1-6849-464f-a986-94b9a6e625e6",
364
+ "orderSysID": "1000633129818889",
365
+ "volumeRemain": "0",
366
+ "price": "183.36",
367
+ "businessValue": "1760183423813",
368
+ "frozenMargin": "0",
369
+ "instrumentID": "SOLUSDT",
370
+ "posiDirection": "2",
371
+ "volumeMode": "1",
372
+ "volume": "0.06",
373
+ "insertTime": "1760183423",
374
+ "copyMemberID": "",
375
+ "position": "0.06",
376
+ "tradePrice": "183.45",
377
+ "leverage": "100",
378
+ "businessResult": "",
379
+ "availableUse": "0",
380
+ "orderStatus": "1",
381
+ "openPrice": "182.94",
382
+ "frozenMoney": "0",
383
+ "remark": "def",
384
+ "reserveUse": "0",
385
+ "sessionNo": "41",
386
+ "isCrossMargin": "1",
387
+ "closeProfit": "0.0306",
388
+ "businessNo": "1001770756852986", # 订单有成交会并入仓位 businessNo
389
+ "relatedOrderSysID": "",
390
+ "positionID": "1000632926272299",
391
+ "mockResp": false,
392
+ "deriveSource": "0",
393
+ "copyOrderID": "",
394
+ "currency": "USDT",
395
+ "turnover": "11.007",
396
+ "frontNo": "-68",
397
+ "direction": "1",
398
+ "orderPriceType": "4",
399
+ "volumeCancled": "0",
400
+ "updateTime": "1760183423",
401
+ "localID": "1000633129818889",
402
+ "volumeTraded": "0.06",
403
+ "appid": "WEB",
404
+ "tradeUnitID": "e1b03fb1-6849-464f-a",
405
+ "businessType": "P",
406
+ "memberID": "e1b03fb1-6849-464f-a986-94b9a6e625e6",
407
+ "timeCondition": "0",
408
+ "copyProfit": "0"
409
+ }
410
+
411
+ """
216
412
 
217
413
  direction_code = self._normalize_direction(direction)
218
414
  offset_code = self._normalize_offset(offset_flag)
@@ -286,3 +286,74 @@ class BitgetDataStore(BitgetV2DataStore):
286
286
  ]
287
287
  """
288
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")
@@ -271,6 +271,7 @@ class Position(DataStore):
271
271
  if not entry:
272
272
  return None
273
273
  position_id = entry.get("PositionID")
274
+ bus_id = entry.get("BusinessNo")
274
275
  if not position_id:
275
276
  return None
276
277
 
@@ -279,6 +280,7 @@ class Position(DataStore):
279
280
 
280
281
  return {
281
282
  "position_id": position_id,
283
+ "bus_id": bus_id,
282
284
  "symbol": entry.get("InstrumentID"),
283
285
  "side": side,
284
286
  "quantity": entry.get("Position"),
@@ -473,6 +475,7 @@ class LbankDataStore(DataStoreCollection):
473
475
  [
474
476
  {
475
477
  "position_id": <仓位ID>,
478
+ "bus_id": <订单ID覆盖>,
476
479
  "symbol": <合约ID>,
477
480
  "side": "long" / "short" / "net",
478
481
  "quantity": <持仓数量>,
@@ -2,9 +2,10 @@ import asyncio
2
2
  import json
3
3
  import time
4
4
  import zlib
5
- from typing import Literal
5
+ from typing import Literal, Union
6
6
 
7
7
  from aiohttp import ClientWebSocketResponse
8
+ from aiohttp.client_exceptions import ContentTypeError
8
9
  import pybotters
9
10
 
10
11
 
@@ -103,28 +104,33 @@ async def test_broker_subbook():
103
104
  async def test_update():
104
105
  async with pybotters.Client(apis='./apis.json') as client:
105
106
  async with Lbank(client) as lb:
106
- # await lb.update('position')
107
- # print(lb.store.position.find())
107
+ await lb.update('position')
108
+ print(lb.store.position.find())
108
109
  # await lb.update('balance')
109
110
  # print(lb.store.balance.find())
110
111
  # await lb.update('detail')
111
112
  # print(lb.store.detail.find())
112
- await lb.update('orders')
113
+ # await lb.update('orders')
114
+ # await lb.update('orders_finish')
113
115
 
114
- print(lb.store.order_finish.find())
116
+ # print(lb.store.order_finish.find({
117
+ # 'order_id': '1000632478428573'
118
+ # }))
115
119
 
116
120
  async def test_place():
117
121
  async with pybotters.Client(apis='./apis.json') as client:
118
122
  async with Lbank(client) as lb:
119
123
  order = await lb.place_order(
120
- "0GUSDT",
124
+ "SOLUSDT",
121
125
  direction="buy",
122
126
  order_type='limit_gtc',
123
- price=3.5342,
124
- volume=4.176760504552669,
127
+ price=182,
128
+ volume=0.03,
125
129
  )
126
130
  print(order)
127
131
 
132
+
133
+
128
134
  async def test_cancel():
129
135
  async with pybotters.Client(apis='./apis.json') as client:
130
136
  async with Lbank(client) as lb:
@@ -132,36 +138,6 @@ async def test_cancel():
132
138
  print(res)
133
139
 
134
140
 
135
- async def wait_order_state_poll(
136
- broker: Lbank,
137
- order_id: str,
138
- symbol: str,
139
- seconds: float,
140
- poll_interval: float = 0.5,
141
- ) -> dict:
142
- """轮询 REST 接口,等待订单进入终态。"""
143
-
144
- async with asyncio.timeout(seconds):
145
- last_snapshot: dict | None = None
146
- while True:
147
- await broker.update("orders")
148
- snapshot = broker.store.orders.get({"order_id": order_id})
149
- if snapshot:
150
- last_snapshot = snapshot
151
- status = snapshot.get("status")
152
- if status in {"filled", "canceled"}:
153
- return snapshot
154
- else:
155
- await broker.update_finish_order(instrument_id=symbol)
156
- finished = broker.store.order_finish.get({"order_id": order_id})
157
- if finished:
158
- return finished
159
- await asyncio.sleep(poll_interval)
160
-
161
- # asyncio.timeout will raise TimeoutError; this return is defensive
162
- return last_snapshot or {}
163
-
164
-
165
141
  async def order_sync_polling(
166
142
  broker: Lbank,
167
143
  *,
@@ -173,12 +149,32 @@ async def order_sync_polling(
173
149
  window_sec: float = 5.0,
174
150
  grace_sec: float = 5.0,
175
151
  poll_interval: float = 0.5,
176
- ) -> dict:
152
+ ) -> Union[dict, None]:
177
153
  """
178
- 由于 LBank 暂无订单推送,这里通过 ``update`` 轮询同步订单状态。
179
-
180
- - window_sec: 主轮询窗口,订单若持续存在则触发撤单流程。
181
- - grace_sec: 撤单后的额外等待窗口。
154
+ 由于 LBank 暂无订单推送,这里通过 REST ``orders`` 与 ``position`` 查询实现订单同步,可在不同状态下返回仓位快照。
155
+
156
+ - window_sec: 主轮询窗口,订单若持续存在则触发撤单流程;
157
+ - grace_sec: 撤单后的额外等待窗口;
158
+ - 返回值示例:
159
+ .. code:: json
160
+
161
+ {
162
+ "position_id": "1000633222380983",
163
+ "bus_id": "1001770970175249",
164
+ "symbol": "SOLUSDT",
165
+ "side": "long",
166
+ "quantity": "0.06",
167
+ "available": "0.0",
168
+ "avg_price": "183.62",
169
+ "entry_price": "183.62",
170
+ "leverage": "100.0",
171
+ "liquidation_price": "0",
172
+ "margin_used": "0.110175",
173
+ "unrealized_pnl": "0.0",
174
+ "realized_pnl": "0.0",
175
+ "update_time": "1760195121",
176
+ "insert_time": "1758806193"
177
+ }
182
178
  """
183
179
 
184
180
  norm_type = order_type.lower()
@@ -198,6 +194,7 @@ async def order_sync_polling(
198
194
  price=price,
199
195
  volume=volume,
200
196
  )
197
+
201
198
  latency = int(time.time() * 1000) - started
202
199
  print(f"下单延迟 {latency} ms")
203
200
 
@@ -210,46 +207,108 @@ async def order_sync_polling(
210
207
  if not order_id:
211
208
  raise RuntimeError(f"place_order 返回缺少 order_id: {resp}")
212
209
 
210
+ position_id = (
211
+ resp.get("PositionID")
212
+ or resp.get("positionID")
213
+ or resp.get("positionId")
214
+ )
215
+
216
+ async def _refresh_position(*, allow_symbol_fallback: bool) -> dict | None:
217
+ try:
218
+ await broker.update("position")
219
+ except ContentTypeError:
220
+ await asyncio.sleep(poll_interval)
221
+ return None
222
+ if position_id:
223
+ pos = broker.store.position.get({"position_id": position_id})
224
+ if pos and pos.get("avg_price") is not None:
225
+ return pos
226
+ if allow_symbol_fallback:
227
+ candidates = broker.store.position.find({"symbol": symbol}) or []
228
+ if candidates:
229
+ pos = candidates[0]
230
+ if pos and pos.get("avg_price") is not None:
231
+ return pos
232
+ return None
233
+
234
+ async def _poll_orders(timeout_sec: float, *, allow_symbol_fallback: bool) -> dict | None:
235
+ nonlocal position_id
236
+ order_seen = False
237
+ async with asyncio.timeout(timeout_sec):
238
+ while True:
239
+ try:
240
+ await broker.update("orders")
241
+ except ContentTypeError:
242
+ await asyncio.sleep(poll_interval)
243
+ continue
244
+ snapshot = broker.store.orders.get({"order_id": order_id})
245
+ if snapshot is None:
246
+ if not order_seen and not allow_symbol_fallback:
247
+ await asyncio.sleep(poll_interval)
248
+ continue
249
+ pos_snapshot = await _refresh_position(allow_symbol_fallback=allow_symbol_fallback)
250
+ if pos_snapshot is not None:
251
+ return pos_snapshot
252
+ await asyncio.sleep(poll_interval)
253
+ continue
254
+ order_seen = True
255
+ position_id = position_id or snapshot.get("position_id")
256
+ await asyncio.sleep(poll_interval)
257
+
213
258
  try:
214
- return await wait_order_state_poll(
215
- broker,
216
- order_id,
217
- symbol,
218
- window_sec,
219
- poll_interval=poll_interval,
220
- )
259
+ polled_position = await _poll_orders(window_sec, allow_symbol_fallback=False)
260
+ if polled_position:
261
+ return polled_position
221
262
  except TimeoutError:
263
+ pass
264
+
265
+ for _attempt in range(3):
222
266
  try:
223
267
  await broker.cancel_order(order_id)
224
- except Exception as exc:
225
- print(f"cancel failed: {exc}")
226
- try:
227
- return await wait_order_state_poll(
228
- broker,
229
- order_id,
230
- symbol,
231
- grace_sec,
232
- poll_interval=poll_interval,
233
- )
234
- except TimeoutError:
235
- return {"order_id": order_id, "symbol": symbol, "state": "timeout"}
268
+ break
269
+ except Exception as e:
270
+ if '不存在' in str(e):
271
+ break
272
+ else:
273
+ print(f'撤单失败, 重试 {_attempt+1}/3: {e}')
274
+ try:
275
+ polled_position = await _poll_orders(grace_sec, allow_symbol_fallback=True)
276
+ if polled_position:
277
+ return polled_position
278
+ except TimeoutError:
279
+ pass
280
+
281
+ # 超过宽限期仍没有仓位变更,尝试最后一次使用 symbol 兜底
282
+ return await _refresh_position(allow_symbol_fallback=True)
236
283
 
237
284
 
238
285
  async def test_order_sync_polling():
239
286
  async with pybotters.Client(apis="./apis.json") as client:
240
287
  async with Lbank(client) as lb:
288
+ await lb.sub_orderbook(["SOLUSDT"], limit=1)
289
+ await lb.store.book.wait()
290
+ bid0 = float(lb.store.book.find({"s": "SOLUSDT", 'S': 'b'})[0]['p'])
291
+ bid0 = bid0 - 0.03
292
+ print(bid0)
293
+
241
294
  result = await order_sync_polling(
242
295
  lb,
243
296
  symbol="SOLUSDT",
244
297
  direction="buy",
245
- order_type="limit_gtc",
246
- price=200.1,
298
+ order_type="limit_GTC",
299
+ price=bid0,
247
300
  volume=0.03,
248
301
  window_sec=3.0,
249
- grace_sec=3.0,
250
- poll_interval=1.0,
302
+ grace_sec=1,
303
+ poll_interval=1
251
304
  )
252
305
  print(result)
253
306
 
307
+ async def test_query_order():
308
+ async with pybotters.Client(apis='./apis.json') as client:
309
+ async with Lbank(client) as lb:
310
+ res = await lb.query_order("1000633129818889")
311
+ print(res)
312
+
254
313
  if __name__ == "__main__":
255
314
  asyncio.run(test_order_sync_polling())
@@ -530,7 +530,7 @@ wheels = [
530
530
 
531
531
  [[package]]
532
532
  name = "hyperquant"
533
- version = "0.71"
533
+ version = "0.73"
534
534
  source = { editable = "." }
535
535
  dependencies = [
536
536
  { name = "aiohttp" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes