hyperquant 0.7__py3-none-any.whl → 0.8__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/bitget.py +245 -35
- hyperquant/broker/lbank.py +239 -5
- hyperquant/broker/models/bitget.py +84 -8
- hyperquant/broker/models/lbank.py +16 -6
- {hyperquant-0.7.dist-info → hyperquant-0.8.dist-info}/METADATA +1 -1
- {hyperquant-0.7.dist-info → hyperquant-0.8.dist-info}/RECORD +7 -7
- {hyperquant-0.7.dist-info → hyperquant-0.8.dist-info}/WHEEL +0 -0
hyperquant/broker/bitget.py
CHANGED
@@ -1,10 +1,9 @@
|
|
1
1
|
from __future__ import annotations
|
2
2
|
|
3
3
|
import asyncio
|
4
|
-
import itertools
|
5
4
|
import logging
|
6
|
-
import
|
7
|
-
from typing import Any,
|
5
|
+
import uuid
|
6
|
+
from typing import Any, Literal
|
8
7
|
|
9
8
|
import pybotters
|
10
9
|
|
@@ -14,9 +13,11 @@ from .lib.util import fmt_value
|
|
14
13
|
logger = logging.getLogger(__name__)
|
15
14
|
|
16
15
|
|
17
|
-
|
18
16
|
class Bitget:
|
19
|
-
"""Bitget public
|
17
|
+
"""Bitget public/privileged client (REST + WS).
|
18
|
+
|
19
|
+
默认只支持单向持仓(One-way mode)。
|
20
|
+
"""
|
20
21
|
|
21
22
|
def __init__(
|
22
23
|
self,
|
@@ -32,70 +33,279 @@ class Bitget:
|
|
32
33
|
self.ws_url = ws_url or "wss://ws.bitget.com/v2/ws/public"
|
33
34
|
self.ws_url_private = ws_url or "wss://ws.bitget.com/v2/ws/private"
|
34
35
|
|
35
|
-
self.
|
36
|
-
|
36
|
+
self.ws_app = None
|
37
|
+
self.has_sub_personal = False
|
38
|
+
|
37
39
|
|
38
40
|
async def __aenter__(self) -> "Bitget":
|
39
41
|
await self.update("detail")
|
40
42
|
return self
|
41
43
|
|
42
|
-
async def __aexit__(self, exc_type, exc, tb) -> None:
|
44
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
|
43
45
|
pass
|
44
46
|
|
47
|
+
async def sub_personal(self) -> None:
|
48
|
+
sub_msg = {
|
49
|
+
"op": "subscribe",
|
50
|
+
"args": [
|
51
|
+
{"instType": "USDT-FUTURES", "channel": "orders", "instId": "default"},
|
52
|
+
{
|
53
|
+
"instType": "USDT-FUTURES",
|
54
|
+
"channel": "positions",
|
55
|
+
"instId": "default",
|
56
|
+
},
|
57
|
+
{"instType": "USDT-FUTURES", "channel": "account", "coin": "default"},
|
58
|
+
],
|
59
|
+
}
|
60
|
+
self.ws_app = await self._ensure_private_ws()
|
61
|
+
|
62
|
+
|
63
|
+
await self.ws_app.current_ws.send_json(sub_msg)
|
64
|
+
|
65
|
+
|
66
|
+
self.has_sub_personal = True
|
67
|
+
|
45
68
|
async def update(
|
46
69
|
self,
|
47
|
-
update_type: Literal["detail",
|
70
|
+
update_type: Literal["detail", "ticker", "all"] = "all",
|
48
71
|
) -> None:
|
49
|
-
|
50
|
-
|
51
|
-
|
72
|
+
"""Refresh cached REST resources."""
|
73
|
+
|
74
|
+
requests: list[Any] = []
|
75
|
+
|
76
|
+
if update_type in {"detail", "all"}:
|
77
|
+
requests.append(
|
52
78
|
self.client.get(
|
53
|
-
f"{self.rest_api}/api/v2/mix/market/contracts
|
79
|
+
f"{self.rest_api}/api/v2/mix/market/contracts",
|
80
|
+
params={"productType": "usdt-futures"},
|
54
81
|
)
|
55
82
|
)
|
56
83
|
|
57
|
-
|
84
|
+
if update_type in {"ticker", "all"}:
|
85
|
+
requests.append(
|
86
|
+
self.client.get(
|
87
|
+
f"{self.rest_api}/api/v2/mix/market/tickers",
|
88
|
+
params={"productType": "usdt-futures"},
|
89
|
+
)
|
90
|
+
)
|
91
|
+
|
92
|
+
if not requests:
|
93
|
+
raise ValueError(f"update_type err: {update_type}")
|
94
|
+
|
95
|
+
await self.store.initialize(*requests)
|
58
96
|
|
59
97
|
async def place_order(
|
60
98
|
self,
|
61
99
|
symbol: str,
|
62
100
|
*,
|
63
|
-
direction: Literal["buy", "sell", "0", "1"],
|
101
|
+
direction: Literal["buy", "sell", "long", "short", "0", "1"],
|
64
102
|
volume: float,
|
65
103
|
price: float | None = None,
|
66
|
-
order_type: Literal[
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
104
|
+
order_type: Literal[
|
105
|
+
"market",
|
106
|
+
"limit_gtc",
|
107
|
+
"limit_ioc",
|
108
|
+
"limit_fok",
|
109
|
+
"limit_post_only",
|
110
|
+
"limit",
|
111
|
+
] = "market",
|
112
|
+
margin_mode: Literal["isolated", "crossed"] = "crossed",
|
113
|
+
product_type: str = "USDT-FUTURES",
|
114
|
+
margin_coin: str = "USDT",
|
115
|
+
reduce_only: bool | None = None,
|
116
|
+
offset_flag: Literal["open", "close", "0", "1"] | None = None,
|
117
|
+
client_order_id: str | None = None
|
118
|
+
) -> dict[str, Any] | None:
|
119
|
+
"""
|
120
|
+
请求成功返回示例:
|
121
|
+
|
122
|
+
.. code:: json
|
123
|
+
|
124
|
+
{
|
125
|
+
"clientOid": "121211212122",
|
126
|
+
"orderId": "121211212122"
|
127
|
+
}
|
128
|
+
"""
|
129
|
+
|
130
|
+
side = self._normalize_direction(direction)
|
131
|
+
order_type_code, force_code = self._resolve_order_type(order_type)
|
132
|
+
|
133
|
+
if reduce_only is None:
|
134
|
+
reduce_only = self._normalize_offset(offset_flag)
|
135
|
+
|
136
|
+
detail = self._get_detail_entry(symbol)
|
137
|
+
volume_str = self._format_with_step(
|
138
|
+
volume, detail.get("step_size") or detail.get("stepSize")
|
139
|
+
)
|
140
|
+
|
141
|
+
payload: dict[str, Any] = {
|
142
|
+
"symbol": symbol,
|
143
|
+
"productType": product_type,
|
144
|
+
"marginMode": margin_mode,
|
145
|
+
"marginCoin": margin_coin,
|
146
|
+
"side": side,
|
147
|
+
"size": volume_str,
|
148
|
+
"orderType": order_type_code,
|
149
|
+
}
|
150
|
+
|
151
|
+
if force_code:
|
152
|
+
payload["force"] = force_code
|
153
|
+
|
154
|
+
if order_type_code == "limit":
|
155
|
+
if price is None:
|
156
|
+
raise ValueError("price is required for Bitget limit orders")
|
157
|
+
payload["price"] = self._format_with_step(
|
158
|
+
price,
|
159
|
+
detail.get("tick_size") or detail.get("tickSize"),
|
160
|
+
)
|
161
|
+
elif price is not None:
|
162
|
+
logger.debug("Price %.8f ignored for market order", price)
|
163
|
+
|
164
|
+
if reduce_only is True:
|
165
|
+
payload["reduceOnly"] = "YES"
|
166
|
+
elif reduce_only is False:
|
167
|
+
payload["reduceOnly"] = "NO"
|
168
|
+
|
169
|
+
if client_order_id:
|
170
|
+
payload["clientOid"] = client_order_id
|
171
|
+
|
172
|
+
res = await self.client.post(
|
173
|
+
f"{self.rest_api}/api/v2/mix/order/place-order",
|
174
|
+
data=payload,
|
175
|
+
)
|
176
|
+
data = await res.json()
|
177
|
+
return self._ensure_ok("place_order", data)
|
74
178
|
|
75
179
|
async def cancel_order(
|
76
180
|
self,
|
77
181
|
order_sys_id: str,
|
78
182
|
*,
|
79
|
-
|
183
|
+
symbol: str,
|
184
|
+
margin_mode: Literal["isolated", "crossed"],
|
185
|
+
product_type: str = "USDT-FUTURES",
|
186
|
+
margin_coin: str = "USDT",
|
187
|
+
client_order_id: str | None = None,
|
80
188
|
) -> dict[str, Any]:
|
81
|
-
|
189
|
+
"""Cancel an order via ``POST /api/v2/mix/order/cancel-order``."""
|
82
190
|
|
191
|
+
payload = {
|
192
|
+
"symbol": symbol,
|
193
|
+
"productType": product_type,
|
194
|
+
"marginMode": margin_mode,
|
195
|
+
"marginCoin": margin_coin,
|
196
|
+
}
|
83
197
|
|
84
|
-
|
85
|
-
|
86
|
-
|
198
|
+
if client_order_id:
|
199
|
+
payload["clientOid"] = client_order_id
|
200
|
+
else:
|
201
|
+
payload["orderId"] = order_sys_id
|
87
202
|
|
88
|
-
|
89
|
-
"
|
90
|
-
|
91
|
-
|
203
|
+
res = await self.client.post(
|
204
|
+
f"{self.rest_api}/api/v2/mix/order/cancel-order",
|
205
|
+
data=payload,
|
206
|
+
)
|
207
|
+
data = await res.json()
|
208
|
+
return self._ensure_ok("cancel_order", data)
|
209
|
+
|
210
|
+
async def sub_orderbook(self, symbols: list[str], channel: str = "books1") -> None:
|
211
|
+
"""Subscribe to Bitget order-book snapshots/updates."""
|
212
|
+
|
213
|
+
submsg = {"op": "subscribe", "args": []}
|
92
214
|
for symbol in symbols:
|
93
215
|
submsg["args"].append(
|
94
|
-
{"instType": "
|
216
|
+
{"instType": "USDT-FUTURES", "channel": channel, "instId": symbol}
|
95
217
|
)
|
96
218
|
|
97
219
|
self.client.ws_connect(
|
98
220
|
self.ws_url,
|
99
221
|
send_json=submsg,
|
100
|
-
hdlr_json=self.store.onmessage
|
101
|
-
)
|
222
|
+
hdlr_json=self.store.onmessage,
|
223
|
+
)
|
224
|
+
|
225
|
+
def _get_detail_entry(self, symbol: str) -> dict[str, Any]:
|
226
|
+
detail = self.store.detail.get({"symbol": symbol})
|
227
|
+
if not detail:
|
228
|
+
raise ValueError(
|
229
|
+
f"Unknown Bitget instrument: {symbol}. Call update('detail') first or provide valid symbol."
|
230
|
+
)
|
231
|
+
return detail
|
232
|
+
|
233
|
+
async def _ensure_private_ws(self):
|
234
|
+
wsqueue = pybotters.WebSocketQueue()
|
235
|
+
ws_app = self.client.ws_connect(
|
236
|
+
self.ws_url_private,
|
237
|
+
hdlr_json=self.store.onmessage,
|
238
|
+
)
|
239
|
+
# async for msg in wsqueue:
|
240
|
+
# print(msg)
|
241
|
+
|
242
|
+
await ws_app._event.wait()
|
243
|
+
await ws_app.current_ws._wait_authtask()
|
244
|
+
return ws_app
|
245
|
+
|
246
|
+
@staticmethod
|
247
|
+
def _format_with_step(value: float, step: Any) -> str:
|
248
|
+
if step in (None, 0, "0"):
|
249
|
+
return str(value)
|
250
|
+
try:
|
251
|
+
step_float = float(step)
|
252
|
+
except (TypeError, ValueError): # pragma: no cover - defensive guard
|
253
|
+
return str(value)
|
254
|
+
if step_float <= 0:
|
255
|
+
return str(value)
|
256
|
+
return fmt_value(value, step_float)
|
257
|
+
|
258
|
+
@staticmethod
|
259
|
+
def _normalize_direction(direction: str) -> str:
|
260
|
+
mapping = {
|
261
|
+
"buy": "buy",
|
262
|
+
"long": "buy",
|
263
|
+
"0": "buy",
|
264
|
+
"sell": "sell",
|
265
|
+
"short": "sell",
|
266
|
+
"1": "sell",
|
267
|
+
}
|
268
|
+
key = str(direction).lower()
|
269
|
+
try:
|
270
|
+
return mapping[key]
|
271
|
+
except KeyError as exc: # pragma: no cover - guard
|
272
|
+
raise ValueError(f"Unsupported direction: {direction}") from exc
|
273
|
+
|
274
|
+
@staticmethod
|
275
|
+
def _normalize_offset(
|
276
|
+
offset: Literal["open", "close", "0", "1"] | None,
|
277
|
+
) -> bool | None:
|
278
|
+
if offset is None:
|
279
|
+
return None
|
280
|
+
mapping = {
|
281
|
+
"open": False,
|
282
|
+
"0": False,
|
283
|
+
"close": True,
|
284
|
+
"1": True,
|
285
|
+
}
|
286
|
+
key = str(offset).lower()
|
287
|
+
if key in mapping:
|
288
|
+
return mapping[key]
|
289
|
+
raise ValueError(f"Unsupported offset_flag: {offset}")
|
290
|
+
|
291
|
+
@staticmethod
|
292
|
+
def _resolve_order_type(order_type: str) -> tuple[str, str | None]:
|
293
|
+
mapping = {
|
294
|
+
"market": ("market", None),
|
295
|
+
"limit": ("limit", "gtc"),
|
296
|
+
"limit_gtc": ("limit", "gtc"),
|
297
|
+
"limit_ioc": ("limit", "ioc"),
|
298
|
+
"limit_fok": ("limit", "fok"),
|
299
|
+
"limit_post_only": ("limit", "post_only"),
|
300
|
+
}
|
301
|
+
key = str(order_type).lower()
|
302
|
+
try:
|
303
|
+
return mapping[key]
|
304
|
+
except KeyError as exc: # pragma: no cover - guard
|
305
|
+
raise ValueError(f"Unsupported order_type: {order_type}") from exc
|
306
|
+
|
307
|
+
@staticmethod
|
308
|
+
def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
|
309
|
+
if not isinstance(data, dict) or data.get("code") != "00000":
|
310
|
+
raise RuntimeError(f"{operation} failed: {data}")
|
311
|
+
return data.get("data") or {}
|
hyperquant/broker/lbank.py
CHANGED
@@ -136,6 +136,162 @@ 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
|
+
"""
|
227
|
+
返回值示例:
|
228
|
+
|
229
|
+
.. code:: json
|
230
|
+
|
231
|
+
{
|
232
|
+
"order_id": "1000632478428573",
|
233
|
+
"instrument_id": "SOLUSDT",
|
234
|
+
"position_id": "1000632478428573",
|
235
|
+
"direction": "0",
|
236
|
+
"offset_flag": "0",
|
237
|
+
"trade_time": 1760123456,
|
238
|
+
"avg_price": 182.5,
|
239
|
+
"volume": 0.03,
|
240
|
+
"turnover": 5.475,
|
241
|
+
"fee": 0.003285,
|
242
|
+
"trade_count": 1
|
243
|
+
}
|
244
|
+
|
245
|
+
如果没有订单成交返回
|
246
|
+
{
|
247
|
+
"order_id": "1000632478428573",
|
248
|
+
"trade_count": 0
|
249
|
+
}
|
250
|
+
"""
|
251
|
+
|
252
|
+
if not order_id:
|
253
|
+
raise ValueError("order_id is required to query order statistics")
|
254
|
+
|
255
|
+
trades = await self.query_trade(
|
256
|
+
order_id,
|
257
|
+
product_group=product_group,
|
258
|
+
page_index=page_index,
|
259
|
+
page_size=page_size,
|
260
|
+
)
|
261
|
+
|
262
|
+
if not trades:
|
263
|
+
return {
|
264
|
+
"order_id": order_id,
|
265
|
+
"trade_count": 0,
|
266
|
+
}
|
267
|
+
|
268
|
+
def _to_float(value: Any) -> float:
|
269
|
+
try:
|
270
|
+
return float(value)
|
271
|
+
except (TypeError, ValueError):
|
272
|
+
return 0.0
|
273
|
+
|
274
|
+
total_volume = sum(_to_float(trade.get("Volume")) for trade in trades)
|
275
|
+
total_turnover = sum(_to_float(trade.get("Turnover")) for trade in trades)
|
276
|
+
total_fee = sum(_to_float(trade.get("Fee")) for trade in trades)
|
277
|
+
|
278
|
+
avg_price = total_turnover / total_volume if total_volume else None
|
279
|
+
last_trade = trades[-1]
|
280
|
+
|
281
|
+
return {
|
282
|
+
"order_id": order_id,
|
283
|
+
"instrument_id": last_trade.get("InstrumentID"),
|
284
|
+
"position_id": last_trade.get("PositionID"),
|
285
|
+
"direction": last_trade.get("Direction"),
|
286
|
+
"offset_flag": last_trade.get("OffsetFlag"),
|
287
|
+
"trade_time": last_trade.get("TradeTime"),
|
288
|
+
"avg_price": avg_price,
|
289
|
+
"volume": total_volume,
|
290
|
+
"turnover": total_turnover,
|
291
|
+
"fee": total_fee,
|
292
|
+
"trade_count": len(trades),
|
293
|
+
}
|
294
|
+
|
139
295
|
def _resolve_instrument(self) -> str | None:
|
140
296
|
detail_entries = self.store.detail.find()
|
141
297
|
if detail_entries:
|
@@ -212,7 +368,71 @@ class Lbank:
|
|
212
368
|
order_proportion: str = "0.0000",
|
213
369
|
client_order_id: str | None = None,
|
214
370
|
) -> dict[str, Any]:
|
215
|
-
"""Create an order using documented REST parameters.
|
371
|
+
"""Create an order using documented REST parameters.
|
372
|
+
|
373
|
+
返回示例:
|
374
|
+
|
375
|
+
.. code:: json
|
376
|
+
|
377
|
+
{
|
378
|
+
"offsetFlag": "5",
|
379
|
+
"orderType": "1",
|
380
|
+
"reserveMode": "0",
|
381
|
+
"fee": "0.0066042",
|
382
|
+
"frozenFee": "0",
|
383
|
+
"ddlnTime": "0",
|
384
|
+
"userID": "lbank_exchange_user",
|
385
|
+
"masterAccountID": "",
|
386
|
+
"exchangeID": "Exchange",
|
387
|
+
"accountID": "e1b03fb1-6849-464f-a986-94b9a6e625e6",
|
388
|
+
"orderSysID": "1000633129818889",
|
389
|
+
"volumeRemain": "0",
|
390
|
+
"price": "183.36",
|
391
|
+
"businessValue": "1760183423813",
|
392
|
+
"frozenMargin": "0",
|
393
|
+
"instrumentID": "SOLUSDT",
|
394
|
+
"posiDirection": "2",
|
395
|
+
"volumeMode": "1",
|
396
|
+
"volume": "0.06",
|
397
|
+
"insertTime": "1760183423",
|
398
|
+
"copyMemberID": "",
|
399
|
+
"position": "0.06",
|
400
|
+
"tradePrice": "183.45",
|
401
|
+
"leverage": "100",
|
402
|
+
"businessResult": "",
|
403
|
+
"availableUse": "0",
|
404
|
+
"orderStatus": "1",
|
405
|
+
"openPrice": "182.94",
|
406
|
+
"frozenMoney": "0",
|
407
|
+
"remark": "def",
|
408
|
+
"reserveUse": "0",
|
409
|
+
"sessionNo": "41",
|
410
|
+
"isCrossMargin": "1",
|
411
|
+
"closeProfit": "0.0306",
|
412
|
+
"businessNo": "1001770756852986", # 订单有成交会并入仓位 businessNo
|
413
|
+
"relatedOrderSysID": "",
|
414
|
+
"positionID": "1000632926272299",
|
415
|
+
"mockResp": false,
|
416
|
+
"deriveSource": "0",
|
417
|
+
"copyOrderID": "",
|
418
|
+
"currency": "USDT",
|
419
|
+
"turnover": "11.007",
|
420
|
+
"frontNo": "-68",
|
421
|
+
"direction": "1",
|
422
|
+
"orderPriceType": "4",
|
423
|
+
"volumeCancled": "0",
|
424
|
+
"updateTime": "1760183423",
|
425
|
+
"localID": "1000633129818889",
|
426
|
+
"volumeTraded": "0.06",
|
427
|
+
"appid": "WEB",
|
428
|
+
"tradeUnitID": "e1b03fb1-6849-464f-a",
|
429
|
+
"businessType": "P",
|
430
|
+
"memberID": "e1b03fb1-6849-464f-a986-94b9a6e625e6",
|
431
|
+
"timeCondition": "0",
|
432
|
+
"copyProfit": "0"
|
433
|
+
}
|
434
|
+
|
435
|
+
"""
|
216
436
|
|
217
437
|
direction_code = self._normalize_direction(direction)
|
218
438
|
offset_code = self._normalize_offset(offset_flag)
|
@@ -244,9 +464,7 @@ class Lbank:
|
|
244
464
|
# logger.warning("Price is ignored for market orders")
|
245
465
|
pass
|
246
466
|
|
247
|
-
|
248
|
-
# payload["LocalID"] = client_order_id
|
249
|
-
print(payload)
|
467
|
+
|
250
468
|
res = await self.client.post(
|
251
469
|
f"{self.front_api}/cfd/cff/v1/SendOrderInsert",
|
252
470
|
json=payload,
|
@@ -277,6 +495,22 @@ class Lbank:
|
|
277
495
|
if not isinstance(data, dict) or data.get("code") != 200:
|
278
496
|
raise RuntimeError(f"{operation} failed: {data}")
|
279
497
|
return data.get("data") or {}
|
498
|
+
|
499
|
+
|
500
|
+
async def set_position_mode(self, mode: Literal["hedge", "oneway"] = "oneway") -> dict[str, Any]:
|
501
|
+
"""设置持仓模式到单向持仓或对冲持仓"""
|
502
|
+
|
503
|
+
mode_code = "2" if mode == "oneway" else "1"
|
504
|
+
payload = {
|
505
|
+
"PositionType": mode_code,
|
506
|
+
}
|
507
|
+
res = await self.client.post(
|
508
|
+
f"{self.front_api}/cfd/action/v1.0/SendMemberAction",
|
509
|
+
json=payload,
|
510
|
+
headers=self._rest_headers,
|
511
|
+
)
|
512
|
+
data = await res.json()
|
513
|
+
return self._ensure_ok("set_position_mode", data)
|
280
514
|
|
281
515
|
@staticmethod
|
282
516
|
def _normalize_direction(direction: str) -> str:
|
@@ -351,4 +585,4 @@ class Lbank:
|
|
351
585
|
batch = send_jsons[i:i+5]
|
352
586
|
await asyncio.gather(*(sub(send_json) for send_json in batch))
|
353
587
|
if i + 5 < len(send_jsons):
|
354
|
-
await asyncio.sleep(0.
|
588
|
+
await asyncio.sleep(0.3)
|
@@ -16,13 +16,18 @@ class Detail(DataStore):
|
|
16
16
|
_KEYS = ["symbol"]
|
17
17
|
|
18
18
|
def _transform(self, entry: dict[str, Any]) -> dict[str, Any] | None:
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
step_size = place_to_step(
|
23
|
-
tick_size = place_to_step(
|
24
|
-
|
25
|
-
entry[
|
19
|
+
step_place = entry.get("volume_place", 1)
|
20
|
+
tick_place = entry.get("price_place", 1)
|
21
|
+
|
22
|
+
step_size = place_to_step(step_place)
|
23
|
+
tick_size = place_to_step(tick_place)
|
24
|
+
|
25
|
+
entry["stepSize"] = step_size
|
26
|
+
entry["tickSize"] = tick_size
|
27
|
+
# expose snake_case aliases for downstream callers keeping legacy naming
|
28
|
+
entry["step_size"] = step_size
|
29
|
+
entry["tick_size"] = tick_size
|
30
|
+
|
26
31
|
return entry
|
27
32
|
|
28
33
|
def _onresponse(self, data: list[dict[str, Any]] | dict[str, Any] | None) -> None:
|
@@ -280,4 +285,75 @@ class BitgetDataStore(BitgetV2DataStore):
|
|
280
285
|
}
|
281
286
|
]
|
282
287
|
"""
|
283
|
-
return self._get("book")
|
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")
|
@@ -184,7 +184,8 @@ class Orders(DataStore):
|
|
184
184
|
"side": direction,
|
185
185
|
"offset": offset_flag,
|
186
186
|
"order_type": order_kind,
|
187
|
-
"price": entry.get("Price"),
|
187
|
+
"price": entry.get('TradePrice') or entry.get("Price"),
|
188
|
+
"order_price": entry.get("OrderPrice") or entry.get('Price'),
|
188
189
|
"quantity": entry.get("Volume"),
|
189
190
|
"filled": entry.get("VolumeTraded"),
|
190
191
|
"remaining": entry.get("VolumeRemain"),
|
@@ -270,14 +271,20 @@ class Position(DataStore):
|
|
270
271
|
if not entry:
|
271
272
|
return None
|
272
273
|
position_id = entry.get("PositionID")
|
274
|
+
bus_id = entry.get("BusinessNo")
|
273
275
|
if not position_id:
|
274
276
|
return None
|
275
|
-
|
276
|
-
|
277
|
-
side =
|
278
|
-
|
277
|
+
|
278
|
+
q = float(entry.get("Position", 0))
|
279
|
+
side = "net"
|
280
|
+
if q > 0:
|
281
|
+
side = "long"
|
282
|
+
elif q < 0:
|
283
|
+
side = "short"
|
284
|
+
|
279
285
|
return {
|
280
286
|
"position_id": position_id,
|
287
|
+
"bus_id": bus_id,
|
281
288
|
"symbol": entry.get("InstrumentID"),
|
282
289
|
"side": side,
|
283
290
|
"quantity": entry.get("Position"),
|
@@ -291,6 +298,7 @@ class Position(DataStore):
|
|
291
298
|
"realized_pnl": entry.get("CloseProfit"),
|
292
299
|
"update_time": entry.get("UpdateTime"),
|
293
300
|
"insert_time": entry.get("InsertTime"),
|
301
|
+
"begin_time": entry.get("BeginTime")
|
294
302
|
}
|
295
303
|
|
296
304
|
def _onresponse(self, data: dict[str, Any] | None) -> None:
|
@@ -472,6 +480,7 @@ class LbankDataStore(DataStoreCollection):
|
|
472
480
|
[
|
473
481
|
{
|
474
482
|
"position_id": <仓位ID>,
|
483
|
+
"bus_id": <订单ID覆盖>,
|
475
484
|
"symbol": <合约ID>,
|
476
485
|
"side": "long" / "short" / "net",
|
477
486
|
"quantity": <持仓数量>,
|
@@ -484,7 +493,8 @@ class LbankDataStore(DataStoreCollection):
|
|
484
493
|
"unrealized_pnl": <未实现盈亏>,
|
485
494
|
"realized_pnl": <已实现盈亏>,
|
486
495
|
"update_time": <更新时间>,
|
487
|
-
"insert_time":
|
496
|
+
"insert_time": <插入时间>,
|
497
|
+
"begin_time": <持仓开始时间>
|
488
498
|
},
|
489
499
|
...
|
490
500
|
]
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: hyperquant
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.8
|
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
|
@@ -5,25 +5,25 @@ 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
7
|
hyperquant/broker/auth.py,sha256=Wst7mTBuUS2BQ5hZd0a8FNNs5Uc01ac9WzJpseTuyAY,7673
|
8
|
-
hyperquant/broker/bitget.py,sha256=
|
8
|
+
hyperquant/broker/bitget.py,sha256=X_S0LKZ7FZAEb6oEMr1vdGP1fondzK74BhmNTpRDSEA,9488
|
9
9
|
hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
|
10
10
|
hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
|
11
|
-
hyperquant/broker/lbank.py,sha256=
|
11
|
+
hyperquant/broker/lbank.py,sha256=ynlBA7TAPjsT_aaAh_G4jse8gNLYdmuQQcDaWSjcxL8,19559
|
12
12
|
hyperquant/broker/ourbit.py,sha256=NUcDSIttf-HGWzoW1uBTrGLPHlkuemMjYCm91MigTno,18228
|
13
13
|
hyperquant/broker/ws.py,sha256=9Zu5JSLj-ylYEVmFmRwvZDDnVYKwb37cLHfZzA0AZGc,2200
|
14
14
|
hyperquant/broker/lib/edgex_sign.py,sha256=lLUCmY8HHRLfLKyGrlTJYaBlSHPsIMWg3EZnQJKcmyk,95785
|
15
15
|
hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb84D8,8296
|
16
16
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
17
17
|
hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
|
18
|
-
hyperquant/broker/models/bitget.py,sha256=
|
18
|
+
hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
|
19
19
|
hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
|
20
20
|
hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
|
21
|
-
hyperquant/broker/models/lbank.py,sha256=
|
21
|
+
hyperquant/broker/models/lbank.py,sha256=vHkNKxIMzpoC_EwcZnEOPOupizF92yGWi9GKxvYYFUQ,19181
|
22
22
|
hyperquant/broker/models/ourbit.py,sha256=xMcbuCEXd3XOpPBq0RYF2zpTFNnxPtuNJZCexMZVZ1k,41965
|
23
23
|
hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw,533
|
24
24
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
25
25
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
26
26
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
27
|
-
hyperquant-0.
|
28
|
-
hyperquant-0.
|
29
|
-
hyperquant-0.
|
27
|
+
hyperquant-0.8.dist-info/METADATA,sha256=sZJCGMDc0ZTKhOfpK8IK7H0XkgKnhQJMX-Fai7aqahM,4316
|
28
|
+
hyperquant-0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
29
|
+
hyperquant-0.8.dist-info/RECORD,,
|
File without changes
|