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,375 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from trd_utils.date_utils.datetime_helpers import dt_from_ts
|
|
11
|
+
from trd_utils.exchanges.base_types import (
|
|
12
|
+
UnifiedPositionInfo,
|
|
13
|
+
UnifiedTraderInfo,
|
|
14
|
+
UnifiedTraderPositions,
|
|
15
|
+
)
|
|
16
|
+
from trd_utils.exchanges.blofin.blofin_types import (
|
|
17
|
+
CmsColorResponse,
|
|
18
|
+
CopyTraderAllOrderHistory,
|
|
19
|
+
CopyTraderAllOrderList,
|
|
20
|
+
CopyTraderInfoResponse,
|
|
21
|
+
CopyTraderOrderHistoryResponse,
|
|
22
|
+
CopyTraderOrderListResponse,
|
|
23
|
+
ShareConfigResponse,
|
|
24
|
+
)
|
|
25
|
+
from trd_utils.cipher import AESCipher
|
|
26
|
+
from trd_utils.exchanges.errors import ExchangeError
|
|
27
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
28
|
+
from trd_utils.types_helper import new_list
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
BASE_PROFILE_URL = "https://blofin.com/copy-trade/details/"
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class BlofinClient(ExchangeBase):
|
|
37
|
+
###########################################################
|
|
38
|
+
# region client parameters
|
|
39
|
+
blofin_api_base_host: str = "https://\u0062lofin.co\u006d"
|
|
40
|
+
blofin_api_base_url: str = "https://\u0062lofin.co\u006d/uapi/v1"
|
|
41
|
+
origin_header: str = "https://\u0062lofin.co\u006d"
|
|
42
|
+
|
|
43
|
+
timezone: str = "Etc/UTC"
|
|
44
|
+
|
|
45
|
+
# endregion
|
|
46
|
+
###########################################################
|
|
47
|
+
# region client constructor
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
account_name: str = "default",
|
|
51
|
+
http_verify: bool = True,
|
|
52
|
+
fav_letter: str = "^",
|
|
53
|
+
read_session_file: bool = True,
|
|
54
|
+
sessions_dir: str = "sessions",
|
|
55
|
+
use_http1: bool = False,
|
|
56
|
+
use_http2: bool = True,
|
|
57
|
+
):
|
|
58
|
+
self.httpx_client = httpx.AsyncClient(
|
|
59
|
+
verify=http_verify,
|
|
60
|
+
http1=use_http1,
|
|
61
|
+
http2=use_http2,
|
|
62
|
+
)
|
|
63
|
+
self.account_name = account_name
|
|
64
|
+
self._fav_letter = fav_letter
|
|
65
|
+
self.sessions_dir = sessions_dir
|
|
66
|
+
self.exchange_name = "blofin"
|
|
67
|
+
|
|
68
|
+
super().__init__()
|
|
69
|
+
|
|
70
|
+
if read_session_file:
|
|
71
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bf")
|
|
72
|
+
|
|
73
|
+
# endregion
|
|
74
|
+
###########################################################
|
|
75
|
+
# region v1/cms/
|
|
76
|
+
async def get_share_config(self) -> ShareConfigResponse:
|
|
77
|
+
headers = self.get_headers()
|
|
78
|
+
return await self.invoke_get(
|
|
79
|
+
f"{self.blofin_api_base_url}/cms/share_config",
|
|
80
|
+
headers=headers,
|
|
81
|
+
model_type=ShareConfigResponse,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def get_cms_color(self) -> CmsColorResponse:
|
|
85
|
+
headers = self.get_headers()
|
|
86
|
+
return await self.invoke_get(
|
|
87
|
+
f"{self.blofin_api_base_url}/cms/color",
|
|
88
|
+
headers=headers,
|
|
89
|
+
model_type=CmsColorResponse,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# endregion
|
|
93
|
+
###########################################################
|
|
94
|
+
# region copy/trader
|
|
95
|
+
async def get_copy_trader_info(self, uid: int) -> CopyTraderInfoResponse:
|
|
96
|
+
payload = {
|
|
97
|
+
"uid": uid,
|
|
98
|
+
}
|
|
99
|
+
headers = self.get_headers()
|
|
100
|
+
return await self.invoke_post(
|
|
101
|
+
f"{self.blofin_api_base_url}/copy/trader/info",
|
|
102
|
+
headers=headers,
|
|
103
|
+
content=payload,
|
|
104
|
+
model_type=CopyTraderInfoResponse,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
async def get_copy_trader_order_list(
|
|
108
|
+
self,
|
|
109
|
+
uid: int | str,
|
|
110
|
+
from_param: int = 0,
|
|
111
|
+
limit_param: int = 20,
|
|
112
|
+
) -> CopyTraderOrderListResponse:
|
|
113
|
+
payload = {
|
|
114
|
+
"from": from_param,
|
|
115
|
+
"limit": limit_param,
|
|
116
|
+
"uid": int(uid),
|
|
117
|
+
}
|
|
118
|
+
headers = self.get_headers()
|
|
119
|
+
return await self.invoke_post(
|
|
120
|
+
f"{self.blofin_api_base_url}/copy/trader/order/list",
|
|
121
|
+
headers=headers,
|
|
122
|
+
content=payload,
|
|
123
|
+
model_type=CopyTraderOrderListResponse,
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
async def get_copy_trader_all_order_list(
|
|
127
|
+
self,
|
|
128
|
+
uid: int,
|
|
129
|
+
from_param: int = 0,
|
|
130
|
+
chunk_limit: int = 20,
|
|
131
|
+
sleep_delay: int = 0.5,
|
|
132
|
+
) -> CopyTraderAllOrderList:
|
|
133
|
+
if chunk_limit < 1:
|
|
134
|
+
raise ValueError("chunk_limit parameter has to be more than 1")
|
|
135
|
+
|
|
136
|
+
result = CopyTraderAllOrderList(
|
|
137
|
+
code=200,
|
|
138
|
+
data=[],
|
|
139
|
+
total_count=0,
|
|
140
|
+
)
|
|
141
|
+
current_id_from = from_param
|
|
142
|
+
while True:
|
|
143
|
+
total_ignored = 0
|
|
144
|
+
current_result = await self.get_copy_trader_order_list(
|
|
145
|
+
uid=uid,
|
|
146
|
+
from_param=current_id_from,
|
|
147
|
+
limit_param=chunk_limit,
|
|
148
|
+
)
|
|
149
|
+
if current_result.code != 200:
|
|
150
|
+
if current_result.msg:
|
|
151
|
+
raise ExchangeError(
|
|
152
|
+
f"blofin get_copy_trader_all_order_list: {current_result.msg}; "
|
|
153
|
+
f"code: {current_result.code}"
|
|
154
|
+
)
|
|
155
|
+
raise ExchangeError(
|
|
156
|
+
"blofin get_copy_trader_all_order_list: unknown error; "
|
|
157
|
+
f"code: {current_result.code}"
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
if not isinstance(current_result, CopyTraderOrderListResponse):
|
|
161
|
+
raise ValueError(
|
|
162
|
+
"get_copy_trader_order_list returned invalid value of "
|
|
163
|
+
f"{type(current_result)}",
|
|
164
|
+
)
|
|
165
|
+
if not current_result.data:
|
|
166
|
+
# we no longer have anything else here
|
|
167
|
+
return result
|
|
168
|
+
|
|
169
|
+
if current_result.data[0].id == current_id_from:
|
|
170
|
+
if len(current_result.data) < 2:
|
|
171
|
+
return result
|
|
172
|
+
current_result.data = current_result.data[1:]
|
|
173
|
+
total_ignored += 1
|
|
174
|
+
elif current_id_from:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"Expected first array to have the same value as from_param: "
|
|
177
|
+
f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
current_id_from = current_result.data[-1].id
|
|
181
|
+
result.data.extend(current_result.data)
|
|
182
|
+
result.total_count += len(current_result.data)
|
|
183
|
+
if len(current_result.data) < chunk_limit - total_ignored:
|
|
184
|
+
# the trader doesn't have any more open orders
|
|
185
|
+
return result
|
|
186
|
+
if result.total_count > len(current_result.data) and sleep_delay:
|
|
187
|
+
# we don't want to sleep after 1 request only
|
|
188
|
+
await asyncio.sleep(sleep_delay)
|
|
189
|
+
|
|
190
|
+
async def get_copy_trader_order_history(
|
|
191
|
+
self,
|
|
192
|
+
uid: int,
|
|
193
|
+
from_param: int = 0,
|
|
194
|
+
limit_param: int = 20,
|
|
195
|
+
) -> CopyTraderOrderHistoryResponse:
|
|
196
|
+
payload = {
|
|
197
|
+
"from": from_param,
|
|
198
|
+
"limit": limit_param,
|
|
199
|
+
"uid": uid,
|
|
200
|
+
}
|
|
201
|
+
headers = self.get_headers()
|
|
202
|
+
return await self.invoke_post(
|
|
203
|
+
f"{self.blofin_api_base_url}/copy/trader/order/history",
|
|
204
|
+
headers=headers,
|
|
205
|
+
content=payload,
|
|
206
|
+
model_type=CopyTraderOrderHistoryResponse,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
async def get_copy_trader_all_order_history(
|
|
210
|
+
self,
|
|
211
|
+
uid: int,
|
|
212
|
+
from_param: int = 0,
|
|
213
|
+
chunk_limit: int = 20,
|
|
214
|
+
sleep_delay: int = 0.5,
|
|
215
|
+
) -> CopyTraderAllOrderHistory:
|
|
216
|
+
if chunk_limit < 1:
|
|
217
|
+
raise ValueError("chunk_limit parameter has to be more than 1")
|
|
218
|
+
|
|
219
|
+
result = CopyTraderAllOrderHistory(
|
|
220
|
+
code=200,
|
|
221
|
+
data=[],
|
|
222
|
+
total_count=0,
|
|
223
|
+
)
|
|
224
|
+
current_id_from = from_param
|
|
225
|
+
while True:
|
|
226
|
+
total_ignored = 0
|
|
227
|
+
current_result = await self.get_copy_trader_order_history(
|
|
228
|
+
uid=uid,
|
|
229
|
+
from_param=current_id_from,
|
|
230
|
+
limit_param=chunk_limit,
|
|
231
|
+
)
|
|
232
|
+
if (
|
|
233
|
+
not current_result
|
|
234
|
+
or not isinstance(current_result, CopyTraderOrderHistoryResponse)
|
|
235
|
+
or not current_result.data
|
|
236
|
+
):
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
if current_result.data[0].id == current_id_from:
|
|
240
|
+
if len(current_result.data) < 2:
|
|
241
|
+
return result
|
|
242
|
+
current_result.data = current_result.data[1:]
|
|
243
|
+
total_ignored += 1
|
|
244
|
+
elif current_id_from:
|
|
245
|
+
raise ValueError(
|
|
246
|
+
"Expected first array to have the same value as from_param: "
|
|
247
|
+
f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
current_id_from = current_result.data[-1].id
|
|
251
|
+
result.data.extend(current_result.data)
|
|
252
|
+
result.total_count += len(current_result.data)
|
|
253
|
+
if len(current_result.data) < chunk_limit - total_ignored:
|
|
254
|
+
# the trader doesn't have any more orders history
|
|
255
|
+
return result
|
|
256
|
+
if result.total_count > len(current_result.data) and sleep_delay:
|
|
257
|
+
# we don't want to sleep after 1 request only
|
|
258
|
+
await asyncio.sleep(sleep_delay)
|
|
259
|
+
|
|
260
|
+
# endregion
|
|
261
|
+
###########################################################
|
|
262
|
+
# region client helper methods
|
|
263
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
264
|
+
the_timestamp = int(time.time() * 1000)
|
|
265
|
+
the_headers = {
|
|
266
|
+
# "Host": self.blofin_api_base_host,
|
|
267
|
+
"Content-Type": "application/json",
|
|
268
|
+
"Accept": "application/json",
|
|
269
|
+
"Origin": self.origin_header,
|
|
270
|
+
"X-Tz": self.timezone,
|
|
271
|
+
"Fp-Request-Id": f"{the_timestamp}.n1fDrN",
|
|
272
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
273
|
+
"User-Agent": self.user_agent,
|
|
274
|
+
"Connection": "close",
|
|
275
|
+
"appsiteid": "0",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if self.x_requested_with:
|
|
279
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
280
|
+
|
|
281
|
+
if needs_auth:
|
|
282
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
283
|
+
return the_headers
|
|
284
|
+
|
|
285
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
286
|
+
"""
|
|
287
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
288
|
+
"""
|
|
289
|
+
# check if path exists
|
|
290
|
+
target_path = Path(file_path)
|
|
291
|
+
if not target_path.exists():
|
|
292
|
+
return self._save_session_file(file_path=file_path)
|
|
293
|
+
|
|
294
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
295
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
296
|
+
json_data: dict = json.loads(content)
|
|
297
|
+
|
|
298
|
+
self.authorization_token = json_data.get(
|
|
299
|
+
"authorization_token",
|
|
300
|
+
self.authorization_token,
|
|
301
|
+
)
|
|
302
|
+
self.timezone = json_data.get("timezone", self.timezone)
|
|
303
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
304
|
+
|
|
305
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
306
|
+
"""
|
|
307
|
+
Saves current information to the session file.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
json_data = {
|
|
311
|
+
"authorization_token": self.authorization_token,
|
|
312
|
+
"timezone": self.timezone,
|
|
313
|
+
"user_agent": self.user_agent,
|
|
314
|
+
}
|
|
315
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
316
|
+
target_path = Path(file_path)
|
|
317
|
+
if not target_path.exists():
|
|
318
|
+
target_path.mkdir(parents=True)
|
|
319
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
320
|
+
|
|
321
|
+
# endregion
|
|
322
|
+
###########################################################
|
|
323
|
+
# region unified methods
|
|
324
|
+
async def get_unified_trader_positions(
|
|
325
|
+
self,
|
|
326
|
+
uid: int | str,
|
|
327
|
+
min_margin: Decimal = 0,
|
|
328
|
+
) -> UnifiedTraderPositions:
|
|
329
|
+
result = await self.get_copy_trader_all_order_list(
|
|
330
|
+
uid=uid,
|
|
331
|
+
)
|
|
332
|
+
unified_result = UnifiedTraderPositions()
|
|
333
|
+
unified_result.positions = new_list()
|
|
334
|
+
for position in result.data:
|
|
335
|
+
unified_pos = UnifiedPositionInfo()
|
|
336
|
+
unified_pos.position_id = position.id or position.order_id
|
|
337
|
+
unified_pos.position_pnl = position.real_pnl or position.pnl
|
|
338
|
+
unified_pos.position_side = (
|
|
339
|
+
"LONG" if position.order_side in ("LONG", "BUY") else "SHORT"
|
|
340
|
+
)
|
|
341
|
+
unified_pos.margin_mode = position.margin_mode
|
|
342
|
+
unified_pos.position_leverage = Decimal(position.leverage)
|
|
343
|
+
unified_pos.position_pair = position.symbol.replace("-", "/")
|
|
344
|
+
unified_pos.open_time = dt_from_ts(position.open_time)
|
|
345
|
+
unified_pos.open_price = position.avg_open_price
|
|
346
|
+
unified_pos.open_price_unit = position.symbol.split("-")[-1]
|
|
347
|
+
unified_pos.initial_margin = position.get_initial_margin()
|
|
348
|
+
if min_margin and (
|
|
349
|
+
not unified_pos.initial_margin
|
|
350
|
+
or unified_pos.initial_margin < min_margin
|
|
351
|
+
):
|
|
352
|
+
continue
|
|
353
|
+
|
|
354
|
+
unified_result.positions.append(unified_pos)
|
|
355
|
+
|
|
356
|
+
return unified_result
|
|
357
|
+
|
|
358
|
+
async def get_unified_trader_info(
|
|
359
|
+
self,
|
|
360
|
+
uid: int | str,
|
|
361
|
+
) -> UnifiedTraderInfo:
|
|
362
|
+
info_resp = await self.get_copy_trader_info(
|
|
363
|
+
uid=uid,
|
|
364
|
+
)
|
|
365
|
+
info = info_resp.data
|
|
366
|
+
unified_info = UnifiedTraderInfo()
|
|
367
|
+
unified_info.trader_id = info.uid
|
|
368
|
+
unified_info.trader_name = info.nick_name
|
|
369
|
+
unified_info.trader_url = f"{BASE_PROFILE_URL}{info.uid}"
|
|
370
|
+
unified_info.win_rate = info.win_rate
|
|
371
|
+
|
|
372
|
+
return unified_info
|
|
373
|
+
|
|
374
|
+
# endregion
|
|
375
|
+
###########################################################
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
from typing import Any
|
|
3
|
+
from trd_utils.types_helper import BaseModel
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
###########################################################
|
|
7
|
+
|
|
8
|
+
# region common types
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BlofinApiResponse(BaseModel):
|
|
12
|
+
code: int = None
|
|
13
|
+
timestamp: int = None
|
|
14
|
+
msg: str = None
|
|
15
|
+
|
|
16
|
+
def __str__(self):
|
|
17
|
+
return f"code: {self.code}; timestamp: {self.timestamp}"
|
|
18
|
+
|
|
19
|
+
def __repr__(self):
|
|
20
|
+
return f"code: {self.code}; timestamp: {self.timestamp}"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# endregion
|
|
24
|
+
|
|
25
|
+
###########################################################
|
|
26
|
+
|
|
27
|
+
# region api-config types
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class PnlShareListInfo(BaseModel):
|
|
31
|
+
background_color: str = None
|
|
32
|
+
background_img_up: str = None
|
|
33
|
+
background_img_down: str = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ShareConfigResult(BaseModel):
|
|
37
|
+
pnl_share_list: list[PnlShareListInfo] = None
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ShareConfigResponse(BlofinApiResponse):
|
|
41
|
+
data: ShareConfigResult = None
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CmsColorResult(BaseModel):
|
|
45
|
+
color: str = None
|
|
46
|
+
city: str = None
|
|
47
|
+
country: str = None
|
|
48
|
+
ip: str = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class CmsColorResponse(BlofinApiResponse):
|
|
52
|
+
data: CmsColorResult = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# endregion
|
|
56
|
+
|
|
57
|
+
###########################################################
|
|
58
|
+
|
|
59
|
+
# region copy-trader types
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class CopyTraderInfoResult(BaseModel):
|
|
63
|
+
aum: str = None
|
|
64
|
+
can_copy: bool = None
|
|
65
|
+
copier_whitelist: bool = None
|
|
66
|
+
follow_state: int = None
|
|
67
|
+
followers: int = None
|
|
68
|
+
followers_max: int = None
|
|
69
|
+
forbidden_follow_type: int = None
|
|
70
|
+
hidden_all: bool = None
|
|
71
|
+
hidden_order: bool = None
|
|
72
|
+
joined_date: int = None
|
|
73
|
+
max_draw_down: Decimal = None
|
|
74
|
+
nick_name: str = None
|
|
75
|
+
order_amount_limit: Any = None
|
|
76
|
+
profile: str = None
|
|
77
|
+
profit_sharing_ratio: Decimal = None
|
|
78
|
+
real_pnl: Decimal = None
|
|
79
|
+
roi_d7: Decimal = None
|
|
80
|
+
self_introduction: str = None
|
|
81
|
+
sharing_period: str = None
|
|
82
|
+
source: int = None
|
|
83
|
+
uid: int = None
|
|
84
|
+
whitelist_copier: bool = None
|
|
85
|
+
win_rate: Decimal = None
|
|
86
|
+
|
|
87
|
+
def get_profile_url(self) -> str:
|
|
88
|
+
return f"https://blofin.com/copy-trade/details/{self.uid}"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class CopyTraderInfoResponse(BlofinApiResponse):
|
|
92
|
+
data: CopyTraderInfoResult = None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class CopyTraderSingleOrderInfo(BaseModel):
|
|
96
|
+
id: int = None
|
|
97
|
+
symbol: str = None
|
|
98
|
+
leverage: int = None
|
|
99
|
+
order_side: str = None
|
|
100
|
+
avg_open_price: Decimal = None
|
|
101
|
+
quantity: Decimal = None
|
|
102
|
+
quantity_cont: Any = None
|
|
103
|
+
open_time: int = None
|
|
104
|
+
close_time: Any = None
|
|
105
|
+
avg_close_price: Decimal = None
|
|
106
|
+
real_pnl: Any = None
|
|
107
|
+
close_type: Any = None
|
|
108
|
+
roe: Decimal = None
|
|
109
|
+
followers_profit: Decimal = None
|
|
110
|
+
followers: Any = None
|
|
111
|
+
order_id: Any = None
|
|
112
|
+
sharing: Any = None
|
|
113
|
+
order_state: Any = None
|
|
114
|
+
trader_name: Any = None
|
|
115
|
+
mark_price: Any = None
|
|
116
|
+
tp_trigger_price: Any = None
|
|
117
|
+
tp_order_type: Any = None
|
|
118
|
+
sl_trigger_price: Any = None
|
|
119
|
+
sl_order_type: Any = None
|
|
120
|
+
margin_mode: str = None
|
|
121
|
+
time_in_force: Any = None
|
|
122
|
+
position_side: str = None
|
|
123
|
+
order_category: Any = None
|
|
124
|
+
price: Any = None
|
|
125
|
+
fill_quantity: Any = None
|
|
126
|
+
fill_quantity_cont: Any = None
|
|
127
|
+
pnl: Decimal = None
|
|
128
|
+
cancel_source: Any = None
|
|
129
|
+
order_type: Any = None
|
|
130
|
+
order_open_state: Any = None
|
|
131
|
+
amount: Any = None
|
|
132
|
+
filled_amount: Any = None
|
|
133
|
+
create_time: Any = None
|
|
134
|
+
update_time: Any = None
|
|
135
|
+
open_fee: Any = None
|
|
136
|
+
close_fee: Any = None
|
|
137
|
+
id_md5: Any = None
|
|
138
|
+
tp_sl: Any = None
|
|
139
|
+
trader_uid: Any = None
|
|
140
|
+
available_quantity: Any = None
|
|
141
|
+
available_quantity_cont: Any = None
|
|
142
|
+
show_in_kline: Any = None
|
|
143
|
+
unrealized_pnl: Any = None
|
|
144
|
+
unrealized_pnl_ratio: Any = None
|
|
145
|
+
broker_id: Any = None
|
|
146
|
+
position_change_history: Any = None
|
|
147
|
+
user_id: Any = None
|
|
148
|
+
|
|
149
|
+
def get_initial_margin(self) -> Decimal:
|
|
150
|
+
if not self.avg_open_price or not self.quantity or not self.leverage:
|
|
151
|
+
return None
|
|
152
|
+
return (self.avg_open_price * self.quantity) / self.leverage
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class CopyTraderOrderListResponse(BlofinApiResponse):
|
|
156
|
+
data: list[CopyTraderSingleOrderInfo] = None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class CopyTraderAllOrderList(CopyTraderOrderListResponse):
|
|
160
|
+
total_count: int = None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class CopyTraderOrderHistoryResponse(BlofinApiResponse):
|
|
164
|
+
data: list[CopyTraderSingleOrderInfo] = None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class CopyTraderAllOrderHistory(CopyTraderOrderHistoryResponse):
|
|
168
|
+
total_count: int = None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# endregion
|
|
172
|
+
|
|
173
|
+
###########################################################
|