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.
- {hypertrader-0.1.0 → hypertrader-0.1.2}/PKG-INFO +8 -1
- {hypertrader-0.1.0 → hypertrader-0.1.2}/README.md +6 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/PKG-INFO +8 -1
- {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/entry_points.txt +1 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/requires.txt +1 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.py +7 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/auto_trader.py +398 -195
- {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/position_management.py +85 -34
- {hypertrader-0.1.0 → hypertrader-0.1.2}/setup.py +3 -1
- {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/SOURCES.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/dependency_links.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/hypertrader.egg-info/top_level.txt +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/market_maker.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/position_watcher.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/modes/trailing_stop.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/setup.cfg +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/constants.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/helpers.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/style.py +0 -0
- {hypertrader-0.1.0 → hypertrader-0.1.2}/utils/timer.py +0 -0
- {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.
|
|
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.
|
|
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
|
|
@@ -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
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
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
|
-
|
|
1814
|
-
|
|
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
|
|
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
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
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
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
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
|
|
1953
|
-
print('=' * 40 + f'
|
|
1954
|
-
await
|
|
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
|
-
|
|
1959
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
2073
|
-
f"
|
|
2074
|
-
f"
|
|
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
|
-
|
|
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}\""
|
|
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
|
-
|
|
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
|
|
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.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|