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 +35 -0
- hyperquant/broker/edgex.py +35 -23
- hyperquant/broker/lbank.py +260 -17
- hyperquant/broker/models/edgex.py +18 -0
- hyperquant/broker/models/lbank.py +342 -0
- {hyperquant-0.66.dist-info → hyperquant-0.68.dist-info}/METADATA +1 -1
- {hyperquant-0.66.dist-info → hyperquant-0.68.dist-info}/RECORD +8 -8
- {hyperquant-0.66.dist-info → hyperquant-0.68.dist-info}/WHEEL +0 -0
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
|
)
|
hyperquant/broker/edgex.py
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
"
|
151
|
-
|
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
|
-
|
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
|
|
hyperquant/broker/lbank.py
CHANGED
@@ -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
|
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(
|
48
|
-
|
49
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
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
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
json={"ProductGroup":
|
63
|
-
headers=
|
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.
|
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=
|
8
|
-
hyperquant/broker/edgex.py,sha256=
|
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=
|
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=
|
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=
|
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.
|
26
|
-
hyperquant-0.
|
27
|
-
hyperquant-0.
|
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,,
|
File without changes
|