hyperquant 0.88__py3-none-any.whl → 0.91__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/coinup.py +590 -0
- hyperquant/broker/coinw.py +47 -11
- hyperquant/broker/models/coinup.py +333 -0
- hyperquant/broker/models/coinw.py +49 -34
- {hyperquant-0.88.dist-info → hyperquant-0.91.dist-info}/METADATA +4 -2
- {hyperquant-0.88.dist-info → hyperquant-0.91.dist-info}/RECORD +7 -5
- {hyperquant-0.88.dist-info → hyperquant-0.91.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
from typing import Any, Literal, Sequence
|
|
9
|
+
|
|
10
|
+
import pybotters
|
|
11
|
+
import rnet
|
|
12
|
+
|
|
13
|
+
from .models.coinup import CoinUpDataStore
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
_DEFAULT_SECURITY_INFO = (
|
|
18
|
+
'{"log_BSDeviceFingerprint":"0","log_original":"0","log_CHFIT_DEVICEID":"0"}'
|
|
19
|
+
)
|
|
20
|
+
_SECRET_PREFIX = "HJ@%*AZ_J"
|
|
21
|
+
_DEFAULT_UA = (
|
|
22
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) "
|
|
23
|
+
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
|
24
|
+
"Chrome/141.0.0.0 Safari/537.36"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Coinup:
|
|
29
|
+
"""CoinUp 永续合约客户端(REST via rnet + WebSocket depth)。
|
|
30
|
+
|
|
31
|
+
与 CoinW 客户端结构保持一致,差异点:
|
|
32
|
+
|
|
33
|
+
- REST 请求使用 :mod:`rnet`,以规避指纹检测。
|
|
34
|
+
- 仅订单簿 ``book`` 频道依赖 WebSocket,其他数据通过 REST 刷新。
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
client: pybotters.Client,
|
|
40
|
+
*,
|
|
41
|
+
rest_client: rnet.Client | None = None,
|
|
42
|
+
rest_api_common: str | None = None,
|
|
43
|
+
rest_api_futures: str | None = None,
|
|
44
|
+
ws_url: str | None = None,
|
|
45
|
+
security_info: str | None = None,
|
|
46
|
+
exchange_token: str | None = None,
|
|
47
|
+
emulation: rnet.Emulation | rnet.EmulationOption | None = None,
|
|
48
|
+
rest_headers_common: dict[str, str] | None = None,
|
|
49
|
+
rest_headers_futures: dict[str, str] | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
self.client = client
|
|
52
|
+
self.store = CoinUpDataStore()
|
|
53
|
+
|
|
54
|
+
self.rest_api_common = rest_api_common or "https://www.coinup.io/fe-co-api"
|
|
55
|
+
self.rest_api_futures = rest_api_futures or "https://futures.coinup.io/fe-co-api"
|
|
56
|
+
self.ws_url = ws_url or "wss://futuresws.marketketac.com/kline-api/ws"
|
|
57
|
+
|
|
58
|
+
self.security_info = security_info or _DEFAULT_SECURITY_INFO
|
|
59
|
+
self._emulation = emulation or rnet.Emulation.Safari26
|
|
60
|
+
|
|
61
|
+
self._rest_client = rest_client or rnet.Client(
|
|
62
|
+
emulation=self._emulation,
|
|
63
|
+
headers={"User-Agent": _DEFAULT_UA},
|
|
64
|
+
allow_redirects=False,
|
|
65
|
+
history=False,
|
|
66
|
+
)
|
|
67
|
+
self._owns_rest_client = rest_client is None
|
|
68
|
+
|
|
69
|
+
common_headers = {
|
|
70
|
+
"Content-Type": "application/json",
|
|
71
|
+
"Origin": "https://www.coinup.io",
|
|
72
|
+
"Referer": "https://www.coinup.io/",
|
|
73
|
+
"User-Agent": _DEFAULT_UA,
|
|
74
|
+
}
|
|
75
|
+
futures_headers = {
|
|
76
|
+
"Content-Type": "application/json",
|
|
77
|
+
"Origin": "https://futures.coinup.io",
|
|
78
|
+
"Referer": "https://futures.coinup.io/zh_CN/trade",
|
|
79
|
+
"User-Agent": _DEFAULT_UA,
|
|
80
|
+
"exchange-client": "pc",
|
|
81
|
+
"exchange-language": "zh_CN",
|
|
82
|
+
}
|
|
83
|
+
if rest_headers_common:
|
|
84
|
+
common_headers.update(rest_headers_common)
|
|
85
|
+
if rest_headers_futures:
|
|
86
|
+
futures_headers.update(rest_headers_futures)
|
|
87
|
+
|
|
88
|
+
self._exchange_token = exchange_token or self._extract_exchange_token()
|
|
89
|
+
if self._exchange_token:
|
|
90
|
+
futures_headers["exchange-token"] = self._exchange_token
|
|
91
|
+
|
|
92
|
+
self._headers_common = common_headers
|
|
93
|
+
self._headers_futures = futures_headers
|
|
94
|
+
|
|
95
|
+
async def __aenter__(self) -> "Coinup":
|
|
96
|
+
await self.update("detail")
|
|
97
|
+
return self
|
|
98
|
+
|
|
99
|
+
async def __aexit__(self, exc_type, exc, tb) -> None: # pragma: no cover - symmetry
|
|
100
|
+
await self.aclose()
|
|
101
|
+
|
|
102
|
+
async def aclose(self) -> None:
|
|
103
|
+
"""Close the underlying rnet client if we created it."""
|
|
104
|
+
|
|
105
|
+
if self._owns_rest_client and hasattr(self._rest_client, "close"):
|
|
106
|
+
await self._rest_client.close()
|
|
107
|
+
|
|
108
|
+
def get_contract_id(self, symbol: str) -> str | None:
|
|
109
|
+
"""根据交易对获取合约 ID。"""
|
|
110
|
+
|
|
111
|
+
detail = self.store.detail.get({"symbol": symbol})
|
|
112
|
+
if detail is None:
|
|
113
|
+
return None
|
|
114
|
+
contract_id = detail.get("id")
|
|
115
|
+
if contract_id is None:
|
|
116
|
+
return None
|
|
117
|
+
return str(contract_id)
|
|
118
|
+
|
|
119
|
+
async def update(
|
|
120
|
+
self,
|
|
121
|
+
update_type: Literal[
|
|
122
|
+
"detail",
|
|
123
|
+
"position",
|
|
124
|
+
"balance",
|
|
125
|
+
"orders",
|
|
126
|
+
"history_order",
|
|
127
|
+
"history_orders",
|
|
128
|
+
"all",
|
|
129
|
+
] = "all",
|
|
130
|
+
*,
|
|
131
|
+
detail_payload: dict[str, Any] | None = None,
|
|
132
|
+
assets_payload: dict[str, Any] | None = None,
|
|
133
|
+
assets_endpoint: Literal["get_assets_list", "wallet_and_unrealized"] = "get_assets_list",
|
|
134
|
+
orders_payload: dict[str, Any] | None = None,
|
|
135
|
+
history_orders_payload: dict[str, Any] | None = None,
|
|
136
|
+
) -> None:
|
|
137
|
+
"""刷新本地缓存,所有 REST 请求通过 rnet 发送。
|
|
138
|
+
|
|
139
|
+
- detail: ``POST /common/public_info`` (公共接口)
|
|
140
|
+
- position/balance: ``POST /position/get_assets_list`` (返回仓位 & 余额)
|
|
141
|
+
- 备选 ``/position/wallet_and_unrealized`` 可通过 ``assets_endpoint`` 指定
|
|
142
|
+
- orders: ``POST /order/current_order_list`` (私有)
|
|
143
|
+
- history_order: ``POST /order/history_order_list`` (私有)
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
include_detail = update_type in {"detail", "all"}
|
|
147
|
+
include_position = update_type in {"position", "all"}
|
|
148
|
+
include_balance = update_type in {"balance", "all"}
|
|
149
|
+
include_orders = update_type in {"orders", "all"}
|
|
150
|
+
include_history_orders = update_type in {
|
|
151
|
+
"history_order",
|
|
152
|
+
"history_orders",
|
|
153
|
+
"all",
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
include_assets = include_position or include_balance
|
|
157
|
+
|
|
158
|
+
if not (include_detail or include_assets or include_orders or include_history_orders):
|
|
159
|
+
raise ValueError(f"Unsupported update_type: {update_type}")
|
|
160
|
+
|
|
161
|
+
tasks: dict[str, asyncio.Task[Any]] = {}
|
|
162
|
+
|
|
163
|
+
if include_detail:
|
|
164
|
+
tasks["detail"] = asyncio.create_task(
|
|
165
|
+
self._fetch_detail(detail_payload)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
if include_assets:
|
|
169
|
+
tasks["assets"] = asyncio.create_task(
|
|
170
|
+
self._fetch_assets(assets_payload, endpoint=assets_endpoint)
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
if include_orders:
|
|
174
|
+
tasks["orders"] = asyncio.create_task(self._fetch_orders(orders_payload))
|
|
175
|
+
|
|
176
|
+
if include_history_orders:
|
|
177
|
+
tasks["history_orders"] = asyncio.create_task(
|
|
178
|
+
self._fetch_history_orders(history_orders_payload)
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
results: dict[str, Any] = {}
|
|
182
|
+
try:
|
|
183
|
+
for key, task in tasks.items():
|
|
184
|
+
results[key] = await task
|
|
185
|
+
except Exception:
|
|
186
|
+
for task in tasks.values():
|
|
187
|
+
task.cancel()
|
|
188
|
+
raise
|
|
189
|
+
|
|
190
|
+
if include_detail and "detail" in results:
|
|
191
|
+
self.store.detail._onresponse(results["detail"])
|
|
192
|
+
|
|
193
|
+
assets_data = results.get("assets")
|
|
194
|
+
if assets_data is not None:
|
|
195
|
+
if include_position:
|
|
196
|
+
self.store.position._onresponse(assets_data)
|
|
197
|
+
if include_balance:
|
|
198
|
+
self.store.balance._onresponse(assets_data)
|
|
199
|
+
|
|
200
|
+
if include_orders:
|
|
201
|
+
orders_data = results.get("orders")
|
|
202
|
+
if orders_data is not None:
|
|
203
|
+
self.store.orders._onresponse(orders_data)
|
|
204
|
+
|
|
205
|
+
if include_history_orders:
|
|
206
|
+
history_data = results.get("history_orders")
|
|
207
|
+
if history_data is not None:
|
|
208
|
+
self.store.history_orders._onresponse(history_data)
|
|
209
|
+
|
|
210
|
+
async def sub_orderbook(
|
|
211
|
+
self,
|
|
212
|
+
channels: Sequence[str] | str,
|
|
213
|
+
*,
|
|
214
|
+
depth_step: str = "step0",
|
|
215
|
+
depth_limit: int | None = None,
|
|
216
|
+
) -> pybotters.ws.WebSocketApp:
|
|
217
|
+
"""订阅订单簿深度频道。
|
|
218
|
+
|
|
219
|
+
参数 ``channels`` 支持 ``subSymbol`` 或完整频道名称:
|
|
220
|
+
|
|
221
|
+
- ``"e_wlfiusdt"`` -> 发送 ``market_e_wlfiusdt_depth_step0``
|
|
222
|
+
- ``"market_e_wlfiusdt_depth_step0"`` -> 原样发送
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
if isinstance(channels, str):
|
|
226
|
+
channels = [channels]
|
|
227
|
+
|
|
228
|
+
payloads = []
|
|
229
|
+
for channel in channels:
|
|
230
|
+
ch = channel.lower()
|
|
231
|
+
if not ch.startswith("market_"):
|
|
232
|
+
ch = f"market_{ch}_depth_{depth_step}"
|
|
233
|
+
payloads.append(
|
|
234
|
+
{
|
|
235
|
+
"event": "sub",
|
|
236
|
+
"params": {
|
|
237
|
+
"channel": ch,
|
|
238
|
+
"cb_id": ch.split("_depth_", 1)[0].replace("market_", ""),
|
|
239
|
+
},
|
|
240
|
+
}
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if not payloads:
|
|
244
|
+
raise ValueError("channels must not be empty")
|
|
245
|
+
|
|
246
|
+
self.store.book.limit = depth_limit
|
|
247
|
+
|
|
248
|
+
ws_headers = {
|
|
249
|
+
"Origin": "https://futures.coinup.io",
|
|
250
|
+
"Referer": "https://futures.coinup.io/zh_CN/trade",
|
|
251
|
+
"User-Agent": _DEFAULT_UA,
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
ws_app = self.client.ws_connect(
|
|
255
|
+
self.ws_url,
|
|
256
|
+
hdlr_bytes=self.store.onmessage,
|
|
257
|
+
headers=ws_headers,
|
|
258
|
+
autoping=False,
|
|
259
|
+
)
|
|
260
|
+
await ws_app._event.wait()
|
|
261
|
+
|
|
262
|
+
for payload in payloads:
|
|
263
|
+
await ws_app.current_ws.send_json(payload)
|
|
264
|
+
await asyncio.sleep(0.05)
|
|
265
|
+
|
|
266
|
+
return ws_app
|
|
267
|
+
|
|
268
|
+
async def place_order(
|
|
269
|
+
self,
|
|
270
|
+
symbol: str,
|
|
271
|
+
*,
|
|
272
|
+
side: Literal["buy", "sell", "BUY", "SELL"],
|
|
273
|
+
volume: float | int | str,
|
|
274
|
+
order_type: Literal["limit", "market", 1, 2] = "limit",
|
|
275
|
+
price: float | int | str | None = None,
|
|
276
|
+
position_type: int | str = 1,
|
|
277
|
+
leverage_level: int | str = 1,
|
|
278
|
+
offset: Literal["open", "close", "OPEN", "CLOSE"] = "open",
|
|
279
|
+
order_unit: int | str = 2,
|
|
280
|
+
trigger_price: float | int | str | None = None,
|
|
281
|
+
is_condition_order: bool = False,
|
|
282
|
+
is_oto: bool = False,
|
|
283
|
+
is_check_liq: int | bool = 1,
|
|
284
|
+
secret: str | None = None,
|
|
285
|
+
take_profit_trigger: float | int | str | None = None,
|
|
286
|
+
take_profit_price: float | int | str | None = 0,
|
|
287
|
+
take_profit_type: int | None = 2,
|
|
288
|
+
stop_loss_trigger: float | int | str | None = None,
|
|
289
|
+
stop_loss_price: float | int | str | None = 0,
|
|
290
|
+
stop_loss_type: int | None = 2,
|
|
291
|
+
extra_params: dict[str, Any] | None = None,
|
|
292
|
+
) -> dict[str, Any]:
|
|
293
|
+
"""
|
|
294
|
+
``POST /order/order_create`` 下单(默认支持限价/市价)。
|
|
295
|
+
当 close 时, volume为张数
|
|
296
|
+
|
|
297
|
+
Args:
|
|
298
|
+
symbol: 交易对符号(如 "BTC_USDT"),将自动解析为 contract_id。
|
|
299
|
+
"""
|
|
300
|
+
contract_id = self.get_contract_id(symbol)
|
|
301
|
+
if contract_id is None:
|
|
302
|
+
raise ValueError(f"Invalid symbol: {symbol}")
|
|
303
|
+
|
|
304
|
+
normalized_side = self._normalize_side(side)
|
|
305
|
+
normalized_offset = self._normalize_offset(offset)
|
|
306
|
+
order_type_code = self._normalize_order_type(order_type)
|
|
307
|
+
|
|
308
|
+
if order_type_code == 1 and price is None:
|
|
309
|
+
raise ValueError("price is required for CoinUp limit orders")
|
|
310
|
+
price_value = self._format_price(price)
|
|
311
|
+
if order_type_code == 1 and price_value is None:
|
|
312
|
+
raise ValueError("price is required for CoinUp limit orders")
|
|
313
|
+
if price_value is None:
|
|
314
|
+
price_value = 0
|
|
315
|
+
|
|
316
|
+
volume_str = self._format_volume(volume)
|
|
317
|
+
trigger_price_value = self._format_price(trigger_price)
|
|
318
|
+
take_profit_trigger_value = self._format_price(take_profit_trigger)
|
|
319
|
+
take_profit_price_value = self._format_price(take_profit_price)
|
|
320
|
+
stop_loss_trigger_value = self._format_price(stop_loss_trigger)
|
|
321
|
+
stop_loss_price_value = self._format_price(stop_loss_price)
|
|
322
|
+
|
|
323
|
+
payload: dict[str, Any] = {
|
|
324
|
+
"contractId": contract_id,
|
|
325
|
+
"positionType": int(position_type),
|
|
326
|
+
"side": normalized_side,
|
|
327
|
+
"leverageLevel": int(leverage_level),
|
|
328
|
+
"price": price_value,
|
|
329
|
+
"volume": volume_str,
|
|
330
|
+
"triggerPrice": trigger_price_value,
|
|
331
|
+
"open": normalized_offset,
|
|
332
|
+
"type": order_type_code,
|
|
333
|
+
"isConditionOrder": bool(is_condition_order),
|
|
334
|
+
"isOto": bool(is_oto),
|
|
335
|
+
"orderUnit": int(order_unit),
|
|
336
|
+
"isCheckLiq": int(is_check_liq),
|
|
337
|
+
"takerProfitTrigger": take_profit_trigger_value,
|
|
338
|
+
"takerProfitPrice": take_profit_price_value,
|
|
339
|
+
"takerProfitType": take_profit_type,
|
|
340
|
+
"stopLossTrigger": stop_loss_trigger_value,
|
|
341
|
+
"stopLossPrice": stop_loss_price_value,
|
|
342
|
+
"stopLossType": stop_loss_type,
|
|
343
|
+
}
|
|
344
|
+
if extra_params:
|
|
345
|
+
payload.update(extra_params)
|
|
346
|
+
|
|
347
|
+
if secret is None:
|
|
348
|
+
payload["secret"] = self._generate_secret(
|
|
349
|
+
contract_id=str(contract_id),
|
|
350
|
+
leverage_level=str(leverage_level),
|
|
351
|
+
position_type=str(position_type),
|
|
352
|
+
price=price_value,
|
|
353
|
+
side=normalized_side,
|
|
354
|
+
order_type=str(order_type_code),
|
|
355
|
+
volume=volume_str,
|
|
356
|
+
)
|
|
357
|
+
else:
|
|
358
|
+
payload["secret"] = secret
|
|
359
|
+
|
|
360
|
+
body = self._build_payload(payload)
|
|
361
|
+
data = await self._rest_post(
|
|
362
|
+
f"{self.rest_api_futures}/order/order_create",
|
|
363
|
+
body,
|
|
364
|
+
self._headers_futures,
|
|
365
|
+
)
|
|
366
|
+
result = self._ensure_success("place_order", data)
|
|
367
|
+
return result.get("data") or {}
|
|
368
|
+
|
|
369
|
+
async def cancel_order(
|
|
370
|
+
self,
|
|
371
|
+
symbol: str,
|
|
372
|
+
order_id: str | int,
|
|
373
|
+
*,
|
|
374
|
+
is_condition_order: bool = False,
|
|
375
|
+
extra_params: dict[str, Any] | None = None,
|
|
376
|
+
) -> dict[str, Any] | None:
|
|
377
|
+
"""
|
|
378
|
+
``POST /order/order_cancel`` 取消指定订单。
|
|
379
|
+
|
|
380
|
+
Args:
|
|
381
|
+
symbol: 交易对符号(如 "BTC_USDT"),将自动解析为 contract_id。
|
|
382
|
+
"""
|
|
383
|
+
contract_id = self.get_contract_id(symbol)
|
|
384
|
+
if contract_id is None:
|
|
385
|
+
raise ValueError(f"Invalid symbol: {symbol}")
|
|
386
|
+
|
|
387
|
+
payload: dict[str, Any] = {
|
|
388
|
+
"contractId": contract_id,
|
|
389
|
+
"orderId": str(order_id),
|
|
390
|
+
"isConditionOrder": bool(is_condition_order),
|
|
391
|
+
}
|
|
392
|
+
if extra_params:
|
|
393
|
+
payload.update(extra_params)
|
|
394
|
+
|
|
395
|
+
body = self._build_payload(payload)
|
|
396
|
+
data = await self._rest_post(
|
|
397
|
+
f"{self.rest_api_futures}/order/order_cancel",
|
|
398
|
+
body,
|
|
399
|
+
self._headers_futures,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
result = self._ensure_success("cancel_order", data)
|
|
403
|
+
return result.get("data")
|
|
404
|
+
|
|
405
|
+
def set_exchange_token(self, token: str | None) -> None:
|
|
406
|
+
"""Update the ``exchange-token`` header used for CoinUp private REST calls."""
|
|
407
|
+
|
|
408
|
+
self._exchange_token = token
|
|
409
|
+
if token:
|
|
410
|
+
self._headers_futures["exchange-token"] = token
|
|
411
|
+
else:
|
|
412
|
+
self._headers_futures.pop("exchange-token", None)
|
|
413
|
+
|
|
414
|
+
def _build_payload(self, overrides: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
415
|
+
payload = {
|
|
416
|
+
"uaTime": datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S"),
|
|
417
|
+
"securityInfo": self.security_info,
|
|
418
|
+
}
|
|
419
|
+
if overrides:
|
|
420
|
+
payload.update(overrides)
|
|
421
|
+
return payload
|
|
422
|
+
|
|
423
|
+
async def _fetch_detail(self, payload: dict[str, Any] | None) -> Any:
|
|
424
|
+
url = f"{self.rest_api_common}/common/public_info"
|
|
425
|
+
data = await self._rest_post(
|
|
426
|
+
url,
|
|
427
|
+
self._build_payload(payload),
|
|
428
|
+
self._headers_common,
|
|
429
|
+
)
|
|
430
|
+
return data
|
|
431
|
+
|
|
432
|
+
async def _fetch_assets(
|
|
433
|
+
self,
|
|
434
|
+
payload: dict[str, Any] | None,
|
|
435
|
+
*,
|
|
436
|
+
endpoint: str,
|
|
437
|
+
) -> Any:
|
|
438
|
+
url = f"{self.rest_api_futures}/position/{endpoint}"
|
|
439
|
+
data = await self._rest_post(
|
|
440
|
+
url,
|
|
441
|
+
self._build_payload(payload),
|
|
442
|
+
self._headers_futures,
|
|
443
|
+
)
|
|
444
|
+
return data
|
|
445
|
+
|
|
446
|
+
async def _rest_post(
|
|
447
|
+
self,
|
|
448
|
+
url: str,
|
|
449
|
+
payload: dict[str, Any],
|
|
450
|
+
headers: dict[str, str],
|
|
451
|
+
) -> Any:
|
|
452
|
+
response = await self._rest_client.post(
|
|
453
|
+
url,
|
|
454
|
+
json=payload,
|
|
455
|
+
headers=headers,
|
|
456
|
+
)
|
|
457
|
+
try:
|
|
458
|
+
status = response.status.as_int()
|
|
459
|
+
if status >= 400:
|
|
460
|
+
text = await response.text()
|
|
461
|
+
raise RuntimeError(
|
|
462
|
+
f"CoinUp REST request failed ({status}): {text}"
|
|
463
|
+
)
|
|
464
|
+
return await response.json()
|
|
465
|
+
finally:
|
|
466
|
+
await response.close()
|
|
467
|
+
|
|
468
|
+
async def _fetch_orders(self, payload: dict[str, Any] | None) -> Any:
|
|
469
|
+
url = f"{self.rest_api_futures}/order/current_order_list"
|
|
470
|
+
data = await self._rest_post(
|
|
471
|
+
url,
|
|
472
|
+
self._build_payload(payload or {"contractId": ""}),
|
|
473
|
+
self._headers_futures,
|
|
474
|
+
)
|
|
475
|
+
return data
|
|
476
|
+
|
|
477
|
+
async def _fetch_history_orders(self, payload: dict[str, Any] | None) -> Any:
|
|
478
|
+
url = f"{self.rest_api_futures}/order/history_order_list"
|
|
479
|
+
data = await self._rest_post(
|
|
480
|
+
url,
|
|
481
|
+
self._build_payload(payload or {"contractId": ""}),
|
|
482
|
+
self._headers_futures,
|
|
483
|
+
)
|
|
484
|
+
return data
|
|
485
|
+
|
|
486
|
+
def _extract_exchange_token(self) -> str | None:
|
|
487
|
+
"""Best-effort fetch of token from pybotters credential store."""
|
|
488
|
+
|
|
489
|
+
session = getattr(self.client, "_session", None)
|
|
490
|
+
if not session:
|
|
491
|
+
return None
|
|
492
|
+
apis = getattr(session, "__dict__", {}).get("_apis")
|
|
493
|
+
if not isinstance(apis, dict):
|
|
494
|
+
return None
|
|
495
|
+
creds = apis.get("coinup")
|
|
496
|
+
if not creds:
|
|
497
|
+
return None
|
|
498
|
+
token = creds[0]
|
|
499
|
+
return str(token) if token else None
|
|
500
|
+
|
|
501
|
+
@staticmethod
|
|
502
|
+
def _ensure_success(operation: str, payload: Any) -> dict[str, Any]:
|
|
503
|
+
if not isinstance(payload, dict):
|
|
504
|
+
raise RuntimeError(f"{operation} failed: unexpected response {payload}")
|
|
505
|
+
code = payload.get("code")
|
|
506
|
+
succ = payload.get("succ")
|
|
507
|
+
if code is not None and str(code) != "0":
|
|
508
|
+
raise RuntimeError(f"{operation} failed: {payload}")
|
|
509
|
+
if succ is False:
|
|
510
|
+
raise RuntimeError(f"{operation} failed: {payload}")
|
|
511
|
+
return payload
|
|
512
|
+
|
|
513
|
+
@staticmethod
|
|
514
|
+
def _normalize_side(side: str) -> str:
|
|
515
|
+
value = str(side).upper()
|
|
516
|
+
if value not in {"BUY", "SELL"}:
|
|
517
|
+
raise ValueError(f"Unsupported side: {side}")
|
|
518
|
+
return value
|
|
519
|
+
|
|
520
|
+
@staticmethod
|
|
521
|
+
def _normalize_offset(offset: str) -> str:
|
|
522
|
+
value = str(offset).upper()
|
|
523
|
+
mapping = {"OPEN": "OPEN", "CLOSE": "CLOSE"}
|
|
524
|
+
try:
|
|
525
|
+
return mapping[value]
|
|
526
|
+
except KeyError as exc:
|
|
527
|
+
raise ValueError(f"Unsupported offset: {offset}") from exc
|
|
528
|
+
|
|
529
|
+
@staticmethod
|
|
530
|
+
def _normalize_order_type(order_type: Literal["limit", "market", 1, 2]) -> int:
|
|
531
|
+
mapping = {
|
|
532
|
+
"limit": 1,
|
|
533
|
+
"market": 2,
|
|
534
|
+
1: 1,
|
|
535
|
+
2: 2,
|
|
536
|
+
}
|
|
537
|
+
try:
|
|
538
|
+
return mapping[order_type] # type: ignore[index]
|
|
539
|
+
except KeyError as exc:
|
|
540
|
+
raise ValueError(f"Unsupported order_type: {order_type}") from exc
|
|
541
|
+
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _generate_secret(
|
|
544
|
+
contract_id: str,
|
|
545
|
+
leverage_level: str,
|
|
546
|
+
position_type: str,
|
|
547
|
+
price: float | int | str,
|
|
548
|
+
side: str,
|
|
549
|
+
order_type: str,
|
|
550
|
+
volume: str,
|
|
551
|
+
) -> str:
|
|
552
|
+
data = {
|
|
553
|
+
"contractId": contract_id,
|
|
554
|
+
"leverageLevel": leverage_level,
|
|
555
|
+
"positionType": position_type,
|
|
556
|
+
"price": Coinup._stringify_number(price),
|
|
557
|
+
"side": side,
|
|
558
|
+
"type": order_type,
|
|
559
|
+
"volume": Coinup._stringify_number(volume),
|
|
560
|
+
}
|
|
561
|
+
text = json.dumps(data, ensure_ascii=False, separators=(",", ":"))
|
|
562
|
+
raw = f"{_SECRET_PREFIX}{text}"
|
|
563
|
+
return hashlib.md5(raw.encode("utf-8")).hexdigest()
|
|
564
|
+
|
|
565
|
+
@staticmethod
|
|
566
|
+
def _format_price(value: float | int | str | None) -> float | int | str | None:
|
|
567
|
+
if value is None:
|
|
568
|
+
return None
|
|
569
|
+
if isinstance(value, (float, int)):
|
|
570
|
+
return float(value)
|
|
571
|
+
return value
|
|
572
|
+
|
|
573
|
+
@staticmethod
|
|
574
|
+
def _format_volume(value: float | int | str) -> str:
|
|
575
|
+
return str(value)
|
|
576
|
+
|
|
577
|
+
@staticmethod
|
|
578
|
+
def _stringify_number(value: float | int | str | None) -> str:
|
|
579
|
+
if value is None:
|
|
580
|
+
return "0"
|
|
581
|
+
if isinstance(value, str):
|
|
582
|
+
return value
|
|
583
|
+
if isinstance(value, int):
|
|
584
|
+
return str(value)
|
|
585
|
+
if isinstance(value, float):
|
|
586
|
+
if value.is_integer():
|
|
587
|
+
return str(int(value))
|
|
588
|
+
text = format(value, "f").rstrip("0").rstrip(".")
|
|
589
|
+
return text or "0"
|
|
590
|
+
return str(value)
|
hyperquant/broker/coinw.py
CHANGED
|
@@ -60,11 +60,10 @@ class Coinw:
|
|
|
60
60
|
"all",
|
|
61
61
|
] = "all",
|
|
62
62
|
*,
|
|
63
|
-
instrument: str | None = None,
|
|
64
63
|
position_type: Literal["execute", "plan", "planTrigger"] = "execute",
|
|
65
64
|
page: int | None = None,
|
|
66
65
|
page_size: int | None = None,
|
|
67
|
-
|
|
66
|
+
instrument: str | None = None,
|
|
68
67
|
) -> None:
|
|
69
68
|
"""刷新本地缓存,使用 CoinW REST API。
|
|
70
69
|
|
|
@@ -108,16 +107,8 @@ class Coinw:
|
|
|
108
107
|
)
|
|
109
108
|
|
|
110
109
|
if include_position:
|
|
111
|
-
if not instrument:
|
|
112
|
-
raise ValueError("instrument is required when updating positions")
|
|
113
|
-
params = {"instrument": instrument}
|
|
114
|
-
if open_ids:
|
|
115
|
-
params["openIds"] = open_ids
|
|
116
110
|
requests.append(
|
|
117
|
-
self.client.get(
|
|
118
|
-
f"{self.rest_api}/v1/perpum/positions",
|
|
119
|
-
params=params,
|
|
120
|
-
)
|
|
111
|
+
self.client.get(f"{self.rest_api}/v1/perpum/positions/all")
|
|
121
112
|
)
|
|
122
113
|
|
|
123
114
|
if include_balance:
|
|
@@ -186,6 +177,51 @@ class Coinw:
|
|
|
186
177
|
data = await res.json()
|
|
187
178
|
return self._ensure_ok("place_order", data)
|
|
188
179
|
|
|
180
|
+
async def close_position(
|
|
181
|
+
self,
|
|
182
|
+
open_id: str | int,
|
|
183
|
+
*,
|
|
184
|
+
position_type: Literal["plan", "planTrigger", "execute"] = "plan",
|
|
185
|
+
close_num: str | float | int | None = None,
|
|
186
|
+
close_rate: str | float | int | None = None,
|
|
187
|
+
order_price: str | float | None = None,
|
|
188
|
+
instrument: str | None = None,
|
|
189
|
+
) -> dict[str, Any]:
|
|
190
|
+
"""关闭单个仓位(``DELETE /v1/perpum/positions``)。
|
|
191
|
+
|
|
192
|
+
Params
|
|
193
|
+
------
|
|
194
|
+
open_id: ``openId`` / 持仓唯一 ID。
|
|
195
|
+
position_type: 订单类型 ``plan`` / ``planTrigger`` / ``execute``。
|
|
196
|
+
close_num: 按合约数量平仓(与 ``close_rate`` 至少指定其一)。
|
|
197
|
+
close_rate: 按比例平仓(0-1)。
|
|
198
|
+
order_price: 限价平仓时指定价格。
|
|
199
|
+
instrument: 交易品种(部分情况下需要传入,例如限价单)。
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
if close_num is None and close_rate is None:
|
|
203
|
+
raise ValueError("close_num or close_rate must be provided")
|
|
204
|
+
|
|
205
|
+
payload: dict[str, Any] = {
|
|
206
|
+
"id": str(open_id),
|
|
207
|
+
"positionType": position_type,
|
|
208
|
+
}
|
|
209
|
+
if close_num is not None:
|
|
210
|
+
payload["closeNum"] = str(close_num)
|
|
211
|
+
if close_rate is not None:
|
|
212
|
+
payload["closeRate"] = str(close_rate)
|
|
213
|
+
if order_price is not None:
|
|
214
|
+
payload["orderPrice"] = str(order_price)
|
|
215
|
+
if instrument is not None:
|
|
216
|
+
payload["instrument"] = instrument
|
|
217
|
+
|
|
218
|
+
res = await self.client.delete(
|
|
219
|
+
f"{self.rest_api}/v1/perpum/positions",
|
|
220
|
+
data=payload,
|
|
221
|
+
)
|
|
222
|
+
data = await res.json()
|
|
223
|
+
return self._ensure_ok("close_position", data)
|
|
224
|
+
|
|
189
225
|
async def place_order_web(
|
|
190
226
|
self,
|
|
191
227
|
instrument: str,
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any, Awaitable, TYPE_CHECKING
|
|
7
|
+
import zlib
|
|
8
|
+
|
|
9
|
+
from aiohttp import ClientResponse
|
|
10
|
+
from pybotters.store import DataStore, DataStoreCollection
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from pybotters.typedefs import Item
|
|
14
|
+
from pybotters.ws import ClientWebSocketResponse
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# {"event_rep":"","channel":"market_e_wlfiusdt_depth_step0","data":null,"tick":{"asks":[[0.1402,9864],[0.1403,23388],[0.1404,9531],[0.1405,4995],[0.1406,3074],[0.1407,18736],[0.1408,3514],[0.1409,6326],[0.141,13217],[0.1411,18253],[0.1412,12214],[0.1413,15243],[0.1414,3606],[0.1415,14894],[0.1416,7932],[0.1417,4973],[0.1418,13031],[0.1419,19793],[0.142,17093],[0.1421,19395],[0.1422,12793],[0.1423,17846],[0.1424,15320],[0.1425,13313],[0.1426,20405],[0.1427,6611],[0.1428,17688],[0.1429,16308],[0.143,10073],[0.1431,15438]],"buys":[[0.1401,9473],[0.14,17486],[0.1399,11957],[0.1398,4824],[0.1397,18447],[0.1396,16929],[0.1395,19859],[0.1394,7283],[0.1393,19609],[0.1392,13638],[0.1391,4146],[0.139,3924],[0.1389,21574],[0.1388,14692],[0.1387,13772],[0.1386,21153],[0.1385,19533],[0.1384,20164],[0.1383,2645],[0.1382,17852],[0.1381,21453],[0.138,19162],[0.1379,17365],[0.1378,9061],[0.1377,14713],[0.1376,12023],[0.1375,11245],[0.1374,9633],[0.1373,5124],[0.1372,5140]]},"ts":1761239630000,"status":"ok"}
|
|
19
|
+
|
|
20
|
+
class Book(DataStore):
|
|
21
|
+
|
|
22
|
+
_KEYS = ["s", "S", "p"]
|
|
23
|
+
|
|
24
|
+
def _init(self) -> None:
|
|
25
|
+
self.limit: int | None = None
|
|
26
|
+
|
|
27
|
+
def _on_message(self, msg: Any) -> None:
|
|
28
|
+
asks = msg["tick"]["asks"]
|
|
29
|
+
bids = msg["tick"]["buys"]
|
|
30
|
+
if self.limit is not None:
|
|
31
|
+
asks = asks[: self.limit]
|
|
32
|
+
bids = bids[: self.limit]
|
|
33
|
+
chanel = msg["channel"]
|
|
34
|
+
symbol = chanel.split("_")[2].upper()
|
|
35
|
+
asks = [
|
|
36
|
+
{"s": symbol, "S": "a", "p": float(price), "q": float(quantity)} for price, quantity in asks
|
|
37
|
+
]
|
|
38
|
+
bids = [
|
|
39
|
+
{"s": symbol, "S": "b", "p": float(price), "q": float(quantity)} for price, quantity in bids
|
|
40
|
+
]
|
|
41
|
+
self._clear()
|
|
42
|
+
self._insert(asks + bids)
|
|
43
|
+
|
|
44
|
+
class Detail(DataStore):
|
|
45
|
+
|
|
46
|
+
_KEYS = ["symbol"]
|
|
47
|
+
|
|
48
|
+
def _onresponse(self, data: Any) -> None:
|
|
49
|
+
data = data.get("data", {})
|
|
50
|
+
clist = data.get("contractList", [])
|
|
51
|
+
# coinResultVo -> marginCoinPrecision 取出来
|
|
52
|
+
for c in clist:
|
|
53
|
+
p = c.get("coinResultVo", {}).get("marginCoinPrecision")
|
|
54
|
+
p = 10 ** (-p)
|
|
55
|
+
c["tick_size"] = p
|
|
56
|
+
c['TickSize'] = p # 兼容用大写开头的字段
|
|
57
|
+
self._update(clist)
|
|
58
|
+
|
|
59
|
+
class Position(DataStore):
|
|
60
|
+
|
|
61
|
+
_KEYS = ["symbol"]
|
|
62
|
+
|
|
63
|
+
def _onresponse(self, data: Any) -> None:
|
|
64
|
+
data = data.get("data", [])
|
|
65
|
+
p_list = data.get("positionList", [])
|
|
66
|
+
self._clear()
|
|
67
|
+
self._update(p_list)
|
|
68
|
+
|
|
69
|
+
class Balance(DataStore):
|
|
70
|
+
|
|
71
|
+
_KEYS = ["symbol"]
|
|
72
|
+
|
|
73
|
+
def _onresponse(self, data: Any) -> None:
|
|
74
|
+
data = data.get("data", [])
|
|
75
|
+
b_list = data.get("accountList", [])
|
|
76
|
+
self._clear()
|
|
77
|
+
self._update(b_list)
|
|
78
|
+
|
|
79
|
+
class Orders(DataStore):
|
|
80
|
+
|
|
81
|
+
_KEYS = ["orderId"]
|
|
82
|
+
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
85
|
+
order_id = entry.get("orderId")
|
|
86
|
+
if order_id is None:
|
|
87
|
+
return None
|
|
88
|
+
normalized = dict(entry)
|
|
89
|
+
normalized["orderId"] = str(order_id)
|
|
90
|
+
return normalized
|
|
91
|
+
|
|
92
|
+
def _onresponse(self, data: Any) -> None:
|
|
93
|
+
payload: Any = data
|
|
94
|
+
if isinstance(data, dict):
|
|
95
|
+
payload = data.get("data") or data
|
|
96
|
+
if isinstance(payload, dict):
|
|
97
|
+
payload = payload.get("orderList") or payload.get("orders") or []
|
|
98
|
+
|
|
99
|
+
items: list[dict[str, Any]] = []
|
|
100
|
+
if isinstance(payload, list):
|
|
101
|
+
for entry in payload:
|
|
102
|
+
if not isinstance(entry, dict):
|
|
103
|
+
continue
|
|
104
|
+
normalized = self._normalize(entry)
|
|
105
|
+
if normalized:
|
|
106
|
+
items.append(normalized)
|
|
107
|
+
|
|
108
|
+
self._clear()
|
|
109
|
+
if items:
|
|
110
|
+
self._insert(items)
|
|
111
|
+
|
|
112
|
+
class HistoryOrders(DataStore):
|
|
113
|
+
|
|
114
|
+
_KEYS = ["orderId"]
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
118
|
+
order_id = entry.get("orderId")
|
|
119
|
+
if order_id is None:
|
|
120
|
+
return None
|
|
121
|
+
normalized = dict(entry)
|
|
122
|
+
normalized["orderId"] = str(order_id)
|
|
123
|
+
return normalized
|
|
124
|
+
|
|
125
|
+
def _onresponse(self, data: Any) -> None:
|
|
126
|
+
payload: Any = data
|
|
127
|
+
if isinstance(data, dict):
|
|
128
|
+
payload = data.get("data") or data
|
|
129
|
+
if isinstance(payload, dict):
|
|
130
|
+
payload = payload.get("orderList") or payload.get("orders") or []
|
|
131
|
+
|
|
132
|
+
items: list[dict[str, Any]] = []
|
|
133
|
+
if isinstance(payload, list):
|
|
134
|
+
for entry in payload:
|
|
135
|
+
if not isinstance(entry, dict):
|
|
136
|
+
continue
|
|
137
|
+
normalized = self._normalize(entry)
|
|
138
|
+
if normalized:
|
|
139
|
+
items.append(normalized)
|
|
140
|
+
|
|
141
|
+
self._clear()
|
|
142
|
+
if items:
|
|
143
|
+
self._insert(items)
|
|
144
|
+
|
|
145
|
+
class CoinUpDataStore(DataStoreCollection):
|
|
146
|
+
def _init(self) -> None:
|
|
147
|
+
self._create("book", datastore_class=Book)
|
|
148
|
+
self._create("detail", datastore_class=Detail)
|
|
149
|
+
self._create("position", datastore_class=Position)
|
|
150
|
+
self._create("balance", datastore_class=Balance)
|
|
151
|
+
self._create("orders", datastore_class=Orders)
|
|
152
|
+
self._create("history_orders", datastore_class=HistoryOrders)
|
|
153
|
+
|
|
154
|
+
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
|
155
|
+
decompressed = zlib.decompress(msg, 16 + zlib.MAX_WBITS)
|
|
156
|
+
text = decompressed.decode("utf-8")
|
|
157
|
+
data = json.loads(text)
|
|
158
|
+
chanel = data.get("channel", "")
|
|
159
|
+
if 'depth' in chanel:
|
|
160
|
+
self.book._on_message(data)
|
|
161
|
+
|
|
162
|
+
def onresponse(self, data: Any) -> None:
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@property
|
|
167
|
+
def book(self) -> Book:
|
|
168
|
+
"""
|
|
169
|
+
.. code:: json
|
|
170
|
+
|
|
171
|
+
{
|
|
172
|
+
"s": "BTCUSDT",
|
|
173
|
+
"S": "a", # 卖单
|
|
174
|
+
"p": "95640.3",
|
|
175
|
+
"q": "0.807"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
"""
|
|
179
|
+
return self._get("book")
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def detail(self) -> Detail:
|
|
183
|
+
"""
|
|
184
|
+
.. code:: json
|
|
185
|
+
|
|
186
|
+
{
|
|
187
|
+
"id": 117,
|
|
188
|
+
"contractName": "E-RESOLV-USDT",
|
|
189
|
+
"symbol": "RESOLV-USDT",
|
|
190
|
+
"contractType": "E",
|
|
191
|
+
"coType": "E",
|
|
192
|
+
"contractShowType": "USDT合约",
|
|
193
|
+
"deliveryKind": "0",
|
|
194
|
+
"contractSide": 1,
|
|
195
|
+
"multiplier": 22.8000000000000000,
|
|
196
|
+
"multiplierCoin": "RESOLV",
|
|
197
|
+
"marginCoin": "USDT",
|
|
198
|
+
"originalCoin": "USDT",
|
|
199
|
+
"marginRate": 1.00000000,
|
|
200
|
+
"capitalStartTime": 0,
|
|
201
|
+
"capitalFrequency": 8,
|
|
202
|
+
"settlementFrequency": 1,
|
|
203
|
+
"brokerId": 1,
|
|
204
|
+
"base": "RESOLV",
|
|
205
|
+
"quote": "USDT",
|
|
206
|
+
"coinResultVo": {
|
|
207
|
+
"symbolPricePrecision": 5,
|
|
208
|
+
"depth": [
|
|
209
|
+
"5",
|
|
210
|
+
"4",
|
|
211
|
+
"3"
|
|
212
|
+
],
|
|
213
|
+
"minOrderVolume": 1,
|
|
214
|
+
"minOrderMoney": 1,
|
|
215
|
+
"maxMarketVolume": 5000000,
|
|
216
|
+
"maxMarketMoney": 6411360,
|
|
217
|
+
"maxLimitVolume": 5000000,
|
|
218
|
+
"maxLimitMoney": 5000000.0000000000000000,
|
|
219
|
+
"priceRange": 0.3000000000,
|
|
220
|
+
"marginCoinPrecision": 4,
|
|
221
|
+
"fundsInStatus": 1,
|
|
222
|
+
"fundsOutStatus": 1
|
|
223
|
+
},
|
|
224
|
+
"sort": 100,
|
|
225
|
+
"maxLever": 75,
|
|
226
|
+
"minLever": 1,
|
|
227
|
+
"contractOtherName": "RESOLV/USDT",
|
|
228
|
+
"subSymbol": "e_resolvusdt",
|
|
229
|
+
"classification": 1,
|
|
230
|
+
"nextCapitalSettTime": 1761292800000,
|
|
231
|
+
"tick_size": 0.0001
|
|
232
|
+
}
|
|
233
|
+
"""
|
|
234
|
+
return self._get("detail")
|
|
235
|
+
|
|
236
|
+
@property
|
|
237
|
+
def position(self) -> Position:
|
|
238
|
+
"""
|
|
239
|
+
_key: symbol
|
|
240
|
+
|
|
241
|
+
.. code:: json
|
|
242
|
+
|
|
243
|
+
{
|
|
244
|
+
"id": 256538,
|
|
245
|
+
"contractId": 169,
|
|
246
|
+
"contractName": "E-WLFI-USDT",
|
|
247
|
+
"contractOtherName": "WLFI/USDT",
|
|
248
|
+
"symbol": "WLFI-USDT",
|
|
249
|
+
"positionVolume": 1.0,
|
|
250
|
+
"canCloseVolume": 1.0,
|
|
251
|
+
"closeVolume": 0.0,
|
|
252
|
+
"openAvgPrice": 0.1409,
|
|
253
|
+
"indexPrice": 0.14040034,
|
|
254
|
+
"reducePrice": -0.9769279224708908,
|
|
255
|
+
"holdAmount": 16.53718437074,
|
|
256
|
+
"marginRate": 7.852395215348719,
|
|
257
|
+
"realizedAmount": 0.0,
|
|
258
|
+
"returnRate": -0.0177310149041873,
|
|
259
|
+
"orderSide": "BUY",
|
|
260
|
+
"positionType": 1,
|
|
261
|
+
"canUseAmount": 16.11598335074,
|
|
262
|
+
"canSubMarginAmount": 0,
|
|
263
|
+
"openRealizedAmount": -0.0074949,
|
|
264
|
+
"keepRate": 0.015,
|
|
265
|
+
"maxFeeRate": 2.0E-4,
|
|
266
|
+
"unRealizedAmount": -0.0074949,
|
|
267
|
+
"leverageLevel": 5,
|
|
268
|
+
"positionBalance": 2.1060051,
|
|
269
|
+
"tradeFee": "-0.0004",
|
|
270
|
+
"capitalFee": "0",
|
|
271
|
+
"closeProfit": "0",
|
|
272
|
+
"settleProfit": "0",
|
|
273
|
+
"shareAmount": "0",
|
|
274
|
+
"historyRealizedAmount": "-0.0004227",
|
|
275
|
+
"profitRealizedAmount": "-0.0004",
|
|
276
|
+
"openAmount": 0.4227,
|
|
277
|
+
"adlLevel": 2
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
"""
|
|
281
|
+
return self._get("position")
|
|
282
|
+
|
|
283
|
+
@property
|
|
284
|
+
def balance(self) -> Balance:
|
|
285
|
+
"""
|
|
286
|
+
_key: symbol
|
|
287
|
+
|
|
288
|
+
.. code:: json
|
|
289
|
+
|
|
290
|
+
{
|
|
291
|
+
"symbol": "USDT",
|
|
292
|
+
"originalCoin": "USDT",
|
|
293
|
+
"unRealizedAmount": "-0.0074949",
|
|
294
|
+
"realizedAmount": "0",
|
|
295
|
+
"totalMargin": "16.53718437074",
|
|
296
|
+
"totalAmount": "16.53718437074",
|
|
297
|
+
"canUseAmount": 16.11598335074,
|
|
298
|
+
"availableAmount": 16.11598335074,
|
|
299
|
+
"isolateMargin": "0",
|
|
300
|
+
"walletBalance": "16.54467927074",
|
|
301
|
+
"lockAmount": "0",
|
|
302
|
+
"accountNormal": "16.54467927074",
|
|
303
|
+
"totalMarginRate": "7.8523952153487187"
|
|
304
|
+
}
|
|
305
|
+
"""
|
|
306
|
+
return self._get("balance")
|
|
307
|
+
|
|
308
|
+
@property
|
|
309
|
+
def orders(self) -> Orders:
|
|
310
|
+
"""
|
|
311
|
+
_key: orderId
|
|
312
|
+
|
|
313
|
+
.. code:: json
|
|
314
|
+
|
|
315
|
+
{
|
|
316
|
+
"orderId": "2951913499074783723",
|
|
317
|
+
"contractId": 169,
|
|
318
|
+
"side": "SELL",
|
|
319
|
+
"price": 0.15,
|
|
320
|
+
"volume": 1,
|
|
321
|
+
"status": 0
|
|
322
|
+
}
|
|
323
|
+
"""
|
|
324
|
+
return self._get("orders")
|
|
325
|
+
|
|
326
|
+
@property
|
|
327
|
+
def history_orders(self) -> HistoryOrders:
|
|
328
|
+
"""
|
|
329
|
+
_key: orderId
|
|
330
|
+
|
|
331
|
+
历史委托记录,与 ``orders`` 结构一致。
|
|
332
|
+
"""
|
|
333
|
+
return self._get("history_orders")
|
|
@@ -282,20 +282,6 @@ class Position(DataStore):
|
|
|
282
282
|
|
|
283
283
|
_KEYS = ["openId"]
|
|
284
284
|
|
|
285
|
-
@staticmethod
|
|
286
|
-
def _normalize(entry: dict[str, Any]) -> dict[str, Any] | None:
|
|
287
|
-
open_id = entry.get("openId")
|
|
288
|
-
if open_id is None:
|
|
289
|
-
return None
|
|
290
|
-
normalized = dict(entry)
|
|
291
|
-
normalized["openId"] = str(open_id)
|
|
292
|
-
normalized["status"] = str(entry.get("status") or entry.get("orderStatus") or "").lower()
|
|
293
|
-
normalized["is_closed"] = normalized["status"] in {"close", "closed", "finish"}
|
|
294
|
-
normalized["currentPiece"] = str(entry.get("currentPiece")) if entry.get("currentPiece") is not None else None
|
|
295
|
-
normalized["closedPiece"] = str(entry.get("closedPiece")) if entry.get("closedPiece") is not None else None
|
|
296
|
-
normalized["quantity"] = str(entry.get("quantity")) if entry.get("quantity") is not None else None
|
|
297
|
-
normalized["updatedDate"] = entry.get("updatedDate")
|
|
298
|
-
return normalized
|
|
299
285
|
|
|
300
286
|
def _onresponse(self, data: Any) -> None:
|
|
301
287
|
payload = []
|
|
@@ -308,9 +294,8 @@ class Position(DataStore):
|
|
|
308
294
|
for entry in payload or []:
|
|
309
295
|
if not isinstance(entry, dict):
|
|
310
296
|
continue
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
items.append(normalized)
|
|
297
|
+
entry['openId'] = str(entry.get("id"))
|
|
298
|
+
items.append(entry)
|
|
314
299
|
|
|
315
300
|
self._clear()
|
|
316
301
|
if items:
|
|
@@ -318,6 +303,7 @@ class Position(DataStore):
|
|
|
318
303
|
|
|
319
304
|
def _on_message(self, msg: dict[str, Any]) -> None:
|
|
320
305
|
data = msg.get("data")
|
|
306
|
+
|
|
321
307
|
if isinstance(data, dict) and data.get("result") is not None:
|
|
322
308
|
return
|
|
323
309
|
|
|
@@ -337,17 +323,14 @@ class Position(DataStore):
|
|
|
337
323
|
for entry in entries:
|
|
338
324
|
if not isinstance(entry, dict):
|
|
339
325
|
continue
|
|
340
|
-
normalized =
|
|
341
|
-
if not normalized:
|
|
342
|
-
continue
|
|
326
|
+
normalized = entry
|
|
343
327
|
|
|
344
|
-
criteria = {"openId": normalized["openId"]}
|
|
345
328
|
|
|
346
|
-
if normalized.get("
|
|
347
|
-
to_delete.append(
|
|
329
|
+
if normalized.get("status") == 'close':
|
|
330
|
+
to_delete.append(normalized)
|
|
348
331
|
continue
|
|
349
332
|
|
|
350
|
-
if self.find(
|
|
333
|
+
if self.find(normalized):
|
|
351
334
|
to_update.append(normalized)
|
|
352
335
|
else:
|
|
353
336
|
to_insert.append(normalized)
|
|
@@ -460,6 +443,7 @@ class CoinwFuturesDataStore(DataStoreCollection):
|
|
|
460
443
|
|
|
461
444
|
def onmessage(self, msg: Item, ws: ClientWebSocketResponse | None = None) -> None:
|
|
462
445
|
msg_type = msg.get("type")
|
|
446
|
+
# print(msg)
|
|
463
447
|
if msg_type == "depth":
|
|
464
448
|
self.book._on_message(msg)
|
|
465
449
|
elif msg_type == "order":
|
|
@@ -481,7 +465,7 @@ class CoinwFuturesDataStore(DataStoreCollection):
|
|
|
481
465
|
self.ticker._onresponse(data)
|
|
482
466
|
elif res.url.path == "/v1/perpum/orders/open":
|
|
483
467
|
self.orders._onresponse(data)
|
|
484
|
-
elif res.url.path == "/v1/perpum/positions":
|
|
468
|
+
elif res.url.path == "/v1/perpum/positions/all":
|
|
485
469
|
self.position._onresponse(data)
|
|
486
470
|
elif res.url.path == "/v1/perpum/account/getUserAssets":
|
|
487
471
|
self.balance._onresponse(data)
|
|
@@ -666,17 +650,48 @@ class CoinwFuturesDataStore(DataStoreCollection):
|
|
|
666
650
|
- REST: ``GET /v1/perpum/positions``
|
|
667
651
|
- WebSocket: ``type == "position"``
|
|
668
652
|
|
|
669
|
-
|
|
653
|
+
.. code:: json
|
|
670
654
|
|
|
671
655
|
{
|
|
672
|
-
"
|
|
673
|
-
"
|
|
674
|
-
"
|
|
675
|
-
"
|
|
676
|
-
"
|
|
677
|
-
"
|
|
678
|
-
"
|
|
679
|
-
|
|
656
|
+
"currentPiece": "0",
|
|
657
|
+
"isProfession": 0,
|
|
658
|
+
"leverage": "10",
|
|
659
|
+
"originalType": "execute",
|
|
660
|
+
"orderId": "33309059291614824",
|
|
661
|
+
"contractType": 1,
|
|
662
|
+
"openId": "2435521222638707873",
|
|
663
|
+
"fee": "0.00020724",
|
|
664
|
+
"openPrice": "0.3456",
|
|
665
|
+
"orderStatus": "finish",
|
|
666
|
+
"instrument": "JUP",
|
|
667
|
+
"quantityUnit": 1,
|
|
668
|
+
"source": "api",
|
|
669
|
+
"updatedDate": 1761192795412,
|
|
670
|
+
"positionModel": 1,
|
|
671
|
+
"feeRate": "0.0006",
|
|
672
|
+
"netProfit": "-0.00040724",
|
|
673
|
+
"baseSize": "1",
|
|
674
|
+
"quote": "usdt",
|
|
675
|
+
"liquidateBy": "manual",
|
|
676
|
+
"totalPiece": "1",
|
|
677
|
+
"orderPrice": "0",
|
|
678
|
+
"id": "23469279597150213",
|
|
679
|
+
"fundingSettle": "0",
|
|
680
|
+
"direction": "long",
|
|
681
|
+
"margin": "0.03435264",
|
|
682
|
+
"takerMaker": 1,
|
|
683
|
+
"indexPrice": "0.3455",
|
|
684
|
+
"quantity": "0.03456",
|
|
685
|
+
"userId": "1757458",
|
|
686
|
+
"closedPiece": "1",
|
|
687
|
+
"createdDate": 1761192793000,
|
|
688
|
+
"hedgeId": "23469279597150214",
|
|
689
|
+
"closePrice": "0.3454",
|
|
690
|
+
"positionMargin": "0.03435264",
|
|
691
|
+
"base": "jup",
|
|
692
|
+
"realPrice": "0.3454",
|
|
693
|
+
"status": "close"
|
|
694
|
+
}
|
|
680
695
|
"""
|
|
681
696
|
|
|
682
697
|
return self._get("position", Position)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: hyperquant
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.91
|
|
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
|
|
@@ -12,15 +12,17 @@ Classifier: Intended Audience :: Developers
|
|
|
12
12
|
Classifier: License :: OSI Approved :: MIT License
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Topic :: Office/Business :: Financial :: Investment
|
|
15
|
-
Requires-Python: >=3.
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
16
|
Requires-Dist: aiohttp>=3.10.4
|
|
17
17
|
Requires-Dist: colorama>=0.4.6
|
|
18
18
|
Requires-Dist: cryptography>=44.0.2
|
|
19
|
+
Requires-Dist: curl-cffi>=0.13.0
|
|
19
20
|
Requires-Dist: duckdb>=1.2.2
|
|
20
21
|
Requires-Dist: numpy>=1.21.0
|
|
21
22
|
Requires-Dist: pandas>=2.2.3
|
|
22
23
|
Requires-Dist: pybotters>=1.9.1
|
|
23
24
|
Requires-Dist: pyecharts>=2.0.8
|
|
25
|
+
Requires-Dist: rnet==3.0.0rc10
|
|
24
26
|
Description-Content-Type: text/markdown
|
|
25
27
|
|
|
26
28
|
# minquant
|
|
@@ -6,7 +6,8 @@ hyperquant/logkit.py,sha256=nUo7nx5eONvK39GOhWwS41zNRL756P2J7-5xGzwXnTY,8462
|
|
|
6
6
|
hyperquant/notikit.py,sha256=x5yAZ_tAvLQRXcRbcg-VabCaN45LUhvlTZnUqkIqfAA,3596
|
|
7
7
|
hyperquant/broker/auth.py,sha256=xNZEQP0LRRV9BkT2uXBJ-vFfeahUnRVq1bjIT6YbQu8,10089
|
|
8
8
|
hyperquant/broker/bitget.py,sha256=X_S0LKZ7FZAEb6oEMr1vdGP1fondzK74BhmNTpRDSEA,9488
|
|
9
|
-
hyperquant/broker/
|
|
9
|
+
hyperquant/broker/coinup.py,sha256=tADmydit0_Aspv0747fBUk9ZFRCkkWnb16LBlw2twWQ,20357
|
|
10
|
+
hyperquant/broker/coinw.py,sha256=SnJU0vASh77rfcpMGWaIfTblQSjQk3vjlW_4juYdbcs,17214
|
|
10
11
|
hyperquant/broker/edgex.py,sha256=TqUO2KRPLN_UaxvtLL6HnA9dAQXC1sGxOfqTHd6W5k8,18378
|
|
11
12
|
hyperquant/broker/hyperliquid.py,sha256=7MxbI9OyIBcImDelPJu-8Nd53WXjxPB5TwE6gsjHbto,23252
|
|
12
13
|
hyperquant/broker/lbank.py,sha256=98M5wmSoeHwbBYMA3rh25zqLb6fQKVaEmwqALF5nOvY,22181
|
|
@@ -17,7 +18,8 @@ hyperquant/broker/lib/hpstore.py,sha256=LnLK2zmnwVvhEbLzYI-jz_SfYpO1Dv2u2cJaRAb8
|
|
|
17
18
|
hyperquant/broker/lib/hyper_types.py,sha256=HqjjzjUekldjEeVn6hxiWA8nevAViC2xHADOzDz9qyw,991
|
|
18
19
|
hyperquant/broker/lib/util.py,sha256=iMU1qF0CHj5zzlIMEQGwjz-qtEVosEe7slXOCuB7Rcw,566
|
|
19
20
|
hyperquant/broker/models/bitget.py,sha256=0RwDY75KrJb-c-oYoMxbqxWfsILe-n_Npojz4UFUq7c,11389
|
|
20
|
-
hyperquant/broker/models/
|
|
21
|
+
hyperquant/broker/models/coinup.py,sha256=1g-peGSpZvVI17IVhu10kjUsZOb14e6q9aa8RL7iDS0,11101
|
|
22
|
+
hyperquant/broker/models/coinw.py,sha256=LvLMVP7i-qkkTK1ubw8eBkMK2RQmFoKPxdKqmC4IToY,22157
|
|
21
23
|
hyperquant/broker/models/edgex.py,sha256=vPAkceal44cjTYKQ_0BoNAskOpmkno_Yo1KxgMLPc6Y,33954
|
|
22
24
|
hyperquant/broker/models/hyperliquid.py,sha256=c4r5739ibZfnk69RxPjQl902AVuUOwT8RNvKsMtwXBY,9459
|
|
23
25
|
hyperquant/broker/models/lbank.py,sha256=vHkNKxIMzpoC_EwcZnEOPOupizF92yGWi9GKxvYYFUQ,19181
|
|
@@ -26,6 +28,6 @@ hyperquant/datavison/_util.py,sha256=92qk4vO856RqycO0YqEIHJlEg-W9XKapDVqAMxe6rbw
|
|
|
26
28
|
hyperquant/datavison/binance.py,sha256=3yNKTqvt_vUQcxzeX4ocMsI5k6Q6gLZrvgXxAEad6Kc,5001
|
|
27
29
|
hyperquant/datavison/coinglass.py,sha256=PEjdjISP9QUKD_xzXNzhJ9WFDTlkBrRQlVL-5pxD5mo,10482
|
|
28
30
|
hyperquant/datavison/okx.py,sha256=yg8WrdQ7wgWHNAInIgsWPM47N3Wkfr253169IPAycAY,6898
|
|
29
|
-
hyperquant-0.
|
|
30
|
-
hyperquant-0.
|
|
31
|
-
hyperquant-0.
|
|
31
|
+
hyperquant-0.91.dist-info/METADATA,sha256=5nL2dFJgOtm_XeMbETtaHqppY_Y4wHIrNaMtTEVeF58,4382
|
|
32
|
+
hyperquant-0.91.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
33
|
+
hyperquant-0.91.dist-info/RECORD,,
|
|
File without changes
|