trd-utils 0.0.14__py3-none-any.whl → 0.0.16__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.
Potentially problematic release.
This version of trd-utils might be problematic. Click here for more details.
- trd_utils/__init__.py +1 -1
- trd_utils/date_utils/__init__.py +8 -0
- trd_utils/date_utils/datetime_helpers.py +16 -0
- trd_utils/exchanges/README.md +201 -0
- trd_utils/exchanges/__init__.py +13 -3
- trd_utils/exchanges/base_types.py +106 -0
- trd_utils/exchanges/blofin/__init__.py +1 -1
- trd_utils/exchanges/blofin/blofin_client.py +78 -69
- trd_utils/exchanges/blofin/blofin_types.py +35 -35
- trd_utils/exchanges/bx_ultra/bx_ultra_client.py +124 -70
- trd_utils/exchanges/exchange_base.py +70 -12
- trd_utils/exchanges/hyperliquid/README.md +3 -0
- trd_utils/exchanges/hyperliquid/__init__.py +7 -0
- trd_utils/exchanges/hyperliquid/hyperliquid_client.py +138 -0
- trd_utils/exchanges/hyperliquid/hyperliquid_types.py +109 -0
- trd_utils/types_helper/__init__.py +1 -1
- trd_utils/types_helper/base_model.py +1 -1
- {trd_utils-0.0.14.dist-info → trd_utils-0.0.16.dist-info}/METADATA +1 -1
- trd_utils-0.0.16.dist-info/RECORD +31 -0
- trd_utils-0.0.14.dist-info/RECORD +0 -23
- {trd_utils-0.0.14.dist-info → trd_utils-0.0.16.dist-info}/LICENSE +0 -0
- {trd_utils-0.0.14.dist-info → trd_utils-0.0.16.dist-info}/WHEEL +0 -0
|
@@ -64,7 +64,7 @@ class CopyTraderInfoResult(BaseModel):
|
|
|
64
64
|
joined_date: int = None
|
|
65
65
|
max_draw_down: Decimal = None
|
|
66
66
|
nick_name: str = None
|
|
67
|
-
order_amount_limit: None
|
|
67
|
+
order_amount_limit: Any = None
|
|
68
68
|
profile: str = None
|
|
69
69
|
profit_sharing_ratio: Decimal = None
|
|
70
70
|
real_pnl: Decimal = None
|
|
@@ -89,7 +89,7 @@ class CopyTraderSingleOrderInfo(BaseModel):
|
|
|
89
89
|
order_side: str = None
|
|
90
90
|
avg_open_price: str = None
|
|
91
91
|
quantity: str = None
|
|
92
|
-
quantity_cont: None
|
|
92
|
+
quantity_cont: Any = None
|
|
93
93
|
open_time: int = None
|
|
94
94
|
close_time: Any = None
|
|
95
95
|
avg_close_price: Decimal = None
|
|
@@ -100,41 +100,41 @@ class CopyTraderSingleOrderInfo(BaseModel):
|
|
|
100
100
|
followers: Any = None
|
|
101
101
|
order_id: Any = None
|
|
102
102
|
sharing: Any = None
|
|
103
|
-
order_state: None
|
|
104
|
-
trader_name: None
|
|
105
|
-
mark_price: None
|
|
106
|
-
tp_trigger_price: None
|
|
107
|
-
tp_order_type: None
|
|
108
|
-
sl_trigger_price: None
|
|
109
|
-
sl_order_type: None
|
|
103
|
+
order_state: Any = None
|
|
104
|
+
trader_name: Any = None
|
|
105
|
+
mark_price: Any = None
|
|
106
|
+
tp_trigger_price: Any = None
|
|
107
|
+
tp_order_type: Any = None
|
|
108
|
+
sl_trigger_price: Any = None
|
|
109
|
+
sl_order_type: Any = None
|
|
110
110
|
margin_mode: str = None
|
|
111
|
-
time_in_force: None
|
|
111
|
+
time_in_force: Any = None
|
|
112
112
|
position_side: str = None
|
|
113
|
-
order_category: None
|
|
114
|
-
price: None
|
|
115
|
-
fill_quantity: None
|
|
116
|
-
fill_quantity_cont: None
|
|
117
|
-
pnl: None
|
|
118
|
-
cancel_source: None
|
|
119
|
-
order_type: None
|
|
120
|
-
order_open_state: None
|
|
121
|
-
amount: None
|
|
122
|
-
filled_amount: None
|
|
123
|
-
create_time: None
|
|
124
|
-
update_time: None
|
|
125
|
-
open_fee: None
|
|
126
|
-
close_fee: None
|
|
127
|
-
id_md5: None
|
|
128
|
-
tp_sl: None
|
|
129
|
-
trader_uid: None
|
|
130
|
-
available_quantity: None
|
|
131
|
-
available_quantity_cont: None
|
|
132
|
-
show_in_kline: None
|
|
133
|
-
unrealized_pnl: None
|
|
134
|
-
unrealized_pnl_ratio: None
|
|
135
|
-
broker_id: None
|
|
136
|
-
position_change_history: None
|
|
137
|
-
user_id: None
|
|
113
|
+
order_category: Any = None
|
|
114
|
+
price: Any = None
|
|
115
|
+
fill_quantity: Any = None
|
|
116
|
+
fill_quantity_cont: Any = None
|
|
117
|
+
pnl: Decimal = None
|
|
118
|
+
cancel_source: Any = None
|
|
119
|
+
order_type: Any = None
|
|
120
|
+
order_open_state: Any = None
|
|
121
|
+
amount: Any = None
|
|
122
|
+
filled_amount: Any = None
|
|
123
|
+
create_time: Any = None
|
|
124
|
+
update_time: Any = None
|
|
125
|
+
open_fee: Any = None
|
|
126
|
+
close_fee: Any = None
|
|
127
|
+
id_md5: Any = None
|
|
128
|
+
tp_sl: Any = None
|
|
129
|
+
trader_uid: Any = None
|
|
130
|
+
available_quantity: Any = None
|
|
131
|
+
available_quantity_cont: Any = None
|
|
132
|
+
show_in_kline: Any = None
|
|
133
|
+
unrealized_pnl: Any = None
|
|
134
|
+
unrealized_pnl_ratio: Any = None
|
|
135
|
+
broker_id: Any = None
|
|
136
|
+
position_change_history: Any = None
|
|
137
|
+
user_id: Any = None
|
|
138
138
|
|
|
139
139
|
class CopyTraderOrderListResponse(BlofinApiResponse):
|
|
140
140
|
data: list[CopyTraderSingleOrderInfo] = None
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""
|
|
2
|
+
BxUltra exchange subclass
|
|
3
|
+
"""
|
|
2
4
|
|
|
3
5
|
import asyncio
|
|
4
6
|
from decimal import Decimal
|
|
5
7
|
import json
|
|
6
8
|
import logging
|
|
7
|
-
from typing import Type
|
|
8
9
|
import uuid
|
|
9
10
|
|
|
10
11
|
import httpx
|
|
@@ -12,6 +13,11 @@ import httpx
|
|
|
12
13
|
import time
|
|
13
14
|
from pathlib import Path
|
|
14
15
|
|
|
16
|
+
from trd_utils.exchanges.base_types import (
|
|
17
|
+
UnifiedPositionInfo,
|
|
18
|
+
UnifiedTraderInfo,
|
|
19
|
+
UnifiedTraderPositions,
|
|
20
|
+
)
|
|
15
21
|
from trd_utils.exchanges.bx_ultra.bx_utils import do_ultra_ss
|
|
16
22
|
from trd_utils.exchanges.bx_ultra.bx_types import (
|
|
17
23
|
AssetsInfoResponse,
|
|
@@ -30,7 +36,6 @@ from trd_utils.exchanges.bx_ultra.bx_types import (
|
|
|
30
36
|
ZenDeskABStatusResponse,
|
|
31
37
|
ZenDeskAuthResponse,
|
|
32
38
|
ZoneModuleListResponse,
|
|
33
|
-
BxApiResponse,
|
|
34
39
|
)
|
|
35
40
|
from trd_utils.cipher import AESCipher
|
|
36
41
|
|
|
@@ -49,9 +54,17 @@ WEB_APP_VERSION = "4.78.12"
|
|
|
49
54
|
TG_APP_VERSION = "5.0.15"
|
|
50
55
|
|
|
51
56
|
ACCEPT_ENCODING_HEADER = "gzip, deflate, br, zstd"
|
|
57
|
+
BASE_PROFILE_URL = "https://bingx.com/en/CopyTrading/"
|
|
52
58
|
|
|
53
59
|
logger = logging.getLogger(__name__)
|
|
54
60
|
|
|
61
|
+
# The cache in which we will be storing the api identities.
|
|
62
|
+
# The key of this dict is uid (long user identifier), to api-identity.
|
|
63
|
+
# Why is this a global variable, and not a class attribute? because as far as
|
|
64
|
+
# I've observed, api-identities in bx (unlike Telegram's access-hashes) are not
|
|
65
|
+
# specific to the current session that is fetching them,
|
|
66
|
+
user_api_identity_cache: dict[int, int] = {}
|
|
67
|
+
|
|
55
68
|
|
|
56
69
|
class BXUltraClient(ExchangeBase):
|
|
57
70
|
###########################################################
|
|
@@ -119,7 +132,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
119
132
|
f"{self.we_api_base_url}/coin/v1/zone/module-info",
|
|
120
133
|
headers=headers,
|
|
121
134
|
params=params,
|
|
122
|
-
|
|
135
|
+
model_type=ZoneModuleListResponse,
|
|
123
136
|
)
|
|
124
137
|
|
|
125
138
|
async def get_user_favorite_quotation(
|
|
@@ -136,7 +149,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
136
149
|
f"{self.we_api_base_url}/coin/v1/user/favorite/quotation",
|
|
137
150
|
headers=headers,
|
|
138
151
|
params=params,
|
|
139
|
-
|
|
152
|
+
model_type=UserFavoriteQuotationResponse,
|
|
140
153
|
)
|
|
141
154
|
|
|
142
155
|
async def get_quotation_rank(
|
|
@@ -153,7 +166,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
153
166
|
f"{self.we_api_base_url}/coin/v1/rank/quotation-rank",
|
|
154
167
|
headers=headers,
|
|
155
168
|
params=params,
|
|
156
|
-
|
|
169
|
+
model_type=QuotationRankResponse,
|
|
157
170
|
)
|
|
158
171
|
|
|
159
172
|
async def get_hot_search(
|
|
@@ -170,7 +183,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
170
183
|
f"{self.we_api_base_url}/coin/v1/quotation/hot-search",
|
|
171
184
|
headers=headers,
|
|
172
185
|
params=params,
|
|
173
|
-
|
|
186
|
+
model_type=HotSearchResponse,
|
|
174
187
|
)
|
|
175
188
|
|
|
176
189
|
async def get_homepage(
|
|
@@ -187,7 +200,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
187
200
|
f"{self.we_api_base_url}/coin/v1/discovery/homepage",
|
|
188
201
|
headers=headers,
|
|
189
202
|
params=params,
|
|
190
|
-
|
|
203
|
+
model_type=HomePageResponse,
|
|
191
204
|
)
|
|
192
205
|
|
|
193
206
|
# endregion
|
|
@@ -198,15 +211,17 @@ class BXUltraClient(ExchangeBase):
|
|
|
198
211
|
return await self.invoke_get(
|
|
199
212
|
f"{self.we_api_base_url}/customer/v1/zendesk/ab-status",
|
|
200
213
|
headers=headers,
|
|
201
|
-
|
|
214
|
+
model_type=ZenDeskABStatusResponse,
|
|
202
215
|
)
|
|
216
|
+
|
|
203
217
|
async def do_zendesk_auth(self) -> ZenDeskAuthResponse:
|
|
204
218
|
headers = self.get_headers(needs_auth=True)
|
|
205
219
|
return await self.invoke_get(
|
|
206
220
|
f"{self.we_api_base_url}/customer/v1/zendesk/auth/jwt",
|
|
207
221
|
headers=headers,
|
|
208
|
-
|
|
222
|
+
model_type=ZenDeskAuthResponse,
|
|
209
223
|
)
|
|
224
|
+
|
|
210
225
|
# endregion
|
|
211
226
|
###########################################################
|
|
212
227
|
# region platform-tool
|
|
@@ -215,7 +230,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
215
230
|
return await self.invoke_get(
|
|
216
231
|
f"{self.we_api_base_url}/platform-tool/v1/hint/list",
|
|
217
232
|
headers=headers,
|
|
218
|
-
|
|
233
|
+
model_type=HintListResponse,
|
|
219
234
|
)
|
|
220
235
|
|
|
221
236
|
# endregion
|
|
@@ -226,7 +241,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
226
241
|
return await self.invoke_get(
|
|
227
242
|
f"{self.we_api_base_url}/asset-manager/v1/assets/account-total-overview",
|
|
228
243
|
headers=headers,
|
|
229
|
-
|
|
244
|
+
model_type=AssetsInfoResponse,
|
|
230
245
|
)
|
|
231
246
|
|
|
232
247
|
# endregion
|
|
@@ -255,7 +270,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
255
270
|
f"{self.we_api_base_url}/v4/contract/order/hold",
|
|
256
271
|
headers=headers,
|
|
257
272
|
params=params,
|
|
258
|
-
|
|
273
|
+
model_type=ContractsListResponse,
|
|
259
274
|
)
|
|
260
275
|
|
|
261
276
|
async def get_contract_order_history(
|
|
@@ -282,7 +297,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
282
297
|
f"{self.we_api_base_url}/v2/contract/order/history",
|
|
283
298
|
headers=headers,
|
|
284
299
|
params=params,
|
|
285
|
-
|
|
300
|
+
model_type=ContractOrdersHistoryResponse,
|
|
286
301
|
)
|
|
287
302
|
|
|
288
303
|
async def get_today_contract_earnings(
|
|
@@ -389,7 +404,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
389
404
|
# endregion
|
|
390
405
|
###########################################################
|
|
391
406
|
# region copy-trade-facade
|
|
392
|
-
async def
|
|
407
|
+
async def get_copy_trader_positions(
|
|
393
408
|
self,
|
|
394
409
|
uid: int | str,
|
|
395
410
|
api_identity: str,
|
|
@@ -409,7 +424,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
409
424
|
f"{self.we_api_base_url}/copy-trade-facade/v2/real/trader/positions",
|
|
410
425
|
headers=headers,
|
|
411
426
|
params=params,
|
|
412
|
-
|
|
427
|
+
model_type=CopyTraderTradePositionsResponse,
|
|
413
428
|
)
|
|
414
429
|
|
|
415
430
|
async def search_copy_traders(
|
|
@@ -446,7 +461,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
446
461
|
headers=headers,
|
|
447
462
|
params=params,
|
|
448
463
|
content=payload,
|
|
449
|
-
|
|
464
|
+
model_type=SearchCopyTradersResponse,
|
|
450
465
|
)
|
|
451
466
|
|
|
452
467
|
async def get_copy_trader_futures_stats(
|
|
@@ -454,6 +469,11 @@ class BXUltraClient(ExchangeBase):
|
|
|
454
469
|
uid: int | str,
|
|
455
470
|
api_identity: str,
|
|
456
471
|
) -> CopyTraderFuturesStatsResponse:
|
|
472
|
+
"""
|
|
473
|
+
Returns futures statistics of a certain trader.
|
|
474
|
+
If you do not have the api_identity parameter, please first invoke
|
|
475
|
+
get_copy_trader_resume method and get it from there.
|
|
476
|
+
"""
|
|
457
477
|
params = {
|
|
458
478
|
"uid": f"{uid}",
|
|
459
479
|
"apiIdentity": f"{api_identity}",
|
|
@@ -463,7 +483,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
463
483
|
f"{self.we_api_base_url}/copy-trade-facade/v4/trader/account/futures/stat",
|
|
464
484
|
headers=headers,
|
|
465
485
|
params=params,
|
|
466
|
-
|
|
486
|
+
model_type=CopyTraderFuturesStatsResponse,
|
|
467
487
|
)
|
|
468
488
|
|
|
469
489
|
async def get_copy_trader_resume(
|
|
@@ -478,9 +498,22 @@ class BXUltraClient(ExchangeBase):
|
|
|
478
498
|
f"{self.we_api_base_url}/copy-trade-facade/v1/trader/resume",
|
|
479
499
|
headers=headers,
|
|
480
500
|
params=params,
|
|
481
|
-
|
|
501
|
+
model_type=CopyTraderResumeResponse,
|
|
482
502
|
)
|
|
483
503
|
|
|
504
|
+
async def get_trader_api_identity(
|
|
505
|
+
self,
|
|
506
|
+
uid: int | str,
|
|
507
|
+
) -> int | str:
|
|
508
|
+
api_identity = user_api_identity_cache.get(uid, None)
|
|
509
|
+
if not api_identity:
|
|
510
|
+
resume = await self.get_copy_trader_resume(
|
|
511
|
+
uid=uid,
|
|
512
|
+
)
|
|
513
|
+
api_identity = resume.data.api_identity
|
|
514
|
+
user_api_identity_cache[uid] = api_identity
|
|
515
|
+
return api_identity
|
|
516
|
+
|
|
484
517
|
# endregion
|
|
485
518
|
###########################################################
|
|
486
519
|
# region welfare
|
|
@@ -490,7 +523,7 @@ class BXUltraClient(ExchangeBase):
|
|
|
490
523
|
f"{self.original_base_host}/api/act-operation/v1/welfare/sign-in/do",
|
|
491
524
|
headers=headers,
|
|
492
525
|
content="",
|
|
493
|
-
|
|
526
|
+
model_type=None,
|
|
494
527
|
)
|
|
495
528
|
|
|
496
529
|
# endregion
|
|
@@ -540,56 +573,6 @@ class BXUltraClient(ExchangeBase):
|
|
|
540
573
|
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
541
574
|
return the_headers
|
|
542
575
|
|
|
543
|
-
async def invoke_get(
|
|
544
|
-
self,
|
|
545
|
-
url: str,
|
|
546
|
-
headers: dict | None = None,
|
|
547
|
-
params: dict | None = None,
|
|
548
|
-
model: Type[BxApiResponse] | None = None,
|
|
549
|
-
parse_float=Decimal,
|
|
550
|
-
) -> "BxApiResponse":
|
|
551
|
-
"""
|
|
552
|
-
Invokes the specific request to the specific url with the specific params and headers.
|
|
553
|
-
"""
|
|
554
|
-
response = await self.httpx_client.get(
|
|
555
|
-
url=url,
|
|
556
|
-
headers=headers,
|
|
557
|
-
params=params,
|
|
558
|
-
)
|
|
559
|
-
return model.deserialize(response.json(parse_float=parse_float))
|
|
560
|
-
|
|
561
|
-
async def invoke_post(
|
|
562
|
-
self,
|
|
563
|
-
url: str,
|
|
564
|
-
headers: dict | None = None,
|
|
565
|
-
params: dict | None = None,
|
|
566
|
-
content: dict | str | bytes = "",
|
|
567
|
-
model: Type[BxApiResponse] | None = None,
|
|
568
|
-
parse_float=Decimal,
|
|
569
|
-
) -> "BxApiResponse":
|
|
570
|
-
"""
|
|
571
|
-
Invokes the specific request to the specific url with the specific params and headers.
|
|
572
|
-
"""
|
|
573
|
-
|
|
574
|
-
if isinstance(content, dict):
|
|
575
|
-
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
576
|
-
|
|
577
|
-
response = await self.httpx_client.post(
|
|
578
|
-
url=url,
|
|
579
|
-
headers=headers,
|
|
580
|
-
params=params,
|
|
581
|
-
content=content,
|
|
582
|
-
)
|
|
583
|
-
if not model:
|
|
584
|
-
return response.json()
|
|
585
|
-
|
|
586
|
-
return model.deserialize(response.json(parse_float=parse_float))
|
|
587
|
-
|
|
588
|
-
async def aclose(self) -> None:
|
|
589
|
-
await self.httpx_client.aclose()
|
|
590
|
-
logger.info("BXUltraClient closed")
|
|
591
|
-
return True
|
|
592
|
-
|
|
593
576
|
def read_from_session_file(self, file_path: str) -> None:
|
|
594
577
|
"""
|
|
595
578
|
Reads from session file; if it doesn't exist, creates it.
|
|
@@ -654,3 +637,74 @@ class BXUltraClient(ExchangeBase):
|
|
|
654
637
|
|
|
655
638
|
# endregion
|
|
656
639
|
###########################################################
|
|
640
|
+
# region unified methods
|
|
641
|
+
|
|
642
|
+
async def get_unified_trader_positions(
|
|
643
|
+
self,
|
|
644
|
+
uid: int | str,
|
|
645
|
+
) -> UnifiedTraderPositions:
|
|
646
|
+
global user_api_identity_cache
|
|
647
|
+
|
|
648
|
+
api_identity = await self.get_trader_api_identity(
|
|
649
|
+
uid=uid,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
result = await self.get_copy_trader_positions(
|
|
653
|
+
uid=uid,
|
|
654
|
+
api_identity=api_identity,
|
|
655
|
+
page_size=50, # TODO: make this dynamic I guess...
|
|
656
|
+
)
|
|
657
|
+
if result.data.hide:
|
|
658
|
+
# TODO: do proper exceptions here...
|
|
659
|
+
raise ValueError("The trader has made their positions hidden")
|
|
660
|
+
unified_result = UnifiedTraderPositions()
|
|
661
|
+
unified_result.positions = []
|
|
662
|
+
for position in result.data.positions:
|
|
663
|
+
unified_pos = UnifiedPositionInfo()
|
|
664
|
+
unified_pos.position_id = position.position_no
|
|
665
|
+
unified_pos.position_pnl = position.unrealized_pnl
|
|
666
|
+
unified_pos.position_side = position.position_side
|
|
667
|
+
unified_pos.margin_mode = "isolated" # TODO: fix this
|
|
668
|
+
unified_pos.position_leverage = position.leverage
|
|
669
|
+
unified_pos.position_pair = (
|
|
670
|
+
position.symbol
|
|
671
|
+
) # TODO: make sure correct format
|
|
672
|
+
unified_pos.open_time = None # TODO: do something for this?
|
|
673
|
+
unified_pos.open_price = position.avg_price
|
|
674
|
+
unified_pos.open_price_unit = position.symbol.split("-")[-1] # TODO
|
|
675
|
+
unified_result.positions.append(unified_pos)
|
|
676
|
+
|
|
677
|
+
return unified_result
|
|
678
|
+
|
|
679
|
+
async def get_unified_trader_info(
|
|
680
|
+
self,
|
|
681
|
+
uid: int | str,
|
|
682
|
+
) -> UnifiedTraderInfo:
|
|
683
|
+
resume_resp = await self.get_copy_trader_resume(
|
|
684
|
+
uid=uid,
|
|
685
|
+
)
|
|
686
|
+
if resume_resp.code != 0 and not resume_resp.data:
|
|
687
|
+
if resume_resp.msg:
|
|
688
|
+
raise ValueError(f"got error from API: {resume_resp.msg}")
|
|
689
|
+
raise ValueError(
|
|
690
|
+
f"got unknown error from bx API while fetching resume for {uid}"
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
resume = resume_resp.data
|
|
694
|
+
api_identity = resume.api_identity
|
|
695
|
+
|
|
696
|
+
info_resp = await self.get_copy_trader_futures_stats(
|
|
697
|
+
uid=uid,
|
|
698
|
+
api_identity=api_identity,
|
|
699
|
+
)
|
|
700
|
+
info = info_resp.data
|
|
701
|
+
unified_info = UnifiedTraderInfo()
|
|
702
|
+
unified_info.trader_id = resume.trader_info.uid
|
|
703
|
+
unified_info.trader_name = resume.trader_info.nick_name
|
|
704
|
+
unified_info.trader_url = f"{BASE_PROFILE_URL}{uid}"
|
|
705
|
+
unified_info.win_rate = Decimal(info.win_rate.rstrip("%")) / 100
|
|
706
|
+
|
|
707
|
+
return unified_info
|
|
708
|
+
|
|
709
|
+
# endregion
|
|
710
|
+
###########################################################
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
2
1
|
from decimal import Decimal
|
|
3
|
-
|
|
2
|
+
import json
|
|
3
|
+
from typing import Any, Type
|
|
4
4
|
from abc import ABC
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
7
7
|
|
|
8
|
+
from trd_utils.exchanges.base_types import UnifiedTraderInfo, UnifiedTraderPositions
|
|
9
|
+
from trd_utils.types_helper.base_model import BaseModel
|
|
10
|
+
|
|
8
11
|
|
|
9
12
|
class ExchangeBase(ABC):
|
|
10
13
|
###########################################################
|
|
@@ -25,6 +28,38 @@ class ExchangeBase(ABC):
|
|
|
25
28
|
|
|
26
29
|
_fav_letter: str = "^"
|
|
27
30
|
# endregion
|
|
31
|
+
###########################################################
|
|
32
|
+
|
|
33
|
+
# region abstract trading methods
|
|
34
|
+
|
|
35
|
+
async def get_unified_trader_positions(
|
|
36
|
+
self,
|
|
37
|
+
uid: int | str,
|
|
38
|
+
) -> UnifiedTraderPositions:
|
|
39
|
+
"""
|
|
40
|
+
Returns the unified version of all currently open positions of the specific
|
|
41
|
+
trader. Note that different exchanges might fill different fields, according to the
|
|
42
|
+
data they provide in their public APIs.
|
|
43
|
+
If you want to fetch past positions history, you have to use another method.
|
|
44
|
+
"""
|
|
45
|
+
raise NotImplementedError(
|
|
46
|
+
"This method is not implemented in ExchangeBase class. "
|
|
47
|
+
"Please use a real exchange class inheriting and implementing this method."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
async def get_unified_trader_info(self, uid: int | str) -> UnifiedTraderInfo:
|
|
51
|
+
"""
|
|
52
|
+
Returns information about a specific trader.
|
|
53
|
+
Different exchanges might return and fill different information according to the
|
|
54
|
+
data returned from their public APIs.
|
|
55
|
+
"""
|
|
56
|
+
raise NotImplementedError(
|
|
57
|
+
"This method is not implemented in ExchangeBase class. "
|
|
58
|
+
"Please use a real exchange class inheriting and implementing this method."
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# endregion
|
|
62
|
+
|
|
28
63
|
###########################################################
|
|
29
64
|
# region client helper methods
|
|
30
65
|
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
@@ -33,32 +68,55 @@ class ExchangeBase(ABC):
|
|
|
33
68
|
async def invoke_get(
|
|
34
69
|
self,
|
|
35
70
|
url: str,
|
|
36
|
-
headers: dict | None,
|
|
37
|
-
params: dict | None,
|
|
38
|
-
|
|
71
|
+
headers: dict | None = None,
|
|
72
|
+
params: dict | None = None,
|
|
73
|
+
model_type: Type[BaseModel] | None = None,
|
|
39
74
|
parse_float=Decimal,
|
|
40
|
-
) ->
|
|
75
|
+
) -> "BaseModel":
|
|
41
76
|
"""
|
|
42
77
|
Invokes the specific request to the specific url with the specific params and headers.
|
|
43
78
|
"""
|
|
44
|
-
|
|
79
|
+
response = await self.httpx_client.get(
|
|
80
|
+
url=url,
|
|
81
|
+
headers=headers,
|
|
82
|
+
params=params,
|
|
83
|
+
)
|
|
84
|
+
return model_type.deserialize(response.json(parse_float=parse_float))
|
|
45
85
|
|
|
46
86
|
async def invoke_post(
|
|
47
87
|
self,
|
|
48
88
|
url: str,
|
|
49
89
|
headers: dict | None = None,
|
|
50
90
|
params: dict | None = None,
|
|
51
|
-
content: str | bytes = "",
|
|
52
|
-
|
|
91
|
+
content: dict | str | bytes = "",
|
|
92
|
+
model_type: Type[BaseModel] | None = None,
|
|
53
93
|
parse_float=Decimal,
|
|
54
|
-
):
|
|
94
|
+
) -> "BaseModel":
|
|
55
95
|
"""
|
|
56
96
|
Invokes the specific request to the specific url with the specific params and headers.
|
|
57
97
|
"""
|
|
58
|
-
|
|
98
|
+
|
|
99
|
+
if isinstance(content, dict):
|
|
100
|
+
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
101
|
+
|
|
102
|
+
response = await self.httpx_client.post(
|
|
103
|
+
url=url,
|
|
104
|
+
headers=headers,
|
|
105
|
+
params=params,
|
|
106
|
+
content=content,
|
|
107
|
+
)
|
|
108
|
+
if not model_type:
|
|
109
|
+
return response.json()
|
|
110
|
+
|
|
111
|
+
return model_type.deserialize(response.json(parse_float=parse_float))
|
|
112
|
+
|
|
59
113
|
|
|
60
114
|
async def aclose(self) -> None:
|
|
61
|
-
|
|
115
|
+
await self.httpx_client.aclose()
|
|
116
|
+
|
|
117
|
+
# endregion
|
|
118
|
+
###########################################################
|
|
119
|
+
# region data-files related methods
|
|
62
120
|
|
|
63
121
|
def read_from_session_file(self, file_path: str) -> None:
|
|
64
122
|
"""
|