trd-utils 0.0.57__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.
- trd_utils/__init__.py +3 -0
- trd_utils/cipher/__init__.py +44 -0
- trd_utils/common_utils/float_utils.py +21 -0
- trd_utils/common_utils/wallet_utils.py +26 -0
- trd_utils/date_utils/__init__.py +8 -0
- trd_utils/date_utils/datetime_helpers.py +25 -0
- trd_utils/exchanges/README.md +203 -0
- trd_utils/exchanges/__init__.py +28 -0
- trd_utils/exchanges/base_types.py +229 -0
- trd_utils/exchanges/binance/__init__.py +13 -0
- trd_utils/exchanges/binance/binance_client.py +389 -0
- trd_utils/exchanges/binance/binance_types.py +116 -0
- trd_utils/exchanges/blofin/__init__.py +6 -0
- trd_utils/exchanges/blofin/blofin_client.py +375 -0
- trd_utils/exchanges/blofin/blofin_types.py +173 -0
- trd_utils/exchanges/bx_ultra/__init__.py +6 -0
- trd_utils/exchanges/bx_ultra/bx_types.py +1338 -0
- trd_utils/exchanges/bx_ultra/bx_ultra_client.py +1123 -0
- trd_utils/exchanges/bx_ultra/bx_utils.py +51 -0
- trd_utils/exchanges/errors.py +10 -0
- trd_utils/exchanges/exchange_base.py +301 -0
- trd_utils/exchanges/hyperliquid/README.md +3 -0
- trd_utils/exchanges/hyperliquid/__init__.py +7 -0
- trd_utils/exchanges/hyperliquid/hyperliquid_client.py +292 -0
- trd_utils/exchanges/hyperliquid/hyperliquid_types.py +183 -0
- trd_utils/exchanges/okx/__init__.py +6 -0
- trd_utils/exchanges/okx/okx_client.py +219 -0
- trd_utils/exchanges/okx/okx_types.py +197 -0
- trd_utils/exchanges/price_fetcher.py +48 -0
- trd_utils/html_utils/__init__.py +26 -0
- trd_utils/html_utils/html_formats.py +72 -0
- trd_utils/tradingview/__init__.py +8 -0
- trd_utils/tradingview/tradingview_client.py +128 -0
- trd_utils/tradingview/tradingview_types.py +185 -0
- trd_utils/types_helper/__init__.py +12 -0
- trd_utils/types_helper/base_model.py +350 -0
- trd_utils/types_helper/decorators.py +20 -0
- trd_utils/types_helper/model_config.py +6 -0
- trd_utils/types_helper/ultra_list.py +39 -0
- trd_utils/types_helper/utils.py +40 -0
- trd_utils-0.0.57.dist-info/METADATA +42 -0
- trd_utils-0.0.57.dist-info/RECORD +44 -0
- trd_utils-0.0.57.dist-info/WHEEL +4 -0
- trd_utils-0.0.57.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from trd_utils.types_helper import BaseModel, new_list
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
###########################################################
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# region Common types
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class HyperLiquidApiResponse(BaseModel):
|
|
12
|
+
time: int = None
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# endregion
|
|
16
|
+
|
|
17
|
+
###########################################################
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# region info types
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class CumFundingInfo(BaseModel):
|
|
24
|
+
all_time: str = None
|
|
25
|
+
since_open: str = None
|
|
26
|
+
since_change: str = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class LeverageInfo(BaseModel):
|
|
30
|
+
type: str = None
|
|
31
|
+
value: int = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class PositionInfo(BaseModel):
|
|
35
|
+
coin: str = None
|
|
36
|
+
szi: Decimal = None
|
|
37
|
+
leverage: LeverageInfo = None
|
|
38
|
+
entry_px: Decimal = None
|
|
39
|
+
position_value: Decimal = None
|
|
40
|
+
unrealized_pnl: Decimal = None
|
|
41
|
+
return_on_equity: Decimal = None
|
|
42
|
+
liquidation_px: Decimal = None
|
|
43
|
+
margin_used: Decimal = None
|
|
44
|
+
max_leverage: int = None
|
|
45
|
+
cum_funding: CumFundingInfo = None
|
|
46
|
+
|
|
47
|
+
def get_side(self) -> str:
|
|
48
|
+
if self.szi > 0:
|
|
49
|
+
return "LONG"
|
|
50
|
+
elif self.szi < 0:
|
|
51
|
+
return "SHORT"
|
|
52
|
+
return "UNKNOWN_SIDE"
|
|
53
|
+
|
|
54
|
+
def get_position_id(self) -> str:
|
|
55
|
+
"""
|
|
56
|
+
As far as I know, the API endpoint does not return the position id,
|
|
57
|
+
maybe it only returns it to the account owner?
|
|
58
|
+
In any case, we will have to somehow fake it in order to be able to compare
|
|
59
|
+
it with other positions...
|
|
60
|
+
"""
|
|
61
|
+
return (
|
|
62
|
+
(f"{self.coin}-{self.leverage.value}-{1 if self.szi > 0 else 0}")
|
|
63
|
+
.encode("utf-8")
|
|
64
|
+
.hex()
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def get_leverage(self) -> str:
|
|
68
|
+
return f"{self.leverage.value}x ({self.leverage.type})"
|
|
69
|
+
|
|
70
|
+
def __repr__(self):
|
|
71
|
+
return (
|
|
72
|
+
f"{self.get_side()} {self.get_leverage()} {self.coin} "
|
|
73
|
+
f"Margin: {self.margin_used}, PNL: {self.unrealized_pnl}"
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
def __str__(self):
|
|
77
|
+
return self.__repr__()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AssetPosition(BaseModel):
|
|
81
|
+
type: str = None
|
|
82
|
+
position: PositionInfo = None
|
|
83
|
+
|
|
84
|
+
def __repr__(self):
|
|
85
|
+
return f"{self.position}; {self.type}"
|
|
86
|
+
|
|
87
|
+
def __str__(self):
|
|
88
|
+
return self.__str__()
|
|
89
|
+
|
|
90
|
+
def get_position_id(self) -> str:
|
|
91
|
+
return self.position.get_position_id()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class MarginSummaryInfo(BaseModel):
|
|
95
|
+
account_value: Decimal = None
|
|
96
|
+
total_ntl_pos: Decimal = None
|
|
97
|
+
total_raw_usd: Decimal = None
|
|
98
|
+
total_margin_used: Decimal = None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class TraderPositionsInfoResponse(BaseModel):
|
|
102
|
+
margin_summary: MarginSummaryInfo = None
|
|
103
|
+
cross_margin_summary: MarginSummaryInfo = None
|
|
104
|
+
cross_maintenance_margin_used: Decimal = None
|
|
105
|
+
withdrawable: Decimal = None
|
|
106
|
+
asset_positions: list[AssetPosition] = None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class MetaAssetCtxSingleInfo(BaseModel):
|
|
110
|
+
symbol: str = None
|
|
111
|
+
mark_px: Decimal = None
|
|
112
|
+
prev_day_px: Decimal = None
|
|
113
|
+
change_abs: Decimal = None
|
|
114
|
+
change_pct: Decimal = None
|
|
115
|
+
funding: Decimal = None
|
|
116
|
+
day_ntl_vlm: Decimal = None
|
|
117
|
+
open_interest: Decimal = None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class MetaAssetCtxResponse(BaseModel):
|
|
121
|
+
assets: list[MetaAssetCtxSingleInfo] = None
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def parse_from_api_resp(
|
|
125
|
+
data: dict,
|
|
126
|
+
allow_delisted: bool = False,
|
|
127
|
+
) -> "MetaAssetCtxResponse":
|
|
128
|
+
resp = MetaAssetCtxResponse()
|
|
129
|
+
resp.assets = new_list()
|
|
130
|
+
meta, asset_ctxs = data
|
|
131
|
+
if not isinstance(meta, dict) or not isinstance(asset_ctxs, list):
|
|
132
|
+
raise ValueError("Unexpected response structure from metaAndAssetCtxs")
|
|
133
|
+
|
|
134
|
+
universe = meta.get("universe", [])
|
|
135
|
+
|
|
136
|
+
for meta_entry, ctx in zip(universe, asset_ctxs, strict=False):
|
|
137
|
+
if not isinstance(meta_entry, dict) or not isinstance(ctx, dict):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
name = meta_entry["name"] # e.g. "BTC", "ETH", "SOL", ...
|
|
141
|
+
if not allow_delisted and meta_entry.get("isDelisted", False):
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
# Convert interesting fields to floats
|
|
145
|
+
mark_px = float(ctx["markPx"])
|
|
146
|
+
prev_day_px_str = ctx.get("prevDayPx") # may be missing for some markets
|
|
147
|
+
prev_day_px = (
|
|
148
|
+
float(prev_day_px_str)
|
|
149
|
+
if prev_day_px_str not in (None, "", "0")
|
|
150
|
+
else None
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
funding = float(ctx["funding"])
|
|
154
|
+
day_ntl_vlm = float(ctx["dayNtlVlm"])
|
|
155
|
+
open_interest = float(ctx["openInterest"])
|
|
156
|
+
|
|
157
|
+
if prev_day_px and prev_day_px != 0.0:
|
|
158
|
+
change_abs = mark_px - prev_day_px
|
|
159
|
+
change_pct = (change_abs / prev_day_px) * 100.0
|
|
160
|
+
else:
|
|
161
|
+
change_abs = None
|
|
162
|
+
change_pct = None
|
|
163
|
+
|
|
164
|
+
resp.assets.append(
|
|
165
|
+
MetaAssetCtxSingleInfo(
|
|
166
|
+
**{
|
|
167
|
+
"symbol": name,
|
|
168
|
+
"mark_px": mark_px,
|
|
169
|
+
"prev_day_px": prev_day_px,
|
|
170
|
+
"change_abs": change_abs,
|
|
171
|
+
"change_pct": change_pct,
|
|
172
|
+
"funding": funding,
|
|
173
|
+
"day_ntl_vlm": day_ntl_vlm,
|
|
174
|
+
"open_interest": open_interest,
|
|
175
|
+
},
|
|
176
|
+
)
|
|
177
|
+
)
|
|
178
|
+
return resp
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
# endregion
|
|
182
|
+
|
|
183
|
+
###########################################################
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import time
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from trd_utils.cipher import AESCipher
|
|
10
|
+
from trd_utils.exchanges.base_types import (
|
|
11
|
+
UnifiedPositionInfo,
|
|
12
|
+
UnifiedTraderInfo,
|
|
13
|
+
UnifiedTraderPositions,
|
|
14
|
+
)
|
|
15
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
16
|
+
from trd_utils.exchanges.okx.okx_types import (
|
|
17
|
+
AppContextUserInfo,
|
|
18
|
+
CurrentUserPositionsResponse,
|
|
19
|
+
UserInfoHtmlParser,
|
|
20
|
+
UserInfoInitialProps,
|
|
21
|
+
)
|
|
22
|
+
from trd_utils.types_helper import new_list
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
BASE_PROFILE_URL = "https://www.okx.com/copy-trading/account/"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OkxClient(ExchangeBase):
|
|
30
|
+
###########################################################
|
|
31
|
+
# region client parameters
|
|
32
|
+
okx_api_base_host: str = "https://www.okx.com"
|
|
33
|
+
okx_api_base_url: str = "https://www.okx.com"
|
|
34
|
+
okx_api_v5_url: str = "https://www.okx.com/priapi/v5"
|
|
35
|
+
origin_header: str = "https://www.okx.com"
|
|
36
|
+
|
|
37
|
+
# endregion
|
|
38
|
+
###########################################################
|
|
39
|
+
# region client constructor
|
|
40
|
+
def __init__(
|
|
41
|
+
self,
|
|
42
|
+
account_name: str = "default",
|
|
43
|
+
http_verify: bool = True,
|
|
44
|
+
fav_letter: str = "^",
|
|
45
|
+
read_session_file: bool = False,
|
|
46
|
+
sessions_dir: str = "sessions",
|
|
47
|
+
use_http1: bool = True,
|
|
48
|
+
use_http2: bool = False,
|
|
49
|
+
):
|
|
50
|
+
# it looks like hyperliquid's api endpoints don't support http2 :(
|
|
51
|
+
self.httpx_client = httpx.AsyncClient(
|
|
52
|
+
verify=http_verify,
|
|
53
|
+
http1=use_http1,
|
|
54
|
+
http2=use_http2,
|
|
55
|
+
)
|
|
56
|
+
self.account_name = account_name
|
|
57
|
+
self._fav_letter = fav_letter
|
|
58
|
+
self.sessions_dir = sessions_dir
|
|
59
|
+
self.exchange_name = "okx"
|
|
60
|
+
|
|
61
|
+
super().__init__()
|
|
62
|
+
|
|
63
|
+
if read_session_file:
|
|
64
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.okx")
|
|
65
|
+
|
|
66
|
+
# endregion
|
|
67
|
+
###########################################################
|
|
68
|
+
# region positions endpoints
|
|
69
|
+
async def get_trader_positions(
|
|
70
|
+
self,
|
|
71
|
+
uid: int | str,
|
|
72
|
+
) -> CurrentUserPositionsResponse:
|
|
73
|
+
params = {
|
|
74
|
+
"uniqueName": f"{uid}",
|
|
75
|
+
"t": f"{int(time.time() * 1000)}",
|
|
76
|
+
}
|
|
77
|
+
headers = self.get_headers()
|
|
78
|
+
return await self.invoke_get(
|
|
79
|
+
f"{self.okx_api_v5_url}/ecotrade/public/community/user/position-current",
|
|
80
|
+
headers=headers,
|
|
81
|
+
params=params,
|
|
82
|
+
model_type=CurrentUserPositionsResponse,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# endregion
|
|
86
|
+
###########################################################
|
|
87
|
+
# region another-thing
|
|
88
|
+
|
|
89
|
+
async def get_copy_trader_info(
|
|
90
|
+
self,
|
|
91
|
+
uid: int | str,
|
|
92
|
+
) -> UserInfoInitialProps:
|
|
93
|
+
params = {
|
|
94
|
+
"tab": "trade",
|
|
95
|
+
}
|
|
96
|
+
headers = self.get_headers()
|
|
97
|
+
result: bytes = await self.invoke_get(
|
|
98
|
+
f"{self.okx_api_base_host}/copy-trading/account/{uid}",
|
|
99
|
+
headers=headers,
|
|
100
|
+
params=params,
|
|
101
|
+
model_type=AppContextUserInfo,
|
|
102
|
+
raw_data=True,
|
|
103
|
+
)
|
|
104
|
+
parser = UserInfoHtmlParser("__app_data_for_ssr__")
|
|
105
|
+
parser.feed(result.decode("utf-8"))
|
|
106
|
+
if not parser.found_value:
|
|
107
|
+
raise ValueError("Okx API returned invalid response")
|
|
108
|
+
|
|
109
|
+
return AppContextUserInfo(
|
|
110
|
+
**(json.loads(parser.found_value)["appContext"]),
|
|
111
|
+
).initial_props
|
|
112
|
+
|
|
113
|
+
# endregion
|
|
114
|
+
###########################################################
|
|
115
|
+
# region client helper methods
|
|
116
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
117
|
+
the_headers = {
|
|
118
|
+
# "Host": self.hyperliquid_api_base_host,
|
|
119
|
+
"Content-Type": "application/json",
|
|
120
|
+
"Accept": "application/json",
|
|
121
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
122
|
+
"User-Agent": self.user_agent,
|
|
123
|
+
"Connection": "close",
|
|
124
|
+
"appsiteid": "0",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if self.x_requested_with:
|
|
128
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
129
|
+
|
|
130
|
+
if needs_auth:
|
|
131
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
132
|
+
return the_headers
|
|
133
|
+
|
|
134
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
135
|
+
"""
|
|
136
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
137
|
+
"""
|
|
138
|
+
# check if path exists
|
|
139
|
+
target_path = Path(file_path)
|
|
140
|
+
if not target_path.exists():
|
|
141
|
+
return self._save_session_file(file_path=file_path)
|
|
142
|
+
|
|
143
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
144
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
145
|
+
json_data: dict = json.loads(content)
|
|
146
|
+
|
|
147
|
+
self.authorization_token = json_data.get(
|
|
148
|
+
"authorization_token",
|
|
149
|
+
self.authorization_token,
|
|
150
|
+
)
|
|
151
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
152
|
+
|
|
153
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
154
|
+
"""
|
|
155
|
+
Saves current information to the session file.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
json_data = {
|
|
159
|
+
"authorization_token": self.authorization_token,
|
|
160
|
+
"user_agent": self.user_agent,
|
|
161
|
+
}
|
|
162
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
163
|
+
target_path = Path(file_path)
|
|
164
|
+
if not target_path.exists():
|
|
165
|
+
target_path.mkdir(parents=True)
|
|
166
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
167
|
+
|
|
168
|
+
# endregion
|
|
169
|
+
###########################################################
|
|
170
|
+
# region unified methods
|
|
171
|
+
async def get_unified_trader_positions(
|
|
172
|
+
self,
|
|
173
|
+
uid: int | str,
|
|
174
|
+
min_margin: Decimal = 0,
|
|
175
|
+
) -> UnifiedTraderPositions:
|
|
176
|
+
result = await self.get_trader_positions(
|
|
177
|
+
uid=uid,
|
|
178
|
+
)
|
|
179
|
+
unified_result = UnifiedTraderPositions()
|
|
180
|
+
unified_result.positions = new_list()
|
|
181
|
+
for position in result.data[0].pos_data:
|
|
182
|
+
if min_margin and (not position.margin or position.margin < min_margin):
|
|
183
|
+
continue
|
|
184
|
+
unified_pos = UnifiedPositionInfo()
|
|
185
|
+
unified_pos.position_id = position.pos_id
|
|
186
|
+
unified_pos.position_pnl = round(position.realized_pnl, 3)
|
|
187
|
+
unified_pos.position_side = position.get_side()
|
|
188
|
+
unified_pos.margin_mode = position.mgn_mode
|
|
189
|
+
unified_pos.position_leverage = position.lever
|
|
190
|
+
unified_pos.position_pair = position.get_pair()
|
|
191
|
+
unified_pos.open_time = position.c_time
|
|
192
|
+
unified_pos.open_price = position.avg_px
|
|
193
|
+
unified_pos.open_price_unit = position.quote_ccy
|
|
194
|
+
unified_pos.initial_margin = position.margin
|
|
195
|
+
unified_result.positions.append(unified_pos)
|
|
196
|
+
|
|
197
|
+
return unified_result
|
|
198
|
+
|
|
199
|
+
async def get_unified_trader_info(
|
|
200
|
+
self,
|
|
201
|
+
uid: int | str,
|
|
202
|
+
) -> UnifiedTraderInfo:
|
|
203
|
+
result = await self.get_copy_trader_info(
|
|
204
|
+
uid=uid,
|
|
205
|
+
)
|
|
206
|
+
account_info = result.pre_process.leader_account_info
|
|
207
|
+
overview = result.overview_data
|
|
208
|
+
|
|
209
|
+
unified_info = UnifiedTraderInfo()
|
|
210
|
+
unified_info.trader_id = account_info.unique_name or uid
|
|
211
|
+
unified_info.trader_name = account_info.en_nick_name or account_info.nick_name
|
|
212
|
+
unified_info.trader_url = f"{BASE_PROFILE_URL}{uid}"
|
|
213
|
+
if overview:
|
|
214
|
+
unified_info.win_rate = overview.win_rate
|
|
215
|
+
|
|
216
|
+
return unified_info
|
|
217
|
+
|
|
218
|
+
# endregion
|
|
219
|
+
###########################################################
|
|
@@ -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: Decimal = 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
|
+
###########################################################
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
import pytz
|
|
7
|
+
from trd_utils.types_helper import BaseModel
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MinimalCandleInfo(BaseModel):
|
|
11
|
+
# The pair in format of BTC/USDT.
|
|
12
|
+
pair: str = None
|
|
13
|
+
|
|
14
|
+
# This candle's open price.
|
|
15
|
+
open_price: Decimal = None
|
|
16
|
+
|
|
17
|
+
# The close price.
|
|
18
|
+
close_price: Decimal = None
|
|
19
|
+
|
|
20
|
+
# volume in the first pair (e.g. BTC).
|
|
21
|
+
volume: Decimal = None
|
|
22
|
+
|
|
23
|
+
# volume in the second part of the pair (e.g. USDT).
|
|
24
|
+
quote_volume: Decimal = None
|
|
25
|
+
|
|
26
|
+
# The time this candle info was retrieved.
|
|
27
|
+
fetched_at: datetime = None
|
|
28
|
+
|
|
29
|
+
def __init__(self, **kwargs):
|
|
30
|
+
super().__init__(**kwargs)
|
|
31
|
+
self.fetched_at = datetime.now(tz=pytz.UTC)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class IPriceFetcher:
|
|
35
|
+
"""
|
|
36
|
+
The IPriceFetcher class acts as an interface for classes that support
|
|
37
|
+
fetching last candle of a specific pair, without any specific floodwait or
|
|
38
|
+
ratelimit applied on the method itself (because e.g. they are fetching it
|
|
39
|
+
through a background websocket connection).
|
|
40
|
+
Please do not use this class directly, instead use a class that inherits
|
|
41
|
+
and implements the methods of this class (e.g. one of the exchange classes).
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
async def do_price_subscribe(self) -> None:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
async def get_last_candle(self, pair: str) -> MinimalCandleInfo:
|
|
48
|
+
pass
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
|
|
2
|
+
from .html_formats import (
|
|
3
|
+
get_html_normal,
|
|
4
|
+
html_normal,
|
|
5
|
+
html_mono,
|
|
6
|
+
html_in_parenthesis,
|
|
7
|
+
html_bold,
|
|
8
|
+
html_italic,
|
|
9
|
+
html_link,
|
|
10
|
+
html_code_snippets,
|
|
11
|
+
html_pre,
|
|
12
|
+
html_spoiler
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"get_html_normal",
|
|
17
|
+
"html_normal",
|
|
18
|
+
"html_mono",
|
|
19
|
+
"html_in_parenthesis",
|
|
20
|
+
"html_bold",
|
|
21
|
+
"html_italic",
|
|
22
|
+
"html_link",
|
|
23
|
+
"html_code_snippets",
|
|
24
|
+
"html_pre",
|
|
25
|
+
"html_spoiler"
|
|
26
|
+
]
|