hyperquant 0.82__tar.gz → 0.83__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.

Potentially problematic release.


This version of hyperquant might be problematic. Click here for more details.

Files changed (51) hide show
  1. {hyperquant-0.82 → hyperquant-0.83}/PKG-INFO +1 -1
  2. {hyperquant-0.82 → hyperquant-0.83}/apis.json +4 -0
  3. {hyperquant-0.82 → hyperquant-0.83}/pyproject.toml +1 -1
  4. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/auth.py +74 -1
  5. hyperquant-0.83/src/hyperquant/broker/coinw.py +405 -0
  6. hyperquant-0.83/src/hyperquant/broker/models/coinw.py +612 -0
  7. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/ws.py +53 -4
  8. hyperquant-0.83/tests/test_coinw.py +211 -0
  9. {hyperquant-0.82 → hyperquant-0.83}/uv.lock +1 -1
  10. {hyperquant-0.82 → hyperquant-0.83}/.gitignore +0 -0
  11. {hyperquant-0.82 → hyperquant-0.83}/.python-version +0 -0
  12. {hyperquant-0.82 → hyperquant-0.83}/README.md +0 -0
  13. {hyperquant-0.82 → hyperquant-0.83}/data/alpine_smoke.log +0 -0
  14. {hyperquant-0.82 → hyperquant-0.83}/data/logs/notikit.log +0 -0
  15. {hyperquant-0.82 → hyperquant-0.83}/data/logs/test_order_sync.log +0 -0
  16. {hyperquant-0.82 → hyperquant-0.83}/data/records_swap.csv +0 -0
  17. {hyperquant-0.82 → hyperquant-0.83}/data/records_swapc.csv +0 -0
  18. {hyperquant-0.82 → hyperquant-0.83}/doc/lbank.md +0 -0
  19. {hyperquant-0.82 → hyperquant-0.83}/pub.sh +0 -0
  20. {hyperquant-0.82 → hyperquant-0.83}/requirements-dev.lock +0 -0
  21. {hyperquant-0.82 → hyperquant-0.83}/requirements.lock +0 -0
  22. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/__init__.py +0 -0
  23. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/bitget.py +0 -0
  24. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/edgex.py +0 -0
  25. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/hyperliquid.py +0 -0
  26. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/lbank.py +0 -0
  27. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/lib/edgex_sign.py +0 -0
  28. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/lib/hpstore.py +0 -0
  29. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/lib/hyper_types.py +0 -0
  30. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/lib/util.py +0 -0
  31. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/models/bitget.py +0 -0
  32. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/models/edgex.py +0 -0
  33. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/models/hyperliquid.py +0 -0
  34. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/models/lbank.py +0 -0
  35. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/models/ourbit.py +0 -0
  36. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/broker/ourbit.py +0 -0
  37. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/core.py +0 -0
  38. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/datavison/_util.py +0 -0
  39. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/datavison/binance.py +0 -0
  40. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/datavison/coinglass.py +0 -0
  41. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/datavison/okx.py +0 -0
  42. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/db.py +0 -0
  43. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/draw.py +0 -0
  44. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/logkit.py +0 -0
  45. {hyperquant-0.82 → hyperquant-0.83}/src/hyperquant/notikit.py +0 -0
  46. {hyperquant-0.82 → hyperquant-0.83}/tests/test_bitget.py +0 -0
  47. {hyperquant-0.82 → hyperquant-0.83}/tests/test_draw.py +0 -0
  48. {hyperquant-0.82 → hyperquant-0.83}/tests/test_edgex.py +0 -0
  49. {hyperquant-0.82 → hyperquant-0.83}/tests/test_lbank.py +0 -0
  50. {hyperquant-0.82 → hyperquant-0.83}/tests/test_ourbit.py +0 -0
  51. {hyperquant-0.82 → hyperquant-0.83}/tests/tmp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperquant
3
- Version: 0.82
3
+ Version: 0.83
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
@@ -14,5 +14,9 @@
14
14
  "bg_03e0445d9282f248d22842cfe6f30192",
15
15
  "67ec894753d75fec12332881278420863a960ec39c8f5acf1de88aa1da926854",
16
16
  "huainian0408"
17
+ ],
18
+ "coinw":[
19
+ "eedb6bc3-fc05-45b3-9bbc-53d2fd1b3a04",
20
+ "RGMYGU2XHXZNKRKGR6TFU1BZRKXYMOQKZUYY"
17
21
  ]
18
22
  }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "hyperquant"
3
- version = "0.82"
3
+ version = "0.83"
4
4
  description = "A minimal yet hyper-efficient backtesting framework for quantitative trading"
5
5
  authors = [
6
6
  { name = "MissinA", email = "1421329142@qq.com" }
@@ -198,6 +198,75 @@ class Auth:
198
198
 
199
199
  return args
200
200
 
201
+ @staticmethod
202
+ def coinw(args: tuple[str, URL], kwargs: dict[str, Any]) -> tuple[str, URL]:
203
+ method: str = args[0]
204
+ url: URL = args[1]
205
+ headers: CIMultiDict = kwargs["headers"]
206
+
207
+ session = kwargs["session"]
208
+ try:
209
+ api_key, secret, _ = session.__dict__["_apis"][pybotters.auth.Hosts.items[url.host].name]
210
+ except (KeyError, ValueError):
211
+ raise RuntimeError("CoinW credentials (api_key, secret) are required")
212
+
213
+ timestamp = str(int(time.time() * 1000))
214
+ method_upper = method.upper()
215
+
216
+ params = kwargs.get("params")
217
+ query_string = ""
218
+ if isinstance(params, dict) and params:
219
+ query_items = [
220
+ f"{key}={value}"
221
+ for key, value in params.items()
222
+ if value is not None
223
+ ]
224
+ query_string = "&".join(query_items)
225
+ elif url.query_string:
226
+ query_string = url.query_string
227
+
228
+ body_str = ""
229
+
230
+ if method_upper == "GET":
231
+ body = None
232
+ data = None
233
+ else:
234
+ body = kwargs.get("json")
235
+ data = kwargs.get("data")
236
+ payload = body if body is not None else data
237
+ if isinstance(payload, (dict, list)):
238
+ body_str = pyjson.dumps(payload, separators=(",", ":"), ensure_ascii=False)
239
+ kwargs["data"] = body_str
240
+ kwargs.pop("json", None)
241
+ elif payload is not None:
242
+ body_str = str(payload)
243
+ kwargs["data"] = body_str
244
+ kwargs.pop("json", None)
245
+
246
+ if query_string:
247
+ path = f"{url.raw_path}?{query_string}"
248
+ else:
249
+ path = url.raw_path
250
+
251
+ message = f"{timestamp}{method_upper}{path}{body_str}"
252
+ signature = hmac.new(
253
+ secret, message.encode("utf-8"), hashlib.sha256
254
+ ).digest()
255
+ signature_b64 = base64.b64encode(signature).decode("ascii")
256
+
257
+ headers.update(
258
+ {
259
+ "sign": signature_b64,
260
+ "api_key": api_key,
261
+ "timestamp": timestamp,
262
+ }
263
+ )
264
+
265
+ if method_upper in {"POST", "PUT", "PATCH", "DELETE"} and "data" in kwargs:
266
+ headers.setdefault("Content-Type", "application/json")
267
+
268
+ return args
269
+
201
270
  pybotters.auth.Hosts.items["futures.ourbit.com"] = pybotters.auth.Item(
202
271
  "ourbit", Auth.ourbit
203
272
  )
@@ -220,4 +289,8 @@ pybotters.auth.Hosts.items["quote.edgex.exchange"] = pybotters.auth.Item(
220
289
 
221
290
  pybotters.auth.Hosts.items["uuapi.rerrkvifj.com"] = pybotters.auth.Item(
222
291
  "lbank", Auth.lbank
223
- )
292
+ )
293
+
294
+ pybotters.auth.Hosts.items["api.coinw.com"] = pybotters.auth.Item(
295
+ "coinw", Auth.coinw
296
+ )
@@ -0,0 +1,405 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import logging
5
+ from typing import Any, Literal, Sequence
6
+
7
+ import pybotters
8
+
9
+ from .models.coinw import CoinwFuturesDataStore
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class Coinw:
15
+ """CoinW 永续合约客户端(REST + WebSocket)。"""
16
+
17
+ def __init__(
18
+ self,
19
+ client: pybotters.Client,
20
+ *,
21
+ rest_api: str | None = None,
22
+ ws_url: str | None = None,
23
+ web_api: str | None = None,
24
+ ) -> None:
25
+ self.client = client
26
+ self.store = CoinwFuturesDataStore()
27
+
28
+ self.rest_api = rest_api or "https://api.coinw.com"
29
+ self.ws_url_public = ws_url or "wss://ws.futurescw.com/perpum"
30
+ self.ws_url_private = self.ws_url_public
31
+ self.web_api = web_api or "https://futuresapi.coinw.com"
32
+
33
+ self._ws_private: pybotters.ws.WebSocketApp | None = None
34
+ self._ws_private_ready = asyncio.Event()
35
+ self._ws_headers = {
36
+ "Origin": "https://www.coinw.com",
37
+ "Referer": "https://www.coinw.com/",
38
+ "User-Agent": (
39
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
40
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
41
+ ),
42
+ }
43
+
44
+ async def __aenter__(self) -> "Coinw":
45
+ await self.update("detail")
46
+ return self
47
+
48
+ async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
49
+ return None
50
+
51
+ async def update(
52
+ self,
53
+ update_type: Literal[
54
+ "detail",
55
+ "orders",
56
+ "position",
57
+ "balance",
58
+ "all",
59
+ ] = "all",
60
+ *,
61
+ instrument: str | None = None,
62
+ position_type: Literal["execute", "plan", "planTrigger"] = "execute",
63
+ page: int | None = None,
64
+ page_size: int | None = None,
65
+ open_ids: str | None = None,
66
+ ) -> None:
67
+ """刷新本地缓存,使用 CoinW REST API。
68
+
69
+ - detail: ``GET /v1/perpum/instruments`` (公共)
70
+ - orders: ``GET /v1/perpum/orders/open`` (私有,需要 ``instrument``)
71
+ - position: ``GET /v1/perpum/positions`` (私有,需要 ``instrument``)
72
+ - balance: ``GET /v1/perpum/account/getUserAssets`` (私有)
73
+ """
74
+
75
+ requests: list[Any] = []
76
+
77
+ include_detail = update_type in {"detail", "all"}
78
+ include_orders = update_type in {"orders", "all"}
79
+ include_position = update_type in {"position", "all"}
80
+ include_balance = update_type in {"balance", "all"}
81
+
82
+ if include_detail:
83
+ requests.append(self.client.get(f"{self.rest_api}/v1/perpum/instruments"))
84
+
85
+ if include_orders:
86
+ if not instrument:
87
+ raise ValueError("instrument is required when updating orders")
88
+ params: dict[str, Any] = {
89
+ "instrument": instrument,
90
+ "positionType": position_type,
91
+ }
92
+ if page is not None:
93
+ params["page"] = page
94
+ if page_size is not None:
95
+ params["pageSize"] = page_size
96
+ requests.append(
97
+ self.client.get(
98
+ f"{self.rest_api}/v1/perpum/orders/open",
99
+ params=params,
100
+ )
101
+ )
102
+
103
+ if include_position:
104
+ if not instrument:
105
+ raise ValueError("instrument is required when updating positions")
106
+ params = {"instrument": instrument}
107
+ if open_ids:
108
+ params["openIds"] = open_ids
109
+ requests.append(
110
+ self.client.get(
111
+ f"{self.rest_api}/v1/perpum/positions",
112
+ params=params,
113
+ )
114
+ )
115
+
116
+ if include_balance:
117
+ requests.append(
118
+ self.client.get(f"{self.rest_api}/v1/perpum/account/getUserAssets")
119
+ )
120
+
121
+ if not requests:
122
+ raise ValueError(f"update_type err: {update_type}")
123
+
124
+ await self.store.initialize(*requests)
125
+
126
+ async def place_order(
127
+ self,
128
+ instrument: str,
129
+ *,
130
+ direction: Literal["long", "short"],
131
+ leverage: int,
132
+ quantity: float | str,
133
+ quantity_unit: Literal[0, 1, 2, "quote", "contract", "base"] = 0,
134
+ position_model: Literal[0, 1, "isolated", "cross"] = 0,
135
+ position_type: Literal["execute", "plan", "planTrigger"] = "execute",
136
+ price: float | None = None,
137
+ trigger_price: float | None = None,
138
+ trigger_type: Literal[0, 1] | None = None,
139
+ stop_loss_price: float | None = None,
140
+ stop_profit_price: float | None = None,
141
+ third_order_id: str | None = None,
142
+ use_almighty_gold: bool | None = None,
143
+ gold_id: int | None = None,
144
+ ) -> dict[str, Any]:
145
+ """``POST /v1/perpum/order`` 下单。"""
146
+
147
+ payload: dict[str, Any] = {
148
+ "instrument": instrument,
149
+ "direction": self._normalize_direction(direction),
150
+ "leverage": int(leverage),
151
+ "quantityUnit": self._normalize_quantity_unit(quantity_unit),
152
+ "quantity": self._format_quantity(quantity),
153
+ "positionModel": self._normalize_position_model(position_model),
154
+ "positionType": position_type,
155
+ }
156
+
157
+ if price is not None:
158
+ payload["openPrice"] = price
159
+ if trigger_price is not None:
160
+ payload["triggerPrice"] = trigger_price
161
+ if trigger_type is not None:
162
+ payload["triggerType"] = int(trigger_type)
163
+ if stop_loss_price is not None:
164
+ payload["stopLossPrice"] = stop_loss_price
165
+ if stop_profit_price is not None:
166
+ payload["stopProfitPrice"] = stop_profit_price
167
+ if third_order_id:
168
+ payload["thirdOrderId"] = third_order_id
169
+ if use_almighty_gold is not None:
170
+ payload["useAlmightyGold"] = int(bool(use_almighty_gold))
171
+ if gold_id is not None:
172
+ payload["goldId"] = int(gold_id)
173
+
174
+ res = await self.client.post(
175
+ f"{self.rest_api}/v1/perpum/order",
176
+ data=payload,
177
+ )
178
+
179
+ data = await res.json()
180
+ return self._ensure_ok("place_order", data)
181
+
182
+ async def place_order_web(
183
+ self,
184
+ instrument: str,
185
+ *,
186
+ direction: Literal["long", "short"],
187
+ leverage: int | str,
188
+ quantity_unit: Literal[0, 1, 2],
189
+ quantity: str | float | int,
190
+ position_model: Literal[0, 1] = 1,
191
+ position_type: Literal["plan", "planTrigger", "execute"] = 'plan',
192
+ open_price: str | float | None = None,
193
+ contract_type: int = 1,
194
+ data_type: str = "trade_take",
195
+ device_id: str,
196
+ token: str,
197
+ headers: dict[str, str] | None = None,
198
+ ) -> dict[str, Any]:
199
+ """使用 Web 前端接口下单,绕过部分 API 频控策略。
200
+
201
+ 注意此接口需要传入真实浏览器参数,如 ``device_id`` 与 ``token``。
202
+ """
203
+
204
+ if not device_id or not token:
205
+ raise ValueError("device_id and token are required for place_order_web")
206
+
207
+ url = f"{self.web_api}/v1/futuresc/thirdClient/trade/{instrument}/open"
208
+
209
+ payload: dict[str, Any] = {
210
+ "instrument": instrument,
211
+ "direction": direction,
212
+ "leverage": str(leverage),
213
+ "quantityUnit": quantity_unit,
214
+ "quantity": str(quantity),
215
+ "positionModel": position_model,
216
+ "positionType": position_type,
217
+ "contractType": contract_type,
218
+ "dataType": data_type,
219
+ }
220
+ if open_price is not None:
221
+ payload["openPrice"] = str(open_price)
222
+
223
+ base_headers = {
224
+ "accept": "application/json, text/plain, */*",
225
+ "accept-language": "zh_CN",
226
+ "appversion": "100.100.100",
227
+ "cache-control": "no-cache",
228
+ "clienttag": "web",
229
+ "content-type": "application/json",
230
+ "cwdeviceid": device_id,
231
+ "deviceid": device_id,
232
+ "devicename": "Chrome V141.0.0.0 (macOS)",
233
+ "language": "zh_CN",
234
+ "logintoken": token,
235
+ "origin": "https://www.coinw.com",
236
+ "pragma": "no-cache",
237
+ "priority": "u=1, i",
238
+ "referer": "https://www.coinw.com/",
239
+ "sec-ch-ua": '"Microsoft Edge";v="141", "Not?A_Brand";v="8", "Chromium";v="141"',
240
+ "sec-ch-ua-mobile": "?0",
241
+ "sec-ch-ua-platform": '"macOS"',
242
+ "sec-fetch-dest": "empty",
243
+ "sec-fetch-mode": "cors",
244
+ "sec-fetch-site": "same-site",
245
+ "selecttype": "USD",
246
+ "systemversion": "macOS 10.15.7",
247
+ "thirdappid": "coinw",
248
+ "thirdapptoken": token,
249
+ "token": token,
250
+ "user-agent": (
251
+ "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
252
+ "AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 "
253
+ "Safari/537.36 Edg/141.0.0.0"
254
+ ),
255
+ "withcredentials": "true",
256
+ "x-authorization": token,
257
+ "x-language": "zh_CN",
258
+ "x-locale": "zh_CN",
259
+ "x-requested-with": "XMLHttpRequest",
260
+ }
261
+ if headers:
262
+ base_headers.update(headers)
263
+
264
+ res = await self.client.post(
265
+ url,
266
+ json=payload,
267
+ headers=base_headers,
268
+ auth=None,
269
+ )
270
+ return await res.json()
271
+
272
+ async def cancel_order(self, order_id: str | int) -> dict[str, Any]:
273
+ """``DELETE /v1/perpum/order`` 取消单个订单。"""
274
+
275
+ res = await self.client.delete(
276
+ f"{self.rest_api}/v1/perpum/order",
277
+ data={"id": str(order_id)},
278
+ )
279
+ data = await res.json()
280
+ return self._ensure_ok("cancel_order", data)
281
+
282
+ async def sub_personal(self) -> None:
283
+ """订阅订单、持仓、资产私有频道。"""
284
+
285
+ ws_app = await self._ensure_private_ws()
286
+ payloads = [
287
+ {"event": "sub", "params": {"biz": "futures", "type": "order"}},
288
+ {"event": "sub", "params": {"biz": "futures", "type": "position"}},
289
+ {"event": "sub", "params": {"biz": "futures", "type": "position_change"}},
290
+ {"event": "sub", "params": {"biz": "futures", "type": "assets"}},
291
+ ]
292
+ for payload in payloads:
293
+ if ws_app.current_ws.closed:
294
+ raise ConnectionError("CoinW private websocket closed before subscription.")
295
+ await ws_app.current_ws.send_json(payload)
296
+ await asyncio.sleep(0.05)
297
+
298
+ async def sub_orderbook(
299
+ self,
300
+ pair_codes: Sequence[str] | str,
301
+ *,
302
+ depth_limit: int | None = None,
303
+ biz: str = "futures",
304
+ ) -> pybotters.ws.WebSocketApp:
305
+ """订阅 ``type=depth`` 订单簿数据,批量控制发送频率。"""
306
+
307
+ if isinstance(pair_codes, str):
308
+ pair_codes = [pair_codes]
309
+
310
+ pair_list = [code for code in pair_codes if code]
311
+ if not pair_list:
312
+ raise ValueError("pair_codes must not be empty")
313
+
314
+ self.store.book.limit = depth_limit
315
+
316
+ subscriptions = [
317
+ {"event": "sub", "params": {"biz": biz, "type": "depth", "pairCode": code}}
318
+ for code in pair_list
319
+ ]
320
+
321
+ ws_app = self.client.ws_connect(
322
+ self.ws_url_public,
323
+ hdlr_json=self.store.onmessage,
324
+ headers=self._ws_headers,
325
+ )
326
+ await ws_app._event.wait()
327
+
328
+ chunk_size = 10
329
+ for idx in range(0, len(subscriptions), chunk_size):
330
+ batch = subscriptions[idx : idx + chunk_size]
331
+ for msg in batch:
332
+ await ws_app.current_ws.send_json(msg)
333
+ if idx + chunk_size < len(subscriptions):
334
+ await asyncio.sleep(2.05)
335
+
336
+ return ws_app
337
+
338
+ async def _ensure_private_ws(self) -> pybotters.ws.WebSocketApp:
339
+
340
+ ws_app = self.client.ws_connect(
341
+ self.ws_url_private,
342
+ hdlr_json=self.store.onmessage,
343
+ headers=self._ws_headers,
344
+ )
345
+ await ws_app._event.wait()
346
+ await ws_app.current_ws._wait_authtask()
347
+
348
+ return ws_app
349
+
350
+ @staticmethod
351
+ def _normalize_direction(direction: str) -> str:
352
+ allowed = {"long", "short"}
353
+ value = str(direction).lower()
354
+ if value not in allowed:
355
+ raise ValueError(f"Unsupported direction: {direction}")
356
+ return value
357
+
358
+ @staticmethod
359
+ def _normalize_quantity_unit(
360
+ unit: Literal[0, 1, 2, "quote", "contract", "base"],
361
+ ) -> int:
362
+ mapping = {
363
+ 0: 0,
364
+ 1: 1,
365
+ 2: 2,
366
+ "quote": 0,
367
+ "contract": 1,
368
+ "base": 2,
369
+ }
370
+ try:
371
+ return mapping[unit] # type: ignore[index]
372
+ except KeyError as exc: # pragma: no cover - guard
373
+ raise ValueError(f"Unsupported quantity_unit: {unit}") from exc
374
+
375
+ @staticmethod
376
+ def _normalize_position_model(
377
+ model: Literal[0, 1, "isolated", "cross"],
378
+ ) -> int:
379
+ mapping = {
380
+ 0: 0,
381
+ 1: 1,
382
+ "isolated": 0,
383
+ "cross": 1,
384
+ }
385
+ try:
386
+ return mapping[model] # type: ignore[index]
387
+ except KeyError as exc: # pragma: no cover - guard
388
+ raise ValueError(f"Unsupported position_model: {model}") from exc
389
+
390
+ @staticmethod
391
+ def _format_quantity(quantity: float | str) -> str:
392
+ if isinstance(quantity, str):
393
+ return quantity
394
+ return str(quantity)
395
+
396
+ @staticmethod
397
+ def _ensure_ok(operation: str, data: Any) -> dict[str, Any]:
398
+ """CoinW REST 成功时返回 ``{'code': 0, ...}``。"""
399
+
400
+ if not isinstance(data, dict) or data.get("code") != 0:
401
+ raise RuntimeError(f"{operation} failed: {data}")
402
+ payload = data.get("data")
403
+ if isinstance(payload, dict):
404
+ return payload
405
+ return {"data": payload}