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.
- {hypertrader-0.1.1 → hypertrader-0.1.3}/PKG-INFO +2 -1
- {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/PKG-INFO +2 -1
- {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/entry_points.txt +1 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/requires.txt +1 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.py +1 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/auto_trader.py +225 -13
- {hypertrader-0.1.1 → hypertrader-0.1.3}/setup.py +3 -1
- {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/helpers.py +1 -1
- {hypertrader-0.1.1 → hypertrader-0.1.3}/README.md +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/SOURCES.txt +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/dependency_links.txt +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/hypertrader.egg-info/top_level.txt +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/market_maker.py +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/position_management.py +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/position_watcher.py +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/modes/trailing_stop.py +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/setup.cfg +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/constants.py +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/style.py +0 -0
- {hypertrader-0.1.1 → hypertrader-0.1.3}/utils/timer.py +0 -0
- {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.
|
|
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.
|
|
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"
|
|
@@ -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
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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'
|
|
2219
|
-
await
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|