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