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
trd_utils/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+
2
+ __version__ = "0.0.57"
3
+
@@ -0,0 +1,44 @@
1
+
2
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
3
+ from cryptography.hazmat.backends import default_backend
4
+ from cryptography.hazmat.primitives import padding
5
+ from base64 import b64encode, b64decode
6
+ from os import urandom
7
+
8
+
9
+ class AESCipher:
10
+ def __init__(self, key: str, fav_letter: str):
11
+ if len(key) > 32:
12
+ raise ValueError("Key length must be 32 bytes or less")
13
+ elif len(key) < 32:
14
+ key = key.ljust(len(key) + (32 - len(key) % 32), fav_letter)
15
+
16
+ key = key.encode('utf-8')
17
+ if len(key) != 32:
18
+ raise ValueError("Key length must be 32 bytes")
19
+
20
+ self.key = key
21
+ self.backend = default_backend()
22
+
23
+ def encrypt(self, plaintext):
24
+ if isinstance(plaintext, str):
25
+ plaintext = plaintext.encode('utf-8')
26
+
27
+ iv = urandom(16)
28
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
29
+ padder = padding.PKCS7(128).padder()
30
+ padded_data = padder.update(plaintext) + padder.finalize()
31
+ encryptor = cipher.encryptor()
32
+ ciphertext = encryptor.update(padded_data) + encryptor.finalize()
33
+ return b64encode(iv + ciphertext).decode('utf-8')
34
+
35
+ def decrypt(self, b64_encrypted_data):
36
+ encrypted_data = b64decode(b64_encrypted_data)
37
+ iv = encrypted_data[:16]
38
+ ciphertext = encrypted_data[16:]
39
+ cipher = Cipher(algorithms.AES(self.key), modes.CBC(iv), backend=self.backend)
40
+ unpadder = padding.PKCS7(128).unpadder()
41
+ decryptor = cipher.decryptor()
42
+ padded_plaintext = decryptor.update(ciphertext) + decryptor.finalize()
43
+ plaintext = unpadder.update(padded_plaintext) + unpadder.finalize()
44
+ return plaintext
@@ -0,0 +1,21 @@
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")
12
+
13
+ def as_decimal(value) -> Decimal:
14
+ if value is None:
15
+ return None
16
+
17
+ if isinstance(value, Decimal):
18
+ # prevent extra allocation
19
+ return value
20
+
21
+ return Decimal(value)
@@ -0,0 +1,26 @@
1
+ """
2
+ General usage wallet-related utils code.
3
+ """
4
+
5
+
6
+ def shorten_wallet_address(
7
+ address: str,
8
+ start_chars: int = 7,
9
+ end_chars: int = 6,
10
+ ):
11
+ """
12
+ Shortens an Ethereum address by keeping a specific number of characters
13
+ from the beginning and end, separated by '...'.
14
+
15
+ Args:
16
+ address (str): The Ethereum address to shorten
17
+ start_chars (int): Number of characters to keep from the beginning (default: 7)
18
+ end_chars (int): Number of characters to keep from the end (default: 6)
19
+
20
+ Returns:
21
+ str: The shortened address
22
+ """
23
+ if not address or len(address) <= start_chars + end_chars:
24
+ return address
25
+
26
+ return f"{address[:start_chars]}...{address[-end_chars:]}"
@@ -0,0 +1,8 @@
1
+
2
+ from .datetime_helpers import dt_from_ts
3
+
4
+
5
+ __all__ = [
6
+ "dt_from_ts",
7
+ ]
8
+
@@ -0,0 +1,25 @@
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)
17
+
18
+ def dt_to_ts(dt: datetime, default: int = 0) -> int:
19
+ """
20
+ Return dt in ms as a timestamp in UTC.
21
+ If dt is None, return the given default.
22
+ """
23
+ if dt:
24
+ return int(dt.timestamp() * 1000)
25
+ return default
@@ -0,0 +1,203 @@
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
+ use_http1: bool = False,
52
+ use_http2: bool = True,
53
+ ):
54
+ self.httpx_client = httpx.AsyncClient(
55
+ verify=http_verify,
56
+ http1=use_http1,
57
+ http2=use_http2,
58
+ )
59
+ self.account_name = account_name
60
+ self._fav_letter = fav_letter
61
+ self.sessions_dir = sessions_dir
62
+
63
+ if read_session_file:
64
+ self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bf")
65
+
66
+ # endregion
67
+ ###########################################################
68
+ # region something
69
+ # async def get_something_info(self) -> SomethingInfoResponse:
70
+ # headers = self.get_headers()
71
+ # return await self.invoke_get(
72
+ # f"{self.my_exchange_api_base_url}/something/info",
73
+ # headers=headers,
74
+ # model=SomethingInfoResponse,
75
+ # )
76
+ # endregion
77
+ ###########################################################
78
+ # region another-thing
79
+ # async def get_another_thing_info(self, uid: int) -> AnotherThingInfoResponse:
80
+ # payload = {
81
+ # "uid": uid,
82
+ # }
83
+ # headers = self.get_headers()
84
+ # return await self.invoke_post(
85
+ # f"{self.my_exchange_api_base_url}/another-thing/info",
86
+ # headers=headers,
87
+ # content=payload,
88
+ # model=CopyTraderInfoResponse,
89
+ # )
90
+
91
+ # endregion
92
+ ###########################################################
93
+ # region client helper methods
94
+ def get_headers(self, payload=None, needs_auth: bool = False) -> dict:
95
+ the_timestamp = int(time.time() * 1000)
96
+ the_headers = {
97
+ # "Host": self.my_exchange_api_base_host,
98
+ "Content-Type": "application/json",
99
+ "Accept": "application/json",
100
+ "Origin": self.origin_header,
101
+ "X-Tz": self.timezone,
102
+ "Fp-Request-Id": f"{the_timestamp}.n1fDrN",
103
+ "Accept-Encoding": "gzip, deflate, br, zstd",
104
+ "User-Agent": self.user_agent,
105
+ "Connection": "close",
106
+ "appsiteid": "0",
107
+ }
108
+
109
+ if self.x_requested_with:
110
+ the_headers["X-Requested-With"] = self.x_requested_with
111
+
112
+ if needs_auth:
113
+ the_headers["Authorization"] = f"Bearer {self.authorization_token}"
114
+ return the_headers
115
+
116
+ async def invoke_get(
117
+ self,
118
+ url: str,
119
+ headers: dict | None = None,
120
+ params: dict | None = None,
121
+ model: Type[MyExchangeApiResponse] | None = None,
122
+ parse_float=Decimal,
123
+ ) -> "MyExchangeApiResponse":
124
+ """
125
+ Invokes the specific request to the specific url with the specific params and headers.
126
+ """
127
+ response = await self.httpx_client.get(
128
+ url=url,
129
+ headers=headers,
130
+ params=params,
131
+ )
132
+ return model.deserialize(response.json(parse_float=parse_float))
133
+
134
+ async def invoke_post(
135
+ self,
136
+ url: str,
137
+ headers: dict | None = None,
138
+ params: dict | None = None,
139
+ content: dict | str | bytes = "",
140
+ model: Type[MyExchangeApiResponse] | None = None,
141
+ parse_float=Decimal,
142
+ ) -> "MyExchangeApiResponse":
143
+ """
144
+ Invokes the specific request to the specific url with the specific params and headers.
145
+ """
146
+
147
+ if isinstance(content, dict):
148
+ content = json.dumps(content, separators=(",", ":"), sort_keys=True)
149
+
150
+ response = await self.httpx_client.post(
151
+ url=url,
152
+ headers=headers,
153
+ params=params,
154
+ content=content,
155
+ )
156
+ if not model:
157
+ return response.json()
158
+
159
+ return model.deserialize(response.json(parse_float=parse_float))
160
+
161
+ async def aclose(self) -> None:
162
+ await self.httpx_client.aclose()
163
+ logger.info("MyExchangeClient closed")
164
+ return True
165
+
166
+ def read_from_session_file(self, file_path: str) -> None:
167
+ """
168
+ Reads from session file; if it doesn't exist, creates it.
169
+ """
170
+ # check if path exists
171
+ target_path = Path(file_path)
172
+ if not target_path.exists():
173
+ return self._save_session_file(file_path=file_path)
174
+
175
+ aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
176
+ content = aes.decrypt(target_path.read_text()).decode("utf-8")
177
+ json_data: dict = json.loads(content)
178
+
179
+ self.authorization_token = json_data.get(
180
+ "authorization_token",
181
+ self.authorization_token,
182
+ )
183
+ self.timezone = json_data.get("timezone", self.timezone)
184
+ self.user_agent = json_data.get("user_agent", self.user_agent)
185
+
186
+ def _save_session_file(self, file_path: str) -> None:
187
+ """
188
+ Saves current information to the session file.
189
+ """
190
+
191
+ json_data = {
192
+ "authorization_token": self.authorization_token,
193
+ "timezone": self.timezone,
194
+ "user_agent": self.user_agent,
195
+ }
196
+ aes = AESCipher(key=f"bf_{self.account_name}_bf", fav_letter=self._fav_letter)
197
+ target_path = Path(file_path)
198
+ target_path.write_text(aes.encrypt(json.dumps(json_data)))
199
+
200
+ # endregion
201
+ ###########################################################
202
+
203
+ ```
@@ -0,0 +1,28 @@
1
+ from .exchange_base import ExchangeBase
2
+ from .base_types import (
3
+ UnifiedTraderInfo,
4
+ UnifiedTraderPositions,
5
+ UnifiedPositionInfo,
6
+ UnifiedFuturesMarketInfo,
7
+ UnifiedSingleFutureMarketInfo,
8
+ )
9
+ from .binance import BinanceClient
10
+ from .blofin import BlofinClient
11
+ from .bx_ultra import BXUltraClient
12
+ from .hyperliquid import HyperLiquidClient
13
+ from .okx import OkxClient
14
+
15
+
16
+ __all__ = [
17
+ "ExchangeBase",
18
+ "BXUltraClient",
19
+ "BinanceClient",
20
+ "BlofinClient",
21
+ "HyperLiquidClient",
22
+ "OkxClient",
23
+ "UnifiedTraderInfo",
24
+ "UnifiedTraderPositions",
25
+ "UnifiedPositionInfo",
26
+ "UnifiedFuturesMarketInfo",
27
+ "UnifiedSingleFutureMarketInfo",
28
+ ]
@@ -0,0 +1,229 @@
1
+ from datetime import datetime
2
+ from decimal import Decimal
3
+ import statistics
4
+
5
+ from trd_utils.types_helper.base_model import BaseModel
6
+
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
+ # The total position size.
42
+ position_size: Decimal | None = None
43
+
44
+ # The initial amount of open_price_unit that the trader has put to open
45
+ # this position.
46
+ # Note that not all public APIs might provide this field.
47
+ initial_margin: Decimal | None = None
48
+
49
+ # The last price of this pair on the target exchange.
50
+ # not all exchanges support this yet, so use it with caution.
51
+ last_price: Decimal | None = None
52
+
53
+ # The last volume of this pair being traded on the target exchange.
54
+ # not all exchanges support this yet, so use it with caution.
55
+ last_volume: Decimal | None = None
56
+
57
+ def recalculate_pnl(self) -> tuple[Decimal, Decimal]:
58
+ """
59
+ Recalculates the PnL based on the available data.
60
+ This requires `last_price`, `open_price`, `initial_margin`,
61
+ and `position_leverage` to be set.
62
+
63
+ Returns:
64
+ The recalculated (PnL, percentage) as a Decimal, or None if calculation
65
+ is not possible with the current data.
66
+ """
67
+ if not self.position_leverage:
68
+ self.position_leverage = 1
69
+
70
+ if not all([self.last_price, self.open_price, self.initial_margin]):
71
+ # Not enough data to calculate PnL.
72
+ return None
73
+
74
+ price_change_percentage = (self.last_price - self.open_price) / self.open_price
75
+ if self.position_side == "SHORT":
76
+ # For a short position, profit is made when the price goes down.
77
+ price_change_percentage *= -1
78
+
79
+ pnl_percentage = self.position_leverage * price_change_percentage
80
+ # PnL = Initial Margin * Leverage * Price Change %
81
+ pnl = self.initial_margin * pnl_percentage
82
+ self.position_pnl = pnl
83
+ return (pnl, pnl_percentage)
84
+
85
+ def __str__(self):
86
+ parts = []
87
+
88
+ # Add position pair and ID
89
+ parts.append(
90
+ f"Position: {self.position_pair or 'Unknown'} (ID: {self.position_id or 'N/A'})"
91
+ )
92
+
93
+ # Add side and leverage
94
+ side_str = f"Side: {self.position_side or 'Unknown'}"
95
+ if self.position_leverage is not None:
96
+ side_str += f", {self.position_leverage}x"
97
+ parts.append(side_str)
98
+
99
+ # Add margin mode if available
100
+ if self.margin_mode:
101
+ parts.append(f"Margin: {self.margin_mode}")
102
+
103
+ # Add open price if available
104
+ price_str = "Open price: "
105
+ if self.open_price is not None:
106
+ price_str += f"{self.open_price}"
107
+ if self.open_price_unit:
108
+ price_str += f" {self.open_price_unit}"
109
+ else:
110
+ price_str += "N/A"
111
+ parts.append(price_str)
112
+
113
+ # Add open time if available
114
+ if self.open_time:
115
+ parts.append(f"Opened: {self.open_time.strftime('%Y-%m-%d %H:%M:%S')}")
116
+
117
+ # Add PNL if available
118
+ if self.position_pnl is not None:
119
+ parts.append(f"PNL: {self.position_pnl}")
120
+
121
+ return " | ".join(parts)
122
+
123
+ def __repr__(self):
124
+ return self.__str__()
125
+
126
+
127
+ class UnifiedTraderPositions(BaseModel):
128
+ positions: list[UnifiedPositionInfo] = None
129
+
130
+
131
+ class UnifiedTraderInfo(BaseModel):
132
+ # Trader's id. Either int or str. In DEXes (such as HyperLiquid),
133
+ # this might be wallet address of the trader.
134
+ trader_id: int | str = None
135
+
136
+ # Name of the trader
137
+ trader_name: str = None
138
+
139
+ # The URL in which we can see the trader's profile
140
+ trader_url: str = None
141
+
142
+ # Trader's win-rate. Not all exchanges might support this field.
143
+ win_rate: Decimal = None
144
+
145
+ def get_win_rate_str(self) -> str:
146
+ return str(round(self.win_rate, 2)) if self.win_rate is not None else "N/A"
147
+
148
+ def __str__(self):
149
+ return (
150
+ f"{self.trader_name} ({self.trader_id})"
151
+ f" | Win Rate: {self.get_win_rate_str()}"
152
+ f" | Profile: {self.trader_url}"
153
+ )
154
+
155
+ def __repr__(self):
156
+ return self.__str__()
157
+
158
+ class UnifiedMarketStatistics(BaseModel):
159
+ mean_change_24h: Decimal = None
160
+ stdev_change_24h: Decimal = None
161
+ median_volume_24h: Decimal = None
162
+
163
+ class UnifiedSingleFutureMarketInfo(BaseModel):
164
+ name: str = None
165
+ pair: str = None
166
+ price: Decimal = None
167
+ previous_day_price: Decimal = None
168
+ absolute_change_24h: Decimal = None
169
+ percentage_change_24h: Decimal = None
170
+ funding_rate: Decimal = None
171
+ daily_volume: Decimal = None
172
+ open_interest: Decimal = None
173
+
174
+ def __str__(self):
175
+ return (
176
+ f"{self.name} | Price: {round(self.price, 4)} "
177
+ f"| 24h Change: {round(self.percentage_change_24h, 4)}% "
178
+ f"| 24h Volume: {round(self.daily_volume, 4)} "
179
+ f"| Funding Rate: {round(self.funding_rate, 6)}% "
180
+ f"| Preferred Side: {self.get_preferred_position_side()}"
181
+ )
182
+
183
+ def __repr__(self):
184
+ return self.__str__()
185
+
186
+ def get_preferred_position_side(self) -> str:
187
+ return "LONG" if self.funding_rate <= 0 else "SHORT"
188
+
189
+ def get_z_score_24h(self, market_stats: UnifiedMarketStatistics) -> Decimal | None:
190
+ if not market_stats.stdev_change_24h:
191
+ return None
192
+
193
+ z_score = (
194
+ self.percentage_change_24h - market_stats.mean_change_24h
195
+ ) / market_stats.stdev_change_24h
196
+ return z_score
197
+
198
+ class UnifiedFuturesMarketInfo(BaseModel):
199
+ sorted_markets: list[UnifiedSingleFutureMarketInfo] = None
200
+
201
+ def __str__(self):
202
+ return f"Total Markets: {len(self.sorted_markets) if self.sorted_markets else 0}"
203
+
204
+ def __repr__(self):
205
+ return self.__str__()
206
+
207
+ def find_market_by_name(
208
+ self,
209
+ name: str,
210
+ ) -> UnifiedSingleFutureMarketInfo | None:
211
+ if not self.sorted_markets:
212
+ return None
213
+
214
+ for market in self.sorted_markets:
215
+ if market.name.lower() == name.lower():
216
+ return market
217
+
218
+ return None
219
+
220
+ def get_statistics(self) -> UnifiedMarketStatistics:
221
+ changes_24h = [m.percentage_change_24h for m in self.sorted_markets]
222
+ volumes_24h = [m.daily_volume for m in self.sorted_markets]
223
+
224
+ s_obj = UnifiedMarketStatistics()
225
+ s_obj.mean_change_24h = statistics.mean(changes_24h)
226
+ s_obj.stdev_change_24h = statistics.stdev(changes_24h)
227
+ s_obj.median_volume_24h = statistics.median(volumes_24h)
228
+
229
+ return s_obj
@@ -0,0 +1,13 @@
1
+
2
+ from .binance_client import BinanceClient
3
+ from .binance_types import (
4
+ BinanceLeaderboardPosition,
5
+ BinanceTicker24h,
6
+ )
7
+
8
+ __all__ = [
9
+ "BinanceClient",
10
+ "BinanceLeaderboardPosition",
11
+ "BinanceTicker24h",
12
+ ]
13
+