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.
- {hypertrader-0.1.0 → hypertrader-0.1.1}/PKG-INFO +7 -1
- {hypertrader-0.1.0 → hypertrader-0.1.1}/README.md +6 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/PKG-INFO +7 -1
- {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.py +6 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/auto_trader.py +307 -184
- {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/position_management.py +85 -34
- {hypertrader-0.1.0 → hypertrader-0.1.1}/setup.py +1 -1
- {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/SOURCES.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/dependency_links.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/entry_points.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/requires.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/hypertrader.egg-info/top_level.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/market_maker.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/position_watcher.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/modes/trailing_stop.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/setup.cfg +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/constants.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/helpers.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/style.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.1}/utils/timer.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
|
|
1814
|
-
|
|
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
|
|
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
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
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
|
|
2073
|
-
f"
|
|
2074
|
-
f"
|
|
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
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
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
|
-
|
|
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
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|