hypertrader 0.1.1__tar.gz → 0.1.3__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.
Files changed (21) hide show
  1. {hypertrader-0.1.1 → hypertrader-0.1.3}/PKG-INFO +2 -1
  2. {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/PKG-INFO +2 -1
  3. {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/entry_points.txt +1 -0
  4. {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/requires.txt +1 -0
  5. {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.py +1 -0
  6. {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/auto_trader.py +225 -13
  7. {hypertrader-0.1.1 → hypertrader-0.1.3}/setup.py +3 -1
  8. {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/helpers.py +1 -1
  9. {hypertrader-0.1.1 → hypertrader-0.1.3}/README.md +0 -0
  10. {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/SOURCES.txt +0 -0
  11. {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/dependency_links.txt +0 -0
  12. {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/top_level.txt +0 -0
  13. {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/market_maker.py +0 -0
  14. {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/position_management.py +0 -0
  15. {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/position_watcher.py +0 -0
  16. {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/trailing_stop.py +0 -0
  17. {hypertrader-0.1.1 → hypertrader-0.1.3}/setup.cfg +0 -0
  18. {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/constants.py +0 -0
  19. {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/style.py +0 -0
  20. {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/timer.py +0 -0
  21. {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -15,6 +15,7 @@ Requires-Dist: eth-account
15
15
  Requires-Dist: hyperliquid-python-sdk-async
16
16
  Requires-Dist: python-dotenv
17
17
  Requires-Dist: uvloop
18
+ Requires-Dist: colored
18
19
  Provides-Extra: auto
19
20
  Requires-Dist: numpy; extra == "auto"
20
21
  Requires-Dist: TA-Lib; extra == "auto"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.1
3
+ Version: 0.1.3
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -15,6 +15,7 @@ Requires-Dist: eth-account
15
15
  Requires-Dist: hyperliquid-python-sdk-async
16
16
  Requires-Dist: python-dotenv
17
17
  Requires-Dist: uvloop
18
+ Requires-Dist: colored
18
19
  Provides-Extra: auto
19
20
  Requires-Dist: numpy; extra == "auto"
20
21
  Requires-Dist: TA-Lib; extra == "auto"
@@ -1,2 +1,3 @@
1
1
  [console_scripts]
2
+ ht = hypertrader:main
2
3
  hypertrader = hypertrader:main
@@ -2,6 +2,7 @@ eth-account
2
2
  hyperliquid-python-sdk-async
3
3
  python-dotenv
4
4
  uvloop
5
+ colored
5
6
 
6
7
  [auto]
7
8
  numpy
@@ -145,6 +145,7 @@ def build_arg_parser() -> argparse.ArgumentParser:
145
145
  " market_maker Event-driven MM: build a pyramiding ladder around recent volatility."
146
146
  ),
147
147
  formatter_class=argparse.RawDescriptionHelpFormatter,
148
+ usage="Full documentation located at https://github.com/darkerego/hypertrader.",
148
149
  )
149
150
  subparsers = parser.add_subparsers(dest="command", required=True)
150
151
 
@@ -18,7 +18,7 @@ from utils.constants import AUTO_TRADES_LOG_FILE, INTERVAL_TO_MS, cp
18
18
  from utils.helpers import parse_interval_list, init_clients, _try_float, parse_fractional_pct, \
19
19
  extract_account_balance_from_user_state, round_size_for_hyperliquid, get_user_state_with_retry, get_all_mids, \
20
20
  fetch_recent_candles, compute_position_unrealized_pnl, close_clients, compute_default_stop_loss_pct, \
21
- extract_closed_pnl_from_fill
21
+ extract_closed_pnl_from_fill, is_rate_limit_error
22
22
 
23
23
  logger = logging.getLogger(__name__)
24
24
  _AUTO_TRADES_LOGGER: Optional[logging.Logger] = None
@@ -202,6 +202,142 @@ class AutoManagedTradeResult:
202
202
  closed_ms: int
203
203
 
204
204
 
205
+ @dataclass
206
+ class AutoStartupBackfillState:
207
+ """Track startup candle backfill progress and block entries until it completes."""
208
+ enabled: bool
209
+ pause_seconds: float = 3.0
210
+ pause_between_batches: bool = False
211
+ complete: bool = False
212
+ saw_rate_limit_this_iteration: bool = False
213
+ pending_pairs: set[Tuple[str, str]] | None = None
214
+ expected_pairs: set[Tuple[str, str]] | None = None
215
+ successful_pairs: set[Tuple[str, str]] | None = None
216
+ ignored_pairs: set[Tuple[str, str]] | None = None
217
+ excluded_coins: set[str] | None = None
218
+
219
+ def __post_init__(self) -> None:
220
+ if self.pending_pairs is None:
221
+ self.pending_pairs = set()
222
+ if self.expected_pairs is None:
223
+ self.expected_pairs = set()
224
+ if self.successful_pairs is None:
225
+ self.successful_pairs = set()
226
+ if self.ignored_pairs is None:
227
+ self.ignored_pairs = set()
228
+ if self.excluded_coins is None:
229
+ self.excluded_coins = set()
230
+
231
+ def _refresh_completion(self) -> None:
232
+ if not self.enabled or self.complete or not self.expected_pairs:
233
+ return
234
+ completed_pairs = (self.successful_pairs or set()) | (self.ignored_pairs or set())
235
+ if completed_pairs >= self.expected_pairs:
236
+ self.complete = True
237
+ if self.pause_between_batches:
238
+ print("[AUTO] Initial REST candle backfill completed; resuming normal scan cadence.")
239
+ else:
240
+ print("[AUTO] Startup candle backfill completed; auto entries are now enabled.")
241
+
242
+ def set_expected_pairs(self, coins: List[str], intervals: List[str]) -> None:
243
+ if not self.enabled or self.complete or self.expected_pairs:
244
+ return
245
+ normalized_coins = [str(coin).upper() for coin in coins if str(coin).strip()]
246
+ normalized_intervals = [str(interval) for interval in intervals if str(interval).strip()]
247
+ self.expected_pairs = {
248
+ (coin, interval)
249
+ for coin in normalized_coins
250
+ for interval in normalized_intervals
251
+ }
252
+ if self.expected_pairs:
253
+ print(
254
+ f"[AUTO] Startup candle backfill tracking {len(self.expected_pairs)} "
255
+ f"coin/interval pair(s) before auto entries are allowed."
256
+ )
257
+
258
+ def begin_iteration(self) -> None:
259
+ if not self.enabled or self.complete:
260
+ return
261
+ self.saw_rate_limit_this_iteration = False
262
+
263
+ def finish_iteration(self) -> None:
264
+ if (
265
+ not self.enabled
266
+ or self.complete
267
+ or self.saw_rate_limit_this_iteration
268
+ or not self.pause_between_batches
269
+ ):
270
+ return
271
+ self.complete = True
272
+ print(
273
+ "[AUTO] Startup candle backfill gating cleared after a full scan iteration "
274
+ "completed without rate limits; auto entries are now enabled."
275
+ )
276
+
277
+ def register_pending(self, coin: str, interval: str) -> None:
278
+ if not self.enabled or self.complete:
279
+ return
280
+ pair = (str(coin).upper(), str(interval))
281
+ if self.expected_pairs and pair not in self.expected_pairs:
282
+ return
283
+ self.pending_pairs.add(pair)
284
+
285
+ def mark_success(self, coin: str, interval: str) -> None:
286
+ if not self.enabled or self.complete:
287
+ return
288
+ pair = (str(coin).upper(), str(interval))
289
+ self.pending_pairs.discard(pair)
290
+ if self.expected_pairs and pair in self.expected_pairs:
291
+ self.successful_pairs.add(pair)
292
+ self._refresh_completion()
293
+
294
+ def mark_non_rate_limit_failure(self, coin: str, interval: str) -> None:
295
+ if not self.enabled or self.complete:
296
+ return
297
+ pair = (str(coin).upper(), str(interval))
298
+ self.pending_pairs.discard(pair)
299
+
300
+ def mark_market_unusable(self, coin: str, reason: str = "") -> None:
301
+ if not self.enabled or self.complete:
302
+ return
303
+ normalized_coin = str(coin).upper()
304
+ first_exclusion = normalized_coin not in self.excluded_coins
305
+ self.excluded_coins.add(normalized_coin)
306
+
307
+ if self.expected_pairs:
308
+ matching_pairs = {pair for pair in self.expected_pairs if pair[0] == normalized_coin}
309
+ self.pending_pairs.difference_update(matching_pairs)
310
+ self.ignored_pairs.update(matching_pairs)
311
+
312
+ if first_exclusion:
313
+ suffix = f": {reason}" if reason else ""
314
+ print(
315
+ f"[AUTO] Excluding {normalized_coin} from auto scans after non-rate-limit candle failure{suffix}"
316
+ )
317
+ self._refresh_completion()
318
+
319
+ def is_coin_excluded(self, coin: str) -> bool:
320
+ return str(coin).upper() in (self.excluded_coins or set())
321
+
322
+ def mark_rate_limit(self) -> None:
323
+ if not self.enabled or self.complete:
324
+ return
325
+ self.saw_rate_limit_this_iteration = True
326
+ if not self.pause_between_batches:
327
+ print(
328
+ f"[AUTO] Initial REST candle backfill hit rate limits; pausing "
329
+ f"{self.pause_seconds:.1f}s between scan batches until startup backfill finishes."
330
+ )
331
+ self.pause_between_batches = True
332
+
333
+ @property
334
+ def remaining_pairs(self) -> int:
335
+ if not self.enabled or self.complete:
336
+ return 0
337
+ completed_pairs = (self.successful_pairs or set()) | (self.ignored_pairs or set())
338
+ return max(0, len(self.expected_pairs or set()) - len(completed_pairs))
339
+
340
+
205
341
  def now_ms() -> int:
206
342
  return int(time.time() * 1000)
207
343
 
@@ -1228,6 +1364,19 @@ def last_finite_index(*arrays: Any) -> int:
1228
1364
  raise RuntimeError("No finite TA-Lib indicator row is available yet; fetch more candles.")
1229
1365
 
1230
1366
 
1367
+ def is_unknown_market_error(exc: BaseException, coin: str) -> bool:
1368
+ """Return True when the candle fetch failed because the market key does not exist."""
1369
+ normalized_coin = str(coin).upper()
1370
+ if isinstance(exc, KeyError):
1371
+ return str(exc).strip("'").upper() == normalized_coin
1372
+ for arg in getattr(exc, "args", ()):
1373
+ if isinstance(arg, KeyError) and str(arg).strip("'").upper() == normalized_coin:
1374
+ return True
1375
+ if isinstance(arg, str) and arg.strip("'").upper() == normalized_coin:
1376
+ return True
1377
+ return False
1378
+
1379
+
1231
1380
  async def compute_auto_interval_signal(
1232
1381
  info: Info,
1233
1382
  coin: str,
@@ -1244,6 +1393,7 @@ async def compute_auto_interval_signal(
1244
1393
  bb_dev: float,
1245
1394
  use_last_closed_candle: bool,
1246
1395
  use_websocket_candles: bool = False,
1396
+ startup_backfill_state: Optional[AutoStartupBackfillState] = None,
1247
1397
  ) -> AutoIntervalSignal:
1248
1398
  """Fetch candles for one interval and compute MACD/SAR/ADX/Bollinger signal state."""
1249
1399
  require_talib_available()
@@ -1252,13 +1402,26 @@ async def compute_auto_interval_signal(
1252
1402
  raise RuntimeError(f"--auto-periods must be at least {minimum_periods} for the chosen indicator settings.")
1253
1403
  # TODO: Determine - is it possible to retrieve candles for multiple coins with one API call? Or is this data \
1254
1404
  # TODO: that is available over the websocket?
1255
- candles = await fetch_recent_candles(
1256
- info,
1257
- coin,
1258
- interval,
1259
- periods,
1260
- use_websocket_candles=use_websocket_candles,
1261
- )
1405
+ if startup_backfill_state is not None:
1406
+ startup_backfill_state.register_pending(coin, interval)
1407
+ try:
1408
+ candles = await fetch_recent_candles(
1409
+ info,
1410
+ coin,
1411
+ interval,
1412
+ periods,
1413
+ use_websocket_candles=use_websocket_candles,
1414
+ )
1415
+ except Exception as exc:
1416
+ if startup_backfill_state is not None:
1417
+ if is_rate_limit_error(exc):
1418
+ startup_backfill_state.mark_rate_limit()
1419
+ else:
1420
+ startup_backfill_state.mark_non_rate_limit_failure(coin, interval)
1421
+ raise
1422
+ else:
1423
+ if startup_backfill_state is not None:
1424
+ startup_backfill_state.mark_success(coin, interval)
1262
1425
  usable_candles = candles[:-1] if use_last_closed_candle and len(candles) > 1 else candles
1263
1426
 
1264
1427
  highs: List[float] = []
@@ -1391,6 +1554,7 @@ async def evaluate_auto_trade_decision(
1391
1554
  use_sar_stop_on_shortest_interval: bool,
1392
1555
  use_websocket_candles: bool = False,
1393
1556
  current_px: Optional[float] = None,
1557
+ startup_backfill_state: Optional[AutoStartupBackfillState] = None,
1394
1558
  ) -> AutoTradeDecision:
1395
1559
  """Evaluate all configured intervals and return an aggregated trade decision."""
1396
1560
  snapshots: List[AutoIntervalSignal] = []
@@ -1414,11 +1578,18 @@ async def evaluate_auto_trade_decision(
1414
1578
  bb_dev=bb_dev,
1415
1579
  use_last_closed_candle=use_last_closed_candle,
1416
1580
  use_websocket_candles=use_websocket_candles,
1581
+ startup_backfill_state=startup_backfill_state,
1417
1582
  )
1418
1583
  )
1419
1584
  except Exception as exc:
1420
1585
  errors.append(f"{interval}: {exc}")
1421
1586
  cp.warning(f"[AUTO-WARN] Failed to compute signal for {coin} {interval}: {exc}")
1587
+ if startup_backfill_state is not None and is_unknown_market_error(exc, coin):
1588
+ startup_backfill_state.mark_market_unusable(coin, str(exc))
1589
+ break
1590
+ if startup_backfill_state is not None and is_rate_limit_error(exc):
1591
+ await asyncio.sleep(startup_backfill_state.pause_seconds)
1592
+
1422
1593
 
1423
1594
  if not snapshots:
1424
1595
  return AutoTradeDecision(
@@ -1585,6 +1756,7 @@ async def scan_auto_trade_candidate(
1585
1756
  use_sar_stop_on_shortest_interval: bool,
1586
1757
  snapshot: AutoScanLoopSnapshot,
1587
1758
  use_websocket_candles: bool = False,
1759
+ startup_backfill_state: Optional[AutoStartupBackfillState] = None,
1588
1760
  ) -> AutoScanCandidate:
1589
1761
  """Scan one market using shared loop snapshots to avoid redundant REST calls."""
1590
1762
  existing_pos = snapshot.positions_by_coin.get(scan_coin.upper())
@@ -1614,6 +1786,7 @@ async def scan_auto_trade_candidate(
1614
1786
  use_sar_stop_on_shortest_interval=use_sar_stop_on_shortest_interval,
1615
1787
  use_websocket_candles=use_websocket_candles,
1616
1788
  current_px=snapshot.mids.get(scan_coin.upper()),
1789
+ startup_backfill_state=startup_backfill_state,
1617
1790
  )
1618
1791
  print_auto_decision(decision, scan_coin)
1619
1792
 
@@ -1785,7 +1958,9 @@ async def run_auto_trader(
1785
1958
  if not owns_clients and (account_address is None or info is None or exchange is None):
1786
1959
  raise RuntimeError("Pass account_address, info, and exchange together when reusing initialized clients.")
1787
1960
  completed_trades = 0
1961
+ _iter = 0
1788
1962
  auto_trades_logger = get_auto_trades_logger()
1963
+ startup_backfill_state = AutoStartupBackfillState(enabled=True)
1789
1964
 
1790
1965
  try:
1791
1966
  if owns_clients:
@@ -2044,7 +2219,18 @@ async def run_auto_trader(
2044
2219
  stop_requested = True
2045
2220
  stop_reason = f"completed {completed_trades} trade(s) and auto looping is disabled"
2046
2221
 
2222
+ async def _sleep_until_next_scan_batch() -> None:
2223
+ if startup_backfill_state.enabled and startup_backfill_state.pause_between_batches and not startup_backfill_state.complete:
2224
+ print(
2225
+ f"[AUTO] Startup REST candle backfill still in progress; "
2226
+ f"sleeping {startup_backfill_state.pause_seconds:.1f}s before the normal scan interval."
2227
+ )
2228
+ await asyncio.sleep(startup_backfill_state.pause_seconds)
2229
+ else:
2230
+ await asyncio.sleep(scan_interval)
2231
+
2047
2232
  while True:
2233
+ _iter +=1
2048
2234
  await _drain_completed_trade_tasks()
2049
2235
  if stop_requested:
2050
2236
  if active_trade_tasks:
@@ -2115,6 +2301,20 @@ async def run_auto_trader(
2115
2301
  await asyncio.sleep(scan_interval)
2116
2302
  continue
2117
2303
 
2304
+ if startup_backfill_state.enabled:
2305
+ eligible_scan_coins = [
2306
+ scan_coin
2307
+ for scan_coin in eligible_scan_coins
2308
+ if not startup_backfill_state.is_coin_excluded(scan_coin)
2309
+ ]
2310
+ if not eligible_scan_coins:
2311
+ print("[AUTO] All scan candidates were excluded after non-rate-limit candle backfill failures.")
2312
+ await asyncio.sleep(scan_interval)
2313
+ continue
2314
+
2315
+ startup_backfill_state.set_expected_pairs(eligible_scan_coins, intervals)
2316
+ startup_backfill_state.begin_iteration()
2317
+
2118
2318
  shared_snapshot = await build_auto_scan_loop_snapshot(
2119
2319
  info=info,
2120
2320
  account_address=account_address,
@@ -2130,7 +2330,8 @@ async def run_auto_trader(
2130
2330
  await asyncio.sleep(scan_interval)
2131
2331
  continue
2132
2332
 
2133
- scan_concurrency = min(max_concurrent_scans, max(1, len(eligible_scan_coins)))
2333
+ #scan_concurrency = min(max_concurrent_scans, max(1, len(eligible_scan_coins)))
2334
+ scan_concurrency = max(1, max_concurrent_scans)
2134
2335
  scan_semaphore = asyncio.Semaphore(scan_concurrency)
2135
2336
 
2136
2337
  async def _scan_with_limit(scan_coin: str) -> AutoScanCandidate:
@@ -2159,6 +2360,7 @@ async def run_auto_trader(
2159
2360
  use_sar_stop_on_shortest_interval=use_sar_stop_on_shortest_interval,
2160
2361
  snapshot=shared_snapshot,
2161
2362
  use_websocket_candles=use_websocket_candles,
2363
+ startup_backfill_state=startup_backfill_state,
2162
2364
  )
2163
2365
 
2164
2366
  scan_tasks = [asyncio.create_task(_scan_with_limit(scan_coin)) for scan_coin in eligible_scan_coins]
@@ -2213,16 +2415,26 @@ async def run_auto_trader(
2213
2415
  logger.error(exc)
2214
2416
 
2215
2417
  await asyncio.gather(*scan_tasks, return_exceptions=True)
2418
+ startup_backfill_state.finish_iteration()
2419
+
2420
+ if startup_backfill_state.enabled and not startup_backfill_state.complete:
2421
+ print(
2422
+ f"[AUTO] Startup candle backfill still in progress; "
2423
+ f"blocking entries until {startup_backfill_state.remaining_pairs} "
2424
+ f"coin/interval pair(s) finish loading."
2425
+ )
2426
+ await _sleep_until_next_scan_batch()
2427
+ continue
2216
2428
 
2217
2429
  if not launch_specs:
2218
- print('=' * 40 + f'Sleeping {scan_interval} seconds .. ' + '=' * 40)
2219
- await asyncio.sleep(scan_interval)
2430
+ print('=' * 40 + f'Iteration: {_iter}, sleeping {scan_interval} seconds (--scan-interval).' + '=' * 40)
2431
+ await _sleep_until_next_scan_batch()
2220
2432
  continue
2221
2433
 
2222
2434
  if dry_run:
2223
2435
  for launch_coin, _launch_decision, _launch_size in launch_specs:
2224
2436
  print(f"[AUTO] Dry run enabled; {launch_coin} signal will not be traded.")
2225
- await asyncio.sleep(scan_interval)
2437
+ await _sleep_until_next_scan_batch()
2226
2438
  continue
2227
2439
 
2228
2440
  for launch_coin, launch_decision, launch_size in launch_specs:
@@ -2256,7 +2468,7 @@ async def run_auto_trader(
2256
2468
  )
2257
2469
 
2258
2470
  metrics_start_time_ms = int(time.time() * 1000)
2259
- await asyncio.sleep(scan_interval)
2471
+ await _sleep_until_next_scan_batch()
2260
2472
  except KeyboardInterrupt:
2261
2473
  print("\n[!] Caught Ctrl+C, stopping auto trader.")
2262
2474
  finally:
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="hypertrader",
12
- version="0.1.1",
12
+ version="0.1.3",
13
13
  description="Async Hyperliquid trading helper built on the async SDK.",
14
14
  long_description=README.read_text(encoding="utf-8"),
15
15
  long_description_content_type="text/markdown",
@@ -26,6 +26,7 @@ setup(
26
26
  "hyperliquid-python-sdk-async",
27
27
  "python-dotenv",
28
28
  "uvloop",
29
+ "colored",
29
30
  ],
30
31
  extras_require={
31
32
  "auto": [
@@ -36,6 +37,7 @@ setup(
36
37
  entry_points={
37
38
  "console_scripts": [
38
39
  "hypertrader=hypertrader:main",
40
+ "ht=hypertrader:main",
39
41
  ],
40
42
  },
41
43
  classifiers=[
@@ -611,7 +611,7 @@ async def get_open_orders_for_coin(info: Info, account_address: str, coin: str)
611
611
  def is_rate_limit_error(exc: BaseException) -> bool:
612
612
  """Return True when the SDK surfaced a 429/rate-limit style response."""
613
613
  for arg in getattr(exc, "args", ()):
614
- if arg == 429:
614
+ if str(arg) == '429':
615
615
  return True
616
616
  if isinstance(arg, str) and "429" in arg:
617
617
  return True
File without changes
File without changes
File without changes
File without changes
File without changes