hypertrader 0.1.0__tar.gz → 0.1.2__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.0 → hypertrader-0.1.2}/PKG-INFO +8 -1
  2. {hypertrader-0.1.0 → hypertrader-0.1.2}/README.md +6 -0
  3. {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/PKG-INFO +8 -1
  4. {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/entry_points.txt +1 -0
  5. {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/requires.txt +1 -0
  6. {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.py +7 -0
  7. {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/auto_trader.py +398 -195
  8. {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/position_management.py +85 -34
  9. {hypertrader-0.1.0 → hypertrader-0.1.2}/setup.py +3 -1
  10. {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/SOURCES.txt +0 -0
  11. {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/dependency_links.txt +0 -0
  12. {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/top_level.txt +0 -0
  13. {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/market_maker.py +0 -0
  14. {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/position_watcher.py +0 -0
  15. {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/trailing_stop.py +0 -0
  16. {hypertrader-0.1.0 → hypertrader-0.1.2}/setup.cfg +0 -0
  17. {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/constants.py +0 -0
  18. {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/helpers.py +0 -0
  19. {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/style.py +0 -0
  20. {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/timer.py +0 -0
  21. {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/worker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.0
3
+ Version: 0.1.2
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"
@@ -58,6 +59,12 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
58
59
  - Network access to Hyperliquid APIs
59
60
  - Account created with referral code [DARKEREGO](https://app.hyperliquid.xyz/join/DARKEREGO) (see account creation [*])
60
61
 
62
+ Install from pip:
63
+
64
+ <pre>
65
+ pip install hypertrader[auto]
66
+ </pre>
67
+
61
68
  Base dependencies:
62
69
 
63
70
  ```bash
@@ -28,6 +28,12 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
28
28
  - Network access to Hyperliquid APIs
29
29
  - Account created with referral code [DARKEREGO](https://app.hyperliquid.xyz/join/DARKEREGO) (see account creation [*])
30
30
 
31
+ Install from pip:
32
+
33
+ <pre>
34
+ pip install hypertrader[auto]
35
+ </pre>
36
+
31
37
  Base dependencies:
32
38
 
33
39
  ```bash
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hypertrader
3
- Version: 0.1.0
3
+ Version: 0.1.2
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"
@@ -58,6 +59,12 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
58
59
  - Network access to Hyperliquid APIs
59
60
  - Account created with referral code [DARKEREGO](https://app.hyperliquid.xyz/join/DARKEREGO) (see account creation [*])
60
61
 
62
+ Install from pip:
63
+
64
+ <pre>
65
+ pip install hypertrader[auto]
66
+ </pre>
67
+
61
68
  Base dependencies:
62
69
 
63
70
  ```bash
@@ -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
 
@@ -267,6 +268,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
267
268
  help="Seconds between signal scans. Default: 30.0.")
268
269
  auto_parser.add_argument("--max-concurrent-scans", type=int, default=3,
269
270
  help="Maximum markets to scan concurrently per auto loop. Default: 3.")
271
+ auto_parser.add_argument("--max-positions", type=int, default=3,
272
+ help="Maximum concurrently open or actively managed auto positions. Default: 3.")
270
273
  auto_parser.add_argument("--min-agreement", type=int, default=0,
271
274
  help="Intervals required to agree. 0 means all configured intervals. Default: 0.")
272
275
  auto_parser.add_argument("--adx-threshold", type=float, default=20.0,
@@ -549,6 +552,9 @@ async def async_main(argv: Optional[List[str]] = None) -> None:
549
552
  if args.max_concurrent_scans <= 0:
550
553
  print("[ERROR] --max-concurrent-scans must be > 0.")
551
554
  sys.exit(1)
555
+ if args.max_positions <= 0:
556
+ print("[ERROR] --max-positions must be > 0.")
557
+ sys.exit(1)
552
558
  if args.min_agreement < 0:
553
559
  print("[ERROR] --min-agreement must be >= 0.")
554
560
  sys.exit(1)
@@ -625,6 +631,7 @@ async def async_main(argv: Optional[List[str]] = None) -> None:
625
631
  periods=args.auto_periods,
626
632
  scan_interval=args.scan_interval,
627
633
  max_concurrent_scans=args.max_concurrent_scans,
634
+ max_positions=args.max_positions,
628
635
  min_agreement=args.min_agreement,
629
636
  adx_threshold=args.adx_threshold,
630
637
  take_profit_pct=args.take_profit_pct,
@@ -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
@@ -156,6 +156,7 @@ class CoinTradeSessionState:
156
156
  consecutive_losses: int = 0
157
157
  cooldown_until_ms: int = 0
158
158
  cooldown_reason: str = ""
159
+ post_trade_cooldown_until_ms: int = 0
159
160
  last_cycle_pnl: float = 0.0
160
161
  last_cycle_started_ms: int = 0
161
162
  last_cycle_closed_ms: int = 0
@@ -189,6 +190,65 @@ class AutoScanCandidate:
189
190
  rejection_reason: Optional[str] = None
190
191
 
191
192
 
193
+ @dataclass
194
+ class AutoManagedTradeResult:
195
+ """Completed auto-managed trade metadata returned by a background task."""
196
+ coin: str
197
+ direction: str
198
+ size: float
199
+ take_profit_pct: float
200
+ stop_loss_pct: Optional[float]
201
+ started_ms: int
202
+ closed_ms: int
203
+
204
+
205
+ @dataclass
206
+ class AutoStartupBackfillState:
207
+ """Track initial REST candle backfill progress and temporary 429 pacing."""
208
+ enabled: bool
209
+ pause_seconds: float = 3.0
210
+ pause_between_batches: bool = False
211
+ complete: bool = False
212
+ pending_pairs: set[Tuple[str, str]] | None = None
213
+
214
+ def __post_init__(self) -> None:
215
+ if self.pending_pairs is None:
216
+ self.pending_pairs = set()
217
+
218
+ def register_pending(self, coin: str, interval: str) -> None:
219
+ if not self.enabled or self.complete:
220
+ return
221
+ self.pending_pairs.add((str(coin).upper(), str(interval)))
222
+
223
+ def mark_success(self, coin: str, interval: str) -> None:
224
+ if not self.enabled or self.complete:
225
+ 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.")
231
+
232
+ def mark_non_rate_limit_failure(self, coin: str, interval: str) -> None:
233
+ if not self.enabled or self.complete:
234
+ 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.")
240
+
241
+ def mark_rate_limit(self) -> None:
242
+ if not self.enabled or self.complete:
243
+ return
244
+ if not self.pause_between_batches:
245
+ print(
246
+ f"[AUTO] Initial REST candle backfill hit rate limits; pausing "
247
+ f"{self.pause_seconds:.1f}s between scan batches until startup backfill finishes."
248
+ )
249
+ self.pause_between_batches = True
250
+
251
+
192
252
  def now_ms() -> int:
193
253
  return int(time.time() * 1000)
194
254
 
@@ -267,6 +327,16 @@ def coin_is_blocked_by_risk(
267
327
  return False, ""
268
328
 
269
329
 
330
+ def coin_is_in_post_trade_cooldown(
331
+ coin_state: CoinTradeSessionState,
332
+ current_time_ms: int,
333
+ ) -> Tuple[bool, str]:
334
+ if coin_state.post_trade_cooldown_until_ms > current_time_ms:
335
+ remaining_seconds = (coin_state.post_trade_cooldown_until_ms - current_time_ms) / 1000.0
336
+ return True, f"post-trade cooldown active {remaining_seconds:.1f}s remaining"
337
+ return False, ""
338
+
339
+
270
340
  def set_coin_cooldown(
271
341
  coin_state: CoinTradeSessionState,
272
342
  cooldown_seconds: float,
@@ -1221,6 +1291,7 @@ async def compute_auto_interval_signal(
1221
1291
  bb_dev: float,
1222
1292
  use_last_closed_candle: bool,
1223
1293
  use_websocket_candles: bool = False,
1294
+ startup_backfill_state: Optional[AutoStartupBackfillState] = None,
1224
1295
  ) -> AutoIntervalSignal:
1225
1296
  """Fetch candles for one interval and compute MACD/SAR/ADX/Bollinger signal state."""
1226
1297
  require_talib_available()
@@ -1229,13 +1300,26 @@ async def compute_auto_interval_signal(
1229
1300
  raise RuntimeError(f"--auto-periods must be at least {minimum_periods} for the chosen indicator settings.")
1230
1301
  # TODO: Determine - is it possible to retrieve candles for multiple coins with one API call? Or is this data \
1231
1302
  # TODO: that is available over the websocket?
1232
- candles = await fetch_recent_candles(
1233
- info,
1234
- coin,
1235
- interval,
1236
- periods,
1237
- use_websocket_candles=use_websocket_candles,
1238
- )
1303
+ if startup_backfill_state is not None:
1304
+ startup_backfill_state.register_pending(coin, interval)
1305
+ try:
1306
+ candles = await fetch_recent_candles(
1307
+ info,
1308
+ coin,
1309
+ interval,
1310
+ periods,
1311
+ use_websocket_candles=use_websocket_candles,
1312
+ )
1313
+ except Exception as exc:
1314
+ if startup_backfill_state is not None:
1315
+ if is_rate_limit_error(exc):
1316
+ startup_backfill_state.mark_rate_limit()
1317
+ else:
1318
+ startup_backfill_state.mark_non_rate_limit_failure(coin, interval)
1319
+ raise
1320
+ else:
1321
+ if startup_backfill_state is not None:
1322
+ startup_backfill_state.mark_success(coin, interval)
1239
1323
  usable_candles = candles[:-1] if use_last_closed_candle and len(candles) > 1 else candles
1240
1324
 
1241
1325
  highs: List[float] = []
@@ -1368,6 +1452,7 @@ async def evaluate_auto_trade_decision(
1368
1452
  use_sar_stop_on_shortest_interval: bool,
1369
1453
  use_websocket_candles: bool = False,
1370
1454
  current_px: Optional[float] = None,
1455
+ startup_backfill_state: Optional[AutoStartupBackfillState] = None,
1371
1456
  ) -> AutoTradeDecision:
1372
1457
  """Evaluate all configured intervals and return an aggregated trade decision."""
1373
1458
  snapshots: List[AutoIntervalSignal] = []
@@ -1391,6 +1476,7 @@ async def evaluate_auto_trade_decision(
1391
1476
  bb_dev=bb_dev,
1392
1477
  use_last_closed_candle=use_last_closed_candle,
1393
1478
  use_websocket_candles=use_websocket_candles,
1479
+ startup_backfill_state=startup_backfill_state,
1394
1480
  )
1395
1481
  )
1396
1482
  except Exception as exc:
@@ -1562,6 +1648,7 @@ async def scan_auto_trade_candidate(
1562
1648
  use_sar_stop_on_shortest_interval: bool,
1563
1649
  snapshot: AutoScanLoopSnapshot,
1564
1650
  use_websocket_candles: bool = False,
1651
+ startup_backfill_state: Optional[AutoStartupBackfillState] = None,
1565
1652
  ) -> AutoScanCandidate:
1566
1653
  """Scan one market using shared loop snapshots to avoid redundant REST calls."""
1567
1654
  existing_pos = snapshot.positions_by_coin.get(scan_coin.upper())
@@ -1591,6 +1678,7 @@ async def scan_auto_trade_candidate(
1591
1678
  use_sar_stop_on_shortest_interval=use_sar_stop_on_shortest_interval,
1592
1679
  use_websocket_candles=use_websocket_candles,
1593
1680
  current_px=snapshot.mids.get(scan_coin.upper()),
1681
+ startup_backfill_state=startup_backfill_state,
1594
1682
  )
1595
1683
  print_auto_decision(decision, scan_coin)
1596
1684
 
@@ -1628,6 +1716,7 @@ async def run_auto_trader(
1628
1716
  periods: int,
1629
1717
  scan_interval: float,
1630
1718
  max_concurrent_scans: int,
1719
+ max_positions: int,
1631
1720
  min_agreement: int,
1632
1721
  adx_threshold: float,
1633
1722
  take_profit_pct: Optional[float],
@@ -1702,6 +1791,8 @@ async def run_auto_trader(
1702
1791
  raise RuntimeError("--scan-interval must be > 0.")
1703
1792
  if max_concurrent_scans <= 0:
1704
1793
  raise RuntimeError("--max-concurrent-scans must be > 0.")
1794
+ if max_positions <= 0:
1795
+ raise RuntimeError("--max-positions must be > 0.")
1705
1796
  if min_agreement < 0:
1706
1797
  raise RuntimeError("--min-agreement must be >= 0. Use 0 to require all configured intervals.")
1707
1798
  if adx_threshold < 0.0:
@@ -1759,7 +1850,11 @@ async def run_auto_trader(
1759
1850
  if not owns_clients and (account_address is None or info is None or exchange is None):
1760
1851
  raise RuntimeError("Pass account_address, info, and exchange together when reusing initialized clients.")
1761
1852
  completed_trades = 0
1853
+ _iter = 0
1762
1854
  auto_trades_logger = get_auto_trades_logger()
1855
+ startup_backfill_state = AutoStartupBackfillState(
1856
+ enabled=(coin is None and not use_websocket_candles),
1857
+ )
1763
1858
 
1764
1859
  try:
1765
1860
  if owns_clients:
@@ -1767,6 +1862,9 @@ async def run_auto_trader(
1767
1862
  metrics_start_time_ms = int(time.time() * 1000)
1768
1863
  risk_session = AutoRiskSessionState(started_ms=metrics_start_time_ms)
1769
1864
  coin_sessions: Dict[str, CoinTradeSessionState] = {}
1865
+ active_trade_tasks: Dict[str, asyncio.Task[AutoManagedTradeResult]] = {}
1866
+ stop_requested = False
1867
+ stop_reason = ""
1770
1868
  print("============================================================")
1771
1869
  print(" Hyperliquid Async Auto Trader")
1772
1870
  print("============================================================")
@@ -1793,6 +1891,7 @@ async def run_auto_trader(
1793
1891
  print(f"Trailing TP pct: {trailing_tp_profit_pct * 100:.4f}% of favorable unrealized profit")
1794
1892
  print(f"Scan interval: {scan_interval:.2f}s")
1795
1893
  print(f"Scan concurrency: {max_concurrent_scans}")
1894
+ print(f"Max positions: {max_positions}")
1796
1895
  print(f"Dry run: {dry_run}")
1797
1896
  print(f"Max trades: {'unlimited' if max_trades == 0 else max_trades}")
1798
1897
  print(f"Loop after trade: {loop_after_trade}")
@@ -1809,9 +1908,238 @@ async def run_auto_trader(
1809
1908
  print(f" session_giveback_pct={session_giveback_pct}")
1810
1909
  print("============================================================")
1811
1910
 
1911
+ async def _run_managed_auto_trade(
1912
+ managed_coin: str,
1913
+ managed_direction: str,
1914
+ managed_size: float,
1915
+ managed_tp_pct: float,
1916
+ managed_sl_pct: Optional[float],
1917
+ managed_sl_trigger_px: Optional[float],
1918
+ managed_shortest_snapshot: Optional[AutoIntervalSignal],
1919
+ ) -> AutoManagedTradeResult:
1920
+ trade_cycle_started_ms = now_ms()
1921
+ await run_bracket_entry(
1922
+ coin=managed_coin,
1923
+ direction=managed_direction,
1924
+ size=managed_size,
1925
+ take_profit_pct=managed_tp_pct,
1926
+ stop_loss_pct=managed_sl_pct,
1927
+ stop_loss_trigger_px=managed_sl_trigger_px,
1928
+ take_profit_levels=take_profit_levels,
1929
+ use_trailing_tp=use_trailing_tp,
1930
+ trailing_tp_trigger_level=trailing_tp_trigger_level,
1931
+ trailing_tp_profit_pct=trailing_tp_profit_pct,
1932
+ entry_retries=entry_retries,
1933
+ entry_repost_interval=entry_repost_interval,
1934
+ poll_interval=poll_interval,
1935
+ tp_reversal_pct=tp_reversal_pct,
1936
+ entry_tif=entry_tif,
1937
+ tp_tif=tp_tif,
1938
+ market_fallback=market_fallback,
1939
+ market_slippage=market_slippage,
1940
+ cancel_existing_tpsl=cancel_existing_tpsl,
1941
+ tp_reversal_limit_exit=tp_reversal_limit_exit,
1942
+ tp_reversal_stop_buffer_pct=tp_reversal_stop_buffer_pct,
1943
+ use_testnet=use_testnet,
1944
+ use_websocket=use_websocket,
1945
+ hide_orders=hide_orders,
1946
+ auto_sar_stop_interval=(
1947
+ managed_shortest_snapshot.interval
1948
+ if use_sar_stop_on_shortest_interval and managed_shortest_snapshot is not None
1949
+ else None
1950
+ ),
1951
+ auto_sar_stop_periods=periods if use_sar_stop_on_shortest_interval else None,
1952
+ auto_sar_acceleration=sar_acceleration if use_sar_stop_on_shortest_interval else None,
1953
+ auto_sar_maximum=sar_maximum if use_sar_stop_on_shortest_interval else None,
1954
+ auto_use_last_closed_candle=use_last_closed_candle,
1955
+ auto_use_websocket_candles=use_websocket_candles,
1956
+ account_address=account_address,
1957
+ info=info,
1958
+ exchange=exchange,
1959
+ )
1960
+ return AutoManagedTradeResult(
1961
+ coin=managed_coin,
1962
+ direction=managed_direction,
1963
+ size=managed_size,
1964
+ take_profit_pct=managed_tp_pct,
1965
+ stop_loss_pct=managed_sl_pct,
1966
+ started_ms=trade_cycle_started_ms,
1967
+ closed_ms=now_ms(),
1968
+ )
1969
+
1970
+ async def _drain_completed_trade_tasks() -> None:
1971
+ nonlocal completed_trades, stop_requested, stop_reason
1972
+ finished_coins = [task_coin for task_coin, task in active_trade_tasks.items() if task.done()]
1973
+ for finished_coin in finished_coins:
1974
+ task = active_trade_tasks.pop(finished_coin)
1975
+ try:
1976
+ trade_result = task.result()
1977
+ except asyncio.CancelledError:
1978
+ print(f"[AUTO] Managed trade task for {finished_coin} was cancelled.")
1979
+ continue
1980
+ except Exception as exc:
1981
+ print(f"[AUTO-WARN] Managed trade task failed for {finished_coin}: {exc}")
1982
+ logger.exception("[AUTO] Managed trade task failed for %s", finished_coin)
1983
+ continue
1984
+
1985
+ completed_trades += 1
1986
+ auto_trades_logger.info(
1987
+ "[AUTO-TRADE-COMPLETE] coin=%s direction=%s size=%.8f tp_pct=%.8f sl_pct=%s completed_trades=%d",
1988
+ trade_result.coin,
1989
+ trade_result.direction,
1990
+ trade_result.size,
1991
+ trade_result.take_profit_pct,
1992
+ f"{trade_result.stop_loss_pct:.8f}" if trade_result.stop_loss_pct is not None else "N/A",
1993
+ completed_trades,
1994
+ )
1995
+
1996
+ if info is None or exchange is None:
1997
+ raise RuntimeError("Initialized clients became unavailable while draining auto trade tasks.")
1998
+
1999
+ cycle_pnl, cycle_fills, pnl_reason = await fetch_trade_cycle_net_pnl(
2000
+ info=info,
2001
+ account_address=account_address,
2002
+ coin=trade_result.coin,
2003
+ start_time_ms=trade_result.started_ms,
2004
+ end_time_ms=trade_result.closed_ms,
2005
+ )
2006
+ if pnl_reason:
2007
+ print(
2008
+ f"[AUTO-WARN] {trade_result.coin} post-trade PnL unavailable; "
2009
+ f"risk session not updated for this cycle: {pnl_reason}"
2010
+ )
2011
+ _append_risk_event_log(
2012
+ risk_session_log,
2013
+ {
2014
+ "ts_ms": trade_result.closed_ms,
2015
+ "event": "cycle_pnl_unavailable",
2016
+ "coin": trade_result.coin,
2017
+ "reason": pnl_reason,
2018
+ "session_pnl": risk_session.realized_pnl,
2019
+ },
2020
+ )
2021
+ else:
2022
+ coin_state = get_or_create_coin_session(coin_sessions, trade_result.coin)
2023
+ coin_reason, session_reason = update_risk_after_trade_cycle(
2024
+ coin_state=coin_state,
2025
+ session_state=risk_session,
2026
+ cycle_pnl=cycle_pnl,
2027
+ cycle_started_ms=trade_result.started_ms,
2028
+ cycle_closed_ms=trade_result.closed_ms,
2029
+ max_coin_trades_per_session=max_coin_trades_per_session,
2030
+ coin_session_cooldown_seconds=coin_session_cooldown_seconds,
2031
+ coin_session_profit_target=coin_session_profit_target,
2032
+ coin_session_min_profit_to_lock=coin_session_min_profit_to_lock,
2033
+ coin_session_giveback_pct=coin_session_giveback_pct,
2034
+ cooldown_after_loss_following_wins=cooldown_after_loss_following_wins,
2035
+ session_profit_target=session_profit_target,
2036
+ session_max_loss=session_max_loss,
2037
+ session_giveback_pct=session_giveback_pct,
2038
+ )
2039
+ print(
2040
+ f"[AUTO-RISK] {trade_result.coin} cycle_pnl={cycle_pnl:.6f} "
2041
+ f"coin_pnl={coin_state.realized_pnl:.6f} coin_peak={coin_state.peak_pnl:.6f} "
2042
+ f"coin_cycles={coin_state.completed_cycles} session_pnl={risk_session.realized_pnl:.6f} "
2043
+ f"session_peak={risk_session.peak_pnl:.6f}"
2044
+ )
2045
+ _append_risk_event_log(
2046
+ risk_session_log,
2047
+ {
2048
+ "ts_ms": trade_result.closed_ms,
2049
+ "event": "cycle_complete",
2050
+ "coin": trade_result.coin,
2051
+ "cycle_pnl": cycle_pnl,
2052
+ "coin_pnl": coin_state.realized_pnl,
2053
+ "coin_peak_pnl": coin_state.peak_pnl,
2054
+ "coin_cycles": coin_state.completed_cycles,
2055
+ "session_pnl": risk_session.realized_pnl,
2056
+ "session_peak_pnl": risk_session.peak_pnl,
2057
+ "fills_count": len(cycle_fills),
2058
+ },
2059
+ )
2060
+ if coin_reason is not None:
2061
+ print(
2062
+ f"[AUTO-RISK] {trade_result.coin} entering cooldown for "
2063
+ f"{coin_session_cooldown_seconds:.1f}s reason=\"{coin_reason}\""
2064
+ )
2065
+ _append_risk_event_log(
2066
+ risk_session_log,
2067
+ {
2068
+ "ts_ms": trade_result.closed_ms,
2069
+ "event": "coin_cooldown",
2070
+ "coin": trade_result.coin,
2071
+ "reason": coin_reason,
2072
+ "coin_pnl": coin_state.realized_pnl,
2073
+ "session_pnl": risk_session.realized_pnl,
2074
+ },
2075
+ )
2076
+ if session_reason is not None:
2077
+ print(f"[AUTO-RISK] Session stop triggered: {session_reason}")
2078
+ _append_risk_event_log(
2079
+ risk_session_log,
2080
+ {
2081
+ "ts_ms": trade_result.closed_ms,
2082
+ "event": "session_stop",
2083
+ "coin": trade_result.coin,
2084
+ "reason": session_reason,
2085
+ "coin_pnl": coin_state.realized_pnl,
2086
+ "session_pnl": risk_session.realized_pnl,
2087
+ },
2088
+ )
2089
+
2090
+ if cooldown_after_trade > 0.0:
2091
+ coin_state = get_or_create_coin_session(coin_sessions, trade_result.coin)
2092
+ coin_state.post_trade_cooldown_until_ms = max(
2093
+ coin_state.post_trade_cooldown_until_ms,
2094
+ trade_result.closed_ms + int(cooldown_after_trade * 1000),
2095
+ )
2096
+ cooldown_now_ms = now_ms()
2097
+ remaining_seconds = max(
2098
+ 0.0,
2099
+ (coin_state.post_trade_cooldown_until_ms - cooldown_now_ms) / 1000.0,
2100
+ )
2101
+ print(
2102
+ f"[AUTO] {trade_result.coin} post-trade cooldown active for "
2103
+ f"{remaining_seconds:.2f}s after close."
2104
+ )
2105
+
2106
+ if risk_session.stopped:
2107
+ stop_requested = True
2108
+ stop_reason = risk_session.stop_reason or "session risk stop"
2109
+ elif 0 < max_trades <= completed_trades:
2110
+ stop_requested = True
2111
+ stop_reason = f"max trades reached ({completed_trades})"
2112
+ elif not loop_after_trade:
2113
+ stop_requested = True
2114
+ stop_reason = f"completed {completed_trades} trade(s) and auto looping is disabled"
2115
+
2116
+ async def _sleep_until_next_scan_batch() -> None:
2117
+ if startup_backfill_state.enabled and startup_backfill_state.pause_between_batches and not startup_backfill_state.complete:
2118
+ print(
2119
+ f"[AUTO] Startup REST candle backfill still in progress; "
2120
+ f"sleeping {startup_backfill_state.pause_seconds:.1f}s before the normal scan interval."
2121
+ )
2122
+ await asyncio.sleep(startup_backfill_state.pause_seconds)
2123
+ await asyncio.sleep(scan_interval)
2124
+
1812
2125
  while True:
1813
- if 0 < max_trades <= completed_trades:
1814
- print(f"[AUTO] Max trades reached ({completed_trades}); exiting auto mode.")
2126
+ _iter +=1
2127
+ await _drain_completed_trade_tasks()
2128
+ if stop_requested:
2129
+ if active_trade_tasks:
2130
+ print(
2131
+ f"[AUTO] Stop requested ({stop_reason}); waiting for "
2132
+ f"{len(active_trade_tasks)} active managed trade(s) to finish."
2133
+ )
2134
+ done, _pending = await asyncio.wait(
2135
+ list(active_trade_tasks.values()),
2136
+ timeout=min(scan_interval, 5.0),
2137
+ return_when=asyncio.FIRST_COMPLETED,
2138
+ )
2139
+ if not done:
2140
+ await asyncio.sleep(min(scan_interval, 1.0))
2141
+ continue
2142
+ print(f"[AUTO] {stop_reason}; exiting auto mode.")
1815
2143
  return
1816
2144
 
1817
2145
  if coin is not None:
@@ -1836,6 +2164,13 @@ async def run_auto_trader(
1836
2164
  risk_session_log=risk_session_log,
1837
2165
  session_pnl=risk_session.realized_pnl,
1838
2166
  )
2167
+ in_post_trade_cooldown, post_trade_reason = coin_is_in_post_trade_cooldown(
2168
+ coin_state,
2169
+ current_time_ms,
2170
+ )
2171
+ if in_post_trade_cooldown:
2172
+ print(f"[AUTO] Skipping {scan_coin}: {post_trade_reason}")
2173
+ continue
1839
2174
  is_blocked, block_reason = coin_is_blocked_by_risk(coin_state, current_time_ms)
1840
2175
  if is_blocked:
1841
2176
  print(f"[AUTO-RISK] Skipping {scan_coin}: {block_reason}")
@@ -1855,7 +2190,7 @@ async def run_auto_trader(
1855
2190
  eligible_scan_coins.append(scan_coin)
1856
2191
 
1857
2192
  if not eligible_scan_coins:
1858
- print("[AUTO-RISK] All scan candidates are blocked by coin session controls.")
2193
+ print("[AUTO] All scan candidates are temporarily blocked by cooldown or coin session controls.")
1859
2194
  await asyncio.sleep(scan_interval)
1860
2195
  continue
1861
2196
 
@@ -1864,9 +2199,15 @@ async def run_auto_trader(
1864
2199
  account_address=account_address,
1865
2200
  metrics_start_time_ms=metrics_start_time_ms,
1866
2201
  )
1867
- selected_coin: Optional[str] = None
1868
- selected_decision: Optional[AutoTradeDecision] = None
1869
- selected_size: Optional[float] = None
2202
+ reserved_coins = set(active_trade_tasks.keys()) | set(shared_snapshot.positions_by_coin.keys())
2203
+ available_slots = max_positions - len(reserved_coins)
2204
+ if available_slots <= 0:
2205
+ print(
2206
+ f"[AUTO] Max position capacity reached "
2207
+ f"({len(reserved_coins)}/{max_positions}); delaying new entries."
2208
+ )
2209
+ await asyncio.sleep(scan_interval)
2210
+ continue
1870
2211
 
1871
2212
  scan_concurrency = min(max_concurrent_scans, max(1, len(eligible_scan_coins)))
1872
2213
  scan_semaphore = asyncio.Semaphore(scan_concurrency)
@@ -1897,9 +2238,11 @@ async def run_auto_trader(
1897
2238
  use_sar_stop_on_shortest_interval=use_sar_stop_on_shortest_interval,
1898
2239
  snapshot=shared_snapshot,
1899
2240
  use_websocket_candles=use_websocket_candles,
2241
+ startup_backfill_state=startup_backfill_state,
1900
2242
  )
1901
2243
 
1902
2244
  scan_tasks = [asyncio.create_task(_scan_with_limit(scan_coin)) for scan_coin in eligible_scan_coins]
2245
+ launch_specs: List[Tuple[str, AutoTradeDecision, float]] = []
1903
2246
  try:
1904
2247
  for completed_task in asyncio.as_completed(scan_tasks):
1905
2248
  try:
@@ -1922,6 +2265,12 @@ async def run_auto_trader(
1922
2265
 
1923
2266
  if candidate.decision is None or candidate.decision.direction is None or candidate.decision.take_profit_pct is None:
1924
2267
  continue
2268
+ if candidate.coin in reserved_coins:
2269
+ print(
2270
+ f"[AUTO] {candidate.coin} is already reserved by an active position or managed task; "
2271
+ f"skipping duplicate auto entry."
2272
+ )
2273
+ continue
1925
2274
 
1926
2275
  try:
1927
2276
  resolved_size, size_reason = await resolve_auto_trade_size(
@@ -1936,204 +2285,58 @@ async def run_auto_trader(
1936
2285
  print(f"[AUTO-WARN] {candidate.coin} size resolution failed; skipping trade candidate: {exc}")
1937
2286
  continue
1938
2287
  print(f"[AUTO] {candidate.coin} size resolved to {resolved_size:.8f} ({size_reason})")
1939
-
1940
- selected_coin = candidate.coin
1941
- selected_decision = candidate.decision
1942
- selected_size = resolved_size
1943
- break
2288
+ launch_specs.append((candidate.coin, candidate.decision, resolved_size))
2289
+ reserved_coins.add(candidate.coin)
2290
+ if len(launch_specs) >= available_slots:
2291
+ continue
1944
2292
  except Exception as exc:
1945
2293
  logger.error(exc)
1946
2294
 
1947
- for task in scan_tasks:
1948
- if not task.done():
1949
- task.cancel()
1950
2295
  await asyncio.gather(*scan_tasks, return_exceptions=True)
1951
2296
 
1952
- if selected_coin is None or selected_decision is None or selected_size is None:
1953
- print('=' * 40 + f'Sleeping {scan_interval} seconds .. ' + '=' * 40)
1954
- await asyncio.sleep(scan_interval)
2297
+ if not launch_specs:
2298
+ print('=' * 40 + f'Iteration: {_iter}, sleeping {scan_interval} seconds (--scan-interval). ' + '=' * 40)
2299
+ await _sleep_until_next_scan_batch()
1955
2300
  continue
1956
2301
 
1957
2302
  if dry_run:
1958
- print(f"[AUTO] Dry run enabled; {selected_coin} signal will not be traded.")
1959
- await asyncio.sleep(scan_interval)
2303
+ for launch_coin, _launch_decision, _launch_size in launch_specs:
2304
+ print(f"[AUTO] Dry run enabled; {launch_coin} signal will not be traded.")
2305
+ await _sleep_until_next_scan_batch()
1960
2306
  continue
1961
2307
 
1962
- direction = selected_decision.direction
1963
- auto_tp_pct = selected_decision.take_profit_pct
1964
- auto_sl_pct = stop_loss_pct if stop_loss_pct is not None else selected_decision.stop_loss_pct
1965
- auto_sl_trigger_px = None
1966
- if use_sar_stop_on_shortest_interval:
1967
- auto_sl_trigger_px = selected_decision.stop_loss_trigger_px
1968
- shortest_snapshot = get_shortest_interval_snapshot(selected_decision.snapshots)
1969
- sl_display = f"{auto_sl_pct * 100:.4f}%" if auto_sl_pct is not None else "N/A"
1970
- print(
1971
- f"[AUTO] Trading {direction.upper()} {selected_coin}: size={selected_size:.8f}, "
1972
- f"tp_pct={auto_tp_pct * 100:.4f}%, sl_pct={sl_display}, "
1973
- f"sl_trigger={f'{auto_sl_trigger_px:.8f}' if auto_sl_trigger_px is not None else 'N/A'}"
1974
- )
1975
-
1976
- trade_cycle_started_ms = now_ms()
1977
- await run_bracket_entry(
1978
- coin=selected_coin,
1979
- direction=direction,
1980
- size=selected_size,
1981
- take_profit_pct=auto_tp_pct,
1982
- stop_loss_pct=auto_sl_pct,
1983
- stop_loss_trigger_px=auto_sl_trigger_px,
1984
- take_profit_levels=take_profit_levels,
1985
- use_trailing_tp=use_trailing_tp,
1986
- trailing_tp_trigger_level=trailing_tp_trigger_level,
1987
- trailing_tp_profit_pct=trailing_tp_profit_pct,
1988
- entry_retries=entry_retries,
1989
- entry_repost_interval=entry_repost_interval,
1990
- poll_interval=poll_interval,
1991
- tp_reversal_pct=tp_reversal_pct,
1992
- entry_tif=entry_tif,
1993
- tp_tif=tp_tif,
1994
- market_fallback=market_fallback,
1995
- market_slippage=market_slippage,
1996
- cancel_existing_tpsl=cancel_existing_tpsl,
1997
- tp_reversal_limit_exit=tp_reversal_limit_exit,
1998
- tp_reversal_stop_buffer_pct=tp_reversal_stop_buffer_pct,
1999
- use_testnet=use_testnet,
2000
- use_websocket=use_websocket,
2001
- hide_orders=hide_orders,
2002
- auto_sar_stop_interval=(
2003
- shortest_snapshot.interval
2004
- if use_sar_stop_on_shortest_interval and shortest_snapshot is not None
2308
+ for launch_coin, launch_decision, launch_size in launch_specs:
2309
+ direction = launch_decision.direction
2310
+ if direction is None or launch_decision.take_profit_pct is None:
2311
+ continue
2312
+ auto_tp_pct = launch_decision.take_profit_pct
2313
+ auto_sl_pct = stop_loss_pct if stop_loss_pct is not None else launch_decision.stop_loss_pct
2314
+ auto_sl_trigger_px = (
2315
+ launch_decision.stop_loss_trigger_px
2316
+ if use_sar_stop_on_shortest_interval
2005
2317
  else None
2006
- ),
2007
- auto_sar_stop_periods=periods if use_sar_stop_on_shortest_interval else None,
2008
- auto_sar_acceleration=sar_acceleration if use_sar_stop_on_shortest_interval else None,
2009
- auto_sar_maximum=sar_maximum if use_sar_stop_on_shortest_interval else None,
2010
- auto_use_last_closed_candle=use_last_closed_candle,
2011
- auto_use_websocket_candles=use_websocket_candles,
2012
- account_address=account_address,
2013
- info=info,
2014
- exchange=exchange,
2015
- )
2016
- trade_cycle_closed_ms = now_ms()
2017
- completed_trades += 1
2018
- auto_trades_logger.info(
2019
- "[AUTO-TRADE-COMPLETE] coin=%s direction=%s size=%.8f tp_pct=%.8f sl_pct=%s completed_trades=%d",
2020
- selected_coin,
2021
- direction,
2022
- selected_size,
2023
- auto_tp_pct,
2024
- f"{auto_sl_pct:.8f}" if auto_sl_pct is not None else "N/A",
2025
- completed_trades,
2026
- )
2027
-
2028
- if info is None or exchange is None:
2029
- account_address, info, exchange = await init_clients(use_testnet, use_websocket=use_websocket)
2030
-
2031
- cycle_pnl, cycle_fills, pnl_reason = await fetch_trade_cycle_net_pnl(
2032
- info=info,
2033
- account_address=account_address,
2034
- coin=selected_coin,
2035
- start_time_ms=trade_cycle_started_ms,
2036
- end_time_ms=trade_cycle_closed_ms,
2037
- )
2038
- if pnl_reason:
2039
- print(
2040
- f"[AUTO-WARN] {selected_coin} post-trade PnL unavailable; "
2041
- f"risk session not updated for this cycle: {pnl_reason}"
2042
- )
2043
- _append_risk_event_log(
2044
- risk_session_log,
2045
- {
2046
- "ts_ms": trade_cycle_closed_ms,
2047
- "event": "cycle_pnl_unavailable",
2048
- "coin": selected_coin,
2049
- "reason": pnl_reason,
2050
- "session_pnl": risk_session.realized_pnl,
2051
- },
2052
- )
2053
- else:
2054
- coin_state = get_or_create_coin_session(coin_sessions, selected_coin)
2055
- coin_reason, session_reason = update_risk_after_trade_cycle(
2056
- coin_state=coin_state,
2057
- session_state=risk_session,
2058
- cycle_pnl=cycle_pnl,
2059
- cycle_started_ms=trade_cycle_started_ms,
2060
- cycle_closed_ms=trade_cycle_closed_ms,
2061
- max_coin_trades_per_session=max_coin_trades_per_session,
2062
- coin_session_cooldown_seconds=coin_session_cooldown_seconds,
2063
- coin_session_profit_target=coin_session_profit_target,
2064
- coin_session_min_profit_to_lock=coin_session_min_profit_to_lock,
2065
- coin_session_giveback_pct=coin_session_giveback_pct,
2066
- cooldown_after_loss_following_wins=cooldown_after_loss_following_wins,
2067
- session_profit_target=session_profit_target,
2068
- session_max_loss=session_max_loss,
2069
- session_giveback_pct=session_giveback_pct,
2070
2318
  )
2319
+ shortest_snapshot = get_shortest_interval_snapshot(launch_decision.snapshots)
2320
+ sl_display = f"{auto_sl_pct * 100:.4f}%" if auto_sl_pct is not None else "N/A"
2071
2321
  print(
2072
- f"[AUTO-RISK] {selected_coin} cycle_pnl={cycle_pnl:.6f} "
2073
- f"coin_pnl={coin_state.realized_pnl:.6f} coin_peak={coin_state.peak_pnl:.6f} "
2074
- f"coin_cycles={coin_state.completed_cycles} session_pnl={risk_session.realized_pnl:.6f} "
2075
- f"session_peak={risk_session.peak_pnl:.6f}"
2322
+ f"[AUTO] Launching managed task for {direction.upper()} {launch_coin}: size={launch_size:.8f}, "
2323
+ f"tp_pct={auto_tp_pct * 100:.4f}%, sl_pct={sl_display}, "
2324
+ f"sl_trigger={f'{auto_sl_trigger_px:.8f}' if auto_sl_trigger_px is not None else 'N/A'}"
2076
2325
  )
2077
- _append_risk_event_log(
2078
- risk_session_log,
2079
- {
2080
- "ts_ms": trade_cycle_closed_ms,
2081
- "event": "cycle_complete",
2082
- "coin": selected_coin,
2083
- "cycle_pnl": cycle_pnl,
2084
- "coin_pnl": coin_state.realized_pnl,
2085
- "coin_peak_pnl": coin_state.peak_pnl,
2086
- "coin_cycles": coin_state.completed_cycles,
2087
- "session_pnl": risk_session.realized_pnl,
2088
- "session_peak_pnl": risk_session.peak_pnl,
2089
- "fills_count": len(cycle_fills),
2090
- },
2091
- )
2092
- if coin_reason is not None:
2093
- print(
2094
- f"[AUTO-RISK] {selected_coin} entering cooldown for "
2095
- f"{coin_session_cooldown_seconds:.1f}s reason=\"{coin_reason}\""
2096
- )
2097
- _append_risk_event_log(
2098
- risk_session_log,
2099
- {
2100
- "ts_ms": trade_cycle_closed_ms,
2101
- "event": "coin_cooldown",
2102
- "coin": selected_coin,
2103
- "reason": coin_reason,
2104
- "coin_pnl": coin_state.realized_pnl,
2105
- "session_pnl": risk_session.realized_pnl,
2106
- },
2326
+ active_trade_tasks[launch_coin] = asyncio.create_task(
2327
+ _run_managed_auto_trade(
2328
+ managed_coin=launch_coin,
2329
+ managed_direction=direction,
2330
+ managed_size=launch_size,
2331
+ managed_tp_pct=auto_tp_pct,
2332
+ managed_sl_pct=auto_sl_pct,
2333
+ managed_sl_trigger_px=auto_sl_trigger_px,
2334
+ managed_shortest_snapshot=shortest_snapshot,
2107
2335
  )
2108
- if session_reason is not None:
2109
- print(f"[AUTO-RISK] Session stop triggered: {session_reason}")
2110
- _append_risk_event_log(
2111
- risk_session_log,
2112
- {
2113
- "ts_ms": trade_cycle_closed_ms,
2114
- "event": "session_stop",
2115
- "coin": selected_coin,
2116
- "reason": session_reason,
2117
- "coin_pnl": coin_state.realized_pnl,
2118
- "session_pnl": risk_session.realized_pnl,
2119
- },
2120
- )
2121
- if risk_session.stopped:
2122
- return
2123
-
2124
- if 0 < max_trades <= completed_trades:
2125
- print(f"[AUTO] Completed {completed_trades} trade(s); exiting auto mode.")
2126
- return
2127
-
2128
- if not loop_after_trade:
2129
- print(f"[AUTO] Completed {completed_trades} trade(s); exiting because auto looping is disabled.")
2130
- return
2131
-
2132
- if cooldown_after_trade > 0.0:
2133
- print(f"[AUTO] Cooling down for {cooldown_after_trade:.2f}s before resuming scans.")
2134
- await asyncio.sleep(cooldown_after_trade)
2336
+ )
2135
2337
 
2136
2338
  metrics_start_time_ms = int(time.time() * 1000)
2339
+ await _sleep_until_next_scan_batch()
2137
2340
  except KeyboardInterrupt:
2138
2341
  print("\n[!] Caught Ctrl+C, stopping auto trader.")
2139
2342
  finally:
@@ -602,6 +602,81 @@ async def exit_on_tp_reversal(
602
602
  return False
603
603
 
604
604
 
605
+ async def close_position_remainder_with_market_retries(
606
+ info: Info,
607
+ exchange: Exchange,
608
+ account_address: str,
609
+ coin: str,
610
+ side: str,
611
+ market_slippage: float,
612
+ poll_interval: float,
613
+ label: str = "TP-REMAINDER",
614
+ max_attempts: int = 3,
615
+ ) -> bool:
616
+ """Repeatedly reconcile and market-close a residual same-side position until flat."""
617
+ settle_before_close = min(max(poll_interval * 0.25, 0.15), 0.5)
618
+ settle_after_close = min(max(poll_interval * 0.5, 0.25), 1.0)
619
+
620
+ for attempt in range(1, max_attempts + 1):
621
+ await cancel_reduce_only_orders_for_coin(info, exchange, account_address, coin, only_tpsl=False)
622
+ await asyncio.sleep(settle_before_close)
623
+
624
+ remainder_pos = await get_position_for_coin(info, account_address, coin)
625
+ if remainder_pos is None:
626
+ print(f"[{label}] {coin} is already flat after cleanup.")
627
+ return True
628
+
629
+ try:
630
+ _, remainder_signed_size, _, remainder_side, remainder_abs = parse_position_snapshot(remainder_pos)
631
+ except RuntimeError as exc:
632
+ print(f"[{label}] {coin} remainder check could not parse live position: {exc}")
633
+ return False
634
+
635
+ if remainder_side != side or remainder_abs <= 0.0:
636
+ print(f"[{label}] {coin} no longer has a managed residual position.")
637
+ return True
638
+
639
+ print(
640
+ f"[{label}] attempt {attempt}/{max_attempts} closing residual {coin} position: "
641
+ f"signed={remainder_signed_size:.8f} abs={remainder_abs:.8f}"
642
+ )
643
+ try:
644
+ resp = await exchange.market_close(coin, slippage=market_slippage)
645
+ print(f"[{label}] market_close response: {resp}")
646
+ except Exception as exc:
647
+ print(f"[ERROR] {label} market_close failed for {coin} on attempt {attempt}/{max_attempts}: {exc}")
648
+ if attempt >= max_attempts:
649
+ return False
650
+ await asyncio.sleep(settle_after_close)
651
+ continue
652
+
653
+ await asyncio.sleep(settle_after_close)
654
+ final_remainder_pos = await get_position_for_coin(info, account_address, coin)
655
+ if final_remainder_pos is None:
656
+ print(f"[{label}] {coin} fully closed after residual market exit.")
657
+ return True
658
+
659
+ try:
660
+ _, final_signed_size, _, final_side, final_abs = parse_position_snapshot(final_remainder_pos)
661
+ except RuntimeError as exc:
662
+ print(f"[{label}-WARN] {coin} post-close position parse failed: {exc}")
663
+ if attempt >= max_attempts:
664
+ return False
665
+ continue
666
+
667
+ if final_side != side or final_abs <= 0.0:
668
+ print(f"[{label}] {coin} no longer has a managed residual position after market exit.")
669
+ return True
670
+
671
+ print(
672
+ f"[{label}-WARN] {coin} still open after attempt {attempt}/{max_attempts}: "
673
+ f"signed={final_signed_size:.8f} abs={final_abs:.8f}. Retrying cleanup."
674
+ )
675
+
676
+ print(f"[{label}-WARN] {coin} residual position remained open after {max_attempts} market-close attempts.")
677
+ return False
678
+
679
+
605
680
  async def place_hidden_take_profit_order(
606
681
  info: Info,
607
682
  exchange: Exchange,
@@ -1425,42 +1500,18 @@ async def monitor_bracket_position(
1425
1500
  f"[TP-REMAINDER] {coin} take-profit ladder is exhausted; "
1426
1501
  "checking whether any position remains open."
1427
1502
  )
1428
- await cancel_reduce_only_orders_for_coin(info, exchange, account_address, coin, only_tpsl=False)
1429
- await asyncio.sleep(min(max(poll_interval * 0.25, 0.15), 0.5))
1430
- remainder_pos = await get_position_for_coin(info, account_address, coin)
1431
- if remainder_pos is None:
1432
- print(f"[TP-REMAINDER] {coin} is already flat after TP cleanup.")
1433
- return
1434
- try:
1435
- _, remainder_signed_size, _, remainder_side, remainder_abs = parse_position_snapshot(
1436
- remainder_pos
1437
- )
1438
- except RuntimeError as exc:
1439
- print(f"[TP-REMAINDER] {coin} remainder check could not parse live position: {exc}")
1440
- return
1441
-
1442
- if remainder_side != side or remainder_abs <= 0.0:
1443
- print(f"[TP-REMAINDER] {coin} no longer has a managed residual position.")
1444
- return
1445
-
1446
- print(
1447
- f"[TP-REMAINDER] Residual {coin} position detected after all TP levels: "
1448
- f"signed={remainder_signed_size:.8f}. Closing remainder with market_close."
1503
+ close_ok = await close_position_remainder_with_market_retries(
1504
+ info=info,
1505
+ exchange=exchange,
1506
+ account_address=account_address,
1507
+ coin=coin,
1508
+ side=side,
1509
+ market_slippage=market_slippage,
1510
+ poll_interval=poll_interval,
1511
+ label="TP-REMAINDER",
1449
1512
  )
1450
- try:
1451
- resp = await exchange.market_close(coin, slippage=market_slippage)
1452
- print(f"[TP-REMAINDER] market_close response: {resp}")
1453
- except Exception as exc:
1454
- print(f"[ERROR] TP remainder market_close failed for {coin}: {exc}")
1455
- tp_remainder_close_in_progress = False
1456
- continue
1457
-
1458
- await asyncio.sleep(min(max(poll_interval * 0.5, 0.25), 1.0))
1459
- final_remainder_pos = await get_position_for_coin(info, account_address, coin)
1460
- if final_remainder_pos is None:
1461
- print(f"[TP-REMAINDER] {coin} fully closed after residual market exit.")
1513
+ if close_ok:
1462
1514
  return
1463
- print(f"[TP-REMAINDER-WARN] {coin} still appears open after residual market exit; retrying monitor.")
1464
1515
  tp_remainder_close_in_progress = False
1465
1516
 
1466
1517
  if tp_orders and use_trailing_tp and not hide_orders and not trailing_tp_armed:
@@ -9,7 +9,7 @@ README = ROOT / "README.md"
9
9
 
10
10
  setup(
11
11
  name="hypertrader",
12
- version="0.1.0",
12
+ version="0.1.2",
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=[
File without changes
File without changes
File without changes
File without changes