Spaces:
Running
Running
Daniel Alnawaz commited on
Hold fix
Browse files- .idea/copilotDiffState.xml +0 -0
- README.md +7 -0
- __pycache__/adaptive_bot.cpython-310.pyc +0 -0
- adaptive_bot.py +64 -1
.idea/copilotDiffState.xml
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
README.md
CHANGED
|
@@ -119,6 +119,12 @@ Safer learning defaults (already enabled by default):
|
|
| 119 |
python adaptive_bot.py --interval 15m --min-model-samples-for-signal 50 --pause-on-negative-expectancy
|
| 120 |
```
|
| 121 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
Enable periodic ML progress updates to Telegram (good for phone monitoring):
|
| 123 |
|
| 124 |
```bash
|
|
@@ -227,6 +233,7 @@ Environment variables used by the container:
|
|
| 227 |
- `BOT_PAUSE_EXPECTANCY_LOOKBACK` (default `30`)
|
| 228 |
- `BOT_PAUSE_EXPECTANCY_THRESHOLD_USD` (default `-1.0`)
|
| 229 |
- `BOT_PAUSE_MAX_DRAWDOWN_PCT` (default `12.0`)
|
|
|
|
| 230 |
- `ALLOW_LIVE_TRADING` (set to `1/true/yes` to manually approve live trading)
|
| 231 |
- `BOT_LIVE_STARTUP_TEST` (set non-empty to add `--live-startup-test`)
|
| 232 |
- `BOT_LIVE_STARTUP_TEST_COIN` (default `BTC`)
|
|
|
|
| 119 |
python adaptive_bot.py --interval 15m --min-model-samples-for-signal 50 --pause-on-negative-expectancy
|
| 120 |
```
|
| 121 |
|
| 122 |
+
Auto-expire the safety pause after 3 hours (default) so the bot can retry entries:
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
python adaptive_bot.py --interval 15m --pause-cooldown-minutes 180
|
| 126 |
+
```
|
| 127 |
+
|
| 128 |
Enable periodic ML progress updates to Telegram (good for phone monitoring):
|
| 129 |
|
| 130 |
```bash
|
|
|
|
| 233 |
- `BOT_PAUSE_EXPECTANCY_LOOKBACK` (default `30`)
|
| 234 |
- `BOT_PAUSE_EXPECTANCY_THRESHOLD_USD` (default `-1.0`)
|
| 235 |
- `BOT_PAUSE_MAX_DRAWDOWN_PCT` (default `12.0`)
|
| 236 |
+
- `BOT_PAUSE_COOLDOWN_MINUTES` (default `180`, set `0` to disable auto-expiry)
|
| 237 |
- `ALLOW_LIVE_TRADING` (set to `1/true/yes` to manually approve live trading)
|
| 238 |
- `BOT_LIVE_STARTUP_TEST` (set non-empty to add `--live-startup-test`)
|
| 239 |
- `BOT_LIVE_STARTUP_TEST_COIN` (default `BTC`)
|
__pycache__/adaptive_bot.cpython-310.pyc
CHANGED
|
Binary files a/__pycache__/adaptive_bot.cpython-310.pyc and b/__pycache__/adaptive_bot.cpython-310.pyc differ
|
|
|
adaptive_bot.py
CHANGED
|
@@ -535,6 +535,7 @@ class AdaptiveHyperliquidBot:
|
|
| 535 |
pause_expectancy_lookback: int,
|
| 536 |
pause_expectancy_threshold_usd: float,
|
| 537 |
pause_max_drawdown_pct: float,
|
|
|
|
| 538 |
telegram_ml_report_hours: float,
|
| 539 |
telegram_ml_report_min_trades: int,
|
| 540 |
health_host: str,
|
|
@@ -567,6 +568,9 @@ class AdaptiveHyperliquidBot:
|
|
| 567 |
self.pause_expectancy_lookback = int(max(10, pause_expectancy_lookback))
|
| 568 |
self.pause_expectancy_threshold_usd = float(pause_expectancy_threshold_usd)
|
| 569 |
self.pause_max_drawdown_pct = float(max(0.0, pause_max_drawdown_pct))
|
|
|
|
|
|
|
|
|
|
| 570 |
self.telegram_ml_report_hours = float(max(0.0, telegram_ml_report_hours))
|
| 571 |
self.telegram_ml_report_min_trades = int(max(0, telegram_ml_report_min_trades))
|
| 572 |
self.health_host = health_host
|
|
@@ -626,6 +630,16 @@ class AdaptiveHyperliquidBot:
|
|
| 626 |
def status_payload(self) -> Dict[str, Any]:
|
| 627 |
metrics = self.learning.global_metrics()
|
| 628 |
approval = self._live_approval_status_details()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 629 |
return {
|
| 630 |
"service": "hyperliquid-adaptive-bot",
|
| 631 |
"status": "ok" if self._running else "stopping",
|
|
@@ -656,6 +670,7 @@ class AdaptiveHyperliquidBot:
|
|
| 656 |
},
|
| 657 |
"model_samples": self.learning.model_samples,
|
| 658 |
"min_model_samples_for_signal": self.min_model_samples_for_signal,
|
|
|
|
| 659 |
}
|
| 660 |
|
| 661 |
def status_overview_payload(self) -> Dict[str, Any]:
|
|
@@ -785,23 +800,64 @@ class AdaptiveHyperliquidBot:
|
|
| 785 |
|
| 786 |
def _entry_pause_reason(self) -> Optional[str]:
|
| 787 |
if not self.pause_on_negative_expectancy:
|
|
|
|
|
|
|
| 788 |
return None
|
| 789 |
pnl = [float(t.get("pnl_usd", 0.0)) for t in self.learning.closed_trades]
|
| 790 |
if len(pnl) < self.pause_expectancy_lookback:
|
|
|
|
|
|
|
| 791 |
return None
|
| 792 |
exp_recent = float(np.mean(pnl[-self.pause_expectancy_lookback:]))
|
| 793 |
if exp_recent >= self.pause_expectancy_threshold_usd:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
return None
|
| 795 |
|
| 796 |
max_dd = self.learning.ml_progress_metrics().get("max_drawdown_pct")
|
| 797 |
max_dd_val = float(max_dd) if max_dd is not None else 0.0
|
| 798 |
if max_dd_val < self.pause_max_drawdown_pct:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 799 |
return None
|
| 800 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 801 |
f"safety_pause expectancy_{self.pause_expectancy_lookback}={exp_recent:+.3f} "
|
| 802 |
f"< {self.pause_expectancy_threshold_usd:+.3f} and max_dd={max_dd_val:.2f}%"
|
| 803 |
)
|
| 804 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
def _live_approval_granted(self) -> bool:
|
| 806 |
return self._live_approval_status_details()["granted"]
|
| 807 |
|
|
@@ -1423,6 +1479,12 @@ def build_arg_parser() -> argparse.ArgumentParser:
|
|
| 1423 |
default=float(os.environ.get("BOT_PAUSE_MAX_DRAWDOWN_PCT", "12.0")),
|
| 1424 |
help="Pause only when max drawdown is also at/above this percentage",
|
| 1425 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1426 |
p.add_argument(
|
| 1427 |
"--telegram-ml-report-hours",
|
| 1428 |
type=float,
|
|
@@ -1466,6 +1528,7 @@ def main() -> None:
|
|
| 1466 |
pause_expectancy_lookback=args.pause_expectancy_lookback,
|
| 1467 |
pause_expectancy_threshold_usd=args.pause_expectancy_threshold_usd,
|
| 1468 |
pause_max_drawdown_pct=args.pause_max_drawdown_pct,
|
|
|
|
| 1469 |
telegram_ml_report_hours=args.telegram_ml_report_hours,
|
| 1470 |
telegram_ml_report_min_trades=args.telegram_ml_report_min_trades,
|
| 1471 |
health_host=args.health_host,
|
|
|
|
| 535 |
pause_expectancy_lookback: int,
|
| 536 |
pause_expectancy_threshold_usd: float,
|
| 537 |
pause_max_drawdown_pct: float,
|
| 538 |
+
pause_cooldown_minutes: float,
|
| 539 |
telegram_ml_report_hours: float,
|
| 540 |
telegram_ml_report_min_trades: int,
|
| 541 |
health_host: str,
|
|
|
|
| 568 |
self.pause_expectancy_lookback = int(max(10, pause_expectancy_lookback))
|
| 569 |
self.pause_expectancy_threshold_usd = float(pause_expectancy_threshold_usd)
|
| 570 |
self.pause_max_drawdown_pct = float(max(0.0, pause_max_drawdown_pct))
|
| 571 |
+
self.pause_cooldown_minutes = float(max(0.0, pause_cooldown_minutes))
|
| 572 |
+
self._safety_pause_started_ts: Optional[float] = None
|
| 573 |
+
self._safety_pause_wait_recovery: bool = False
|
| 574 |
self.telegram_ml_report_hours = float(max(0.0, telegram_ml_report_hours))
|
| 575 |
self.telegram_ml_report_min_trades = int(max(0, telegram_ml_report_min_trades))
|
| 576 |
self.health_host = health_host
|
|
|
|
| 630 |
def status_payload(self) -> Dict[str, Any]:
|
| 631 |
metrics = self.learning.global_metrics()
|
| 632 |
approval = self._live_approval_status_details()
|
| 633 |
+
safety_pause = {
|
| 634 |
+
"enabled": self.pause_on_negative_expectancy,
|
| 635 |
+
"lookback": self.pause_expectancy_lookback,
|
| 636 |
+
"expectancy_threshold_usd": self.pause_expectancy_threshold_usd,
|
| 637 |
+
"max_drawdown_pct": self.pause_max_drawdown_pct,
|
| 638 |
+
"cooldown_minutes": self.pause_cooldown_minutes,
|
| 639 |
+
"active": self._safety_pause_started_ts is not None,
|
| 640 |
+
"active_since_ts": self._safety_pause_started_ts,
|
| 641 |
+
"wait_recovery": self._safety_pause_wait_recovery,
|
| 642 |
+
}
|
| 643 |
return {
|
| 644 |
"service": "hyperliquid-adaptive-bot",
|
| 645 |
"status": "ok" if self._running else "stopping",
|
|
|
|
| 670 |
},
|
| 671 |
"model_samples": self.learning.model_samples,
|
| 672 |
"min_model_samples_for_signal": self.min_model_samples_for_signal,
|
| 673 |
+
"safety_pause": safety_pause,
|
| 674 |
}
|
| 675 |
|
| 676 |
def status_overview_payload(self) -> Dict[str, Any]:
|
|
|
|
| 800 |
|
| 801 |
def _entry_pause_reason(self) -> Optional[str]:
|
| 802 |
if not self.pause_on_negative_expectancy:
|
| 803 |
+
self._safety_pause_started_ts = None
|
| 804 |
+
self._safety_pause_wait_recovery = False
|
| 805 |
return None
|
| 806 |
pnl = [float(t.get("pnl_usd", 0.0)) for t in self.learning.closed_trades]
|
| 807 |
if len(pnl) < self.pause_expectancy_lookback:
|
| 808 |
+
self._safety_pause_started_ts = None
|
| 809 |
+
self._safety_pause_wait_recovery = False
|
| 810 |
return None
|
| 811 |
exp_recent = float(np.mean(pnl[-self.pause_expectancy_lookback:]))
|
| 812 |
if exp_recent >= self.pause_expectancy_threshold_usd:
|
| 813 |
+
if self._safety_pause_wait_recovery:
|
| 814 |
+
logger.info("Safety pause re-armed after expectancy recovered (expectancy_%d=%+.3f).", self.pause_expectancy_lookback, exp_recent)
|
| 815 |
+
self._safety_pause_started_ts = None
|
| 816 |
+
self._safety_pause_wait_recovery = False
|
| 817 |
return None
|
| 818 |
|
| 819 |
max_dd = self.learning.ml_progress_metrics().get("max_drawdown_pct")
|
| 820 |
max_dd_val = float(max_dd) if max_dd is not None else 0.0
|
| 821 |
if max_dd_val < self.pause_max_drawdown_pct:
|
| 822 |
+
if self._safety_pause_wait_recovery:
|
| 823 |
+
logger.info("Safety pause re-armed after drawdown recovered (max_dd=%.2f%%).", max_dd_val)
|
| 824 |
+
self._safety_pause_started_ts = None
|
| 825 |
+
self._safety_pause_wait_recovery = False
|
| 826 |
return None
|
| 827 |
+
|
| 828 |
+
if self._safety_pause_wait_recovery:
|
| 829 |
+
return None
|
| 830 |
+
|
| 831 |
+
reason = (
|
| 832 |
f"safety_pause expectancy_{self.pause_expectancy_lookback}={exp_recent:+.3f} "
|
| 833 |
f"< {self.pause_expectancy_threshold_usd:+.3f} and max_dd={max_dd_val:.2f}%"
|
| 834 |
)
|
| 835 |
|
| 836 |
+
if self.pause_cooldown_minutes <= 0:
|
| 837 |
+
return reason
|
| 838 |
+
|
| 839 |
+
now_ts = time.time()
|
| 840 |
+
if self._safety_pause_started_ts is None:
|
| 841 |
+
self._safety_pause_started_ts = now_ts
|
| 842 |
+
|
| 843 |
+
cooldown_seconds = self.pause_cooldown_minutes * 60.0
|
| 844 |
+
elapsed_seconds = now_ts - self._safety_pause_started_ts
|
| 845 |
+
if elapsed_seconds >= cooldown_seconds:
|
| 846 |
+
self._safety_pause_started_ts = None
|
| 847 |
+
self._safety_pause_wait_recovery = True
|
| 848 |
+
logger.warning(
|
| 849 |
+
"Safety pause cooldown expired after %.1f minutes; allowing entries until metrics recover.",
|
| 850 |
+
self.pause_cooldown_minutes,
|
| 851 |
+
)
|
| 852 |
+
return None
|
| 853 |
+
|
| 854 |
+
remaining_seconds = int(max(0.0, cooldown_seconds - elapsed_seconds))
|
| 855 |
+
remaining_minutes = remaining_seconds // 60
|
| 856 |
+
remaining_remainder = remaining_seconds % 60
|
| 857 |
+
return (
|
| 858 |
+
f"{reason} (cooldown {remaining_minutes:02d}m{remaining_remainder:02d}s remaining)"
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
def _live_approval_granted(self) -> bool:
|
| 862 |
return self._live_approval_status_details()["granted"]
|
| 863 |
|
|
|
|
| 1479 |
default=float(os.environ.get("BOT_PAUSE_MAX_DRAWDOWN_PCT", "12.0")),
|
| 1480 |
help="Pause only when max drawdown is also at/above this percentage",
|
| 1481 |
)
|
| 1482 |
+
p.add_argument(
|
| 1483 |
+
"--pause-cooldown-minutes",
|
| 1484 |
+
type=float,
|
| 1485 |
+
default=float(os.environ.get("BOT_PAUSE_COOLDOWN_MINUTES", "180")),
|
| 1486 |
+
help="Minutes to keep safety pause active before auto-resuming entries until metrics recover (0 disables auto-expiry)",
|
| 1487 |
+
)
|
| 1488 |
p.add_argument(
|
| 1489 |
"--telegram-ml-report-hours",
|
| 1490 |
type=float,
|
|
|
|
| 1528 |
pause_expectancy_lookback=args.pause_expectancy_lookback,
|
| 1529 |
pause_expectancy_threshold_usd=args.pause_expectancy_threshold_usd,
|
| 1530 |
pause_max_drawdown_pct=args.pause_max_drawdown_pct,
|
| 1531 |
+
pause_cooldown_minutes=args.pause_cooldown_minutes,
|
| 1532 |
telegram_ml_report_hours=args.telegram_ml_report_hours,
|
| 1533 |
telegram_ml_report_min_trades=args.telegram_ml_report_min_trades,
|
| 1534 |
health_host=args.health_host,
|