hypertrader 0.1.2__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.
- {hypertrader-0.1.2 → hypertrader-0.1.3}/PKG-INFO +1 -1
- {hypertrader-0.1.2 → hypertrader-0.1.3}/hypertrader.egg-info/PKG-INFO +1 -1
- {hypertrader-0.1.2 → hypertrader-0.1.3}/modes/auto_trader.py +150 -18
- {hypertrader-0.1.2 → hypertrader-0.1.3}/setup.py +1 -1
- {hypertrader-0.1.2 → hypertrader-0.1.3}/utils/helpers.py +1 -1
- {hypertrader-0.1.2 → hypertrader-0.1.3}/README.md +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/hypertrader.egg-info/SOURCES.txt +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/hypertrader.egg-info/dependency_links.txt +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/hypertrader.egg-info/entry_points.txt +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/hypertrader.egg-info/requires.txt +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/hypertrader.egg-info/top_level.txt +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/hypertrader.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/modes/market_maker.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/modes/position_management.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/modes/position_watcher.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/modes/trailing_stop.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/setup.cfg +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/utils/constants.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/utils/style.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/utils/timer.py +0 -0
- {hypertrader-0.1.2 → hypertrader-0.1.3}/utils/worker.py +0 -0
|
@@ -204,43 +204,125 @@ class AutoManagedTradeResult:
|
|
|
204
204
|
|
|
205
205
|
@dataclass
|
|
206
206
|
class AutoStartupBackfillState:
|
|
207
|
-
"""Track
|
|
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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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,7 +2226,8 @@ 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)
|
|
2123
|
-
|
|
2229
|
+
else:
|
|
2230
|
+
await asyncio.sleep(scan_interval)
|
|
2124
2231
|
|
|
2125
2232
|
while True:
|
|
2126
2233
|
_iter +=1
|
|
@@ -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).
|
|
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.
|
|
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",
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|