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,389 @@
1
+ from datetime import datetime
2
+ from decimal import Decimal
3
+ import asyncio
4
+ import logging
5
+ from typing import Callable
6
+ import httpx
7
+ import pytz
8
+
9
+ from trd_utils.exchanges.base_types import (
10
+ UnifiedFuturesMarketInfo,
11
+ UnifiedPositionInfo,
12
+ UnifiedSingleFutureMarketInfo,
13
+ UnifiedTraderInfo,
14
+ UnifiedTraderPositions,
15
+ )
16
+ from trd_utils.exchanges.exchange_base import ExchangeBase
17
+ from trd_utils.exchanges.binance.binance_types import (
18
+ BinanceLeaderboardResponse,
19
+ BinanceLeaderboardBaseInfoResponse,
20
+ BinanceTicker24h,
21
+ BinancePremiumIndex,
22
+ )
23
+ from trd_utils.types_helper import new_list
24
+
25
+ logger = logging.getLogger(__name__)
26
+
27
+ BASE_LEADERBOARD_PROFILE_URL = (
28
+ "https://www.binance.com/en/futures-activity/leaderboard/user/um"
29
+ )
30
+
31
+
32
+ class BinanceClient(ExchangeBase):
33
+ ###########################################################
34
+ # region client parameters
35
+
36
+ # Public Futures API
37
+ binance_fapi_base_url: str = "https://fapi.binance.com"
38
+
39
+ # Internal Web API (for Leaderboard)
40
+ binance_bapi_base_url: str = "https://www.binance.com/bapi/futures"
41
+
42
+ default_quote_token: str = "USDT"
43
+ supported_quote_tokens: list[str] = [
44
+ "USDT",
45
+ "USDC",
46
+ "BUSD",
47
+ "BTC",
48
+ ]
49
+
50
+ # endregion
51
+ ###########################################################
52
+ # region client constructor
53
+ def __init__(
54
+ self,
55
+ account_name: str = "default",
56
+ http_verify: bool = True,
57
+ read_session_file: bool = False,
58
+ sessions_dir: str = "sessions",
59
+ use_http1: bool = True,
60
+ use_http2: bool = False,
61
+ ):
62
+ # Binance supports HTTP2, but we respect the flags passed
63
+ self.httpx_client = httpx.AsyncClient(
64
+ verify=http_verify,
65
+ http1=use_http1,
66
+ http2=use_http2,
67
+ )
68
+ self.account_name = account_name
69
+ self.sessions_dir = sessions_dir
70
+ self.exchange_name = "binance"
71
+
72
+ super().__init__()
73
+
74
+ if read_session_file:
75
+ self.read_from_session_file(f"{sessions_dir}/{self.account_name}.bin")
76
+
77
+ # endregion
78
+ ###########################################################
79
+ # region info endpoints (Leaderboard & FAPI)
80
+
81
+ async def get_leaderboard_positions(
82
+ self,
83
+ encrypted_uid: str,
84
+ ) -> BinanceLeaderboardResponse:
85
+ """
86
+ Fetches positions from the Binance Futures Leaderboard.
87
+ UID must be the 'encryptedUid'.
88
+ """
89
+ payload = {
90
+ "encryptedUid": encrypted_uid,
91
+ "tradeType": "PERPETUAL",
92
+ }
93
+ # BAPI requires headers that look like a web browser
94
+ headers = self.get_headers(needs_browser_simulation=True)
95
+
96
+ return await self.invoke_post(
97
+ f"{self.binance_bapi_base_url}/v1/public/future/leaderboard/getOtherPosition",
98
+ headers=headers,
99
+ content=payload,
100
+ model_type=BinanceLeaderboardResponse,
101
+ )
102
+
103
+ async def get_leaderboard_base_info(
104
+ self,
105
+ encrypted_uid: str,
106
+ ) -> BinanceLeaderboardBaseInfoResponse:
107
+ payload = {
108
+ "encryptedUid": encrypted_uid,
109
+ }
110
+ headers = self.get_headers(needs_browser_simulation=True)
111
+
112
+ return await self.invoke_post(
113
+ f"{self.binance_bapi_base_url}/v2/public/future/leaderboard/getOtherLeaderboardBaseInfo",
114
+ headers=headers,
115
+ content=payload,
116
+ model_type=BinanceLeaderboardBaseInfoResponse,
117
+ )
118
+
119
+ async def get_market_tickers(self) -> list[BinanceTicker24h]:
120
+ """
121
+ Fetches 24hr ticker for all symbols from FAPI.
122
+ """
123
+ headers = self.get_headers()
124
+ data = await self.invoke_get(
125
+ f"{self.binance_fapi_base_url}/fapi/v1/ticker/24hr",
126
+ headers=headers,
127
+ model_type=None, # returns a list, handled below
128
+ )
129
+
130
+ if not isinstance(data, list):
131
+ return []
132
+
133
+ return [BinanceTicker24h.deserialize(item) for item in data]
134
+
135
+ async def get_premium_indices(self) -> list[BinancePremiumIndex]:
136
+ """
137
+ Fetches premium index (includes funding rates) from FAPI.
138
+ """
139
+ headers = self.get_headers()
140
+ data = await self.invoke_get(
141
+ f"{self.binance_fapi_base_url}/fapi/v1/premiumIndex",
142
+ headers=headers,
143
+ model_type=None, # returns a list, handled below
144
+ )
145
+
146
+ if not isinstance(data, list):
147
+ return []
148
+
149
+ return [BinancePremiumIndex.deserialize(item) for item in data]
150
+
151
+ # endregion
152
+ ###########################################################
153
+ # region client helper methods
154
+ def get_headers(
155
+ self,
156
+ payload=None,
157
+ needs_auth: bool = False,
158
+ needs_browser_simulation: bool = False,
159
+ ) -> dict:
160
+ the_headers = {
161
+ "Content-Type": "application/json",
162
+ "Accept": "application/json",
163
+ "User-Agent": self.user_agent,
164
+ }
165
+
166
+ if needs_browser_simulation:
167
+ # The internal B-API often rejects standard API or mobile user agents.
168
+ # We simulate a desktop browser here.
169
+ the_headers["User-Agent"] = (
170
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
171
+ "AppleWebKit/537.36 (KHTML, like Gecko) "
172
+ "Chrome/120.0.0.0 Safari/537.36"
173
+ )
174
+ the_headers["ClientType"] = "web"
175
+ the_headers["Client-Type"] = "web"
176
+ the_headers["lang"] = "en"
177
+
178
+ if self.x_requested_with:
179
+ the_headers["X-Requested-With"] = self.x_requested_with
180
+
181
+ # TODO: Implement API Key signature logic here if private endpoints are added later
182
+ # if needs_auth: ...
183
+
184
+ return the_headers
185
+
186
+ def read_from_session_file(self, file_path: str) -> None:
187
+ # Not strictly needed for public leaderboard scraping, but kept for consistency
188
+ # with the project structure (e.g. loading proxies or optional keys later).
189
+ pass
190
+
191
+ def _save_session_file(self, file_path: str) -> None:
192
+ pass
193
+
194
+ def _extract_quote_token_from_symbol(self, symbol: str) -> str:
195
+ for quote in self.supported_quote_tokens:
196
+ if symbol.endswith(quote):
197
+ return quote
198
+ return None
199
+
200
+ # endregion
201
+ ###########################################################
202
+ # region unified methods
203
+
204
+ async def get_unified_trader_positions(
205
+ self,
206
+ uid: int | str,
207
+ min_margin: Decimal = 0,
208
+ ) -> UnifiedTraderPositions:
209
+ """
210
+ UID must be the encryptedUid (string).
211
+ """
212
+ resp = await self.get_leaderboard_positions(encrypted_uid=str(uid))
213
+
214
+ unified_result = UnifiedTraderPositions()
215
+ unified_result.positions = new_list()
216
+
217
+ if (
218
+ not resp
219
+ or not resp.success
220
+ or not resp.data
221
+ or not resp.data.other_position_ret_list
222
+ ):
223
+ return unified_result
224
+
225
+ for pos in resp.data.other_position_ret_list:
226
+ # Binance Leaderboard usually doesn't provide isolated/cross margin mode info publicly.
227
+ # It also doesn't provide direct margin used, so we estimate it:
228
+ # Margin = (Amount * MarkPrice) / Leverage
229
+ if not pos.mark_price or not pos.leverage:
230
+ continue
231
+
232
+ notional_value = abs(pos.amount * pos.mark_price)
233
+ margin_used = (
234
+ notional_value / Decimal(pos.leverage) if pos.leverage > 0 else 0
235
+ )
236
+
237
+ if min_margin and margin_used < min_margin:
238
+ continue
239
+
240
+ unified_pos = UnifiedPositionInfo()
241
+ unified_pos.position_id = pos.get_position_id(str(uid))
242
+ unified_pos.position_pnl = round(pos.pnl, 3)
243
+ unified_pos.position_side = pos.get_side()
244
+ unified_pos.margin_mode = "cross" # Default assumption for leaderboard
245
+ unified_pos.position_leverage = Decimal(pos.leverage)
246
+ unified_pos.position_pair = pos.symbol
247
+
248
+ # Binance timestamps are in milliseconds
249
+ if pos.update_time_stamp:
250
+ unified_pos.open_time = datetime.fromtimestamp(
251
+ pos.update_time_stamp / 1000, tz=pytz.UTC
252
+ )
253
+
254
+ unified_pos.open_price = pos.entry_price
255
+ unified_pos.open_price_unit = self.default_quote_token
256
+ unified_pos.position_size = abs(pos.amount)
257
+ unified_pos.initial_margin = margin_used
258
+
259
+ # We can fill current price if we want, but it's optional in base_types
260
+ unified_pos.last_price = pos.mark_price
261
+
262
+ unified_result.positions.append(unified_pos)
263
+
264
+ return unified_result
265
+
266
+ async def get_unified_trader_info(
267
+ self,
268
+ uid: int | str,
269
+ ) -> UnifiedTraderInfo:
270
+ resp = await self.get_leaderboard_base_info(encrypted_uid=str(uid))
271
+
272
+ unified_info = UnifiedTraderInfo()
273
+ unified_info.trader_id = str(uid)
274
+
275
+ if resp.success and resp.data:
276
+ unified_info.trader_name = resp.data.nick_name
277
+ else:
278
+ # If the request fails or data is hidden, we set a default
279
+ unified_info.trader_name = "Unknown Binance Trader"
280
+
281
+ unified_info.trader_url = (
282
+ f"{BASE_LEADERBOARD_PROFILE_URL}?encryptedUid={uid}"
283
+ )
284
+ unified_info.win_rate = None # Not provided in the base info endpoint
285
+
286
+ return unified_info
287
+
288
+ async def get_unified_futures_market_info(
289
+ self,
290
+ sort_by: str = "percentage_change_24h",
291
+ descending: bool = True,
292
+ allow_delisted: bool = False,
293
+ filter_quote_token: str | None = None,
294
+ raise_on_invalid: bool = False,
295
+ filter_func: Callable | None = None,
296
+ ) -> UnifiedFuturesMarketInfo:
297
+ # We fetch tickers and premium indices (for funding rates) in parallel
298
+ tickers_task = self.get_market_tickers()
299
+ premium_index_task = self.get_premium_indices()
300
+
301
+ tickers, premium_indices = await asyncio.gather(
302
+ tickers_task, premium_index_task
303
+ )
304
+
305
+ # Map funding rates: Symbol -> FundingRate
306
+ funding_map = {p.symbol: p.last_funding_rate for p in premium_indices}
307
+
308
+ unified_info = UnifiedFuturesMarketInfo()
309
+ unified_info.sorted_markets = []
310
+
311
+ for ticker in tickers:
312
+ if "_" in ticker.symbol:
313
+ # we don't want delivery or other special markets
314
+ continue
315
+
316
+ symbol = ticker.symbol
317
+
318
+ # Parse Base and Quote from the symbol string (e.g. BTCUSDT -> BTC, USDT)
319
+ # Binance Futures symbols are usually {Base}{Quote}.
320
+ base_asset = symbol
321
+ quote_asset = self.default_quote_token
322
+
323
+ # Basic suffix checking to separate Base and Quote
324
+ extracted_quote = self._extract_quote_token_from_symbol(symbol)
325
+ if extracted_quote:
326
+ quote_asset = extracted_quote
327
+ base_asset = symbol[:-len(quote_asset)]
328
+ else:
329
+ if raise_on_invalid:
330
+ raise ValueError(
331
+ f"Unrecognized symbol format: {symbol} "
332
+ "Please report this issue to the developers."
333
+ )
334
+ continue
335
+
336
+ if filter_quote_token and quote_asset != filter_quote_token:
337
+ continue
338
+
339
+ current_market = UnifiedSingleFutureMarketInfo()
340
+ current_market.name = base_asset
341
+ current_market.pair = f"{base_asset}/{quote_asset}:{quote_asset}"
342
+
343
+ current_market.price = ticker.last_price
344
+ if ticker.prev_close_price is None:
345
+ current_market.previous_day_price = ticker.last_price - ticker.price_change
346
+ else:
347
+ current_market.previous_day_price = ticker.prev_close_price
348
+ current_market.absolute_change_24h = ticker.price_change
349
+ current_market.percentage_change_24h = ticker.price_change_percent
350
+ current_market.funding_rate = funding_map.get(ticker.symbol, Decimal(0))
351
+ current_market.daily_volume = ticker.quote_volume # Quote Asset Volume
352
+ current_market.open_interest = Decimal(0)
353
+
354
+ if filter_func:
355
+ filter_args = {
356
+ "pair": current_market.pair,
357
+ "market_info": current_market,
358
+ "raw_ticker": ticker,
359
+ "exchange_client": self,
360
+ }
361
+ # this is defined in exchange base.
362
+ should_include = await self._apply_filter_func(
363
+ filter_func=filter_func,
364
+ func_args=filter_args,
365
+ )
366
+ if not should_include:
367
+ continue
368
+
369
+ unified_info.sorted_markets.append(current_market)
370
+
371
+ if not sort_by:
372
+ return unified_info
373
+
374
+ def key_fn(market: UnifiedSingleFutureMarketInfo):
375
+ val = getattr(market, sort_by, None)
376
+ if val is None:
377
+ return Decimal("-Infinity") if descending else Decimal("Infinity")
378
+ return val
379
+
380
+ unified_info.sorted_markets = new_list(sorted(
381
+ unified_info.sorted_markets,
382
+ key=key_fn,
383
+ reverse=descending,
384
+ ))
385
+ return unified_info
386
+
387
+ # endregion
388
+ ###########################################################
389
+
@@ -0,0 +1,116 @@
1
+
2
+ from decimal import Decimal
3
+ from trd_utils.types_helper import BaseModel
4
+
5
+
6
+ ###########################################################
7
+ # region Leaderboard Types (B-API)
8
+
9
+
10
+ class BinanceLeaderboardPosition(BaseModel):
11
+ symbol: str = None
12
+ entry_price: Decimal = None
13
+ mark_price: Decimal = None
14
+ pnl: Decimal = None
15
+ roe: Decimal = None
16
+ amount: Decimal = None
17
+ leverage: int = None
18
+ update_time_stamp: int = None
19
+
20
+ # Binance sometimes returns this to indicate side, though amount sign is also used.
21
+ # It might be a list or a boolean depending on specific endpoint versions,
22
+ # but for "getOtherPosition" it is often implied.
23
+ # We will define it just in case the API maps it.
24
+ long_short: bool | list = None
25
+
26
+ def get_side(self) -> str:
27
+ """
28
+ Determines the side (LONG/SHORT) of the position.
29
+ """
30
+ # Strategy 1: Check specific long_short field if populated
31
+ if isinstance(self.long_short, bool):
32
+ return "LONG" if self.long_short else "SHORT"
33
+
34
+ if isinstance(self.long_short, list) and len(self.long_short) > 0:
35
+ # sometimes ["LONG"] or ["SHORT"]
36
+ val = str(self.long_short[0]).upper()
37
+ if "LONG" in val:
38
+ return "LONG"
39
+ if "SHORT" in val:
40
+ return "SHORT"
41
+
42
+ # Strategy 2: Check amount sign (standard for most derivatives APIs)
43
+ if self.amount is not None:
44
+ if self.amount > 0:
45
+ return "LONG"
46
+ elif self.amount < 0:
47
+ return "SHORT"
48
+
49
+ return "UNKNOWN_SIDE"
50
+
51
+ def get_position_id(self, uid: str) -> str:
52
+ """
53
+ Generates a synthetic position ID.
54
+ Binance Leaderboard does not provide a persistent ID for the position.
55
+ """
56
+ side_str = self.get_side()
57
+ side_code = 1 if side_str == "LONG" else 0
58
+
59
+ # ID = {UID}-{SYMBOL}-{SIDE_CODE}
60
+ raw_str = f"{uid}-{self.symbol}-{side_code}"
61
+ return raw_str.encode("utf-8").hex()
62
+
63
+
64
+ class BinanceLeaderboardResponseData(BaseModel):
65
+ other_position_ret_list: list[BinanceLeaderboardPosition] = None
66
+ update_time_stamp: int = None
67
+
68
+
69
+ class BinanceLeaderboardResponse(BaseModel):
70
+ code: str = None
71
+ message: str = None
72
+ message_detail: str = None
73
+ data: BinanceLeaderboardResponseData = None
74
+ success: bool = False
75
+
76
+
77
+ class BinanceLeaderboardBaseInfo(BaseModel):
78
+ nick_name: str = None
79
+ user_photo_url: str = None
80
+ introduction: str = None
81
+
82
+
83
+ class BinanceLeaderboardBaseInfoResponse(BaseModel):
84
+ data: BinanceLeaderboardBaseInfo = None
85
+ success: bool = False
86
+ code: str = None
87
+
88
+
89
+ # endregion
90
+
91
+ ###########################################################
92
+ # region Market Data Types (F-API)
93
+
94
+
95
+ class BinanceTicker24h(BaseModel):
96
+ symbol: str = None
97
+ last_price: Decimal = None
98
+ prev_close_price: Decimal | None = None
99
+ price_change: Decimal = None
100
+ price_change_percent: Decimal = None
101
+ weighted_avg_price: Decimal = None
102
+ # quote_volume is the volume in USDT (Turnover)
103
+ quote_volume: Decimal = None
104
+ # volume is the volume in Base Asset (e.g. BTC)
105
+ volume: Decimal = None
106
+
107
+
108
+ class BinancePremiumIndex(BaseModel):
109
+ symbol: str = None
110
+ last_funding_rate: Decimal = None
111
+ mark_price: Decimal = None
112
+ index_price: Decimal = None
113
+ time: int = None
114
+
115
+
116
+ # endregion
@@ -0,0 +1,6 @@
1
+
2
+ from .blofin_client import BlofinClient
3
+
4
+ __all__ = [
5
+ "BlofinClient",
6
+ ]