hypertrader 0.1.0__tar.gz → 0.1.1__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.1}/PKG-INFO +7 -1
  2. {hypertrader-0.1.0 → hypertrader-0.1.1}/README.md +6 -0
  3. {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/PKG-INFO +7 -1
  4. {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.py +6 -0
  5. {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/auto_trader.py +307 -184
  6. {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/position_management.py +85 -34
  7. {hypertrader-0.1.0 → hypertrader-0.1.1}/setup.py +1 -1
  8. {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/SOURCES.txt +0 -0
  9. {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/dependency_links.txt +0 -0
  10. {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/entry_points.txt +0 -0
  11. {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/requires.txt +0 -0
  12. {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/top_level.txt +0 -0
  13. {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/market_maker.py +0 -0
  14. {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/position_watcher.py +0 -0
  15. {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/trailing_stop.py +0 -0
  16. {hypertrader-0.1.0 → hypertrader-0.1.1}/setup.cfg +0 -0
  17. {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/constants.py +0 -0
  18. {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/helpers.py +0 -0
  19. {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/style.py +0 -0
  20. {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/timer.py +0 -0
  21. {hypertrader-0.1.0 → hypertrader-0.1.1}/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.1
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -58,6 +58,12 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
58
58
  - Network access to Hyperliquid APIs
59
59
  - Account created with referral code [DARKEREGO](https://app.hyperliquid.xyz/join/DARKEREGO) (see account creation [*])
60
60
 
61
+ Install from pip:
62
+
63
+ <pre>
64
+ pip install hypertrader[auto]
65
+ </pre>
66
+
61
67
  Base dependencies:
62
68
 
63
69
  ```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.1
4
4
  Summary: Async Hyperliquid trading helper built on the async SDK.
5
5
  Author: darkerego
6
6
  License: MIT
@@ -58,6 +58,12 @@ The canonical bot file in this repo is [`hypertrader.py`](/media/anon/developmen
58
58
  - Network access to Hyperliquid APIs
59
59
  - Account created with referral code [DARKEREGO](https://app.hyperliquid.xyz/join/DARKEREGO) (see account creation [*])
60
60
 
61
+ Install from pip:
62
+
63
+ <pre>
64
+ pip install hypertrader[auto]
65
+ </pre>
66
+
61
67
  Base dependencies:
62
68
 
63
69
  ```bash
@@ -267,6 +267,8 @@ def build_arg_parser() -> argparse.ArgumentParser:
267
267
  help="Seconds between signal scans. Default: 30.0.")
268
268
  auto_parser.add_argument("--max-concurrent-scans", type=int, default=3,
269
269
  help="Maximum markets to scan concurrently per auto loop. Default: 3.")
270
+ auto_parser.add_argument("--max-positions", type=int, default=3,
271
+ help="Maximum concurrently open or actively managed auto positions. Default: 3.")
270
272
  auto_parser.add_argument("--min-agreement", type=int, default=0,
271
273
  help="Intervals required to agree. 0 means all configured intervals. Default: 0.")
272
274
  auto_parser.add_argument("--adx-threshold", type=float, default=20.0,
@@ -549,6 +551,9 @@ async def async_main(argv: Optional[List[str]] = None) -> None:
549
551
  if args.max_concurrent_scans <= 0:
550
552
  print("[ERROR] --max-concurrent-scans must be > 0.")
551
553
  sys.exit(1)
554
+ if args.max_positions <= 0:
555
+ print("[ERROR] --max-positions must be > 0.")
556
+ sys.exit(1)
552
557
  if args.min_agreement < 0:
553
558
  print("[ERROR] --min-agreement must be >= 0.")
554
559
  sys.exit(1)
@@ -625,6 +630,7 @@ async def async_main(argv: Optional[List[str]] = None) -> None:
625
630
  periods=args.auto_periods,
626
631
  scan_interval=args.scan_interval,
627
632
  max_concurrent_scans=args.max_concurrent_scans,
633
+ max_positions=args.max_positions,
628
634
  min_agreement=args.min_agreement,
629
635
  adx_threshold=args.adx_threshold,
630
636
  take_profit_pct=args.take_profit_pct,
@@ -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,18 @@ 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
+
192
205
  def now_ms() -> int:
193
206
  return int(time.time() * 1000)
194
207
 
@@ -267,6 +280,16 @@ def coin_is_blocked_by_risk(
267
280
  return False, ""
268
281
 
269
282
 
283
+ def coin_is_in_post_trade_cooldown(
284
+ coin_state: CoinTradeSessionState,
285
+ current_time_ms: int,
286
+ ) -> Tuple[bool, str]:
287
+ if coin_state.post_trade_cooldown_until_ms > current_time_ms:
288
+ remaining_seconds = (coin_state.post_trade_cooldown_until_ms - current_time_ms) / 1000.0
289
+ return True, f"post-trade cooldown active {remaining_seconds:.1f}s remaining"
290
+ return False, ""
291
+
292
+
270
293
  def set_coin_cooldown(
271
294
  coin_state: CoinTradeSessionState,
272
295
  cooldown_seconds: float,
@@ -1628,6 +1651,7 @@ async def run_auto_trader(
1628
1651
  periods: int,
1629
1652
  scan_interval: float,
1630
1653
  max_concurrent_scans: int,
1654
+ max_positions: int,
1631
1655
  min_agreement: int,
1632
1656
  adx_threshold: float,
1633
1657
  take_profit_pct: Optional[float],
@@ -1702,6 +1726,8 @@ async def run_auto_trader(
1702
1726
  raise RuntimeError("--scan-interval must be > 0.")
1703
1727
  if max_concurrent_scans <= 0:
1704
1728
  raise RuntimeError("--max-concurrent-scans must be > 0.")
1729
+ if max_positions <= 0:
1730
+ raise RuntimeError("--max-positions must be > 0.")
1705
1731
  if min_agreement < 0:
1706
1732
  raise RuntimeError("--min-agreement must be >= 0. Use 0 to require all configured intervals.")
1707
1733
  if adx_threshold < 0.0:
@@ -1767,6 +1793,9 @@ async def run_auto_trader(
1767
1793
  metrics_start_time_ms = int(time.time() * 1000)
1768
1794
  risk_session = AutoRiskSessionState(started_ms=metrics_start_time_ms)
1769
1795
  coin_sessions: Dict[str, CoinTradeSessionState] = {}
1796
+ active_trade_tasks: Dict[str, asyncio.Task[AutoManagedTradeResult]] = {}
1797
+ stop_requested = False
1798
+ stop_reason = ""
1770
1799
  print("============================================================")
1771
1800
  print(" Hyperliquid Async Auto Trader")
1772
1801
  print("============================================================")
@@ -1793,6 +1822,7 @@ async def run_auto_trader(
1793
1822
  print(f"Trailing TP pct: {trailing_tp_profit_pct * 100:.4f}% of favorable unrealized profit")
1794
1823
  print(f"Scan interval: {scan_interval:.2f}s")
1795
1824
  print(f"Scan concurrency: {max_concurrent_scans}")
1825
+ print(f"Max positions: {max_positions}")
1796
1826
  print(f"Dry run: {dry_run}")
1797
1827
  print(f"Max trades: {'unlimited' if max_trades == 0 else max_trades}")
1798
1828
  print(f"Loop after trade: {loop_after_trade}")
@@ -1809,9 +1839,228 @@ async def run_auto_trader(
1809
1839
  print(f" session_giveback_pct={session_giveback_pct}")
1810
1840
  print("============================================================")
1811
1841
 
1842
+ async def _run_managed_auto_trade(
1843
+ managed_coin: str,
1844
+ managed_direction: str,
1845
+ managed_size: float,
1846
+ managed_tp_pct: float,
1847
+ managed_sl_pct: Optional[float],
1848
+ managed_sl_trigger_px: Optional[float],
1849
+ managed_shortest_snapshot: Optional[AutoIntervalSignal],
1850
+ ) -> AutoManagedTradeResult:
1851
+ trade_cycle_started_ms = now_ms()
1852
+ await run_bracket_entry(
1853
+ coin=managed_coin,
1854
+ direction=managed_direction,
1855
+ size=managed_size,
1856
+ take_profit_pct=managed_tp_pct,
1857
+ stop_loss_pct=managed_sl_pct,
1858
+ stop_loss_trigger_px=managed_sl_trigger_px,
1859
+ take_profit_levels=take_profit_levels,
1860
+ use_trailing_tp=use_trailing_tp,
1861
+ trailing_tp_trigger_level=trailing_tp_trigger_level,
1862
+ trailing_tp_profit_pct=trailing_tp_profit_pct,
1863
+ entry_retries=entry_retries,
1864
+ entry_repost_interval=entry_repost_interval,
1865
+ poll_interval=poll_interval,
1866
+ tp_reversal_pct=tp_reversal_pct,
1867
+ entry_tif=entry_tif,
1868
+ tp_tif=tp_tif,
1869
+ market_fallback=market_fallback,
1870
+ market_slippage=market_slippage,
1871
+ cancel_existing_tpsl=cancel_existing_tpsl,
1872
+ tp_reversal_limit_exit=tp_reversal_limit_exit,
1873
+ tp_reversal_stop_buffer_pct=tp_reversal_stop_buffer_pct,
1874
+ use_testnet=use_testnet,
1875
+ use_websocket=use_websocket,
1876
+ hide_orders=hide_orders,
1877
+ auto_sar_stop_interval=(
1878
+ managed_shortest_snapshot.interval
1879
+ if use_sar_stop_on_shortest_interval and managed_shortest_snapshot is not None
1880
+ else None
1881
+ ),
1882
+ auto_sar_stop_periods=periods if use_sar_stop_on_shortest_interval else None,
1883
+ auto_sar_acceleration=sar_acceleration if use_sar_stop_on_shortest_interval else None,
1884
+ auto_sar_maximum=sar_maximum if use_sar_stop_on_shortest_interval else None,
1885
+ auto_use_last_closed_candle=use_last_closed_candle,
1886
+ auto_use_websocket_candles=use_websocket_candles,
1887
+ account_address=account_address,
1888
+ info=info,
1889
+ exchange=exchange,
1890
+ )
1891
+ return AutoManagedTradeResult(
1892
+ coin=managed_coin,
1893
+ direction=managed_direction,
1894
+ size=managed_size,
1895
+ take_profit_pct=managed_tp_pct,
1896
+ stop_loss_pct=managed_sl_pct,
1897
+ started_ms=trade_cycle_started_ms,
1898
+ closed_ms=now_ms(),
1899
+ )
1900
+
1901
+ async def _drain_completed_trade_tasks() -> None:
1902
+ nonlocal completed_trades, stop_requested, stop_reason
1903
+ finished_coins = [task_coin for task_coin, task in active_trade_tasks.items() if task.done()]
1904
+ for finished_coin in finished_coins:
1905
+ task = active_trade_tasks.pop(finished_coin)
1906
+ try:
1907
+ trade_result = task.result()
1908
+ except asyncio.CancelledError:
1909
+ print(f"[AUTO] Managed trade task for {finished_coin} was cancelled.")
1910
+ continue
1911
+ except Exception as exc:
1912
+ print(f"[AUTO-WARN] Managed trade task failed for {finished_coin}: {exc}")
1913
+ logger.exception("[AUTO] Managed trade task failed for %s", finished_coin)
1914
+ continue
1915
+
1916
+ completed_trades += 1
1917
+ auto_trades_logger.info(
1918
+ "[AUTO-TRADE-COMPLETE] coin=%s direction=%s size=%.8f tp_pct=%.8f sl_pct=%s completed_trades=%d",
1919
+ trade_result.coin,
1920
+ trade_result.direction,
1921
+ trade_result.size,
1922
+ trade_result.take_profit_pct,
1923
+ f"{trade_result.stop_loss_pct:.8f}" if trade_result.stop_loss_pct is not None else "N/A",
1924
+ completed_trades,
1925
+ )
1926
+
1927
+ if info is None or exchange is None:
1928
+ raise RuntimeError("Initialized clients became unavailable while draining auto trade tasks.")
1929
+
1930
+ cycle_pnl, cycle_fills, pnl_reason = await fetch_trade_cycle_net_pnl(
1931
+ info=info,
1932
+ account_address=account_address,
1933
+ coin=trade_result.coin,
1934
+ start_time_ms=trade_result.started_ms,
1935
+ end_time_ms=trade_result.closed_ms,
1936
+ )
1937
+ if pnl_reason:
1938
+ print(
1939
+ f"[AUTO-WARN] {trade_result.coin} post-trade PnL unavailable; "
1940
+ f"risk session not updated for this cycle: {pnl_reason}"
1941
+ )
1942
+ _append_risk_event_log(
1943
+ risk_session_log,
1944
+ {
1945
+ "ts_ms": trade_result.closed_ms,
1946
+ "event": "cycle_pnl_unavailable",
1947
+ "coin": trade_result.coin,
1948
+ "reason": pnl_reason,
1949
+ "session_pnl": risk_session.realized_pnl,
1950
+ },
1951
+ )
1952
+ else:
1953
+ coin_state = get_or_create_coin_session(coin_sessions, trade_result.coin)
1954
+ coin_reason, session_reason = update_risk_after_trade_cycle(
1955
+ coin_state=coin_state,
1956
+ session_state=risk_session,
1957
+ cycle_pnl=cycle_pnl,
1958
+ cycle_started_ms=trade_result.started_ms,
1959
+ cycle_closed_ms=trade_result.closed_ms,
1960
+ max_coin_trades_per_session=max_coin_trades_per_session,
1961
+ coin_session_cooldown_seconds=coin_session_cooldown_seconds,
1962
+ coin_session_profit_target=coin_session_profit_target,
1963
+ coin_session_min_profit_to_lock=coin_session_min_profit_to_lock,
1964
+ coin_session_giveback_pct=coin_session_giveback_pct,
1965
+ cooldown_after_loss_following_wins=cooldown_after_loss_following_wins,
1966
+ session_profit_target=session_profit_target,
1967
+ session_max_loss=session_max_loss,
1968
+ session_giveback_pct=session_giveback_pct,
1969
+ )
1970
+ print(
1971
+ f"[AUTO-RISK] {trade_result.coin} cycle_pnl={cycle_pnl:.6f} "
1972
+ f"coin_pnl={coin_state.realized_pnl:.6f} coin_peak={coin_state.peak_pnl:.6f} "
1973
+ f"coin_cycles={coin_state.completed_cycles} session_pnl={risk_session.realized_pnl:.6f} "
1974
+ f"session_peak={risk_session.peak_pnl:.6f}"
1975
+ )
1976
+ _append_risk_event_log(
1977
+ risk_session_log,
1978
+ {
1979
+ "ts_ms": trade_result.closed_ms,
1980
+ "event": "cycle_complete",
1981
+ "coin": trade_result.coin,
1982
+ "cycle_pnl": cycle_pnl,
1983
+ "coin_pnl": coin_state.realized_pnl,
1984
+ "coin_peak_pnl": coin_state.peak_pnl,
1985
+ "coin_cycles": coin_state.completed_cycles,
1986
+ "session_pnl": risk_session.realized_pnl,
1987
+ "session_peak_pnl": risk_session.peak_pnl,
1988
+ "fills_count": len(cycle_fills),
1989
+ },
1990
+ )
1991
+ if coin_reason is not None:
1992
+ print(
1993
+ f"[AUTO-RISK] {trade_result.coin} entering cooldown for "
1994
+ f"{coin_session_cooldown_seconds:.1f}s reason=\"{coin_reason}\""
1995
+ )
1996
+ _append_risk_event_log(
1997
+ risk_session_log,
1998
+ {
1999
+ "ts_ms": trade_result.closed_ms,
2000
+ "event": "coin_cooldown",
2001
+ "coin": trade_result.coin,
2002
+ "reason": coin_reason,
2003
+ "coin_pnl": coin_state.realized_pnl,
2004
+ "session_pnl": risk_session.realized_pnl,
2005
+ },
2006
+ )
2007
+ if session_reason is not None:
2008
+ print(f"[AUTO-RISK] Session stop triggered: {session_reason}")
2009
+ _append_risk_event_log(
2010
+ risk_session_log,
2011
+ {
2012
+ "ts_ms": trade_result.closed_ms,
2013
+ "event": "session_stop",
2014
+ "coin": trade_result.coin,
2015
+ "reason": session_reason,
2016
+ "coin_pnl": coin_state.realized_pnl,
2017
+ "session_pnl": risk_session.realized_pnl,
2018
+ },
2019
+ )
2020
+
2021
+ if cooldown_after_trade > 0.0:
2022
+ coin_state = get_or_create_coin_session(coin_sessions, trade_result.coin)
2023
+ coin_state.post_trade_cooldown_until_ms = max(
2024
+ coin_state.post_trade_cooldown_until_ms,
2025
+ trade_result.closed_ms + int(cooldown_after_trade * 1000),
2026
+ )
2027
+ cooldown_now_ms = now_ms()
2028
+ remaining_seconds = max(
2029
+ 0.0,
2030
+ (coin_state.post_trade_cooldown_until_ms - cooldown_now_ms) / 1000.0,
2031
+ )
2032
+ print(
2033
+ f"[AUTO] {trade_result.coin} post-trade cooldown active for "
2034
+ f"{remaining_seconds:.2f}s after close."
2035
+ )
2036
+
2037
+ if risk_session.stopped:
2038
+ stop_requested = True
2039
+ stop_reason = risk_session.stop_reason or "session risk stop"
2040
+ elif 0 < max_trades <= completed_trades:
2041
+ stop_requested = True
2042
+ stop_reason = f"max trades reached ({completed_trades})"
2043
+ elif not loop_after_trade:
2044
+ stop_requested = True
2045
+ stop_reason = f"completed {completed_trades} trade(s) and auto looping is disabled"
2046
+
1812
2047
  while True:
1813
- if 0 < max_trades <= completed_trades:
1814
- print(f"[AUTO] Max trades reached ({completed_trades}); exiting auto mode.")
2048
+ await _drain_completed_trade_tasks()
2049
+ if stop_requested:
2050
+ if active_trade_tasks:
2051
+ print(
2052
+ f"[AUTO] Stop requested ({stop_reason}); waiting for "
2053
+ f"{len(active_trade_tasks)} active managed trade(s) to finish."
2054
+ )
2055
+ done, _pending = await asyncio.wait(
2056
+ list(active_trade_tasks.values()),
2057
+ timeout=min(scan_interval, 5.0),
2058
+ return_when=asyncio.FIRST_COMPLETED,
2059
+ )
2060
+ if not done:
2061
+ await asyncio.sleep(min(scan_interval, 1.0))
2062
+ continue
2063
+ print(f"[AUTO] {stop_reason}; exiting auto mode.")
1815
2064
  return
1816
2065
 
1817
2066
  if coin is not None:
@@ -1836,6 +2085,13 @@ async def run_auto_trader(
1836
2085
  risk_session_log=risk_session_log,
1837
2086
  session_pnl=risk_session.realized_pnl,
1838
2087
  )
2088
+ in_post_trade_cooldown, post_trade_reason = coin_is_in_post_trade_cooldown(
2089
+ coin_state,
2090
+ current_time_ms,
2091
+ )
2092
+ if in_post_trade_cooldown:
2093
+ print(f"[AUTO] Skipping {scan_coin}: {post_trade_reason}")
2094
+ continue
1839
2095
  is_blocked, block_reason = coin_is_blocked_by_risk(coin_state, current_time_ms)
1840
2096
  if is_blocked:
1841
2097
  print(f"[AUTO-RISK] Skipping {scan_coin}: {block_reason}")
@@ -1855,7 +2111,7 @@ async def run_auto_trader(
1855
2111
  eligible_scan_coins.append(scan_coin)
1856
2112
 
1857
2113
  if not eligible_scan_coins:
1858
- print("[AUTO-RISK] All scan candidates are blocked by coin session controls.")
2114
+ print("[AUTO] All scan candidates are temporarily blocked by cooldown or coin session controls.")
1859
2115
  await asyncio.sleep(scan_interval)
1860
2116
  continue
1861
2117
 
@@ -1864,9 +2120,15 @@ async def run_auto_trader(
1864
2120
  account_address=account_address,
1865
2121
  metrics_start_time_ms=metrics_start_time_ms,
1866
2122
  )
1867
- selected_coin: Optional[str] = None
1868
- selected_decision: Optional[AutoTradeDecision] = None
1869
- selected_size: Optional[float] = None
2123
+ reserved_coins = set(active_trade_tasks.keys()) | set(shared_snapshot.positions_by_coin.keys())
2124
+ available_slots = max_positions - len(reserved_coins)
2125
+ if available_slots <= 0:
2126
+ print(
2127
+ f"[AUTO] Max position capacity reached "
2128
+ f"({len(reserved_coins)}/{max_positions}); delaying new entries."
2129
+ )
2130
+ await asyncio.sleep(scan_interval)
2131
+ continue
1870
2132
 
1871
2133
  scan_concurrency = min(max_concurrent_scans, max(1, len(eligible_scan_coins)))
1872
2134
  scan_semaphore = asyncio.Semaphore(scan_concurrency)
@@ -1900,6 +2162,7 @@ async def run_auto_trader(
1900
2162
  )
1901
2163
 
1902
2164
  scan_tasks = [asyncio.create_task(_scan_with_limit(scan_coin)) for scan_coin in eligible_scan_coins]
2165
+ launch_specs: List[Tuple[str, AutoTradeDecision, float]] = []
1903
2166
  try:
1904
2167
  for completed_task in asyncio.as_completed(scan_tasks):
1905
2168
  try:
@@ -1922,6 +2185,12 @@ async def run_auto_trader(
1922
2185
 
1923
2186
  if candidate.decision is None or candidate.decision.direction is None or candidate.decision.take_profit_pct is None:
1924
2187
  continue
2188
+ if candidate.coin in reserved_coins:
2189
+ print(
2190
+ f"[AUTO] {candidate.coin} is already reserved by an active position or managed task; "
2191
+ f"skipping duplicate auto entry."
2192
+ )
2193
+ continue
1925
2194
 
1926
2195
  try:
1927
2196
  resolved_size, size_reason = await resolve_auto_trade_size(
@@ -1936,204 +2205,58 @@ async def run_auto_trader(
1936
2205
  print(f"[AUTO-WARN] {candidate.coin} size resolution failed; skipping trade candidate: {exc}")
1937
2206
  continue
1938
2207
  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
2208
+ launch_specs.append((candidate.coin, candidate.decision, resolved_size))
2209
+ reserved_coins.add(candidate.coin)
2210
+ if len(launch_specs) >= available_slots:
2211
+ continue
1944
2212
  except Exception as exc:
1945
2213
  logger.error(exc)
1946
2214
 
1947
- for task in scan_tasks:
1948
- if not task.done():
1949
- task.cancel()
1950
2215
  await asyncio.gather(*scan_tasks, return_exceptions=True)
1951
2216
 
1952
- if selected_coin is None or selected_decision is None or selected_size is None:
2217
+ if not launch_specs:
1953
2218
  print('=' * 40 + f'Sleeping {scan_interval} seconds .. ' + '=' * 40)
1954
2219
  await asyncio.sleep(scan_interval)
1955
2220
  continue
1956
2221
 
1957
2222
  if dry_run:
1958
- print(f"[AUTO] Dry run enabled; {selected_coin} signal will not be traded.")
2223
+ for launch_coin, _launch_decision, _launch_size in launch_specs:
2224
+ print(f"[AUTO] Dry run enabled; {launch_coin} signal will not be traded.")
1959
2225
  await asyncio.sleep(scan_interval)
1960
2226
  continue
1961
2227
 
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
2228
+ for launch_coin, launch_decision, launch_size in launch_specs:
2229
+ direction = launch_decision.direction
2230
+ if direction is None or launch_decision.take_profit_pct is None:
2231
+ continue
2232
+ auto_tp_pct = launch_decision.take_profit_pct
2233
+ auto_sl_pct = stop_loss_pct if stop_loss_pct is not None else launch_decision.stop_loss_pct
2234
+ auto_sl_trigger_px = (
2235
+ launch_decision.stop_loss_trigger_px
2236
+ if use_sar_stop_on_shortest_interval
2005
2237
  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
2238
  )
2239
+ shortest_snapshot = get_shortest_interval_snapshot(launch_decision.snapshots)
2240
+ sl_display = f"{auto_sl_pct * 100:.4f}%" if auto_sl_pct is not None else "N/A"
2071
2241
  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}"
2242
+ f"[AUTO] Launching managed task for {direction.upper()} {launch_coin}: size={launch_size:.8f}, "
2243
+ f"tp_pct={auto_tp_pct * 100:.4f}%, sl_pct={sl_display}, "
2244
+ f"sl_trigger={f'{auto_sl_trigger_px:.8f}' if auto_sl_trigger_px is not None else 'N/A'}"
2076
2245
  )
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}\""
2246
+ active_trade_tasks[launch_coin] = asyncio.create_task(
2247
+ _run_managed_auto_trade(
2248
+ managed_coin=launch_coin,
2249
+ managed_direction=direction,
2250
+ managed_size=launch_size,
2251
+ managed_tp_pct=auto_tp_pct,
2252
+ managed_sl_pct=auto_sl_pct,
2253
+ managed_sl_trigger_px=auto_sl_trigger_px,
2254
+ managed_shortest_snapshot=shortest_snapshot,
2096
2255
  )
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
- },
2107
- )
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)
2256
+ )
2135
2257
 
2136
2258
  metrics_start_time_ms = int(time.time() * 1000)
2259
+ await asyncio.sleep(scan_interval)
2137
2260
  except KeyboardInterrupt:
2138
2261
  print("\n[!] Caught Ctrl+C, stopping auto trader.")
2139
2262
  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.1",
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",
File without changes
File without changes
File without changes
File without changes