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.
Files changed (44) hide show
  1. trd_utils/__init__.py +3 -0
  2. trd_utils/cipher/__init__.py +44 -0
  3. trd_utils/common_utils/float_utils.py +21 -0
  4. trd_utils/common_utils/wallet_utils.py +26 -0
  5. trd_utils/date_utils/__init__.py +8 -0
  6. trd_utils/date_utils/datetime_helpers.py +25 -0
  7. trd_utils/exchanges/README.md +203 -0
  8. trd_utils/exchanges/__init__.py +28 -0
  9. trd_utils/exchanges/base_types.py +229 -0
  10. trd_utils/exchanges/binance/__init__.py +13 -0
  11. trd_utils/exchanges/binance/binance_client.py +389 -0
  12. trd_utils/exchanges/binance/binance_types.py +116 -0
  13. trd_utils/exchanges/blofin/__init__.py +6 -0
  14. trd_utils/exchanges/blofin/blofin_client.py +375 -0
  15. trd_utils/exchanges/blofin/blofin_types.py +173 -0
  16. trd_utils/exchanges/bx_ultra/__init__.py +6 -0
  17. trd_utils/exchanges/bx_ultra/bx_types.py +1338 -0
  18. trd_utils/exchanges/bx_ultra/bx_ultra_client.py +1123 -0
  19. trd_utils/exchanges/bx_ultra/bx_utils.py +51 -0
  20. trd_utils/exchanges/errors.py +10 -0
  21. trd_utils/exchanges/exchange_base.py +301 -0
  22. trd_utils/exchanges/hyperliquid/README.md +3 -0
  23. trd_utils/exchanges/hyperliquid/__init__.py +7 -0
  24. trd_utils/exchanges/hyperliquid/hyperliquid_client.py +292 -0
  25. trd_utils/exchanges/hyperliquid/hyperliquid_types.py +183 -0
  26. trd_utils/exchanges/okx/__init__.py +6 -0
  27. trd_utils/exchanges/okx/okx_client.py +219 -0
  28. trd_utils/exchanges/okx/okx_types.py +197 -0
  29. trd_utils/exchanges/price_fetcher.py +48 -0
  30. trd_utils/html_utils/__init__.py +26 -0
  31. trd_utils/html_utils/html_formats.py +72 -0
  32. trd_utils/tradingview/__init__.py +8 -0
  33. trd_utils/tradingview/tradingview_client.py +128 -0
  34. trd_utils/tradingview/tradingview_types.py +185 -0
  35. trd_utils/types_helper/__init__.py +12 -0
  36. trd_utils/types_helper/base_model.py +350 -0
  37. trd_utils/types_helper/decorators.py +20 -0
  38. trd_utils/types_helper/model_config.py +6 -0
  39. trd_utils/types_helper/ultra_list.py +39 -0
  40. trd_utils/types_helper/utils.py +40 -0
  41. trd_utils-0.0.57.dist-info/METADATA +42 -0
  42. trd_utils-0.0.57.dist-info/RECORD +44 -0
  43. trd_utils-0.0.57.dist-info/WHEEL +4 -0
  44. 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,6 @@
1
+ from .okx_client import OkxClient
2
+
3
+
4
+ __all__ = [
5
+ "OkxClient",
6
+ ]
@@ -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
+ ]