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
trd_utils/__init__.py
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def dt_from_ts(timestamp: float) -> datetime:
|
|
9
|
+
"""
|
|
10
|
+
Return a datetime from a timestamp.
|
|
11
|
+
:param timestamp: timestamp in seconds or milliseconds
|
|
12
|
+
"""
|
|
13
|
+
if timestamp > 1e10:
|
|
14
|
+
# Timezone in ms - convert to seconds
|
|
15
|
+
timestamp /= 1000
|
|
16
|
+
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# trd_utils.exchanges
|
|
2
|
+
|
|
3
|
+
All exchange clients are stored inside of their own directory.
|
|
4
|
+
Some of these are not really _exchange_, but a tracker for a specific exchange/blockchain. We will still put them in this section as long as they are for a _specific_ one.
|
|
5
|
+
|
|
6
|
+
If they are for a very general platform, such as tradingview, they should be put in a separate directory entirely.
|
|
7
|
+
|
|
8
|
+
## Writing code for a new exchange
|
|
9
|
+
|
|
10
|
+
Here is a boilerplate code that we can use for creating a new exchange/tracker class:
|
|
11
|
+
|
|
12
|
+
```py
|
|
13
|
+
import asyncio
|
|
14
|
+
from decimal import Decimal
|
|
15
|
+
import json
|
|
16
|
+
import logging
|
|
17
|
+
from typing import Type
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# from trd_utils.exchanges.my_exchange.my_exchange_types import (
|
|
24
|
+
# SomeAPIType
|
|
25
|
+
# )
|
|
26
|
+
from trd_utils.cipher import AESCipher
|
|
27
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
28
|
+
|
|
29
|
+
logger = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class MyExchangeClient(ExchangeBase):
|
|
33
|
+
###########################################################
|
|
34
|
+
# region client parameters
|
|
35
|
+
my_exchange_api_base_host: str = "https://exchange.com"
|
|
36
|
+
my_exchange_api_base_url: str = "https://exchange.com/api/v1"
|
|
37
|
+
origin_header: str = "https://exchange.com/"
|
|
38
|
+
|
|
39
|
+
timezone: str = "Etc/UTC"
|
|
40
|
+
|
|
41
|
+
# endregion
|
|
42
|
+
###########################################################
|
|
43
|
+
# region client constructor
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
account_name: str = "default",
|
|
47
|
+
http_verify: bool = True,
|
|
48
|
+
fav_letter: str = "^",
|
|
49
|
+
read_session_file: bool = True,
|
|
50
|
+
sessions_dir: str = "sessions",
|
|
51
|
+
):
|
|
52
|
+
self.httpx_client = httpx.AsyncClient(
|
|
53
|
+
verify=http_verify,
|
|
54
|
+
http2=True,
|
|
55
|
+
http1=False,
|
|
56
|
+
)
|
|
57
|
+
self.account_name = account_name
|
|
58
|
+
self._fav_letter = fav_letter
|
|
59
|
+
self.sessions_dir = sessions_dir
|
|
60
|
+
|
|
61
|
+
if read_session_file:
|
|
62
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bf")
|
|
63
|
+
|
|
64
|
+
# endregion
|
|
65
|
+
###########################################################
|
|
66
|
+
# region something
|
|
67
|
+
# async def get_something_info(self) -> SomethingInfoResponse:
|
|
68
|
+
# headers = self.get_headers()
|
|
69
|
+
# return await self.invoke_get(
|
|
70
|
+
# f"{self.my_exchange_api_base_url}/something/info",
|
|
71
|
+
# headers=headers,
|
|
72
|
+
# model=SomethingInfoResponse,
|
|
73
|
+
# )
|
|
74
|
+
# endregion
|
|
75
|
+
###########################################################
|
|
76
|
+
# region another-thing
|
|
77
|
+
# async def get_another_thing_info(self, uid: int) -> AnotherThingInfoResponse:
|
|
78
|
+
# payload = {
|
|
79
|
+
# "uid": uid,
|
|
80
|
+
# }
|
|
81
|
+
# headers = self.get_headers()
|
|
82
|
+
# return await self.invoke_post(
|
|
83
|
+
# f"{self.my_exchange_api_base_url}/another-thing/info",
|
|
84
|
+
# headers=headers,
|
|
85
|
+
# content=payload,
|
|
86
|
+
# model=CopyTraderInfoResponse,
|
|
87
|
+
# )
|
|
88
|
+
|
|
89
|
+
# endregion
|
|
90
|
+
###########################################################
|
|
91
|
+
# region client helper methods
|
|
92
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
93
|
+
the_timestamp = int(time.time() * 1000)
|
|
94
|
+
the_headers = {
|
|
95
|
+
# "Host": self.my_exchange_api_base_host,
|
|
96
|
+
"Content-Type": "application/json",
|
|
97
|
+
"Accept": "application/json",
|
|
98
|
+
"Origin": self.origin_header,
|
|
99
|
+
"X-Tz": self.timezone,
|
|
100
|
+
"Fp-Request-Id": f"{the_timestamp}.n1fDrN",
|
|
101
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
102
|
+
"User-Agent": self.user_agent,
|
|
103
|
+
"Connection": "close",
|
|
104
|
+
"appsiteid": "0",
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if self.x_requested_with:
|
|
108
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
109
|
+
|
|
110
|
+
if needs_auth:
|
|
111
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
112
|
+
return the_headers
|
|
113
|
+
|
|
114
|
+
async def invoke_get(
|
|
115
|
+
self,
|
|
116
|
+
url: str,
|
|
117
|
+
headers: dict | None = None,
|
|
118
|
+
params: dict | None = None,
|
|
119
|
+
model: Type[MyExchangeApiResponse] | None = None,
|
|
120
|
+
parse_float=Decimal,
|
|
121
|
+
) -> "MyExchangeApiResponse":
|
|
122
|
+
"""
|
|
123
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
124
|
+
"""
|
|
125
|
+
response = await self.httpx_client.get(
|
|
126
|
+
url=url,
|
|
127
|
+
headers=headers,
|
|
128
|
+
params=params,
|
|
129
|
+
)
|
|
130
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
131
|
+
|
|
132
|
+
async def invoke_post(
|
|
133
|
+
self,
|
|
134
|
+
url: str,
|
|
135
|
+
headers: dict | None = None,
|
|
136
|
+
params: dict | None = None,
|
|
137
|
+
content: dict | str | bytes = "",
|
|
138
|
+
model: Type[MyExchangeApiResponse] | None = None,
|
|
139
|
+
parse_float=Decimal,
|
|
140
|
+
) -> "MyExchangeApiResponse":
|
|
141
|
+
"""
|
|
142
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
if isinstance(content, dict):
|
|
146
|
+
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
147
|
+
|
|
148
|
+
response = await self.httpx_client.post(
|
|
149
|
+
url=url,
|
|
150
|
+
headers=headers,
|
|
151
|
+
params=params,
|
|
152
|
+
content=content,
|
|
153
|
+
)
|
|
154
|
+
if not model:
|
|
155
|
+
return response.json()
|
|
156
|
+
|
|
157
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
158
|
+
|
|
159
|
+
async def aclose(self) -> None:
|
|
160
|
+
await self.httpx_client.aclose()
|
|
161
|
+
logger.info("MyExchangeClient closed")
|
|
162
|
+
return True
|
|
163
|
+
|
|
164
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
167
|
+
"""
|
|
168
|
+
# check if path exists
|
|
169
|
+
target_path = Path(file_path)
|
|
170
|
+
if not target_path.exists():
|
|
171
|
+
return self._save_session_file(file_path=file_path)
|
|
172
|
+
|
|
173
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
174
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
175
|
+
json_data: dict = json.loads(content)
|
|
176
|
+
|
|
177
|
+
self.authorization_token = json_data.get(
|
|
178
|
+
"authorization_token",
|
|
179
|
+
self.authorization_token,
|
|
180
|
+
)
|
|
181
|
+
self.timezone = json_data.get("timezone", self.timezone)
|
|
182
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
183
|
+
|
|
184
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
185
|
+
"""
|
|
186
|
+
Saves current information to the session file.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
json_data = {
|
|
190
|
+
"authorization_token": self.authorization_token,
|
|
191
|
+
"timezone": self.timezone,
|
|
192
|
+
"user_agent": self.user_agent,
|
|
193
|
+
}
|
|
194
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
195
|
+
target_path = Path(file_path)
|
|
196
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
197
|
+
|
|
198
|
+
# endregion
|
|
199
|
+
###########################################################
|
|
200
|
+
|
|
201
|
+
```
|
trd_utils/exchanges/__init__.py
CHANGED
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
|
|
2
2
|
from .exchange_base import ExchangeBase
|
|
3
|
+
from .base_types import (
|
|
4
|
+
UnifiedTraderInfo,
|
|
5
|
+
UnifiedTraderPositions,
|
|
6
|
+
UnifiedPositionInfo,
|
|
7
|
+
)
|
|
3
8
|
from .blofin import BlofinClient
|
|
4
9
|
from .bx_ultra import BXUltraClient
|
|
10
|
+
from .hyperliquid import HyperLiquidClient
|
|
5
11
|
|
|
6
12
|
|
|
7
13
|
__all__ = [
|
|
8
|
-
ExchangeBase,
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
"ExchangeBase",
|
|
15
|
+
"UnifiedTraderInfo",
|
|
16
|
+
"UnifiedTraderPositions",
|
|
17
|
+
"UnifiedPositionInfo",
|
|
18
|
+
"BXUltraClient",
|
|
19
|
+
"BlofinClient",
|
|
20
|
+
"HyperLiquidClient",
|
|
11
21
|
]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from decimal import Decimal
|
|
5
|
+
|
|
6
|
+
from trd_utils.types_helper.base_model import BaseModel
|
|
7
|
+
|
|
8
|
+
class UnifiedPositionInfo(BaseModel):
|
|
9
|
+
# The id of the position.
|
|
10
|
+
position_id: str = None
|
|
11
|
+
|
|
12
|
+
# The pnl (profit) of the position.
|
|
13
|
+
position_pnl: Decimal = None
|
|
14
|
+
|
|
15
|
+
# The position side, either "LONG" or "SHORT".
|
|
16
|
+
position_side: str = None
|
|
17
|
+
|
|
18
|
+
# The position's leverage.
|
|
19
|
+
position_leverage: Decimal = None
|
|
20
|
+
|
|
21
|
+
# The margin mode; e.g. cross or isolated. Please note that
|
|
22
|
+
# different exchanges might provide different kinds of margin modes,
|
|
23
|
+
# depending on what they support, that's why we can't support a unified
|
|
24
|
+
# enum type for this as of yet.
|
|
25
|
+
margin_mode: str = None
|
|
26
|
+
|
|
27
|
+
# The formatted pair string of this position.
|
|
28
|
+
# e.g. BTC/USDT.
|
|
29
|
+
position_pair: str = None
|
|
30
|
+
|
|
31
|
+
# The open time of this position.
|
|
32
|
+
# Note that not all public APIs might provide this field.
|
|
33
|
+
open_time: datetime = None
|
|
34
|
+
|
|
35
|
+
# Open price of the position.
|
|
36
|
+
open_price: Decimal = None
|
|
37
|
+
|
|
38
|
+
# The base unit that the open-price is based on (e.g. USD, USDT, USDC)
|
|
39
|
+
open_price_unit: str | None = None
|
|
40
|
+
|
|
41
|
+
def __str__(self):
|
|
42
|
+
parts = []
|
|
43
|
+
|
|
44
|
+
# Add position pair and ID
|
|
45
|
+
parts.append(f"Position: {self.position_pair or 'Unknown'} (ID: {self.position_id or 'N/A'})")
|
|
46
|
+
|
|
47
|
+
# Add side and leverage
|
|
48
|
+
side_str = f"Side: {self.position_side or 'Unknown'}"
|
|
49
|
+
if self.position_leverage is not None:
|
|
50
|
+
side_str += f", {self.position_leverage}x"
|
|
51
|
+
parts.append(side_str)
|
|
52
|
+
|
|
53
|
+
# Add margin mode if available
|
|
54
|
+
if self.margin_mode:
|
|
55
|
+
parts.append(f"Margin: {self.margin_mode}")
|
|
56
|
+
|
|
57
|
+
# Add open price if available
|
|
58
|
+
price_str = "Open price: "
|
|
59
|
+
if self.open_price is not None:
|
|
60
|
+
price_str += f"{self.open_price}"
|
|
61
|
+
if self.open_price_unit:
|
|
62
|
+
price_str += f" {self.open_price_unit}"
|
|
63
|
+
else:
|
|
64
|
+
price_str += "N/A"
|
|
65
|
+
parts.append(price_str)
|
|
66
|
+
|
|
67
|
+
# Add open time if available
|
|
68
|
+
if self.open_time:
|
|
69
|
+
parts.append(f"Opened: {self.open_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
70
|
+
|
|
71
|
+
# Add PNL if available
|
|
72
|
+
if self.position_pnl is not None:
|
|
73
|
+
parts.append(f"PNL: {self.position_pnl}")
|
|
74
|
+
|
|
75
|
+
return " | ".join(parts)
|
|
76
|
+
|
|
77
|
+
def __repr__(self):
|
|
78
|
+
return self.__str__()
|
|
79
|
+
|
|
80
|
+
class UnifiedTraderPositions(BaseModel):
|
|
81
|
+
positions: list[UnifiedPositionInfo] = None
|
|
82
|
+
|
|
83
|
+
class UnifiedTraderInfo(BaseModel):
|
|
84
|
+
# Trader's id. Either int or str. In DEXes (such as HyperLiquid),
|
|
85
|
+
# this might be wallet address of the trader.
|
|
86
|
+
trader_id: int | str = None
|
|
87
|
+
|
|
88
|
+
# Name of the trader
|
|
89
|
+
trader_name: str = None
|
|
90
|
+
|
|
91
|
+
# The URL in which we can see the trader's profile
|
|
92
|
+
trader_url: str = None
|
|
93
|
+
|
|
94
|
+
# Trader's win-rate. Not all exchanges might support this field.
|
|
95
|
+
win_rate: Decimal = None
|
|
96
|
+
|
|
97
|
+
def __str__(self):
|
|
98
|
+
return (
|
|
99
|
+
f"Trader: {self.trader_name} (ID: {self.trader_id})"
|
|
100
|
+
f"{' | Win Rate: ' + str(round(self.win_rate, 2))}"
|
|
101
|
+
f"{' | Profile: ' + self.trader_url}"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
def __repr__(self):
|
|
105
|
+
return self.__str__()
|
|
106
|
+
|
|
@@ -2,14 +2,18 @@ import asyncio
|
|
|
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
|
import time
|
|
9
8
|
from pathlib import Path
|
|
10
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
|
+
)
|
|
11
16
|
from trd_utils.exchanges.blofin.blofin_types import (
|
|
12
|
-
BlofinApiResponse,
|
|
13
17
|
CmsColorResponse,
|
|
14
18
|
CopyTraderAllOrderHistory,
|
|
15
19
|
CopyTraderAllOrderList,
|
|
@@ -21,6 +25,9 @@ from trd_utils.exchanges.blofin.blofin_types import (
|
|
|
21
25
|
from trd_utils.cipher import AESCipher
|
|
22
26
|
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
23
27
|
|
|
28
|
+
|
|
29
|
+
BASE_PROFILE_URL = "https://blofin.com/copy-trade/details/"
|
|
30
|
+
|
|
24
31
|
logger = logging.getLogger(__name__)
|
|
25
32
|
|
|
26
33
|
|
|
@@ -64,7 +71,7 @@ class BlofinClient(ExchangeBase):
|
|
|
64
71
|
return await self.invoke_get(
|
|
65
72
|
f"{self.blofin_api_base_url}/cms/share_config",
|
|
66
73
|
headers=headers,
|
|
67
|
-
|
|
74
|
+
model_type=ShareConfigResponse,
|
|
68
75
|
)
|
|
69
76
|
|
|
70
77
|
async def get_cms_color(self) -> CmsColorResponse:
|
|
@@ -72,7 +79,7 @@ class BlofinClient(ExchangeBase):
|
|
|
72
79
|
return await self.invoke_get(
|
|
73
80
|
f"{self.blofin_api_base_url}/cms/color",
|
|
74
81
|
headers=headers,
|
|
75
|
-
|
|
82
|
+
model_type=CmsColorResponse,
|
|
76
83
|
)
|
|
77
84
|
|
|
78
85
|
# endregion
|
|
@@ -87,28 +94,28 @@ class BlofinClient(ExchangeBase):
|
|
|
87
94
|
f"{self.blofin_api_base_url}/copy/trader/info",
|
|
88
95
|
headers=headers,
|
|
89
96
|
content=payload,
|
|
90
|
-
|
|
97
|
+
model_type=CopyTraderInfoResponse,
|
|
91
98
|
)
|
|
92
99
|
|
|
93
100
|
async def get_copy_trader_order_list(
|
|
94
101
|
self,
|
|
95
|
-
uid: int,
|
|
102
|
+
uid: int | str,
|
|
96
103
|
from_param: int = 0,
|
|
97
104
|
limit_param: int = 20,
|
|
98
105
|
) -> CopyTraderOrderListResponse:
|
|
99
106
|
payload = {
|
|
100
107
|
"from": from_param,
|
|
101
108
|
"limit": limit_param,
|
|
102
|
-
"uid": uid,
|
|
109
|
+
"uid": int(uid),
|
|
103
110
|
}
|
|
104
111
|
headers = self.get_headers()
|
|
105
112
|
return await self.invoke_post(
|
|
106
113
|
f"{self.blofin_api_base_url}/copy/trader/order/list",
|
|
107
114
|
headers=headers,
|
|
108
115
|
content=payload,
|
|
109
|
-
|
|
116
|
+
model_type=CopyTraderOrderListResponse,
|
|
110
117
|
)
|
|
111
|
-
|
|
118
|
+
|
|
112
119
|
async def get_copy_trader_all_order_list(
|
|
113
120
|
self,
|
|
114
121
|
uid: int,
|
|
@@ -132,10 +139,15 @@ class BlofinClient(ExchangeBase):
|
|
|
132
139
|
from_param=current_id_from,
|
|
133
140
|
limit_param=chunk_limit,
|
|
134
141
|
)
|
|
135
|
-
if not
|
|
136
|
-
|
|
142
|
+
if not isinstance(current_result, CopyTraderOrderListResponse):
|
|
143
|
+
raise ValueError(
|
|
144
|
+
"get_copy_trader_order_list returned invalid value of "
|
|
145
|
+
f"{type(current_result)}",
|
|
146
|
+
)
|
|
147
|
+
if not current_result.data:
|
|
148
|
+
# we no longer have anything else here
|
|
137
149
|
return result
|
|
138
|
-
|
|
150
|
+
|
|
139
151
|
if current_result.data[0].id == current_id_from:
|
|
140
152
|
if len(current_result.data) < 2:
|
|
141
153
|
return result
|
|
@@ -146,7 +158,7 @@ class BlofinClient(ExchangeBase):
|
|
|
146
158
|
"Expected first array to have the same value as from_param: "
|
|
147
159
|
f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
|
|
148
160
|
)
|
|
149
|
-
|
|
161
|
+
|
|
150
162
|
current_id_from = current_result.data[-1].id
|
|
151
163
|
result.data.extend(current_result.data)
|
|
152
164
|
result.total_count += len(current_result.data)
|
|
@@ -157,7 +169,6 @@ class BlofinClient(ExchangeBase):
|
|
|
157
169
|
# we don't want to sleep after 1 request only
|
|
158
170
|
await asyncio.sleep(sleep_delay)
|
|
159
171
|
|
|
160
|
-
|
|
161
172
|
async def get_copy_trader_order_history(
|
|
162
173
|
self,
|
|
163
174
|
uid: int,
|
|
@@ -174,7 +185,7 @@ class BlofinClient(ExchangeBase):
|
|
|
174
185
|
f"{self.blofin_api_base_url}/copy/trader/order/history",
|
|
175
186
|
headers=headers,
|
|
176
187
|
content=payload,
|
|
177
|
-
|
|
188
|
+
model_type=CopyTraderOrderHistoryResponse,
|
|
178
189
|
)
|
|
179
190
|
|
|
180
191
|
async def get_copy_trader_all_order_history(
|
|
@@ -200,10 +211,13 @@ class BlofinClient(ExchangeBase):
|
|
|
200
211
|
from_param=current_id_from,
|
|
201
212
|
limit_param=chunk_limit,
|
|
202
213
|
)
|
|
203
|
-
if
|
|
204
|
-
not current_result
|
|
214
|
+
if (
|
|
215
|
+
not current_result
|
|
216
|
+
or not isinstance(current_result, CopyTraderOrderHistoryResponse)
|
|
217
|
+
or not current_result.data
|
|
218
|
+
):
|
|
205
219
|
return result
|
|
206
|
-
|
|
220
|
+
|
|
207
221
|
if current_result.data[0].id == current_id_from:
|
|
208
222
|
if len(current_result.data) < 2:
|
|
209
223
|
return result
|
|
@@ -214,7 +228,7 @@ class BlofinClient(ExchangeBase):
|
|
|
214
228
|
"Expected first array to have the same value as from_param: "
|
|
215
229
|
f"current_id_from: {current_id_from}; but was: {current_result.data[0].id}"
|
|
216
230
|
)
|
|
217
|
-
|
|
231
|
+
|
|
218
232
|
current_id_from = current_result.data[-1].id
|
|
219
233
|
result.data.extend(current_result.data)
|
|
220
234
|
result.total_count += len(current_result.data)
|
|
@@ -250,56 +264,6 @@ class BlofinClient(ExchangeBase):
|
|
|
250
264
|
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
251
265
|
return the_headers
|
|
252
266
|
|
|
253
|
-
async def invoke_get(
|
|
254
|
-
self,
|
|
255
|
-
url: str,
|
|
256
|
-
headers: dict | None = None,
|
|
257
|
-
params: dict | None = None,
|
|
258
|
-
model: Type[BlofinApiResponse] | None = None,
|
|
259
|
-
parse_float=Decimal,
|
|
260
|
-
) -> "BlofinApiResponse":
|
|
261
|
-
"""
|
|
262
|
-
Invokes the specific request to the specific url with the specific params and headers.
|
|
263
|
-
"""
|
|
264
|
-
response = await self.httpx_client.get(
|
|
265
|
-
url=url,
|
|
266
|
-
headers=headers,
|
|
267
|
-
params=params,
|
|
268
|
-
)
|
|
269
|
-
return model.deserialize(response.json(parse_float=parse_float))
|
|
270
|
-
|
|
271
|
-
async def invoke_post(
|
|
272
|
-
self,
|
|
273
|
-
url: str,
|
|
274
|
-
headers: dict | None = None,
|
|
275
|
-
params: dict | None = None,
|
|
276
|
-
content: dict | str | bytes = "",
|
|
277
|
-
model: Type[BlofinApiResponse] | None = None,
|
|
278
|
-
parse_float=Decimal,
|
|
279
|
-
) -> "BlofinApiResponse":
|
|
280
|
-
"""
|
|
281
|
-
Invokes the specific request to the specific url with the specific params and headers.
|
|
282
|
-
"""
|
|
283
|
-
|
|
284
|
-
if isinstance(content, dict):
|
|
285
|
-
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
286
|
-
|
|
287
|
-
response = await self.httpx_client.post(
|
|
288
|
-
url=url,
|
|
289
|
-
headers=headers,
|
|
290
|
-
params=params,
|
|
291
|
-
content=content,
|
|
292
|
-
)
|
|
293
|
-
if not model:
|
|
294
|
-
return response.json()
|
|
295
|
-
|
|
296
|
-
return model.deserialize(response.json(parse_float=parse_float))
|
|
297
|
-
|
|
298
|
-
async def aclose(self) -> None:
|
|
299
|
-
await self.httpx_client.aclose()
|
|
300
|
-
logger.info("BlofinClient closed")
|
|
301
|
-
return True
|
|
302
|
-
|
|
303
267
|
def read_from_session_file(self, file_path: str) -> None:
|
|
304
268
|
"""
|
|
305
269
|
Reads from session file; if it doesn't exist, creates it.
|
|
@@ -336,3 +300,48 @@ class BlofinClient(ExchangeBase):
|
|
|
336
300
|
|
|
337
301
|
# endregion
|
|
338
302
|
###########################################################
|
|
303
|
+
# region unified methods
|
|
304
|
+
async def get_unified_trader_positions(
|
|
305
|
+
self,
|
|
306
|
+
uid: int | str,
|
|
307
|
+
) -> UnifiedTraderPositions:
|
|
308
|
+
result = await self.get_copy_trader_all_order_list(
|
|
309
|
+
uid=uid,
|
|
310
|
+
)
|
|
311
|
+
unified_result = UnifiedTraderPositions()
|
|
312
|
+
unified_result.positions = []
|
|
313
|
+
for position in result.data:
|
|
314
|
+
unified_pos = UnifiedPositionInfo()
|
|
315
|
+
unified_pos.position_id = position.id or position.order_id
|
|
316
|
+
unified_pos.position_pnl = position.real_pnl or position.pnl
|
|
317
|
+
unified_pos.position_side = (
|
|
318
|
+
"LONG" if position.order_side in ("LONG", "BUY") else "SHORT"
|
|
319
|
+
)
|
|
320
|
+
unified_pos.margin_mode = position.margin_mode
|
|
321
|
+
unified_pos.position_leverage = Decimal(position.leverage)
|
|
322
|
+
unified_pos.position_pair = position.symbol.replace("-", "/")
|
|
323
|
+
unified_pos.open_time = dt_from_ts(position.open_time)
|
|
324
|
+
unified_pos.open_price = position.avg_open_price
|
|
325
|
+
unified_pos.open_price_unit = position.symbol.split("-")[-1]
|
|
326
|
+
unified_result.positions.append(unified_pos)
|
|
327
|
+
|
|
328
|
+
return unified_result
|
|
329
|
+
|
|
330
|
+
async def get_unified_trader_info(
|
|
331
|
+
self,
|
|
332
|
+
uid: int | str,
|
|
333
|
+
) -> UnifiedTraderInfo:
|
|
334
|
+
info_resp = await self.get_copy_trader_info(
|
|
335
|
+
uid=uid,
|
|
336
|
+
)
|
|
337
|
+
info = info_resp.data
|
|
338
|
+
unified_info = UnifiedTraderInfo()
|
|
339
|
+
unified_info.trader_id = info.uid
|
|
340
|
+
unified_info.trader_name = info.nick_name
|
|
341
|
+
unified_info.trader_url = f"{BASE_PROFILE_URL}{info.uid}"
|
|
342
|
+
unified_info.win_rate = info.win_rate
|
|
343
|
+
|
|
344
|
+
return unified_info
|
|
345
|
+
|
|
346
|
+
# endregion
|
|
347
|
+
###########################################################
|