hyperquant 0.7__tar.gz → 0.8__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 (55) hide show
  1. {hyperquant-0.7 → hyperquant-0.8}/PKG-INFO +1 -1
  2. {hyperquant-0.7 → hyperquant-0.8}/apis.json +6 -1
  3. {hyperquant-0.7 → hyperquant-0.8}/pyproject.toml +1 -1
  4. hyperquant-0.8/src/hyperquant/broker/bitget.py +311 -0
  5. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/lbank.py +239 -5
  6. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/models/bitget.py +84 -8
  7. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/models/lbank.py +16 -6
  8. hyperquant-0.8/tests/test_bitget.py +193 -0
  9. {hyperquant-0.7 → hyperquant-0.8}/tests/test_lbank.py +126 -87
  10. hyperquant-0.8/uv.lock +2031 -0
  11. hyperquant-0.7/doc/edgex_act.md +0 -1592
  12. hyperquant-0.7/doc/edgex_debug.md +0 -2
  13. hyperquant-0.7/doc/edgex_order.md +0 -1767
  14. hyperquant-0.7/doc/edgex_ws.md +0 -0
  15. hyperquant-0.7/src/hyperquant/broker/bitget.py +0 -101
  16. hyperquant-0.7/tests/test_bitget.py +0 -59
  17. hyperquant-0.7/uv.lock +0 -1637
  18. {hyperquant-0.7 → hyperquant-0.8}/.gitignore +0 -0
  19. {hyperquant-0.7 → hyperquant-0.8}/.python-version +0 -0
  20. {hyperquant-0.7 → hyperquant-0.8}/README.md +0 -0
  21. {hyperquant-0.7 → hyperquant-0.8}/data/alpine_smoke.log +0 -0
  22. {hyperquant-0.7 → hyperquant-0.8}/data/logs/notikit.log +0 -0
  23. {hyperquant-0.7 → hyperquant-0.8}/data/logs/test_order_sync.log +0 -0
  24. {hyperquant-0.7 → hyperquant-0.8}/data/records_swap.csv +0 -0
  25. {hyperquant-0.7 → hyperquant-0.8}/data/records_swapc.csv +0 -0
  26. {hyperquant-0.7 → hyperquant-0.8}/doc/lbank.md +0 -0
  27. {hyperquant-0.7 → hyperquant-0.8}/pub.sh +0 -0
  28. {hyperquant-0.7 → hyperquant-0.8}/requirements-dev.lock +0 -0
  29. {hyperquant-0.7 → hyperquant-0.8}/requirements.lock +0 -0
  30. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/__init__.py +0 -0
  31. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/auth.py +0 -0
  32. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/edgex.py +0 -0
  33. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/hyperliquid.py +0 -0
  34. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  35. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/lib/hpstore.py +0 -0
  36. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  37. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/lib/util.py +0 -0
  38. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/models/edgex.py +0 -0
  39. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  40. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/models/ourbit.py +0 -0
  41. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/ourbit.py +0 -0
  42. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/broker/ws.py +0 -0
  43. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/core.py +0 -0
  44. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/datavison/_util.py +0 -0
  45. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/datavison/binance.py +0 -0
  46. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/datavison/coinglass.py +0 -0
  47. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/datavison/okx.py +0 -0
  48. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/db.py +0 -0
  49. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/draw.py +0 -0
  50. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/logkit.py +0 -0
  51. {hyperquant-0.7 → hyperquant-0.8}/src/hyperquant/notikit.py +0 -0
  52. {hyperquant-0.7 → hyperquant-0.8}/tests/test_draw.py +0 -0
  53. {hyperquant-0.7 → hyperquant-0.8}/tests/test_edgex.py +0 -0
  54. {hyperquant-0.7 → hyperquant-0.8}/tests/test_ourbit.py +0 -0
  55. {hyperquant-0.7 → hyperquant-0.8}/tests/tmp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.7
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
@@ -8,6 +8,11 @@
8
8
  "3MB7nAnnNPTxAnwdbjDPew-02b746d6a832346a46a97faf054b2909c1a0b58a35e04c3504923a99a5503c1c"
9
9
  ],
10
10
  "lbank": [
11
- "617a2d77b4cd4dfbbaa2094c17017bdb"
11
+ "56f4acfe75094820bffac61b42aab655"
12
+ ],
13
+ "bitget":[
14
+ "bg_03e0445d9282f248d22842cfe6f30192",
15
+ "67ec894753d75fec12332881278420863a960ec39c8f5acf1de88aa1da926854",
16
+ "huainian0408"
12
17
  ]
13
18
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "0.7"
3
+ version = "0.8"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -0,0 +1,311 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ import uuid
6
+ from typing import Any, Literal
7
+
8
+ import pybotters
9
+
10
+ from .models.bitget import BitgetDataStore
11
+ from .lib.util import fmt_value
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ class Bitget:
17
+ """Bitget public/privileged client (REST + WS).
18
+
19
+ 默认只支持单向持仓(One-way mode)。
20
+ """
21
+
22
+ def __init__(
23
+ self,
24
+ client: pybotters.Client,
25
+ *,
26
+ rest_api: str | None = None,
27
+ ws_url: str | None = None,
28
+ ) -> None:
29
+ self.client = client
30
+ self.store = BitgetDataStore()
31
+
32
+ self.rest_api = rest_api or "https://api.bitget.com"
33
+ self.ws_url = ws_url or "wss://ws.bitget.com/v2/ws/public"
34
+ self.ws_url_private = ws_url or "wss://ws.bitget.com/v2/ws/private"
35
+
36
+ self.ws_app = None
37
+ self.has_sub_personal = False
38
+
39
+
40
+ async def __aenter__(self) -> "Bitget":
41
+ await self.update("detail")
42
+ return self
43
+
44
+ async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
45
+ pass
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
+
68
+ async def update(
69
+ self,
70
+ update_type: Literal["detail", "ticker", "all"] = "all",
71
+ ) -> None:
72
+ """Refresh cached REST resources."""
73
+
74
+ requests: list[Any] = []
75
+
76
+ if update_type in {"detail", "all"}:
77
+ requests.append(
78
+ self.client.get(
79
+ f"{self.rest_api}/api/v2/mix/market/contracts",
80
+ params={"productType": "usdt-futures"},
81
+ )
82
+ )
83
+
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)
96
+
97
+ async def place_order(
98
+ self,
99
+ symbol: str,
100
+ *,
101
+ direction: Literal["buy", "sell", "long", "short", "0", "1"],
102
+ volume: float,
103
+ price: float | None = None,
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)
178
+
179
+ async def cancel_order(
180
+ self,
181
+ order_sys_id: str,
182
+ *,
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,
188
+ ) -> dict[str, Any]:
189
+ """Cancel an order via ``POST /api/v2/mix/order/cancel-order``."""
190
+
191
+ payload = {
192
+ "symbol": symbol,
193
+ "productType": product_type,
194
+ "marginMode": margin_mode,
195
+ "marginCoin": margin_coin,
196
+ }
197
+
198
+ if client_order_id:
199
+ payload["clientOid"] = client_order_id
200
+ else:
201
+ payload["orderId"] = order_sys_id
202
+
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": []}
214
+ for symbol in symbols:
215
+ submsg["args"].append(
216
+ {"instType": "USDT-FUTURES", "channel": channel, "instId": symbol}
217
+ )
218
+
219
+ self.client.ws_connect(
220
+ self.ws_url,
221
+ send_json=submsg,
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 {}
@@ -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
- # if client_order_id:
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.1)
588
+ await asyncio.sleep(0.3)