trd-utils 0.0.3__tar.gz → 0.0.5__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.3 → trd_utils-0.0.5}/LICENSE +1 -1
- {trd_utils-0.0.3 → trd_utils-0.0.5}/PKG-INFO +13 -3
- trd_utils-0.0.5/README.md +13 -0
- {trd_utils-0.0.3 → trd_utils-0.0.5}/pyproject.toml +1 -1
- trd_utils-0.0.5/trd_utils/__init__.py +3 -0
- trd_utils-0.0.5/trd_utils/common_utils/float_utils.py +11 -0
- trd_utils-0.0.5/trd_utils/exchanges/__init__.py +11 -0
- trd_utils-0.0.5/trd_utils/exchanges/blofin/__init__.py +6 -0
- trd_utils-0.0.5/trd_utils/exchanges/blofin/blofin_client.py +238 -0
- trd_utils-0.0.5/trd_utils/exchanges/blofin/blofin_types.py +144 -0
- {trd_utils-0.0.3/trd_utils → trd_utils-0.0.5/trd_utils/exchanges}/bx_ultra/bx_types.py +101 -5
- {trd_utils-0.0.3/trd_utils → trd_utils-0.0.5/trd_utils/exchanges}/bx_ultra/bx_ultra_client.py +234 -57
- trd_utils-0.0.3/trd_utils/bx_ultra/common_utils.py → trd_utils-0.0.5/trd_utils/exchanges/bx_ultra/bx_utils.py +0 -8
- trd_utils-0.0.5/trd_utils/exchanges/exchange_base.py +76 -0
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/tradingview/tradingview_client.py +35 -39
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/types_helper/base_model.py +0 -1
- trd_utils-0.0.3/README.md +0 -3
- trd_utils-0.0.3/trd_utils/__init__.py +0 -3
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/cipher/__init__.py +0 -0
- {trd_utils-0.0.3/trd_utils → trd_utils-0.0.5/trd_utils/exchanges}/bx_ultra/__init__.py +0 -0
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/html_utils/__init__.py +0 -0
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/html_utils/html_formats.py +0 -0
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/tradingview/__init__.py +0 -0
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/tradingview/tradingview_types.py +0 -0
- {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/types_helper/__init__.py +0 -0
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
2
|
Name: trd_utils
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Common Basic Utils for Python3. By ALiwoto.
|
|
5
|
-
Home-page: https://github.com/ALiwoto/trd_utils
|
|
6
5
|
Keywords: utils,trd_utils,basic-utils,common-utils
|
|
7
6
|
Author: ALiwoto
|
|
8
7
|
Author-email: aminnimaj@gmail.com
|
|
@@ -20,9 +19,20 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
20
|
Requires-Dist: cryptography (>=41.0.7)
|
|
22
21
|
Requires-Dist: httpx (>=0.21.0)
|
|
22
|
+
Project-URL: Homepage, https://github.com/ALiwoto/trd_utils
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
25
25
|
# Trd Utils
|
|
26
26
|
|
|
27
27
|
Basic common utils for Python.
|
|
28
28
|
|
|
29
|
+
## How to run tests
|
|
30
|
+
|
|
31
|
+
Use this command first:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then run the tests in vscode.
|
|
38
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
default_quantize = Decimal("1.00")
|
|
6
|
+
|
|
7
|
+
def dec_to_str(dec_value: Decimal) -> str:
|
|
8
|
+
return format(dec_value.quantize(default_quantize), "f")
|
|
9
|
+
|
|
10
|
+
def dec_to_normalize(dec_value: Decimal) -> str:
|
|
11
|
+
return format(dec_value.normalize(), "f")
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
from decimal import Decimal
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from typing import Type
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
import time
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from trd_utils.exchanges.blofin.blofin_types import (
|
|
11
|
+
BlofinApiResponse,
|
|
12
|
+
CmsColorResponse,
|
|
13
|
+
CopyTraderInfoResponse,
|
|
14
|
+
CopyTraderOrderHistoryResponse,
|
|
15
|
+
CopyTraderOrderListResponse,
|
|
16
|
+
ShareConfigResponse,
|
|
17
|
+
)
|
|
18
|
+
from trd_utils.cipher import AESCipher
|
|
19
|
+
from trd_utils.exchanges.exchange_base import ExchangeBase
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class BlofinClient(ExchangeBase):
|
|
25
|
+
###########################################################
|
|
26
|
+
# region client parameters
|
|
27
|
+
blofin_api_base_host: str = "https://\u0062lofin.co\u006d"
|
|
28
|
+
blofin_api_base_url: str = "https://\u0062lofin.co\u006d/uapi/v1"
|
|
29
|
+
origin_header: str = "https://\u0062lofin.co\u006d"
|
|
30
|
+
|
|
31
|
+
timezone: str = "Etc/UTC"
|
|
32
|
+
|
|
33
|
+
# endregion
|
|
34
|
+
###########################################################
|
|
35
|
+
# region client constructor
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
account_name: str = "default",
|
|
39
|
+
http_verify: bool = True,
|
|
40
|
+
fav_letter: str = "^",
|
|
41
|
+
read_session_file: bool = True,
|
|
42
|
+
sessions_dir: str = "sessions",
|
|
43
|
+
):
|
|
44
|
+
self.httpx_client = httpx.AsyncClient(
|
|
45
|
+
verify=http_verify,
|
|
46
|
+
http2=True,
|
|
47
|
+
http1=False,
|
|
48
|
+
)
|
|
49
|
+
self.account_name = account_name
|
|
50
|
+
self._fav_letter = fav_letter
|
|
51
|
+
self.sessions_dir = sessions_dir
|
|
52
|
+
|
|
53
|
+
if read_session_file:
|
|
54
|
+
self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bf")
|
|
55
|
+
|
|
56
|
+
# endregion
|
|
57
|
+
###########################################################
|
|
58
|
+
# region v1/cms/
|
|
59
|
+
async def get_share_config(self) -> ShareConfigResponse:
|
|
60
|
+
headers = self.get_headers()
|
|
61
|
+
return await self.invoke_get(
|
|
62
|
+
f"{self.blofin_api_base_url}/cms/share_config",
|
|
63
|
+
headers=headers,
|
|
64
|
+
model=ShareConfigResponse,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
async def get_cms_color(self) -> CmsColorResponse:
|
|
68
|
+
headers = self.get_headers()
|
|
69
|
+
return await self.invoke_get(
|
|
70
|
+
f"{self.blofin_api_base_url}/cms/color",
|
|
71
|
+
headers=headers,
|
|
72
|
+
model=CmsColorResponse,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
# endregion
|
|
76
|
+
###########################################################
|
|
77
|
+
# region copy/trader
|
|
78
|
+
async def get_copy_trader_info(self, uid: int) -> CopyTraderInfoResponse:
|
|
79
|
+
payload = {
|
|
80
|
+
"uid": uid,
|
|
81
|
+
}
|
|
82
|
+
headers = self.get_headers()
|
|
83
|
+
return await self.invoke_post(
|
|
84
|
+
f"{self.blofin_api_base_url}/copy/trader/info",
|
|
85
|
+
headers=headers,
|
|
86
|
+
content=payload,
|
|
87
|
+
model=CopyTraderInfoResponse,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
async def get_copy_trader_order_list(
|
|
91
|
+
self,
|
|
92
|
+
from_param: int,
|
|
93
|
+
limit_param: 0,
|
|
94
|
+
uid: int,
|
|
95
|
+
) -> CopyTraderOrderListResponse:
|
|
96
|
+
payload = {
|
|
97
|
+
"from": from_param,
|
|
98
|
+
"limit": limit_param,
|
|
99
|
+
"uid": uid,
|
|
100
|
+
}
|
|
101
|
+
headers = self.get_headers()
|
|
102
|
+
return await self.invoke_post(
|
|
103
|
+
f"{self.blofin_api_base_url}/copy/trader/order/list",
|
|
104
|
+
headers=headers,
|
|
105
|
+
content=payload,
|
|
106
|
+
model=CopyTraderOrderListResponse,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def get_copy_trader_order_history(
|
|
110
|
+
self,
|
|
111
|
+
from_param: int,
|
|
112
|
+
limit_param: 0,
|
|
113
|
+
uid: int,
|
|
114
|
+
) -> CopyTraderOrderHistoryResponse:
|
|
115
|
+
payload = {
|
|
116
|
+
"from": from_param,
|
|
117
|
+
"limit": limit_param,
|
|
118
|
+
"uid": uid,
|
|
119
|
+
}
|
|
120
|
+
headers = self.get_headers()
|
|
121
|
+
return await self.invoke_post(
|
|
122
|
+
f"{self.blofin_api_base_url}/copy/trader/order/history",
|
|
123
|
+
headers=headers,
|
|
124
|
+
content=payload,
|
|
125
|
+
model=CopyTraderOrderHistoryResponse,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# endregion
|
|
129
|
+
###########################################################
|
|
130
|
+
# region client helper methods
|
|
131
|
+
def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
|
|
132
|
+
the_timestamp = int(time.time() * 1000)
|
|
133
|
+
the_headers = {
|
|
134
|
+
# "Host": self.blofin_api_base_host,
|
|
135
|
+
"Content-Type": "application/json",
|
|
136
|
+
"Accept": "application/json",
|
|
137
|
+
"Origin": self.origin_header,
|
|
138
|
+
"X-Tz": self.timezone,
|
|
139
|
+
"Fp-Request-Id": f"{the_timestamp}.n1fDrN",
|
|
140
|
+
"Accept-Encoding": "gzip, deflate, br, zstd",
|
|
141
|
+
"User-Agent": self.user_agent,
|
|
142
|
+
"Connection": "close",
|
|
143
|
+
"appsiteid": "0",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if self.x_requested_with:
|
|
147
|
+
the_headers["X-Requested-With"] = self.x_requested_with
|
|
148
|
+
|
|
149
|
+
if needs_auth:
|
|
150
|
+
the_headers["Authorization"] = f"Bearer {self.authorization_token}"
|
|
151
|
+
return the_headers
|
|
152
|
+
|
|
153
|
+
async def invoke_get(
|
|
154
|
+
self,
|
|
155
|
+
url: str,
|
|
156
|
+
headers: dict | None = None,
|
|
157
|
+
params: dict | None = None,
|
|
158
|
+
model: Type[BlofinApiResponse] | None = None,
|
|
159
|
+
parse_float=Decimal,
|
|
160
|
+
) -> "BlofinApiResponse":
|
|
161
|
+
"""
|
|
162
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
163
|
+
"""
|
|
164
|
+
response = await self.httpx_client.get(
|
|
165
|
+
url=url,
|
|
166
|
+
headers=headers,
|
|
167
|
+
params=params,
|
|
168
|
+
)
|
|
169
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
170
|
+
|
|
171
|
+
async def invoke_post(
|
|
172
|
+
self,
|
|
173
|
+
url: str,
|
|
174
|
+
headers: dict | None = None,
|
|
175
|
+
params: dict | None = None,
|
|
176
|
+
content: dict | str | bytes = "",
|
|
177
|
+
model: Type[BlofinApiResponse] | None = None,
|
|
178
|
+
parse_float=Decimal,
|
|
179
|
+
) -> "BlofinApiResponse":
|
|
180
|
+
"""
|
|
181
|
+
Invokes the specific request to the specific url with the specific params and headers.
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
if isinstance(content, dict):
|
|
185
|
+
content = json.dumps(content, separators=(",", ":"), sort_keys=True)
|
|
186
|
+
|
|
187
|
+
response = await self.httpx_client.post(
|
|
188
|
+
url=url,
|
|
189
|
+
headers=headers,
|
|
190
|
+
params=params,
|
|
191
|
+
content=content,
|
|
192
|
+
)
|
|
193
|
+
if not model:
|
|
194
|
+
return response.json()
|
|
195
|
+
|
|
196
|
+
return model.deserialize(response.json(parse_float=parse_float))
|
|
197
|
+
|
|
198
|
+
async def aclose(self) -> None:
|
|
199
|
+
await self.httpx_client.aclose()
|
|
200
|
+
logger.info("BlofinClient closed")
|
|
201
|
+
return True
|
|
202
|
+
|
|
203
|
+
def read_from_session_file(self, file_path: str) -> None:
|
|
204
|
+
"""
|
|
205
|
+
Reads from session file; if it doesn't exist, creates it.
|
|
206
|
+
"""
|
|
207
|
+
# check if path exists
|
|
208
|
+
target_path = Path(file_path)
|
|
209
|
+
if not target_path.exists():
|
|
210
|
+
return self._save_session_file(file_path=file_path)
|
|
211
|
+
|
|
212
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
213
|
+
content = aes.decrypt(target_path.read_text()).decode("utf-8")
|
|
214
|
+
json_data: dict = json.loads(content)
|
|
215
|
+
|
|
216
|
+
self.authorization_token = json_data.get(
|
|
217
|
+
"authorization_token",
|
|
218
|
+
self.authorization_token,
|
|
219
|
+
)
|
|
220
|
+
self.timezone = json_data.get("timezone", self.timezone)
|
|
221
|
+
self.user_agent = json_data.get("user_agent", self.user_agent)
|
|
222
|
+
|
|
223
|
+
def _save_session_file(self, file_path: str) -> None:
|
|
224
|
+
"""
|
|
225
|
+
Saves current information to the session file.
|
|
226
|
+
"""
|
|
227
|
+
|
|
228
|
+
json_data = {
|
|
229
|
+
"authorization_token": self.authorization_token,
|
|
230
|
+
"timezone": self.timezone,
|
|
231
|
+
"user_agent": self.user_agent,
|
|
232
|
+
}
|
|
233
|
+
aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
|
|
234
|
+
target_path = Path(file_path)
|
|
235
|
+
target_path.write_text(aes.encrypt(json.dumps(json_data)))
|
|
236
|
+
|
|
237
|
+
# endregion
|
|
238
|
+
###########################################################
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# from typing import Any, Optional
|
|
2
|
+
# from decimal import Decimal
|
|
3
|
+
# from datetime import datetime, timedelta
|
|
4
|
+
# import pytz
|
|
5
|
+
|
|
6
|
+
from decimal import Decimal
|
|
7
|
+
from typing import Any
|
|
8
|
+
from trd_utils.types_helper import BaseModel
|
|
9
|
+
|
|
10
|
+
# from trd_utils.common_utils.float_utils import (
|
|
11
|
+
# dec_to_str,
|
|
12
|
+
# dec_to_normalize,
|
|
13
|
+
# )
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BlofinApiResponse(BaseModel):
|
|
18
|
+
code: int = None
|
|
19
|
+
timestamp: int = None
|
|
20
|
+
msg: str = None
|
|
21
|
+
|
|
22
|
+
def __str__(self):
|
|
23
|
+
return f"code: {self.code}; timestamp: {self.timestamp}"
|
|
24
|
+
|
|
25
|
+
def __repr__(self):
|
|
26
|
+
return f"code: {self.code}; timestamp: {self.timestamp}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
###########################################################
|
|
30
|
+
|
|
31
|
+
class PnlShareListInfo(BaseModel):
|
|
32
|
+
background_color: str = None
|
|
33
|
+
background_img_up: str = None
|
|
34
|
+
background_img_down: str = None
|
|
35
|
+
|
|
36
|
+
class ShareConfigResult(BaseModel):
|
|
37
|
+
pnl_share_list: list[PnlShareListInfo] = None
|
|
38
|
+
|
|
39
|
+
class ShareConfigResponse(BlofinApiResponse):
|
|
40
|
+
data: ShareConfigResult = None
|
|
41
|
+
|
|
42
|
+
class CmsColorResult(BaseModel):
|
|
43
|
+
color: str = None
|
|
44
|
+
city: str = None
|
|
45
|
+
country: str = None
|
|
46
|
+
ip: str = None
|
|
47
|
+
|
|
48
|
+
class CmsColorResponse(BlofinApiResponse):
|
|
49
|
+
data: CmsColorResult = None
|
|
50
|
+
|
|
51
|
+
###########################################################
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CopyTraderInfoResult(BaseModel):
|
|
55
|
+
aum: str = None
|
|
56
|
+
can_copy: bool = None
|
|
57
|
+
copier_whitelist: bool = None
|
|
58
|
+
follow_state: int = None
|
|
59
|
+
followers: int = None
|
|
60
|
+
followers_max: int = None
|
|
61
|
+
forbidden_follow_type: int = None
|
|
62
|
+
hidden_all: bool = None
|
|
63
|
+
hidden_order: bool = None
|
|
64
|
+
joined_date: int = None
|
|
65
|
+
max_draw_down: Decimal = None
|
|
66
|
+
nick_name: str = None
|
|
67
|
+
order_amount_limit: None
|
|
68
|
+
profile: str = None
|
|
69
|
+
profit_sharing_ratio: Decimal = None
|
|
70
|
+
real_pnl: Decimal = None
|
|
71
|
+
roi_d7: Decimal = None
|
|
72
|
+
self_introduction: str = None
|
|
73
|
+
sharing_period: str = None
|
|
74
|
+
source: int = None
|
|
75
|
+
uid: int = None
|
|
76
|
+
whitelist_copier: bool = None
|
|
77
|
+
win_rate: Decimal = None
|
|
78
|
+
|
|
79
|
+
def get_profile_url(self) -> str:
|
|
80
|
+
return f"https://blofin.com/copy-trade/details/{self.uid}"
|
|
81
|
+
|
|
82
|
+
class CopyTraderInfoResponse(BlofinApiResponse):
|
|
83
|
+
data: CopyTraderInfoResult = None
|
|
84
|
+
|
|
85
|
+
class CopyTraderSingleOrderInfo(BaseModel):
|
|
86
|
+
id: int = None
|
|
87
|
+
symbol: str = None
|
|
88
|
+
leverage: int = None
|
|
89
|
+
order_side: str = None
|
|
90
|
+
avg_open_price: str = None
|
|
91
|
+
quantity: str = None
|
|
92
|
+
quantity_cont: None
|
|
93
|
+
open_time: int = None
|
|
94
|
+
close_time: Any = None
|
|
95
|
+
avg_close_price: Decimal = None
|
|
96
|
+
real_pnl: Any = None
|
|
97
|
+
close_type: Any = None
|
|
98
|
+
roe: Decimal = None
|
|
99
|
+
followers_profit: Decimal = None
|
|
100
|
+
followers: Any = None
|
|
101
|
+
order_id: Any = None
|
|
102
|
+
sharing: Any = None
|
|
103
|
+
order_state: None
|
|
104
|
+
trader_name: None
|
|
105
|
+
mark_price: None
|
|
106
|
+
tp_trigger_price: None
|
|
107
|
+
tp_order_type: None
|
|
108
|
+
sl_trigger_price: None
|
|
109
|
+
sl_order_type: None
|
|
110
|
+
margin_mode: str = None
|
|
111
|
+
time_in_force: None
|
|
112
|
+
position_side: str = None
|
|
113
|
+
order_category: None
|
|
114
|
+
price: None
|
|
115
|
+
fill_quantity: None
|
|
116
|
+
fill_quantity_cont: None
|
|
117
|
+
pnl: None
|
|
118
|
+
cancel_source: None
|
|
119
|
+
order_type: None
|
|
120
|
+
order_open_state: None
|
|
121
|
+
amount: None
|
|
122
|
+
filled_amount: None
|
|
123
|
+
create_time: None
|
|
124
|
+
update_time: None
|
|
125
|
+
open_fee: None
|
|
126
|
+
close_fee: None
|
|
127
|
+
id_md5: None
|
|
128
|
+
tp_sl: None
|
|
129
|
+
trader_uid: None
|
|
130
|
+
available_quantity: None
|
|
131
|
+
available_quantity_cont: None
|
|
132
|
+
show_in_kline: None
|
|
133
|
+
unrealized_pnl: None
|
|
134
|
+
unrealized_pnl_ratio: None
|
|
135
|
+
broker_id: None
|
|
136
|
+
position_change_history: None
|
|
137
|
+
user_id: None
|
|
138
|
+
|
|
139
|
+
class CopyTraderOrderListResponse(BlofinApiResponse):
|
|
140
|
+
data: list[CopyTraderSingleOrderInfo] = None
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class CopyTraderOrderHistoryResponse(BlofinApiResponse):
|
|
144
|
+
data: list[CopyTraderSingleOrderInfo] = None
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
from typing import Any, Optional
|
|
2
|
-
from ..types_helper import BaseModel
|
|
3
2
|
from decimal import Decimal
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
import pytz
|
|
4
5
|
|
|
5
|
-
from .
|
|
6
|
+
from trd_utils.types_helper import BaseModel
|
|
7
|
+
|
|
8
|
+
from trd_utils.common_utils.float_utils import (
|
|
6
9
|
dec_to_str,
|
|
7
10
|
dec_to_normalize,
|
|
8
11
|
)
|
|
@@ -690,6 +693,7 @@ class ContractOrderInfo(BaseModel):
|
|
|
690
693
|
close_type: int = None
|
|
691
694
|
status: ContractOrderStatus = None
|
|
692
695
|
open_date: str = None
|
|
696
|
+
close_date: str = None
|
|
693
697
|
fees: Decimal = None
|
|
694
698
|
lever_fee: Decimal = None
|
|
695
699
|
name: str = None
|
|
@@ -730,7 +734,8 @@ class ContractOrderInfo(BaseModel):
|
|
|
730
734
|
return self.sys_force_price
|
|
731
735
|
|
|
732
736
|
def get_profit_str(self) -> str:
|
|
733
|
-
|
|
737
|
+
last_price = self.current_price or self.display_close_price
|
|
738
|
+
profit_or_loss = last_price - self.display_price
|
|
734
739
|
profit_percentage = (profit_or_loss / self.display_price) * 100
|
|
735
740
|
profit_percentage *= 1 if self.is_long() else -1
|
|
736
741
|
return dec_to_str(profit_percentage * self.lever_times)
|
|
@@ -755,8 +760,10 @@ class ContractOrderInfo(BaseModel):
|
|
|
755
760
|
if self.sys_force_price:
|
|
756
761
|
result_str += f"liquidation: {dec_to_normalize(self.sys_force_price)}{separator}"
|
|
757
762
|
|
|
758
|
-
|
|
759
|
-
|
|
763
|
+
if self.current_price:
|
|
764
|
+
result_str += f"current price: {dec_to_normalize(self.current_price)}{separator}"
|
|
765
|
+
elif self.display_close_price:
|
|
766
|
+
result_str += f"close price: {dec_to_normalize(self.display_close_price)}{separator}"
|
|
760
767
|
profit_str = self.get_profit_str()
|
|
761
768
|
result_str += f"profit: {profit_str}%"
|
|
762
769
|
|
|
@@ -768,6 +775,11 @@ class ContractOrderInfo(BaseModel):
|
|
|
768
775
|
def __repr__(self):
|
|
769
776
|
return self.to_str()
|
|
770
777
|
|
|
778
|
+
class ClosedContractOrderInfo(ContractOrderInfo):
|
|
779
|
+
close_type_name: str = None
|
|
780
|
+
gross_earnings: Decimal = None
|
|
781
|
+
position_order: int = None
|
|
782
|
+
|
|
771
783
|
class MarginStatInfo(BaseModel):
|
|
772
784
|
name: str = None
|
|
773
785
|
margin_coin_name: str = None
|
|
@@ -783,3 +795,87 @@ class ContractsListResult(BaseModel):
|
|
|
783
795
|
|
|
784
796
|
class ContractsListResponse(BxApiResponse):
|
|
785
797
|
data: ContractsListResult = None
|
|
798
|
+
|
|
799
|
+
class ContractOrdersHistoryResult(BaseModel):
|
|
800
|
+
orders: list[ClosedContractOrderInfo] = None
|
|
801
|
+
page_id: int = None
|
|
802
|
+
|
|
803
|
+
class ContractOrdersHistoryResponse(BxApiResponse):
|
|
804
|
+
data: ContractOrdersHistoryResult = None
|
|
805
|
+
|
|
806
|
+
def get_today_earnings(self, timezone: Any = pytz.UTC) -> Decimal:
|
|
807
|
+
"""
|
|
808
|
+
Returns the total earnings for today.
|
|
809
|
+
NOTE: This function will return None if there are no orders for today.
|
|
810
|
+
"""
|
|
811
|
+
found_any_for_today: bool = False
|
|
812
|
+
today_earnings = Decimal("0.00")
|
|
813
|
+
today = datetime.now(timezone).date()
|
|
814
|
+
for current_order in self.data.orders:
|
|
815
|
+
# check if the date is for today
|
|
816
|
+
closed_date = datetime.strptime(
|
|
817
|
+
current_order.close_date,
|
|
818
|
+
'%Y-%m-%dT%H:%M:%S.%f%z',
|
|
819
|
+
).astimezone(timezone).date()
|
|
820
|
+
if closed_date == today:
|
|
821
|
+
today_earnings += current_order.gross_earnings
|
|
822
|
+
found_any_for_today = True
|
|
823
|
+
|
|
824
|
+
if not found_any_for_today:
|
|
825
|
+
return None
|
|
826
|
+
|
|
827
|
+
return today_earnings
|
|
828
|
+
|
|
829
|
+
def get_this_week_earnings(self, timezone: Any = pytz.UTC) -> Decimal:
|
|
830
|
+
"""
|
|
831
|
+
Returns the total earnings for this week.
|
|
832
|
+
NOTE: This function will return None if there are no orders for this week.
|
|
833
|
+
"""
|
|
834
|
+
found_any_for_week: bool = False
|
|
835
|
+
week_earnings = Decimal("0.00")
|
|
836
|
+
today = datetime.now(timezone).date()
|
|
837
|
+
week_start = today - timedelta(days=today.weekday())
|
|
838
|
+
for current_order in self.data.orders:
|
|
839
|
+
# check if the date is for this week
|
|
840
|
+
closed_date = datetime.strptime(
|
|
841
|
+
current_order.close_date,
|
|
842
|
+
'%Y-%m-%dT%H:%M:%S.%f%z',
|
|
843
|
+
).astimezone(timezone).date()
|
|
844
|
+
if closed_date >= week_start:
|
|
845
|
+
week_earnings += current_order.gross_earnings
|
|
846
|
+
found_any_for_week = True
|
|
847
|
+
|
|
848
|
+
if not found_any_for_week:
|
|
849
|
+
return None
|
|
850
|
+
|
|
851
|
+
return week_earnings
|
|
852
|
+
|
|
853
|
+
def get_this_month_earnings(self, timezone: Any = pytz.UTC) -> Decimal:
|
|
854
|
+
"""
|
|
855
|
+
Returns the total earnings for this month.
|
|
856
|
+
NOTE: This function will return None if there are no orders for this month.
|
|
857
|
+
"""
|
|
858
|
+
found_any_for_month: bool = False
|
|
859
|
+
month_earnings = Decimal("0.00")
|
|
860
|
+
today = datetime.now(timezone).date()
|
|
861
|
+
month_start = today.replace(day=1)
|
|
862
|
+
for current_order in self.data.orders:
|
|
863
|
+
# check if the date is for this month
|
|
864
|
+
closed_date = datetime.strptime(
|
|
865
|
+
current_order.close_date,
|
|
866
|
+
'%Y-%m-%dT%H:%M:%S.%f%z',
|
|
867
|
+
).astimezone(timezone).date()
|
|
868
|
+
if closed_date >= month_start:
|
|
869
|
+
month_earnings += current_order.gross_earnings
|
|
870
|
+
found_any_for_month = True
|
|
871
|
+
|
|
872
|
+
if not found_any_for_month:
|
|
873
|
+
return None
|
|
874
|
+
|
|
875
|
+
return month_earnings
|
|
876
|
+
|
|
877
|
+
def get_orders_len(self) -> int:
|
|
878
|
+
if not self.data or not self.data.orders:
|
|
879
|
+
return 0
|
|
880
|
+
return len(self.data.orders)
|
|
881
|
+
|