trd-utils 0.0.21__tar.gz → 0.0.23__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of trd-utils might be problematic. Click here for more details.
- {trd_utils-0.0.21 → trd_utils-0.0.23}/PKG-INFO +1 -1
- {trd_utils-0.0.21 → trd_utils-0.0.23}/pyproject.toml +1 -1
- trd_utils-0.0.23/trd_utils/__init__.py +3 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/__init__.py +3 -2
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/blofin/blofin_types.py +28 -9
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/exchange_base.py +12 -1
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/hyperliquid/hyperliquid_client.py +1 -2
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/hyperliquid/hyperliquid_types.py +1 -12
- trd_utils-0.0.23/trd_utils/exchanges/okx/__init__.py +6 -0
- trd_utils-0.0.23/trd_utils/exchanges/okx/okx_client.py +209 -0
- trd_utils-0.0.23/trd_utils/exchanges/okx/okx_types.py +197 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/types_helper/base_model.py +4 -1
- trd_utils-0.0.21/trd_utils/__init__.py +0 -3
- {trd_utils-0.0.21 → trd_utils-0.0.23}/LICENSE +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/README.md +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/cipher/__init__.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/common_utils/float_utils.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/common_utils/wallet_utils.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/date_utils/__init__.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/date_utils/datetime_helpers.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/README.md +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/base_types.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/blofin/__init__.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/blofin/blofin_client.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/bx_ultra/__init__.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/bx_ultra/bx_types.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/bx_ultra/bx_ultra_client.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/hyperliquid/README.md +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/exchanges/hyperliquid/__init__.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/html_utils/__init__.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/html_utils/html_formats.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/tradingview/__init__.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/tradingview/tradingview_client.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/tradingview/tradingview_types.py +0 -0
- {trd_utils-0.0.21 → trd_utils-0.0.23}/trd_utils/types_helper/__init__.py +0 -0
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
|
|
2
1
|
from .exchange_base import ExchangeBase
|
|
3
2
|
from .base_types import (
|
|
4
3
|
UnifiedTraderInfo,
|
|
@@ -8,6 +7,7 @@ from .base_types import (
|
|
|
8
7
|
from .blofin import BlofinClient
|
|
9
8
|
from .bx_ultra import BXUltraClient
|
|
10
9
|
from .hyperliquid import HyperLiquidClient
|
|
10
|
+
from .okx import OkxClient
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
__all__ = [
|
|
@@ -18,4 +18,5 @@ __all__ = [
|
|
|
18
18
|
"BXUltraClient",
|
|
19
19
|
"BlofinClient",
|
|
20
20
|
"HyperLiquidClient",
|
|
21
|
-
|
|
21
|
+
"OkxClient",
|
|
22
|
+
]
|
|
@@ -1,17 +1,11 @@
|
|
|
1
|
-
# from typing import Any, Optional
|
|
2
|
-
# from decimal import Decimal
|
|
3
|
-
# from datetime import datetime, timedelta
|
|
4
|
-
# import pytz
|
|
5
|
-
|
|
6
1
|
from decimal import Decimal
|
|
7
2
|
from typing import Any
|
|
8
3
|
from trd_utils.types_helper import BaseModel
|
|
9
4
|
|
|
10
|
-
# from trd_utils.common_utils.float_utils import (
|
|
11
|
-
# dec_to_str,
|
|
12
|
-
# dec_to_normalize,
|
|
13
|
-
# )
|
|
14
5
|
|
|
6
|
+
###########################################################
|
|
7
|
+
|
|
8
|
+
# region common types
|
|
15
9
|
|
|
16
10
|
|
|
17
11
|
class BlofinApiResponse(BaseModel):
|
|
@@ -26,30 +20,44 @@ class BlofinApiResponse(BaseModel):
|
|
|
26
20
|
return f"code: {self.code}; timestamp: {self.timestamp}"
|
|
27
21
|
|
|
28
22
|
|
|
23
|
+
# endregion
|
|
24
|
+
|
|
29
25
|
###########################################################
|
|
30
26
|
|
|
27
|
+
# region api-config types
|
|
28
|
+
|
|
29
|
+
|
|
31
30
|
class PnlShareListInfo(BaseModel):
|
|
32
31
|
background_color: str = None
|
|
33
32
|
background_img_up: str = None
|
|
34
33
|
background_img_down: str = None
|
|
35
34
|
|
|
35
|
+
|
|
36
36
|
class ShareConfigResult(BaseModel):
|
|
37
37
|
pnl_share_list: list[PnlShareListInfo] = None
|
|
38
38
|
|
|
39
|
+
|
|
39
40
|
class ShareConfigResponse(BlofinApiResponse):
|
|
40
41
|
data: ShareConfigResult = None
|
|
41
42
|
|
|
43
|
+
|
|
42
44
|
class CmsColorResult(BaseModel):
|
|
43
45
|
color: str = None
|
|
44
46
|
city: str = None
|
|
45
47
|
country: str = None
|
|
46
48
|
ip: str = None
|
|
47
49
|
|
|
50
|
+
|
|
48
51
|
class CmsColorResponse(BlofinApiResponse):
|
|
49
52
|
data: CmsColorResult = None
|
|
50
53
|
|
|
54
|
+
|
|
55
|
+
# endregion
|
|
56
|
+
|
|
51
57
|
###########################################################
|
|
52
58
|
|
|
59
|
+
# region copy-trader types
|
|
60
|
+
|
|
53
61
|
|
|
54
62
|
class CopyTraderInfoResult(BaseModel):
|
|
55
63
|
aum: str = None
|
|
@@ -79,9 +87,11 @@ class CopyTraderInfoResult(BaseModel):
|
|
|
79
87
|
def get_profile_url(self) -> str:
|
|
80
88
|
return f"https://blofin.com/copy-trade/details/{self.uid}"
|
|
81
89
|
|
|
90
|
+
|
|
82
91
|
class CopyTraderInfoResponse(BlofinApiResponse):
|
|
83
92
|
data: CopyTraderInfoResult = None
|
|
84
93
|
|
|
94
|
+
|
|
85
95
|
class CopyTraderSingleOrderInfo(BaseModel):
|
|
86
96
|
id: int = None
|
|
87
97
|
symbol: str = None
|
|
@@ -136,14 +146,23 @@ class CopyTraderSingleOrderInfo(BaseModel):
|
|
|
136
146
|
position_change_history: Any = None
|
|
137
147
|
user_id: Any = None
|
|
138
148
|
|
|
149
|
+
|
|
139
150
|
class CopyTraderOrderListResponse(BlofinApiResponse):
|
|
140
151
|
data: list[CopyTraderSingleOrderInfo] = None
|
|
141
152
|
|
|
153
|
+
|
|
142
154
|
class CopyTraderAllOrderList(CopyTraderOrderListResponse):
|
|
143
155
|
total_count: int = None
|
|
144
156
|
|
|
157
|
+
|
|
145
158
|
class CopyTraderOrderHistoryResponse(BlofinApiResponse):
|
|
146
159
|
data: list[CopyTraderSingleOrderInfo] = None
|
|
147
160
|
|
|
161
|
+
|
|
148
162
|
class CopyTraderAllOrderHistory(CopyTraderOrderHistoryResponse):
|
|
149
163
|
total_count: int = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# endregion
|
|
167
|
+
|
|
168
|
+
###########################################################
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
from decimal import Decimal
|
|
2
2
|
import json
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Type
|
|
4
4
|
from abc import ABC
|
|
5
5
|
|
|
6
6
|
import httpx
|
|
@@ -72,6 +72,7 @@ class ExchangeBase(ABC):
|
|
|
72
72
|
params: dict | None = None,
|
|
73
73
|
model_type: Type[BaseModel] | None = None,
|
|
74
74
|
parse_float=Decimal,
|
|
75
|
+
raw_data: bool = False,
|
|
75
76
|
) -> "BaseModel":
|
|
76
77
|
"""
|
|
77
78
|
Invokes the specific request to the specific url with the specific params and headers.
|
|
@@ -81,6 +82,12 @@ class ExchangeBase(ABC):
|
|
|
81
82
|
headers=headers,
|
|
82
83
|
params=params,
|
|
83
84
|
)
|
|
85
|
+
if raw_data:
|
|
86
|
+
return response.content
|
|
87
|
+
|
|
88
|
+
if not model_type:
|
|
89
|
+
return response.json()
|
|
90
|
+
|
|
84
91
|
return model_type.deserialize(response.json(parse_float=parse_float))
|
|
85
92
|
|
|
86
93
|
async def invoke_post(
|
|
@@ -91,6 +98,7 @@ class ExchangeBase(ABC):
|
|
|
91
98
|
content: dict | str | bytes = "",
|
|
92
99
|
model_type: Type[BaseModel] | None = None,
|
|
93
100
|
parse_float=Decimal,
|
|
101
|
+
raw_data: bool = False,
|
|
94
102
|
) -> "BaseModel":
|
|
95
103
|
"""
|
|
96
104
|
Invokes the specific request to the specific url with the specific params and headers.
|
|
@@ -105,6 +113,9 @@ class ExchangeBase(ABC):
|
|
|
105
113
|
params=params,
|
|
106
114
|
content=content,
|
|
107
115
|
)
|
|
116
|
+
if raw_data:
|
|
117
|
+
return response.content
|
|
118
|
+
|
|
108
119
|
if not model_type:
|
|
109
120
|
return response.json()
|
|
110
121
|
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
from decimal import Decimal
|
|
3
3
|
import json
|
|
4
4
|
import logging
|
|
5
|
-
from typing import Type
|
|
6
5
|
import httpx
|
|
7
6
|
|
|
8
7
|
from pathlib import Path
|
|
@@ -11,7 +10,7 @@ from trd_utils.cipher import AESCipher
|
|
|
11
10
|
from trd_utils.common_utils.wallet_utils import shorten_wallet_address
|
|
12
11
|
from trd_utils.exchanges.base_types import UnifiedPositionInfo, UnifiedTraderInfo, UnifiedTraderPositions
|
|
13
12
|
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
14
|
-
from trd_utils.exchanges.hyperliquid.hyperliquid_types import
|
|
13
|
+
from trd_utils.exchanges.hyperliquid.hyperliquid_types import TraderPositionsInfoResponse
|
|
15
14
|
|
|
16
15
|
logger = logging.getLogger(__name__)
|
|
17
16
|
|
|
@@ -58,19 +58,8 @@ class PositionInfo(BaseModel):
|
|
|
58
58
|
In any case, we will have to somehow fake it in order to be able to compare
|
|
59
59
|
it with other positions...
|
|
60
60
|
"""
|
|
61
|
-
entry = self.entry_px
|
|
62
|
-
if entry > 100:
|
|
63
|
-
entry = round(entry, 1)
|
|
64
|
-
elif entry > 10:
|
|
65
|
-
entry = round(entry, 2)
|
|
66
|
-
elif entry > 1:
|
|
67
|
-
entry = round(entry, 3)
|
|
68
|
-
elif entry > 0.1:
|
|
69
|
-
entry = round(entry, 4)
|
|
70
|
-
elif entry > 0.01:
|
|
71
|
-
entry = round(entry, 5)
|
|
72
61
|
return (
|
|
73
|
-
f"{self.coin}-{self.leverage.value}{self.
|
|
62
|
+
f"{self.coin}-{self.leverage.value}-{1 if self.szi > 0 else 0}"
|
|
74
63
|
).encode("utf-8").hex()
|
|
75
64
|
|
|
76
65
|
def get_leverage(self) -> str:
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
import httpx
|
|
5
|
+
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from trd_utils.cipher import AESCipher
|
|
9
|
+
from trd_utils.exchanges.base_types import (
|
|
10
|
+
UnifiedPositionInfo,
|
|
11
|
+
UnifiedTraderInfo,
|
|
12
|
+
UnifiedTraderPositions,
|
|
13
|
+
)
|
|
14
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
15
|
+
from trd_utils.exchanges.okx.okx_types import (
|
|
16
|
+
AppContextUserInfo,
|
|
17
|
+
CurrentUserPositionsResponse,
|
|
18
|
+
UserInfoHtmlParser,
|
|
19
|
+
UserInfoInitialProps,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
BASE_PROFILE_URL = "https://www.okx.com/copy-trading/account/"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class OkxClient(ExchangeBase):
|
|
28
|
+
###########################################################
|
|
29
|
+
# region client parameters
|
|
30
|
+
okx_api_base_host: str = "https://www.okx.com"
|
|
31
|
+
okx_api_base_url: str = "https://www.okx.com"
|
|
32
|
+
okx_api_v5_url: str = "https://www.okx.com/priapi/v5"
|
|
33
|
+
origin_header: str = "https://www.okx.com"
|
|
34
|
+
|
|
35
|
+
# endregion
|
|
36
|
+
###########################################################
|
|
37
|
+
# region client constructor
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
account_name: str = "default",
|
|
41
|
+
http_verify: bool = True,
|
|
42
|
+
fav_letter: str = "^",
|
|
43
|
+
read_session_file: bool = False,
|
|
44
|
+
sessions_dir: str = "sessions",
|
|
45
|
+
use_http1: bool = True,
|
|
46
|
+
use_http2: bool = False,
|
|
47
|
+
):
|
|
48
|
+
# it looks like hyperliquid's api endpoints don't support http2 :(
|
|
49
|
+
self.httpx_client = httpx.AsyncClient(
|
|
50
|
+
verify=http_verify,
|
|
51
|
+
http1=use_http1,
|
|
52
|
+
http2=use_http2,
|
|
53
|
+
)
|
|
54
|
+
self.account_name = account_name
|
|
55
|
+
self._fav_letter = fav_letter
|
|
56
|
+
self.sessions_dir = sessions_dir
|
|
57
|
+
|
|
58
|
+
if read_session_file:
|
|
59
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.okx")
|
|
60
|
+
|
|
61
|
+
# endregion
|
|
62
|
+
###########################################################
|
|
63
|
+
# region positions endpoints
|
|
64
|
+
async def get_trader_positions(
|
|
65
|
+
self,
|
|
66
|
+
uid: int | str,
|
|
67
|
+
) -> CurrentUserPositionsResponse:
|
|
68
|
+
params = {
|
|
69
|
+
"uniqueName": f"{uid}",
|
|
70
|
+
"t": f"{int(time.time() * 1000)}",
|
|
71
|
+
}
|
|
72
|
+
headers = self.get_headers()
|
|
73
|
+
return await self.invoke_get(
|
|
74
|
+
f"{self.okx_api_v5_url}/ecotrade/public/community/user/position-current",
|
|
75
|
+
headers=headers,
|
|
76
|
+
params=params,
|
|
77
|
+
model_type=CurrentUserPositionsResponse,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# endregion
|
|
81
|
+
###########################################################
|
|
82
|
+
# region another-thing
|
|
83
|
+
|
|
84
|
+
async def get_copy_trader_info(
|
|
85
|
+
self,
|
|
86
|
+
uid: int | str,
|
|
87
|
+
) -> UserInfoInitialProps:
|
|
88
|
+
params = {
|
|
89
|
+
"tab": "trade",
|
|
90
|
+
}
|
|
91
|
+
headers = self.get_headers()
|
|
92
|
+
result: bytes = await self.invoke_get(
|
|
93
|
+
f"{self.okx_api_base_host}/copy-trading/account/{uid}",
|
|
94
|
+
headers=headers,
|
|
95
|
+
params=params,
|
|
96
|
+
model_type=AppContextUserInfo,
|
|
97
|
+
raw_data=True,
|
|
98
|
+
)
|
|
99
|
+
parser = UserInfoHtmlParser("__app_data_for_ssr__")
|
|
100
|
+
parser.feed(result.decode("utf-8"))
|
|
101
|
+
if not parser.found_value:
|
|
102
|
+
raise ValueError("Okx API returned invalid response")
|
|
103
|
+
|
|
104
|
+
return AppContextUserInfo(
|
|
105
|
+
**(json.loads(parser.found_value)["appContext"]),
|
|
106
|
+
).initial_props
|
|
107
|
+
|
|
108
|
+
# endregion
|
|
109
|
+
###########################################################
|
|
110
|
+
# region client helper methods
|
|
111
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
112
|
+
the_headers = {
|
|
113
|
+
# "Host": self.hyperliquid_api_base_host,
|
|
114
|
+
"Content-Type": "application/json",
|
|
115
|
+
"Accept": "application/json",
|
|
116
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
117
|
+
"User-Agent": self.user_agent,
|
|
118
|
+
"Connection": "close",
|
|
119
|
+
"appsiteid": "0",
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if self.x_requested_with:
|
|
123
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
124
|
+
|
|
125
|
+
if needs_auth:
|
|
126
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
127
|
+
return the_headers
|
|
128
|
+
|
|
129
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
130
|
+
"""
|
|
131
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
132
|
+
"""
|
|
133
|
+
# check if path exists
|
|
134
|
+
target_path = Path(file_path)
|
|
135
|
+
if not target_path.exists():
|
|
136
|
+
return self._save_session_file(file_path=file_path)
|
|
137
|
+
|
|
138
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
139
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
140
|
+
json_data: dict = json.loads(content)
|
|
141
|
+
|
|
142
|
+
self.authorization_token = json_data.get(
|
|
143
|
+
"authorization_token",
|
|
144
|
+
self.authorization_token,
|
|
145
|
+
)
|
|
146
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
147
|
+
|
|
148
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Saves current information to the session file.
|
|
151
|
+
"""
|
|
152
|
+
|
|
153
|
+
json_data = {
|
|
154
|
+
"authorization_token": self.authorization_token,
|
|
155
|
+
"user_agent": self.user_agent,
|
|
156
|
+
}
|
|
157
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
158
|
+
target_path = Path(file_path)
|
|
159
|
+
if not target_path.exists():
|
|
160
|
+
target_path.mkdir(parents=True)
|
|
161
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
162
|
+
|
|
163
|
+
# endregion
|
|
164
|
+
###########################################################
|
|
165
|
+
# region unified methods
|
|
166
|
+
async def get_unified_trader_positions(
|
|
167
|
+
self,
|
|
168
|
+
uid: int | str,
|
|
169
|
+
) -> UnifiedTraderPositions:
|
|
170
|
+
result = await self.get_trader_positions(
|
|
171
|
+
uid=uid,
|
|
172
|
+
)
|
|
173
|
+
unified_result = UnifiedTraderPositions()
|
|
174
|
+
unified_result.positions = []
|
|
175
|
+
for position in result.data[0].pos_data:
|
|
176
|
+
unified_pos = UnifiedPositionInfo()
|
|
177
|
+
unified_pos.position_id = position.pos_id
|
|
178
|
+
unified_pos.position_pnl = round(position.realized_pnl, 3)
|
|
179
|
+
unified_pos.position_side = position.get_side()
|
|
180
|
+
unified_pos.margin_mode = position.mgn_mode
|
|
181
|
+
unified_pos.position_leverage = position.lever
|
|
182
|
+
unified_pos.position_pair = position.get_pair()
|
|
183
|
+
unified_pos.open_time = position.c_time
|
|
184
|
+
unified_pos.open_price = position.avg_px
|
|
185
|
+
unified_pos.open_price_unit = position.quote_ccy
|
|
186
|
+
unified_result.positions.append(unified_pos)
|
|
187
|
+
|
|
188
|
+
return unified_result
|
|
189
|
+
|
|
190
|
+
async def get_unified_trader_info(
|
|
191
|
+
self,
|
|
192
|
+
uid: int | str,
|
|
193
|
+
) -> UnifiedTraderInfo:
|
|
194
|
+
result = await self.get_copy_trader_info(
|
|
195
|
+
uid=uid,
|
|
196
|
+
)
|
|
197
|
+
account_info = result.pre_process.leader_account_info
|
|
198
|
+
overview = result.overview_data
|
|
199
|
+
|
|
200
|
+
unified_info = UnifiedTraderInfo()
|
|
201
|
+
unified_info.trader_id = account_info.unique_name or uid
|
|
202
|
+
unified_info.trader_name = account_info.en_nick_name or account_info.nick_name
|
|
203
|
+
unified_info.trader_url = f"{BASE_PROFILE_URL}{uid}"
|
|
204
|
+
unified_info.win_rate = overview.win_rate
|
|
205
|
+
|
|
206
|
+
return unified_info
|
|
207
|
+
|
|
208
|
+
# endregion
|
|
209
|
+
###########################################################
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
from html.parser import HTMLParser
|
|
4
|
+
from typing import Any
|
|
5
|
+
from trd_utils.types_helper import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
###########################################################
|
|
9
|
+
|
|
10
|
+
# region common types
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OkxApiResponse(BaseModel):
|
|
14
|
+
code: int = None
|
|
15
|
+
msg: str = None
|
|
16
|
+
|
|
17
|
+
def __str__(self):
|
|
18
|
+
return f"code: {self.code}; timestamp: {self.timestamp}; {getattr(self, 'data', None)}"
|
|
19
|
+
|
|
20
|
+
def __repr__(self):
|
|
21
|
+
return self.__str__()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# endregion
|
|
25
|
+
|
|
26
|
+
###########################################################
|
|
27
|
+
|
|
28
|
+
# region user-positions types
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UserPositionInfo(BaseModel):
|
|
32
|
+
alias: str = None
|
|
33
|
+
avg_px: Decimal = None
|
|
34
|
+
be_px: Decimal = None
|
|
35
|
+
c_time: datetime = None
|
|
36
|
+
fee: Decimal = None
|
|
37
|
+
funding_fee: Decimal = None
|
|
38
|
+
inst_id: str = None
|
|
39
|
+
inst_type: str = None
|
|
40
|
+
last: Decimal = None
|
|
41
|
+
lever: Decimal = None
|
|
42
|
+
liq_px: Decimal = None
|
|
43
|
+
margin: Decimal = None
|
|
44
|
+
mark_px: Decimal = None
|
|
45
|
+
mgn_mode: str = None
|
|
46
|
+
mgn_ratio: Decimal = None
|
|
47
|
+
notional_usd: int = None
|
|
48
|
+
pnl: Decimal = None
|
|
49
|
+
pos: Decimal = None
|
|
50
|
+
pos_ccy: str = None
|
|
51
|
+
pos_id: str = None
|
|
52
|
+
pos_side: str = None # not that position side
|
|
53
|
+
quote_ccy: str = None
|
|
54
|
+
realized_pnl: Decimal = None
|
|
55
|
+
upl: Decimal = None
|
|
56
|
+
upl_ratio: Decimal = None
|
|
57
|
+
|
|
58
|
+
def get_side(self) -> str:
|
|
59
|
+
if self.pos > 0:
|
|
60
|
+
return "LONG"
|
|
61
|
+
return "SHORT"
|
|
62
|
+
|
|
63
|
+
def get_pair(self) -> str:
|
|
64
|
+
my_inst = self.inst_id.split("-")
|
|
65
|
+
if len(my_inst) > 1:
|
|
66
|
+
if my_inst[1] == "USD":
|
|
67
|
+
my_inst[1] = "USDT"
|
|
68
|
+
|
|
69
|
+
return f"{my_inst[0]}/{my_inst[1]}"
|
|
70
|
+
# fallback to USDT
|
|
71
|
+
return f"{self.pos_ccy}/USDT"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CurrentUserPositionsResult(BaseModel):
|
|
76
|
+
long_lever: Decimal = None
|
|
77
|
+
short_lever: Decimal = None
|
|
78
|
+
pos_data: list[UserPositionInfo] = None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class CurrentUserPositionsResponse(OkxApiResponse):
|
|
82
|
+
data: list[CurrentUserPositionsResult] = None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# endregion
|
|
86
|
+
|
|
87
|
+
###########################################################
|
|
88
|
+
|
|
89
|
+
# region User Info types
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class UserOverviewData(BaseModel):
|
|
93
|
+
ccy: str = None
|
|
94
|
+
equity: Decimal = None
|
|
95
|
+
max_retreat: Decimal = None
|
|
96
|
+
onboard_duration: int = None
|
|
97
|
+
pnl: Decimal = None
|
|
98
|
+
pnl_ratio: Decimal = None
|
|
99
|
+
risk_reward_ratio: str = None
|
|
100
|
+
win_rate: Decimal = None
|
|
101
|
+
withdrawal: Decimal = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class AuthInfo(BaseModel):
|
|
105
|
+
is_new_user: bool = None
|
|
106
|
+
user_guidance: bool = None
|
|
107
|
+
is_show_smart_copy: bool = None
|
|
108
|
+
is_cr_market_white_list_user: bool = None
|
|
109
|
+
is_show_min_entry_mount: bool = None
|
|
110
|
+
is_show_trader_tier: bool = None
|
|
111
|
+
auth_info_has_loaded: bool = None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class LeaderAccountInfo(BaseModel):
|
|
115
|
+
unique_name: str = None
|
|
116
|
+
api_trader: int = None
|
|
117
|
+
portrait: str = None
|
|
118
|
+
nick_name: str = None
|
|
119
|
+
en_nick_name: str = None
|
|
120
|
+
sign: str = None
|
|
121
|
+
translated_bio: str = None
|
|
122
|
+
en_sign: str = None
|
|
123
|
+
day: int = None
|
|
124
|
+
count: str = None
|
|
125
|
+
followee_num: int = None
|
|
126
|
+
target_id: str = None
|
|
127
|
+
role_type: int = None
|
|
128
|
+
spot_role_type: int = None
|
|
129
|
+
public_status: int = None
|
|
130
|
+
country_id: str = None
|
|
131
|
+
is_strategy_lead: bool = None
|
|
132
|
+
is_signal_trader: bool = None
|
|
133
|
+
country_name: str = None
|
|
134
|
+
show_country_tag: bool = None
|
|
135
|
+
is_chinese: bool = None
|
|
136
|
+
is_followed: bool = None
|
|
137
|
+
tier: Any = None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class PreProcessUerInfo(BaseModel):
|
|
141
|
+
leader_account_info: LeaderAccountInfo = None
|
|
142
|
+
auth_info: AuthInfo = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class UserInfoInitialProps(BaseModel):
|
|
146
|
+
overview_data: UserOverviewData = None
|
|
147
|
+
pre_process: PreProcessUerInfo = None
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class AppContextUserInfo(BaseModel):
|
|
151
|
+
"""
|
|
152
|
+
The class which holds an AppContext related to a certain user's info
|
|
153
|
+
on the exchange.
|
|
154
|
+
"""
|
|
155
|
+
|
|
156
|
+
initial_props: UserInfoInitialProps = None
|
|
157
|
+
is_ssr: bool = None
|
|
158
|
+
faas_use_ssr: bool = None
|
|
159
|
+
use_ssr: bool = None
|
|
160
|
+
is_ssr_success: bool = None
|
|
161
|
+
dsn: str = None
|
|
162
|
+
template_config: None = None
|
|
163
|
+
version: str = None
|
|
164
|
+
project: str = None
|
|
165
|
+
url_key: str = None
|
|
166
|
+
trace_id: str = None
|
|
167
|
+
enable_rtl: bool = None
|
|
168
|
+
is_apm_proxy_off: int = None
|
|
169
|
+
is_yandex_off: int = None
|
|
170
|
+
is_web_worker_enable: int = None
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
class UserInfoHtmlParser(HTMLParser):
|
|
174
|
+
target_data_id: str = None
|
|
175
|
+
found_value: str = None
|
|
176
|
+
current_tag_has_target: bool = None
|
|
177
|
+
|
|
178
|
+
def __init__(self, target_data_id: str, **kwargs):
|
|
179
|
+
super().__init__(**kwargs)
|
|
180
|
+
self.target_data_id = target_data_id
|
|
181
|
+
self.found_value = None
|
|
182
|
+
self.current_tag_has_target = False
|
|
183
|
+
|
|
184
|
+
def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]):
|
|
185
|
+
attrs_dict = dict(attrs)
|
|
186
|
+
if "data-id" in attrs_dict and attrs_dict["data-id"] == self.target_data_id:
|
|
187
|
+
self.current_tag_has_target = True
|
|
188
|
+
|
|
189
|
+
def handle_data(self, data: str):
|
|
190
|
+
if self.current_tag_has_target:
|
|
191
|
+
self.found_value = data
|
|
192
|
+
self.current_tag_has_target = False
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# endregion
|
|
196
|
+
|
|
197
|
+
###########################################################
|
|
@@ -251,7 +251,10 @@ class BaseModel:
|
|
|
251
251
|
elif expected_type is datetime.datetime:
|
|
252
252
|
try:
|
|
253
253
|
if isinstance(value, str):
|
|
254
|
-
value
|
|
254
|
+
if value.isdigit():
|
|
255
|
+
value = dt_from_ts(int(value))
|
|
256
|
+
else:
|
|
257
|
+
value = dateutil.parser.parse(value)
|
|
255
258
|
elif isinstance(value, int):
|
|
256
259
|
value = dt_from_ts(value)
|
|
257
260
|
except Exception as ex:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|