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.

Files changed (25) hide show
  1. {trd_utils-0.0.3 → trd_utils-0.0.5}/LICENSE +1 -1
  2. {trd_utils-0.0.3 → trd_utils-0.0.5}/PKG-INFO +13 -3
  3. trd_utils-0.0.5/README.md +13 -0
  4. {trd_utils-0.0.3 → trd_utils-0.0.5}/pyproject.toml +1 -1
  5. trd_utils-0.0.5/trd_utils/__init__.py +3 -0
  6. trd_utils-0.0.5/trd_utils/common_utils/float_utils.py +11 -0
  7. trd_utils-0.0.5/trd_utils/exchanges/__init__.py +11 -0
  8. trd_utils-0.0.5/trd_utils/exchanges/blofin/__init__.py +6 -0
  9. trd_utils-0.0.5/trd_utils/exchanges/blofin/blofin_client.py +238 -0
  10. trd_utils-0.0.5/trd_utils/exchanges/blofin/blofin_types.py +144 -0
  11. {trd_utils-0.0.3/trd_utils → trd_utils-0.0.5/trd_utils/exchanges}/bx_ultra/bx_types.py +101 -5
  12. {trd_utils-0.0.3/trd_utils → trd_utils-0.0.5/trd_utils/exchanges}/bx_ultra/bx_ultra_client.py +234 -57
  13. 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
  14. trd_utils-0.0.5/trd_utils/exchanges/exchange_base.py +76 -0
  15. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/tradingview/tradingview_client.py +35 -39
  16. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/types_helper/base_model.py +0 -1
  17. trd_utils-0.0.3/README.md +0 -3
  18. trd_utils-0.0.3/trd_utils/__init__.py +0 -3
  19. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/cipher/__init__.py +0 -0
  20. {trd_utils-0.0.3/trd_utils → trd_utils-0.0.5/trd_utils/exchanges}/bx_ultra/__init__.py +0 -0
  21. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/html_utils/__init__.py +0 -0
  22. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/html_utils/html_formats.py +0 -0
  23. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/tradingview/__init__.py +0 -0
  24. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/tradingview/tradingview_types.py +0 -0
  25. {trd_utils-0.0.3 → trd_utils-0.0.5}/trd_utils/types_helper/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 ALi.w
3
+ Copyright (c) 2024-2025 ALi.w
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,8 +1,7 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: trd_utils
3
- Version: 0.0.3
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,13 @@
1
+ # Trd Utils
2
+
3
+ Basic common utils for Python.
4
+
5
+ ## How to run tests
6
+
7
+ Use this command first:
8
+
9
+ ```bash
10
+ pip install -e .
11
+ ```
12
+
13
+ Then run the tests in vscode.
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "trd_utils"
3
- version = "0.0.3"
3
+ version = "0.0.5"
4
4
  description = "Common Basic Utils for Python3. By ALiwoto."
5
5
  authors = ["ALiwoto <aminnimaj@gmail.com>"]
6
6
  packages = [
@@ -0,0 +1,3 @@
1
+
2
+ __version__ = "0.0.5"
3
+
@@ -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,11 @@
1
+
2
+ from .exchange_base import ExchangeBase
3
+ from .blofin import BlofinClient
4
+ from .bx_ultra import BXUltraClient
5
+
6
+
7
+ __all__ = [
8
+ ExchangeBase,
9
+ BXUltraClient,
10
+ BlofinClient,
11
+ ]
@@ -0,0 +1,6 @@
1
+
2
+ from .blofin_client import BlofinClient
3
+
4
+ __all__ = [
5
+ BlofinClient,
6
+ ]
@@ -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 .common_utils import (
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
- profit_or_loss = self.current_price - self.display_price
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
- result_str += f"current price: {dec_to_normalize(self.current_price)}{separator}"
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
+