hyperquant 0.2__py3-none-any.whl → 0.4__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/__init__.py +4 -1
- hyperquant/broker/auth.py +88 -0
- hyperquant/broker/hyperliquid.py +570 -0
- hyperquant/broker/lib/hpstore.py +252 -0
- hyperquant/broker/lib/hyper_types.py +48 -0
- hyperquant/broker/models/hyperliquid.py +284 -0
- hyperquant/broker/models/ourbit.py +1054 -0
- hyperquant/broker/ourbit.py +500 -0
- hyperquant/broker/ws.py +48 -0
- hyperquant/core.py +77 -26
- hyperquant/datavison/okx.py +177 -0
- hyperquant/notikit.py +124 -0
- {hyperquant-0.2.dist-info → hyperquant-0.4.dist-info}/METADATA +2 -1
- hyperquant-0.4.dist-info/RECORD +21 -0
- hyperquant-0.2.dist-info/RECORD +0 -11
- {hyperquant-0.2.dist-info → hyperquant-0.4.dist-info}/WHEEL +0 -0
@@ -0,0 +1,500 @@
|
|
1
|
+
import asyncio
|
2
|
+
from typing import Literal, Optional
|
3
|
+
import pybotters
|
4
|
+
from .models.ourbit import OurbitSwapDataStore, OurbitSpotDataStore
|
5
|
+
from decimal import Decimal, ROUND_HALF_UP
|
6
|
+
|
7
|
+
|
8
|
+
class OurbitSwap:
|
9
|
+
|
10
|
+
def __init__(self, client: pybotters.Client):
|
11
|
+
"""
|
12
|
+
✅ 完成:
|
13
|
+
下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
|
14
|
+
|
15
|
+
"""
|
16
|
+
self.client = client
|
17
|
+
self.store = OurbitSwapDataStore()
|
18
|
+
self.api_url = "https://futures.ourbit.com"
|
19
|
+
self.ws_url = "wss://futures.ourbit.com/edge"
|
20
|
+
|
21
|
+
async def __aenter__(self) -> "OurbitSwap":
|
22
|
+
client = self.client
|
23
|
+
await self.store.initialize(
|
24
|
+
client.get(f"{self.api_url}/api/v1/contract/detailV2?client=web")
|
25
|
+
)
|
26
|
+
return self
|
27
|
+
|
28
|
+
async def update(
|
29
|
+
self, update_type: Literal["position", "orders", "balance", "ticker", "all"] = "all"
|
30
|
+
):
|
31
|
+
"""由于交易所很多不支持ws推送,这里使用Rest"""
|
32
|
+
all_urls = [
|
33
|
+
f"{self.api_url}/api/v1/private/position/open_positions",
|
34
|
+
f"{self.api_url}/api/v1/private/order/list/open_orders?page_size=200",
|
35
|
+
f"{self.api_url}/api/v1/private/account/assets",
|
36
|
+
f"{self.api_url}/api/v1/contract/ticker",
|
37
|
+
f"{self.api_url}/api/platform/spot/market/v2/symbols"
|
38
|
+
]
|
39
|
+
|
40
|
+
url_map = {
|
41
|
+
"position": [all_urls[0]],
|
42
|
+
"orders": [all_urls[1]],
|
43
|
+
"balance": [all_urls[2]],
|
44
|
+
"ticker": [all_urls[3]],
|
45
|
+
"all": all_urls,
|
46
|
+
}
|
47
|
+
|
48
|
+
try:
|
49
|
+
urls = url_map[update_type]
|
50
|
+
except KeyError:
|
51
|
+
raise ValueError(f"update_type err: {update_type}")
|
52
|
+
|
53
|
+
# 直接传协程进去,initialize 会自己 await
|
54
|
+
await self.store.initialize(*(self.client.get(url) for url in urls))
|
55
|
+
|
56
|
+
async def sub_tickers(self):
|
57
|
+
self.client.ws_connect(
|
58
|
+
self.ws_url,
|
59
|
+
send_json={
|
60
|
+
"method": "sub.tickers",
|
61
|
+
"param": {
|
62
|
+
"timezone": "UTC+8"
|
63
|
+
}
|
64
|
+
},
|
65
|
+
hdlr_json=self.store.onmessage
|
66
|
+
)
|
67
|
+
|
68
|
+
async def sub_orderbook(self, symbols: str | list[str]):
|
69
|
+
if isinstance(symbols, str):
|
70
|
+
symbols = [symbols]
|
71
|
+
|
72
|
+
send_jsons = []
|
73
|
+
# send_json={"method":"sub.depth.step","param":{"symbol":"BTC_USDT","step":"0.1"}},
|
74
|
+
|
75
|
+
for symbol in symbols:
|
76
|
+
step = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
|
77
|
+
|
78
|
+
send_jsons.append({
|
79
|
+
"method": "sub.depth.step",
|
80
|
+
"param": {
|
81
|
+
"symbol": symbol,
|
82
|
+
"step": str(step)
|
83
|
+
}
|
84
|
+
})
|
85
|
+
|
86
|
+
await self.client.ws_connect(
|
87
|
+
self.ws_url,
|
88
|
+
send_json=send_jsons,
|
89
|
+
hdlr_json=self.store.onmessage
|
90
|
+
)
|
91
|
+
|
92
|
+
async def sub_personal(self):
|
93
|
+
self.client.ws_connect(
|
94
|
+
self.ws_url,
|
95
|
+
send_json={ "method": "sub.personal.user.preference"},
|
96
|
+
hdlr_json=self.store.onmessage
|
97
|
+
)
|
98
|
+
|
99
|
+
def ret_content(self, res: pybotters.FetchResult):
|
100
|
+
match res.data:
|
101
|
+
case {"success": True}:
|
102
|
+
return res.data["data"]
|
103
|
+
case _:
|
104
|
+
raise Exception(f"Failed api {res.response.url}: {res.data}")
|
105
|
+
|
106
|
+
|
107
|
+
def fmt_price(self, symbol, price: float) -> float:
|
108
|
+
tick = self.store.detail.find({"symbol": symbol})[0].get("tick_size")
|
109
|
+
tick_dec = Decimal(str(tick))
|
110
|
+
price_dec = Decimal(str(price))
|
111
|
+
return float((price_dec / tick_dec).quantize(Decimal("1"), rounding=ROUND_HALF_UP) * tick_dec)
|
112
|
+
|
113
|
+
async def place_order(
|
114
|
+
self,
|
115
|
+
symbol: str,
|
116
|
+
side: Literal["buy", "sell", "close_buy", "close_sell"],
|
117
|
+
size: float = None,
|
118
|
+
price: float = None,
|
119
|
+
order_type: Literal["market", "limit_GTC", "limit_IOC"] = "market",
|
120
|
+
usdt_amount: Optional[float] = None,
|
121
|
+
leverage: Optional[int] = 20,
|
122
|
+
position_id: Optional[int] = None,
|
123
|
+
):
|
124
|
+
"""
|
125
|
+
size为合约张数, openType 1 为逐仓, 2为全仓
|
126
|
+
|
127
|
+
.. code ::
|
128
|
+
{
|
129
|
+
"orderId": "219602019841167810",
|
130
|
+
"ts": 1756395601543
|
131
|
+
}
|
132
|
+
|
133
|
+
"""
|
134
|
+
if (size is None) == (usdt_amount is None):
|
135
|
+
raise ValueError("params err")
|
136
|
+
|
137
|
+
max_lev = self.store.detail.find({"symbol": symbol})[0].get("max_lev")
|
138
|
+
|
139
|
+
if usdt_amount is not None:
|
140
|
+
cs = self.store.detail.find({"symbol": symbol})[0].get("contract_sz")
|
141
|
+
size = max(int(usdt_amount / cs / price), 1)
|
142
|
+
|
143
|
+
if price is not None:
|
144
|
+
price = self.fmt_price(symbol, price)
|
145
|
+
|
146
|
+
|
147
|
+
leverage = min(max_lev, leverage)
|
148
|
+
|
149
|
+
data = {
|
150
|
+
"symbol": symbol,
|
151
|
+
"side": 1 if side == "buy" else 3,
|
152
|
+
"openType": 2,
|
153
|
+
"type": "5",
|
154
|
+
"vol": size,
|
155
|
+
"leverage": leverage,
|
156
|
+
"marketCeiling": False,
|
157
|
+
"priceProtect": "0",
|
158
|
+
}
|
159
|
+
|
160
|
+
if order_type == "limit_IOC":
|
161
|
+
data["type"] = 3
|
162
|
+
data["price"] = str(price)
|
163
|
+
if order_type == "limit_GTC":
|
164
|
+
data["type"] = "1"
|
165
|
+
data["price"] = str(price)
|
166
|
+
|
167
|
+
if "close" in side:
|
168
|
+
if side == 'close_buy':
|
169
|
+
data["side"] = 2
|
170
|
+
elif side == 'close_sell':
|
171
|
+
data["side"] = 4
|
172
|
+
|
173
|
+
if position_id is None:
|
174
|
+
raise ValueError("position_id is required for closing position")
|
175
|
+
data["positionId"] = position_id
|
176
|
+
# import time
|
177
|
+
# print(time.time(), '下单')
|
178
|
+
res = await self.client.fetch(
|
179
|
+
"POST", f"{self.api_url}/api/v1/private/order/create", data=data
|
180
|
+
)
|
181
|
+
return self.ret_content(res)
|
182
|
+
|
183
|
+
async def place_tpsl(self,
|
184
|
+
position_id: int,
|
185
|
+
take_profit: Optional[float] = None,
|
186
|
+
stop_loss: Optional[float] = None,
|
187
|
+
):
|
188
|
+
"""
|
189
|
+
position_id 持仓ID
|
190
|
+
|
191
|
+
.. code:: json
|
192
|
+
|
193
|
+
{
|
194
|
+
"success": true,
|
195
|
+
"code": 0,
|
196
|
+
"data": 2280508
|
197
|
+
}
|
198
|
+
"""
|
199
|
+
if (take_profit is None) and (stop_loss is None):
|
200
|
+
raise ValueError("params err")
|
201
|
+
|
202
|
+
data = {
|
203
|
+
"positionId": position_id,
|
204
|
+
"profitTrend": "1",
|
205
|
+
"lossTrend": "1",
|
206
|
+
"profitLossVolType": "SAME",
|
207
|
+
"volType": 2,
|
208
|
+
"takeProfitReverse": 2,
|
209
|
+
"stopLossReverse": 2,
|
210
|
+
"priceProtect": "0",
|
211
|
+
}
|
212
|
+
|
213
|
+
if take_profit is not None:
|
214
|
+
data["takeProfitPrice"] = take_profit
|
215
|
+
if stop_loss is not None:
|
216
|
+
data["stopLossPrice"] = stop_loss
|
217
|
+
|
218
|
+
|
219
|
+
res = await self.client.fetch(
|
220
|
+
"POST",
|
221
|
+
f"{self.api_url}/api/v1/private/stoporder/place",
|
222
|
+
data=data
|
223
|
+
)
|
224
|
+
|
225
|
+
return self.ret_content(res)
|
226
|
+
|
227
|
+
async def cancel_orders(self, order_ids: list[str]):
|
228
|
+
res = await self.client.fetch(
|
229
|
+
"POST",
|
230
|
+
f"{self.api_url}/api/v1/private/order/cancel",
|
231
|
+
data=order_ids,
|
232
|
+
)
|
233
|
+
return self.ret_content(res)
|
234
|
+
|
235
|
+
async def query_orders(
|
236
|
+
self,
|
237
|
+
symbol: str,
|
238
|
+
states: list[Literal["filled", "canceled"]], # filled:已成交, canceled:已撤销
|
239
|
+
start_time: Optional[int] = None,
|
240
|
+
end_time: Optional[int] = None,
|
241
|
+
page_size: int = 200,
|
242
|
+
page_num: int = 1,
|
243
|
+
):
|
244
|
+
"""查询历史订单
|
245
|
+
|
246
|
+
Args:
|
247
|
+
symbol: 交易对
|
248
|
+
states: 订单状态列表 ["filled":已成交, "canceled":已撤销]
|
249
|
+
start_time: 开始时间戳(毫秒), 可选
|
250
|
+
end_time: 结束时间戳(毫秒), 可选
|
251
|
+
page_size: 每页数量, 默认200
|
252
|
+
page_num: 页码, 默认1
|
253
|
+
"""
|
254
|
+
state_map = {"filled": 3, "canceled": 4}
|
255
|
+
|
256
|
+
params = {
|
257
|
+
"symbol": symbol,
|
258
|
+
"states": ",".join(str(state_map[state]) for state in states),
|
259
|
+
"page_size": page_size,
|
260
|
+
"page_num": page_num,
|
261
|
+
"category": 1,
|
262
|
+
}
|
263
|
+
|
264
|
+
if start_time:
|
265
|
+
params["start_time"] = start_time
|
266
|
+
if end_time:
|
267
|
+
params["end_time"] = end_time
|
268
|
+
|
269
|
+
res = await self.client.fetch(
|
270
|
+
"GET",
|
271
|
+
f"{self.api_url}/api/v1/private/order/list/history_orders",
|
272
|
+
params=params,
|
273
|
+
)
|
274
|
+
|
275
|
+
return self.ret_content(res)
|
276
|
+
|
277
|
+
async def query_order(self, order_id: str):
|
278
|
+
"""查询单个订单的详细信息
|
279
|
+
|
280
|
+
Args:
|
281
|
+
order_id: 订单ID
|
282
|
+
|
283
|
+
Returns:
|
284
|
+
..code:python
|
285
|
+
|
286
|
+
订单详情数据,例如:
|
287
|
+
[
|
288
|
+
{
|
289
|
+
"id": "38600506", # 成交ID
|
290
|
+
"symbol": "SOL_USDT", # 交易对
|
291
|
+
"side": 4, # 方向(1:买入, 3:卖出, 4:平仓)
|
292
|
+
"vol": 1, # 成交数量
|
293
|
+
"price": 204.11, # 成交价格
|
294
|
+
"fee": 0.00081644, # 手续费
|
295
|
+
"feeCurrency": "USDT", # 手续费币种
|
296
|
+
"profit": -0.0034, # 盈亏
|
297
|
+
"category": 1, # 品类
|
298
|
+
"orderId": "219079365441409152", # 订单ID
|
299
|
+
"timestamp": 1756270991000, # 时间戳
|
300
|
+
"positionMode": 1, # 持仓模式
|
301
|
+
"voucher": false, # 是否使用代金券
|
302
|
+
"taker": true # 是否是taker
|
303
|
+
}
|
304
|
+
]
|
305
|
+
"""
|
306
|
+
res = await self.client.fetch(
|
307
|
+
"GET",
|
308
|
+
f"{self.api_url}/api/v1/private/order/deal_details/{order_id}",
|
309
|
+
)
|
310
|
+
return self.ret_content(res)
|
311
|
+
|
312
|
+
|
313
|
+
class OurbitSpot:
|
314
|
+
|
315
|
+
def __init__(self, client: pybotters.Client):
|
316
|
+
"""
|
317
|
+
✅ 完成:
|
318
|
+
下单, 撤单, 查询资金, 查询持有订单, 查询历史订单
|
319
|
+
|
320
|
+
"""
|
321
|
+
self.client = client
|
322
|
+
self.store = OurbitSpotDataStore()
|
323
|
+
self.api_url = "https://www.ourbit.com"
|
324
|
+
self.ws_url = "wss://www.ourbit.com/ws"
|
325
|
+
|
326
|
+
async def __aenter__(self) -> "OurbitSpot":
|
327
|
+
client = self.client
|
328
|
+
await self.store.initialize(
|
329
|
+
client.get(f"{self.api_url}/api/platform/spot/market/v2/symbols")
|
330
|
+
)
|
331
|
+
return self
|
332
|
+
|
333
|
+
async def update(self, update_type: Literal["orders", "balance", "ticker", "book", "all"] = "all"):
|
334
|
+
|
335
|
+
all_urls = [
|
336
|
+
f"{self.api_url}/api/platform/spot/order/current/orders/v2?orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3",
|
337
|
+
f"{self.api_url}/api/assetbussiness/asset/spot/statistic",
|
338
|
+
f"{self.api_url}/api/platform/spot/market/v2/tickers"
|
339
|
+
]
|
340
|
+
|
341
|
+
# orderTypes=1%2C2%2C3%2C4%2C5%2C100&pageNum=1&pageSize=100&states=0%2C1%2C3
|
342
|
+
|
343
|
+
url_map = {
|
344
|
+
"orders": [all_urls[0]],
|
345
|
+
"balance": [all_urls[1]],
|
346
|
+
"ticker": [all_urls[2]],
|
347
|
+
"all": all_urls
|
348
|
+
}
|
349
|
+
|
350
|
+
try:
|
351
|
+
urls = url_map[update_type]
|
352
|
+
except KeyError:
|
353
|
+
raise ValueError(f"Unknown update type: {update_type}")
|
354
|
+
|
355
|
+
# 直接传协程进去,initialize 会自己 await
|
356
|
+
await self.store.initialize(*(self.client.get(url) for url in urls))
|
357
|
+
|
358
|
+
|
359
|
+
async def sub_personal(self):
|
360
|
+
"""订阅个人频道"""
|
361
|
+
# https://www.ourbit.com/ucenter/api/ws_token
|
362
|
+
res = await self.client.fetch(
|
363
|
+
'GET', f"{self.api_url}/ucenter/api/ws_token"
|
364
|
+
)
|
365
|
+
|
366
|
+
token = res.data['data'].get("wsToken")
|
367
|
+
|
368
|
+
|
369
|
+
self.client.ws_connect(
|
370
|
+
f"{self.ws_url}?wsToken={token}&platform=web",
|
371
|
+
send_json={
|
372
|
+
"method": "SUBSCRIPTION",
|
373
|
+
"params": [
|
374
|
+
"spot@private.orders",
|
375
|
+
"spot@private.trigger.orders",
|
376
|
+
"spot@private.balances"
|
377
|
+
],
|
378
|
+
"id": 1
|
379
|
+
},
|
380
|
+
hdlr_json=self.store.onmessage
|
381
|
+
)
|
382
|
+
|
383
|
+
async def sub_orderbook(self, symbols: str | list[str]):
|
384
|
+
"""订阅订单簿深度数据
|
385
|
+
|
386
|
+
Args:
|
387
|
+
symbols: 交易对符号,可以是单个字符串或字符串列表
|
388
|
+
"""
|
389
|
+
if isinstance(symbols, str):
|
390
|
+
symbols = [symbols]
|
391
|
+
|
392
|
+
# 并发获取每个交易对的初始深度数据
|
393
|
+
tasks = [
|
394
|
+
self.client.fetch('GET', f"{self.api_url}/api/platform/spot/market/depth?symbol={symbol}")
|
395
|
+
for symbol in symbols
|
396
|
+
]
|
397
|
+
|
398
|
+
# 等待所有请求完成
|
399
|
+
responses = await asyncio.gather(*tasks)
|
400
|
+
|
401
|
+
# 处理响应数据
|
402
|
+
for response in responses:
|
403
|
+
self.store.book._onresponse(response.data)
|
404
|
+
|
405
|
+
# 构建订阅参数
|
406
|
+
subscription_params = []
|
407
|
+
for symbol in symbols:
|
408
|
+
subscription_params.append(f"spot@public.increase.aggre.depth@{symbol}")
|
409
|
+
|
410
|
+
|
411
|
+
# 一次sub20个,超过需要分开订阅
|
412
|
+
for i in range(0, len(subscription_params), 20):
|
413
|
+
self.client.ws_connect(
|
414
|
+
'wss://www.ourbit.com/ws?platform=web',
|
415
|
+
send_json={
|
416
|
+
"method": "SUBSCRIPTION",
|
417
|
+
"params": subscription_params[i:i + 20],
|
418
|
+
"id": 2
|
419
|
+
},
|
420
|
+
hdlr_json=self.store.onmessage
|
421
|
+
)
|
422
|
+
|
423
|
+
async def place_order(
|
424
|
+
self,
|
425
|
+
symbol: str,
|
426
|
+
side: Literal["buy", "sell"],
|
427
|
+
price: float,
|
428
|
+
quantity: float = None,
|
429
|
+
order_type: Literal["market", "limit"] = "limit",
|
430
|
+
usdt_amount: float = None
|
431
|
+
):
|
432
|
+
"""现货下单
|
433
|
+
|
434
|
+
Args:
|
435
|
+
symbol: 交易对,如 "SOL_USDT"
|
436
|
+
side: 买卖方向 "buy" 或 "sell"
|
437
|
+
price: 价格
|
438
|
+
quantity: 数量
|
439
|
+
order_type: 订单类型 "market" 或 "limit"
|
440
|
+
usdt_amount: USDT金额,如果指定则根据价格计算数量
|
441
|
+
|
442
|
+
Returns:
|
443
|
+
订单响应数据
|
444
|
+
"""
|
445
|
+
# 解析交易对
|
446
|
+
currency, market = symbol.split("_")
|
447
|
+
|
448
|
+
detail = self.store.detail.get({
|
449
|
+
'name': currency
|
450
|
+
})
|
451
|
+
|
452
|
+
if not detail:
|
453
|
+
raise ValueError(f"Unknown currency: {currency}")
|
454
|
+
|
455
|
+
price_scale = detail.get('price_scale')
|
456
|
+
quantity_scale = detail.get('quantity_scale')
|
457
|
+
|
458
|
+
|
459
|
+
# 如果指定了USDT金额,重新计算数量
|
460
|
+
if usdt_amount is not None:
|
461
|
+
if side == "buy":
|
462
|
+
quantity = usdt_amount / price
|
463
|
+
else:
|
464
|
+
# 卖出时usdt_amount表示要卖出的币种价值
|
465
|
+
quantity = usdt_amount / price
|
466
|
+
|
467
|
+
# 格式化价格和数量
|
468
|
+
if price_scale is not None:
|
469
|
+
price = round(price, price_scale)
|
470
|
+
|
471
|
+
if quantity_scale is not None:
|
472
|
+
quantity = round(quantity, quantity_scale)
|
473
|
+
|
474
|
+
# 构建请求数据
|
475
|
+
data = {
|
476
|
+
"currency": currency,
|
477
|
+
"market": market,
|
478
|
+
"tradeType": side.upper(),
|
479
|
+
"quantity": str(quantity),
|
480
|
+
}
|
481
|
+
|
482
|
+
if order_type == "limit":
|
483
|
+
data["orderType"] = "LIMIT_ORDER"
|
484
|
+
data["price"] = str(price)
|
485
|
+
elif order_type == "market":
|
486
|
+
data["orderType"] = "MARKET_ORDER"
|
487
|
+
# 市价单通常不需要价格参数
|
488
|
+
|
489
|
+
res = await self.client.fetch(
|
490
|
+
"POST",
|
491
|
+
f"{self.api_url}/api/platform/spot/order/place",
|
492
|
+
json=data
|
493
|
+
)
|
494
|
+
|
495
|
+
# 处理响应
|
496
|
+
match res.data:
|
497
|
+
case {"msg": 'success'}:
|
498
|
+
return res.data["data"]
|
499
|
+
case _:
|
500
|
+
raise Exception(f"Failed to place order: {res.data}")
|
hyperquant/broker/ws.py
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
import asyncio
|
2
|
+
import pybotters
|
3
|
+
from pybotters.ws import ClientWebSocketResponse, logger
|
4
|
+
from pybotters.auth import Hosts
|
5
|
+
import yarl
|
6
|
+
|
7
|
+
|
8
|
+
class Heartbeat:
|
9
|
+
@staticmethod
|
10
|
+
async def ourbit(ws: pybotters.ws.ClientWebSocketResponse):
|
11
|
+
while not ws.closed:
|
12
|
+
await ws.send_str('{"method":"ping"}')
|
13
|
+
await asyncio.sleep(10.0)
|
14
|
+
|
15
|
+
async def ourbit_spot(ws: pybotters.ws.ClientWebSocketResponse):
|
16
|
+
while not ws.closed:
|
17
|
+
await ws.send_str('{"method":"ping"}')
|
18
|
+
await asyncio.sleep(10.0)
|
19
|
+
|
20
|
+
pybotters.ws.HeartbeatHosts.items['futures.ourbit.com'] = Heartbeat.ourbit
|
21
|
+
pybotters.ws.HeartbeatHosts.items['www.ourbit.com'] = Heartbeat.ourbit_spot
|
22
|
+
|
23
|
+
class WssAuth:
|
24
|
+
@staticmethod
|
25
|
+
async def ourbit(ws: ClientWebSocketResponse):
|
26
|
+
key: str = ws._response._session.__dict__["_apis"][
|
27
|
+
pybotters.ws.AuthHosts.items[ws._response.url.host].name
|
28
|
+
][0]
|
29
|
+
await ws.send_json(
|
30
|
+
{
|
31
|
+
"method": "login",
|
32
|
+
"param": {
|
33
|
+
"token": key
|
34
|
+
}
|
35
|
+
}
|
36
|
+
)
|
37
|
+
async for msg in ws:
|
38
|
+
# {"channel":"rs.login","data":"success","ts":1756470267848}
|
39
|
+
data = msg.json()
|
40
|
+
if data.get("channel") == "rs.login":
|
41
|
+
if data.get("data") == "success":
|
42
|
+
break
|
43
|
+
else:
|
44
|
+
logger.warning(f"WebSocket login failed: {data}")
|
45
|
+
|
46
|
+
|
47
|
+
|
48
|
+
pybotters.ws.AuthHosts.items['futures.ourbit.com'] = pybotters.auth.Item("ourbit", WssAuth.ourbit)
|
hyperquant/core.py
CHANGED
@@ -350,7 +350,7 @@ class Exchange(ExchangeBase):
|
|
350
350
|
self.record_history(time)
|
351
351
|
|
352
352
|
# 自动更新账户状态
|
353
|
-
self.Update({symbol: price},
|
353
|
+
self.Update({symbol: price}, time=time)
|
354
354
|
|
355
355
|
return trade
|
356
356
|
|
@@ -377,41 +377,92 @@ class Exchange(ExchangeBase):
|
|
377
377
|
trades.append(trade)
|
378
378
|
return trades
|
379
379
|
|
380
|
-
def
|
380
|
+
def _recalc_aggregates(self):
|
381
|
+
"""基于 self.account 中已保存的各 symbol 状态,重算聚合字段。"""
|
382
|
+
usdt = self.account['USDT']
|
383
|
+
usdt['unrealised_profit'] = 0
|
384
|
+
usdt['hold'] = 0
|
385
|
+
usdt['long'] = 0
|
386
|
+
usdt['short'] = 0
|
387
|
+
|
388
|
+
for symbol in self.trade_symbols:
|
389
|
+
if symbol not in self.account:
|
390
|
+
continue
|
391
|
+
sym = self.account[symbol]
|
392
|
+
px = sym.get('price', 0)
|
393
|
+
amt = sym.get('amount', 0)
|
394
|
+
hp = sym.get('hold_price', 0)
|
395
|
+
|
396
|
+
# 仅当价格有效时计入聚合
|
397
|
+
if px is not None and not np.isnan(px) and px != 0:
|
398
|
+
sym['unrealised_profit'] = (px - hp) * amt
|
399
|
+
sym['value'] = amt * px
|
400
|
+
|
401
|
+
if amt > 0:
|
402
|
+
usdt['long'] += sym['value']
|
403
|
+
elif amt < 0:
|
404
|
+
usdt['short'] += sym['value']
|
405
|
+
|
406
|
+
usdt['hold'] += abs(sym['value'])
|
407
|
+
usdt['unrealised_profit'] += sym['unrealised_profit']
|
408
|
+
|
409
|
+
usdt['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + usdt['unrealised_profit'], 6)
|
410
|
+
usdt['leverage'] = round(usdt['hold'] / usdt['total'] if usdt['total'] != 0 else 0.0, 3)
|
411
|
+
|
412
|
+
def Update(self, close_price=None, symbols=None, partial=True, **kwargs):
|
413
|
+
"""
|
414
|
+
更新账户状态。
|
415
|
+
- partial=True:只更新给定 symbols 的逐符号状态,然后对所有符号做一次聚合重算(推荐)。
|
416
|
+
- partial=False:与原逻辑兼容;当提供一部分 symbol 时,也会聚合重算,不会清空未提供符号的信息。
|
417
|
+
|
418
|
+
支持三种入参形式:
|
419
|
+
1) close_price 为 dict/Series:symbols 自动取其键/索引
|
420
|
+
2) close_price 为标量 + symbols 为单个字符串
|
421
|
+
3) 显式传 symbols=list[...],close_price 为 dict/Series(从中取价)
|
422
|
+
如果既不传 close_price 也不传 symbols,则只做一次聚合重算(例如你先前已经手动修改了某些 symbol 的 price)。
|
423
|
+
"""
|
381
424
|
if self.recorded and 'time' not in kwargs:
|
382
425
|
raise ValueError("Time parameter is required in recorded mode.")
|
383
426
|
|
384
427
|
time = kwargs.get('time', pd.Timestamp.now())
|
385
|
-
|
386
|
-
|
387
|
-
self.account['USDT']['long'] = 0
|
388
|
-
self.account['USDT']['short'] = 0
|
428
|
+
|
429
|
+
# 解析 symbols & 价格获取器
|
389
430
|
if symbols is None:
|
390
|
-
# symbols = self.trade_symbols
|
391
|
-
# 如果symbols是dict类型, 则取出所有的key, 如果是Series类型, 则取出所有的index
|
392
431
|
if isinstance(close_price, dict):
|
393
432
|
symbols = list(close_price.keys())
|
394
433
|
elif isinstance(close_price, pd.Series):
|
395
|
-
symbols = close_price.index
|
434
|
+
symbols = list(close_price.index)
|
396
435
|
else:
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
436
|
+
symbols = []
|
437
|
+
elif isinstance(symbols, str):
|
438
|
+
symbols = [symbols]
|
439
|
+
|
440
|
+
def get_px(sym):
|
441
|
+
if isinstance(close_price, (int, float, np.floating)) and len(symbols) == 1:
|
442
|
+
return float(close_price)
|
443
|
+
if isinstance(close_price, dict):
|
444
|
+
return close_price.get(sym, np.nan)
|
445
|
+
if isinstance(close_price, pd.Series):
|
446
|
+
return close_price.get(sym, np.nan)
|
447
|
+
return np.nan
|
448
|
+
|
449
|
+
# 仅更新传入的 symbols(部分更新,不动其它符号已保存信息)
|
450
|
+
for sym in symbols:
|
451
|
+
if sym not in self.trade_symbols or sym not in self.account:
|
452
|
+
# 未登记的交易对直接跳过(或可选择自动登记,但此处保持严格)
|
453
|
+
continue
|
454
|
+
px = get_px(sym)
|
455
|
+
if px is None or np.isnan(px):
|
456
|
+
# 价格无效则不覆盖旧价格
|
401
457
|
continue
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
self.account['USDT']['hold'] += abs(self.account[symbol]['value'])
|
411
|
-
self.account['USDT']['unrealised_profit'] += self.account[symbol]['unrealised_profit']
|
412
|
-
|
413
|
-
self.account['USDT']['total'] = round(self.account['USDT']['realised_profit'] + self.initial_balance + self.account['USDT']['unrealised_profit'], 6)
|
414
|
-
self.account['USDT']['leverage'] = round(self.account['USDT']['hold'] / self.account['USDT']['total'], 3)
|
458
|
+
|
459
|
+
self.account[sym]['price'] = float(px)
|
460
|
+
amt = self.account[sym]['amount']
|
461
|
+
self.account[sym]['value'] = amt * float(px)
|
462
|
+
# 不在这里算 unrealised_profit,聚合阶段统一算
|
463
|
+
|
464
|
+
# 无论 partial 与否,最后都用“账户中保存的所有 symbol 当前状态”做一次聚合重算
|
465
|
+
self._recalc_aggregates()
|
415
466
|
|
416
467
|
# 记录账户总资产到 history
|
417
468
|
if self.recorded:
|