hypertrader 0.1.2__tar.gz → 0.1.4__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.2 → hypertrader-0.1.4}/PKG-INFO +1 -1
  2. {hypertrader-0.1.2 → hypertrader-0.1.4}/hypertrader.egg-info/PKG-INFO +1 -1
  3. {hypertrader-0.1.2 → hypertrader-0.1.4}/modes/auto_trader.py +149 -17
  4. {hypertrader-0.1.2 → hypertrader-0.1.4}/setup.py +1 -1
  5. {hypertrader-0.1.2 → hypertrader-0.1.4}/utils/helpers.py +1 -1
  6. {hypertrader-0.1.2 → hypertrader-0.1.4}/README.md +0 -0
  7. {hypertrader-0.1.2 → hypertrader-0.1.4}/hypertrader.egg-info/SOURCES.txt +0 -0
  8. {hypertrader-0.1.2 → hypertrader-0.1.4}/hypertrader.egg-info/dependency_links.txt +0 -0
  9. {hypertrader-0.1.2 → hypertrader-0.1.4}/hypertrader.egg-info/entry_points.txt +0 -0
  10. {hypertrader-0.1.2 → hypertrader-0.1.4}/hypertrader.egg-info/requires.txt +0 -0
  11. {hypertrader-0.1.2 → hypertrader-0.1.4}/hypertrader.egg-info/top_level.txt +0 -0
  12. {hypertrader-0.1.2 → hypertrader-0.1.4}/hypertrader.py +0 -0
  13. {hypertrader-0.1.2 → hypertrader-0.1.4}/modes/market_maker.py +0 -0
  14. {hypertrader-0.1.2 → hypertrader-0.1.4}/modes/position_management.py +0 -0
  15. {hypertrader-0.1.2 → hypertrader-0.1.4}/modes/position_watcher.py +0 -0
  16. {hypertrader-0.1.2 → hypertrader-0.1.4}/modes/trailing_stop.py +0 -0
  17. {hypertrader-0.1.2 → hypertrader-0.1.4}/setup.cfg +0 -0
  18. {hypertrader-0.1.2 → hypertrader-0.1.4}/utils/constants.py +0 -0
  19. {hypertrader-0.1.2 → hypertrader-0.1.4}/utils/style.py +0 -0
  20. {hypertrader-0.1.2 → hypertrader-0.1.4}/utils/timer.py +0 -0
  21. {hypertrader-0.1.2 → hypertrader-0.1.4}/utils/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.2
3
+ Version: 0.1.4
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -204,43 +204,125 @@ class AutoManagedTradeResult:
204
204
 
205
205
  @dataclass
206
206
  class AutoStartupBackfillState:
207
- """Track initial REST candle backfill progress and temporary 429 pacing."""
207
+ """Track startup candle backfill progress and block entries until it completes."""
208
208
  enabled: bool
209
209
  pause_seconds: float = 3.0
210
210
  pause_between_batches: bool = False
211
211
  complete: bool = False
212
+ saw_rate_limit_this_iteration: bool = False
212
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
213
218
 
214
219
  def __post_init__(self) -> None:
215
220
  if self.pending_pairs is None:
216
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
+ )
217
276
 
218
277
  def register_pending(self, coin: str, interval: str) -> None:
219
278
  if not self.enabled or self.complete:
220
279
  return
221
- self.pending_pairs.add((str(coin).upper(), str(interval)))
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)
222
284
 
223
285
  def mark_success(self, coin: str, interval: str) -> None:
224
286
  if not self.enabled or self.complete:
225
287
  return
226
- self.pending_pairs.discard((str(coin).upper(), str(interval)))
227
- if not self.pending_pairs:
228
- self.complete = True
229
- if self.pause_between_batches:
230
- print("[AUTO] Initial REST candle backfill completed; resuming normal scan cadence.")
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()
231
293
 
232
294
  def mark_non_rate_limit_failure(self, coin: str, interval: str) -> None:
233
295
  if not self.enabled or self.complete:
234
296
  return
235
- self.pending_pairs.discard((str(coin).upper(), str(interval)))
236
- if not self.pending_pairs:
237
- self.complete = True
238
- if self.pause_between_batches:
239
- print("[AUTO] Initial REST candle backfill completed; resuming normal scan cadence.")
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())
240
321
 
241
322
  def mark_rate_limit(self) -> None:
242
323
  if not self.enabled or self.complete:
243
324
  return
325
+ self.saw_rate_limit_this_iteration = True
244
326
  if not self.pause_between_batches:
245
327
  print(
246
328
  f"[AUTO] Initial REST candle backfill hit rate limits; pausing "
@@ -248,6 +330,13 @@ class AutoStartupBackfillState:
248
330
  )
249
331
  self.pause_between_batches = True
250
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
+
251
340
 
252
341
  def now_ms() -> int:
253
342
  return int(time.time() * 1000)
@@ -1275,6 +1364,19 @@ def last_finite_index(*arrays: Any) -> int:
1275
1364
  raise RuntimeError("No finite TA-Lib indicator row is available yet; fetch more candles.")
1276
1365
 
1277
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
+
1278
1380
  async def compute_auto_interval_signal(
1279
1381
  info: Info,
1280
1382
  coin: str,
@@ -1482,6 +1584,12 @@ async def evaluate_auto_trade_decision(
1482
1584
  except Exception as exc:
1483
1585
  errors.append(f"{interval}: {exc}")
1484
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
+
1485
1593
 
1486
1594
  if not snapshots:
1487
1595
  return AutoTradeDecision(
@@ -1852,9 +1960,7 @@ async def run_auto_trader(
1852
1960
  completed_trades = 0
1853
1961
  _iter = 0
1854
1962
  auto_trades_logger = get_auto_trades_logger()
1855
- startup_backfill_state = AutoStartupBackfillState(
1856
- enabled=(coin is None and not use_websocket_candles),
1857
- )
1963
+ startup_backfill_state = AutoStartupBackfillState(enabled=True)
1858
1964
 
1859
1965
  try:
1860
1966
  if owns_clients:
@@ -2120,6 +2226,7 @@ async def run_auto_trader(
2120
2226
  f"sleeping {startup_backfill_state.pause_seconds:.1f}s before the normal scan interval."
2121
2227
  )
2122
2228
  await asyncio.sleep(startup_backfill_state.pause_seconds)
2229
+
2123
2230
  await asyncio.sleep(scan_interval)
2124
2231
 
2125
2232
  while True:
@@ -2194,6 +2301,20 @@ async def run_auto_trader(
2194
2301
  await asyncio.sleep(scan_interval)
2195
2302
  continue
2196
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
+
2197
2318
  shared_snapshot = await build_auto_scan_loop_snapshot(
2198
2319
  info=info,
2199
2320
  account_address=account_address,
@@ -2209,7 +2330,8 @@ async def run_auto_trader(
2209
2330
  await asyncio.sleep(scan_interval)
2210
2331
  continue
2211
2332
 
2212
- 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)
2213
2335
  scan_semaphore = asyncio.Semaphore(scan_concurrency)
2214
2336
 
2215
2337
  async def _scan_with_limit(scan_coin: str) -> AutoScanCandidate:
@@ -2293,9 +2415,19 @@ async def run_auto_trader(
2293
2415
  logger.error(exc)
2294
2416
 
2295
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
2296
2428
 
2297
2429
  if not launch_specs:
2298
- print('=' * 40 + f'Iteration: {_iter}, sleeping {scan_interval} seconds (--scan-interval). ' + '=' * 40)
2430
+ print('=' * 40 + f'Iteration: {_iter}, sleeping {scan_interval} seconds (--scan-interval).' + '=' * 40)
2299
2431
  await _sleep_until_next_scan_batch()
2300
2432
  continue
2301
2433
 
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="hypertrader",
12
- version="0.1.2",
12
+ version="0.1.4",
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",
@@ -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
File without changes