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.
- {hyperquant-0.72 → hyperquant-0.74}/PKG-INFO +1 -1
- {hyperquant-0.72 → hyperquant-0.74}/apis.json +4 -2
- {hyperquant-0.72 → hyperquant-0.74}/pyproject.toml +1 -1
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/bitget.py +1 -1
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lbank.py +197 -1
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/bitget.py +71 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/lbank.py +3 -0
- {hyperquant-0.72 → hyperquant-0.74}/tests/test_lbank.py +125 -66
- {hyperquant-0.72 → hyperquant-0.74}/uv.lock +1 -1
- {hyperquant-0.72 → hyperquant-0.74}/.gitignore +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/.python-version +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/README.md +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/data/alpine_smoke.log +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/data/logs/notikit.log +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/data/logs/test_order_sync.log +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/data/records_swap.csv +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/data/records_swapc.csv +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/doc/lbank.md +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/pub.sh +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/requirements-dev.lock +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/requirements.lock +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/__init__.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/auth.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/edgex.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/hyperliquid.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/hpstore.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/hyper_types.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/lib/util.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/edgex.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/hyperliquid.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/models/ourbit.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/ourbit.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/broker/ws.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/core.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/_util.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/binance.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/coinglass.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/datavison/okx.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/db.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/draw.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/logkit.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/src/hyperquant/notikit.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/tests/test_bitget.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/tests/test_draw.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/tests/test_edgex.py +0 -0
- {hyperquant-0.72 → hyperquant-0.74}/tests/test_ourbit.py +0 -0
- {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.
|
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": [
|
11
|
-
|
10
|
+
"lbank": [
|
11
|
+
"925a645e03c84c8e96b96008e98b75f7"
|
12
|
+
],
|
13
|
+
"bitget":[
|
12
14
|
"bg_03e0445d9282f248d22842cfe6f30192",
|
13
15
|
"67ec894753d75fec12332881278420863a960ec39c8f5acf1de88aa1da926854",
|
14
16
|
"huainian0408"
|
@@ -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": "
|
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
|
-
|
107
|
-
|
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
|
-
"
|
124
|
+
"SOLUSDT",
|
121
125
|
direction="buy",
|
122
126
|
order_type='limit_gtc',
|
123
|
-
price=
|
124
|
-
volume=
|
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 暂无订单推送,这里通过 ``
|
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
|
-
|
215
|
-
|
216
|
-
|
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
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
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="
|
246
|
-
price=
|
298
|
+
order_type="limit_GTC",
|
299
|
+
price=bid0,
|
247
300
|
volume=0.03,
|
248
301
|
window_sec=3.0,
|
249
|
-
grace_sec=
|
250
|
-
poll_interval=1
|
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())
|
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
|
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
|
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
|
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
|
File without changes
|
File without changes
|
File without changes
|