Spaces:
Sleeping
Sleeping
Daniel Alnawaz commited on
diagnostics
Browse files- .idea/copilotDiffState.xml +34 -0
- .idea/workspace.xml +1 -147
- README.md +41 -0
- __pycache__/adaptive_bot.cpython-310.pyc +0 -0
- __pycache__/diagnostics_smoke_test.cpython-310.pyc +0 -0
- __pycache__/ml_diagnostics.cpython-310.pyc +0 -0
- diagnostics_smoke_test.py +63 -0
- ml_diagnostics.py +308 -0
.idea/copilotDiffState.xml
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="CopilotDiffPersistence">
|
| 4 |
+
<option name="pendingDiffs">
|
| 5 |
+
<map>
|
| 6 |
+
<entry key="$PROJECT_DIR$/README.md">
|
| 7 |
+
<value>
|
| 8 |
+
<PendingDiffInfo>
|
| 9 |
+
<option name="filePath" value="$PROJECT_DIR$/README.md" />
|
| 10 |
+
<option name="originalContent" value="--- title: Hyperliquid Adaptive Bot emoji: "" colorFrom: blue colorTo: purple sdk: docker pinned: false --- # Hyperliquid Adaptive Bot This project now includes an adaptive ML bot that uses your existing Hyperliquid framework. ## What it does - Trades **only** `BTC`, `ETH`, and `SOL` - Runs in **paper mode by default** - Learns from closed paper trades with an online ML model (`SGDClassifier`) - Persists everything learned to disk (`learning_state.joblib`) so restart resumes learning - Persists paper runtime state (equity, open positions, entry features) so restart does not break learning continuity - Uses a profitability gate before any live execution is allowed - Sends Telegram alerts on trade open/close and a midnight UTC summary - Sends a readiness alert when paper performance is strong enough for live trading - Optional startup live transaction test (tiny BUY then SELL) to verify live order execution path - Can mirror to live orders only when: - `--enable-live` is set - paper performance is consistently profitable - manual approval is granted (`ALLOW_LIVE_TRADING=1` or approval flag file exists) - `HL_PRIVATE_KEY` is configured ## Files - `hbot.py`: existing Hyperliquid API framework and order placement helpers - `adaptive_bot.py`: new adaptive ML paper/live-gated trading bot - `smoke_test.py`: offline persistence and learning smoke test - `paper_report.json`: runtime status and metrics output (created at runtime) ## Health endpoints (for HF + UptimeRobot) - `GET /` -> lightweight status response (`200`) for uptime checks - `GET /healthz` -> same as root (`200`) - `GET /status` -> concise overview (equity, open position count, performance, live gate) - `GET /status/full` -> full runtime JSON (equity, open positions, metrics, thresholds) - `GET /trades` -> open positions list and count If your Space is `https://chigginrice-hyperliquid-bot.hf.space/`, your existing root monitor will work as-is. ## Quick start 1. Install dependencies: ```bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` 2. Run the offline smoke test: ```bash python smoke_test.py ``` 3. Start the adaptive paper bot: ```bash python adaptive_bot.py --interval 15m --poll-seconds 45 ``` Optional knobs for early growth from scratch: ```bash python adaptive_bot.py --interval 15m --exploration-rate 0.08 ``` ## Optional: allow live mirror only after paper consistency gate ```bash export HL_PRIVATE_KEY="your_private_key" # optional if using delegated account export HL_ACCOUNT_ADDRESS="0x..." python adaptive_bot.py --interval 15m --enable-live ``` By default, live arm still requires manual approval even after the paper gate is passed: ```bash export ALLOW_LIVE_TRADING=1 # or: touch allow_live_trading.flag python adaptive_bot.py --interval 15m --enable-live ``` Strict two-step approval mode (recommended for safety): ```bash export ALLOW_LIVE_TRADING=1 touch allow_live_trading.flag python adaptive_bot.py --interval 15m --enable-live --live-approval-mode both ``` Optional startup live execution test (real tiny transaction): ```bash python adaptive_bot.py --interval 15m --enable-live --live-startup-test --live-startup-test-coin BTC --live-startup-test-notional 12 ``` ## Docker (Hugging Face) `Dockerfile` is included for Docker-based hosting. Build locally: ```bash docker build -t hyperliquid-adaptive-bot . ``` Run locally in paper mode: ```bash docker run --rm \ -e BOT_INTERVAL=15m \ -e BOT_POLL_SECONDS=45 \ -v "$(pwd)/data:/data" \ hyperliquid-adaptive-bot ``` Run with live mode allowed after paper gate passes: ```bash docker run --rm \ -e BOT_ENABLE_LIVE=1 \ -e HL_PRIVATE_KEY="your_private_key" \ -e HL_ACCOUNT_ADDRESS="0x..." \ -v "$(pwd)/data:/data" \ hyperliquid-adaptive-bot ``` Environment variables used by the container: - `BOT_INTERVAL` (default `15m`) - `BOT_POLL_SECONDS` (default `45`) - `BOT_STATE_FILE` (default `/data/learning_state.joblib`) - `BOT_REPORT_FILE` (default `/data/paper_report.json`) - `BOT_ENABLE_LIVE` (set any non-empty value to append `--enable-live`) - `BOT_HEALTH_HOST` (default `0.0.0.0`) - `BOT_HEALTH_PORT` (default `7860`) - `PORT` (default `7860`, used by Hugging Face Spaces) - `ALLOW_LIVE_TRADING` (set to `1/true/yes` to manually approve live trading) - `BOT_LIVE_STARTUP_TEST` (set non-empty to add `--live-startup-test`) - `BOT_LIVE_STARTUP_TEST_COIN` (default `BTC`) - `BOT_LIVE_STARTUP_TEST_NOTIONAL` (default `12`) - `TELEGRAM_BOT_TOKEN` (required for Telegram notifications) - `TELEGRAM_CHAT_ID` (required for Telegram notifications) - `PROXY_URL` (required by `telegram_notifier.py` to forward messages) - `HL_PRIVATE_KEY` (required for live mirror) - `HL_ACCOUNT_ADDRESS` (optional) ## Telegram notifications - Entry alert: sent when a paper position opens - Exit alert: sent when TP/SL closes a position - Midnight summary: sent once per day at `00:00 UTC` - Readiness alert: sent when profitability gate is reached and bot is ready to request live approval Summary includes last-24h trades, win rate, PnL, and overall cumulative stats. ## How the ML learns At a high level, this bot uses online learning so it improves continuously while paper trading: 1. For each coin (`BTC`, `ETH`, `SOL`), it builds a feature vector from latest candles: - short-horizon returns (`ret_1`, `ret_3`, `ret_8`) - trend/momentum (`macd`, scaled `rsi`) - volatility and activity (`atr14`, `vol_z`) 2. `SGDClassifier(loss="log_loss")` predicts probability of a profitable long setup. 3. Entry logic maps probability to actions: - `p_long >= threshold` -> long - `p_long <= 1 - threshold` -> short - otherwise hold - when model is cold, bootstrap mode uses weak momentum direction to generate first labels - optional exploration (`--exploration-rate`) continues collecting data even during neutral model states 4. When a trade closes (TP/SL), outcome is the supervision label: - profitable -> label `1` - losing -> label `0` 5. The model is updated immediately using `partial_fit(...)` on that single labeled example. 6. The confidence threshold adapts from recent win rate (last 30 outcomes), becoming stricter or looser. 7. Learned state is persisted to `learning_state.joblib` and reloaded on restart. 8. Open paper positions and captured entry features are also persisted, so trades crossing restarts still train the model on close. So each closed paper trade directly trains the next decision, and training survives restarts. ## Notes - The consistency gate currently requires: - at least 30 paper trades - win rate >= 58% - profit factor >= 1.25 - State persistence file is configurable with `--state-file` - Runtime report file is configurable with `--report-file` - Live approval can be controlled with: - `--require-live-approval / --no-require-live-approval` - `--live-approval-mode either|both` (default `either`) - `--live-approval-file` (default `allow_live_trading.flag`) - Startup live order test can be controlled with: - `--live-startup-test / --no-live-startup-test` - `--live-startup-test-coin` (default `BTC`) - `--live-startup-test-notional` (default `12`) " />
|
| 11 |
+
<option name="updatedContent" value="--- title: Hyperliquid Adaptive Bot emoji: "" colorFrom: blue colorTo: purple sdk: docker pinned: false --- # Hyperliquid Adaptive Bot This project now includes an adaptive ML bot that uses your existing Hyperliquid framework. ## What it does - Trades **only** `BTC`, `ETH`, and `SOL` - Runs in **paper mode by default** - Learns from closed paper trades with an online ML model (`SGDClassifier`) - Persists everything learned to disk (`learning_state.joblib`) so restart resumes learning - Persists paper runtime state (equity, open positions, entry features) so restart does not break learning continuity - Uses a profitability gate before any live execution is allowed - Sends Telegram alerts on trade open/close and a midnight UTC summary - Sends a readiness alert when paper performance is strong enough for live trading - Optional startup live transaction test (tiny BUY then SELL) to verify live order execution path - Can mirror to live orders only when: - `--enable-live` is set - paper performance is consistently profitable - manual approval is granted (`ALLOW_LIVE_TRADING=1` or approval flag file exists) - `HL_PRIVATE_KEY` is configured ## Files - `hbot.py`: existing Hyperliquid API framework and order placement helpers - `adaptive_bot.py`: new adaptive ML paper/live-gated trading bot - `smoke_test.py`: offline persistence and learning smoke test - `ml_diagnostics.py`: offline + live learning diagnostics report - `diagnostics_smoke_test.py`: quick smoke test for diagnostics logic - `paper_report.json`: runtime status and metrics output (created at runtime) ## Health endpoints (for HF + UptimeRobot) - `GET /` -> lightweight status response (`200`) for uptime checks - `GET /healthz` -> same as root (`200`) - `GET /status` -> concise overview (equity, open position count, performance, live gate) - `GET /status/full` -> full runtime JSON (equity, open positions, metrics, thresholds) - `GET /trades` -> open positions list and count If your Space is `https://chigginrice-hyperliquid-bot.hf.space/`, your existing root monitor will work as-is. ## Quick start 1. Install dependencies: ```bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt ``` 2. Run the offline smoke test: ```bash python smoke_test.py ``` Optional: run diagnostics smoke test: ```bash python diagnostics_smoke_test.py ``` 3. Start the adaptive paper bot: ```bash python adaptive_bot.py --interval 15m --poll-seconds 45 ``` 4. Check ML learning quality from saved state: ```bash python ml_diagnostics.py --state-file learning_state.joblib ``` If the bot is running locally, include live status snapshot: ```bash python ml_diagnostics.py --state-file learning_state.joblib --status-url http://127.0.0.1:7860/status/full ``` JSON output (useful for storing artifacts in CI/HF workflows): ```bash python ml_diagnostics.py --state-file learning_state.joblib --json ``` Optional knobs for early growth from scratch: ```bash python adaptive_bot.py --interval 15m --exploration-rate 0.08 ``` ## Optional: allow live mirror only after paper consistency gate ```bash export HL_PRIVATE_KEY="your_private_key" # optional if using delegated account export HL_ACCOUNT_ADDRESS="0x..." python adaptive_bot.py --interval 15m --enable-live ``` By default, live arm still requires manual approval even after the paper gate is passed: ```bash export ALLOW_LIVE_TRADING=1 # or: touch allow_live_trading.flag python adaptive_bot.py --interval 15m --enable-live ``` Strict two-step approval mode (recommended for safety): ```bash export ALLOW_LIVE_TRADING=1 touch allow_live_trading.flag python adaptive_bot.py --interval 15m --enable-live --live-approval-mode both ``` Optional startup live execution test (real tiny transaction): ```bash python adaptive_bot.py --interval 15m --enable-live --live-startup-test --live-startup-test-coin BTC --live-startup-test-notional 12 ``` ## Docker (Hugging Face) `Dockerfile` is included for Docker-based hosting. Build locally: ```bash docker build -t hyperliquid-adaptive-bot . ``` Run locally in paper mode: ```bash docker run --rm \ -e BOT_INTERVAL=15m \ -e BOT_POLL_SECONDS=45 \ -v "$(pwd)/data:/data" \ hyperliquid-adaptive-bot ``` Run with live mode allowed after paper gate passes: ```bash docker run --rm \ -e BOT_ENABLE_LIVE=1 \ -e HL_PRIVATE_KEY="your_private_key" \ -e HL_ACCOUNT_ADDRESS="0x..." \ -v "$(pwd)/data:/data" \ hyperliquid-adaptive-bot ``` Environment variables used by the container: - `BOT_INTERVAL` (default `15m`) - `BOT_POLL_SECONDS` (default `45`) - `BOT_STATE_FILE` (default `/data/learning_state.joblib`) - `BOT_REPORT_FILE` (default `/data/paper_report.json`) - `BOT_ENABLE_LIVE` (set any non-empty value to append `--enable-live`) - `BOT_HEALTH_HOST` (default `0.0.0.0`) - `BOT_HEALTH_PORT` (default `7860`) - `PORT` (default `7860`, used by Hugging Face Spaces) - `ALLOW_LIVE_TRADING` (set to `1/true/yes` to manually approve live trading) - `BOT_LIVE_STARTUP_TEST` (set non-empty to add `--live-startup-test`) - `BOT_LIVE_STARTUP_TEST_COIN` (default `BTC`) - `BOT_LIVE_STARTUP_TEST_NOTIONAL` (default `12`) - `TELEGRAM_BOT_TOKEN` (required for Telegram notifications) - `TELEGRAM_CHAT_ID` (required for Telegram notifications) - `PROXY_URL` (required by `telegram_notifier.py` to forward messages) - `HL_PRIVATE_KEY` (required for live mirror) - `HL_ACCOUNT_ADDRESS` (optional) ## Telegram notifications - Entry alert: sent when a paper position opens - Exit alert: sent when TP/SL closes a position - Midnight summary: sent once per day at `00:00 UTC` - Readiness alert: sent when profitability gate is reached and bot is ready to request live approval Summary includes last-24h trades, win rate, PnL, and overall cumulative stats. ## How the ML learns At a high level, this bot uses online learning so it improves continuously while paper trading: 1. For each coin (`BTC`, `ETH`, `SOL`), it builds a feature vector from latest candles: - short-horizon returns (`ret_1`, `ret_3`, `ret_8`) - trend/momentum (`macd`, scaled `rsi`) - volatility and activity (`atr14`, `vol_z`) 2. `SGDClassifier(loss="log_loss")` predicts probability of a profitable long setup. 3. Entry logic maps probability to actions: - `p_long >= threshold` -> long - `p_long <= 1 - threshold` -> short - otherwise hold - when model is cold, bootstrap mode uses weak momentum direction to generate first labels - optional exploration (`--exploration-rate`) continues collecting data even during neutral model states 4. When a trade closes (TP/SL), outcome is the supervision label: - profitable -> label `1` - losing -> label `0` 5. The model is updated immediately using `partial_fit(...)` on that single labeled example. 6. The confidence threshold adapts from recent win rate (last 30 outcomes), becoming stricter or looser. 7. Learned state is persisted to `learning_state.joblib` and reloaded on restart. 8. Open paper positions and captured entry features are also persisted, so trades crossing restarts still train the model on close. So each closed paper trade directly trains the next decision, and training survives restarts. ## ML diagnostics `ml_diagnostics.py` helps answer whether the model signal is actually improving. It reports: - overall/per-coin/per-side win rate and expectancy - directional sanity checks: - long winners should have higher `p_long` than long losers - short winners should have lower `p_long` than short losers - direction-aware Brier score (lower is better) - optional runtime snapshot from `/status/full` including rolling Brier/expectancy/drawdown This is useful to catch label/specification issues where the model keeps repeating failing trade patterns. ## Notes - The consistency gate currently requires: - at least 30 paper trades - win rate >= 58% - profit factor >= 1.25 - State persistence file is configurable with `--state-file` - Runtime report file is configurable with `--report-file` - Live approval can be controlled with: - `--require-live-approval / --no-require-live-approval` - `--live-approval-mode either|both` (default `either`) - `--live-approval-file` (default `allow_live_trading.flag`) - Startup live order test can be controlled with: - `--live-startup-test / --no-live-startup-test` - `--live-startup-test-coin` (default `BTC`) - `--live-startup-test-notional` (default `12`) " />
|
| 12 |
+
</PendingDiffInfo>
|
| 13 |
+
</value>
|
| 14 |
+
</entry>
|
| 15 |
+
<entry key="$PROJECT_DIR$/diagnostics_smoke_test.py">
|
| 16 |
+
<value>
|
| 17 |
+
<PendingDiffInfo>
|
| 18 |
+
<option name="filePath" value="$PROJECT_DIR$/diagnostics_smoke_test.py" />
|
| 19 |
+
<option name="updatedContent" value="#!/usr/bin/env python3 """Smoke test for ml_diagnostics report builder.""" from __future__ import annotations import tempfile from pathlib import Path import joblib from ml_diagnostics import build_report def main() -> None: payload = { "closed_trades": [ { "coin": "BTC", "side": "long", "entry_ts": 1000, "pnl_usd": 5.0, "entry_p_long": 0.72, }, { "coin": "BTC", "side": "short", "entry_ts": 2000, "pnl_usd": -3.0, "entry_p_long": 0.81, }, { "coin": "ETH", "side": "short", "entry_ts": 3000, "pnl_usd": 4.0, "entry_p_long": 0.22, }, ], "trade_events": [ {"event": "open", "coin": "BTC", "entry_ts": 1000, "p_long": 0.72}, {"event": "open", "coin": "BTC", "entry_ts": 2000, "p_long": 0.81}, {"event": "open", "coin": "ETH", "entry_ts": 3000, "p_long": 0.22}, ], } with tempfile.TemporaryDirectory() as td: state_path = Path(td) / "learning_state.joblib" joblib.dump(payload, state_path) loaded = joblib.load(state_path) report = build_report(loaded) assert report["state_samples"]["closed_trades"] == 3 assert "overall" in report assert "directional_sanity" in report assert report["brier_direction_aware"] is not None print("diagnostics_smoke_test: OK") if __name__ == "__main__": main() " />
|
| 20 |
+
</PendingDiffInfo>
|
| 21 |
+
</value>
|
| 22 |
+
</entry>
|
| 23 |
+
<entry key="$PROJECT_DIR$/ml_diagnostics.py">
|
| 24 |
+
<value>
|
| 25 |
+
<PendingDiffInfo>
|
| 26 |
+
<option name="filePath" value="$PROJECT_DIR$/ml_diagnostics.py" />
|
| 27 |
+
<option name="updatedContent" value="#!/usr/bin/env python3 """Offline and live diagnostics for adaptive bot learning quality. This script helps validate whether the model is learning directionally useful signals. """ from __future__ import annotations import argparse import json import math import os from collections import defaultdict from typing import Any, Dict, List, Optional, Tuple from urllib.error import URLError from urllib.request import urlopen import joblib def _safe_float(v: Any, default: float = 0.0) -> float: try: return float(v) except Exception: return default def _mean(values: List[float]) -> Optional[float]: return (sum(values) / len(values)) if values else None def _trade_p_long(trade: Dict[str, Any], open_probs: Dict[Tuple[str, int], float]) -> float: if "entry_p_long" in trade and trade["entry_p_long"] is not None: return float(max(0.0, min(1.0, _safe_float(trade["entry_p_long"], 0.5)))) key = (str(trade.get("coin", "")), int(_safe_float(trade.get("entry_ts", 0), 0))) return float(max(0.0, min(1.0, open_probs.get(key, 0.5)))) def _summarize_basic(trades: List[Dict[str, Any]]) -> Dict[str, float]: n = len(trades) wins = sum(1 for t in trades if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0) pnl = sum(_safe_float(t.get("pnl_usd", 0.0), 0.0) for t in trades) return { "trades": float(n), "wins": float(wins), "win_rate": (wins / n) if n else 0.0, "total_pnl_usd": pnl, "expectancy_usd": (pnl / n) if n else 0.0, } def _directional_sanity(trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float]) -> Dict[str, Optional[float]]: longs = [t for t in trades if str(t.get("side", "")).lower() == "long"] shorts = [t for t in trades if str(t.get("side", "")).lower() == "short"] long_win = [_trade_p_long(t, open_probs) for t in longs if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0] long_lose = [_trade_p_long(t, open_probs) for t in longs if _safe_float(t.get("pnl_usd", 0.0), 0.0) <= 0] short_win = [_trade_p_long(t, open_probs) for t in shorts if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0] short_lose = [_trade_p_long(t, open_probs) for t in shorts if _safe_float(t.get("pnl_usd", 0.0), 0.0) <= 0] # For a healthy long-probability model: # - long winners should have higher p_long than long losers # - short winners should have lower p_long than short losers return { "long_winners_avg_p_long": _mean(long_win), "long_losers_avg_p_long": _mean(long_lose), "short_winners_avg_p_long": _mean(short_win), "short_losers_avg_p_long": _mean(short_lose), } def _direction_aware_brier(trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float]) -> Optional[float]: scores: List[float] = [] for t in trades: side = str(t.get("side", "")).lower() if side not in {"long", "short"}: continue p_long = _trade_p_long(t, open_probs) won = 1.0 if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0 else 0.0 # Target for long probability: # - long trade: y=1 if win # - short trade: y=1 if short loses (long would've won) y_long = won if side == "long" else (1.0 - won) scores.append((p_long - y_long) ** 2) return _mean(scores) def _calibration_table( trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float], side: str ) -> List[Dict[str, Any]]: bins: Dict[int, List[Dict[str, Any]]] = defaultdict(list) for t in trades: t_side = str(t.get("side", "")).lower() if t_side != side: continue p = _trade_p_long(t, open_probs) decile = min(9, max(0, int(math.floor(p * 10.0)))) bins[decile].append(t) rows: List[Dict[str, Any]] = [] for decile in range(10): arr = bins.get(decile, []) if not arr: continue n = len(arr) wins = sum(1 for t in arr if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0) avg_p = _mean([_trade_p_long(t, open_probs) for t in arr]) rows.append( { "decile": decile, "range": f"{decile / 10:.1f}-{(decile + 1) / 10:.1f}", "trades": n, "avg_p_long": avg_p, "win_rate": (wins / n) if n else None, } ) return rows def _open_prob_lookup(events: List[Dict[str, Any]]) -> Dict[Tuple[str, int], float]: out: Dict[Tuple[str, int], float] = {} for e in events: if e.get("event") != "open": continue coin = str(e.get("coin", "")) entry_ts = int(_safe_float(e.get("entry_ts", 0), 0)) p_long = float(max(0.0, min(1.0, _safe_float(e.get("p_long", 0.5), 0.5)))) out[(coin, entry_ts)] = p_long return out def _fetch_status(url: str) -> Optional[Dict[str, Any]]: try: with urlopen(url, timeout=3) as response: if response.status != 200: return None body = response.read().decode("utf-8") return json.loads(body) except (URLError, TimeoutError, json.JSONDecodeError, ValueError): return None def build_report(payload: Dict[str, Any], status_payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: closed = payload.get("closed_trades", []) or [] events = payload.get("trade_events", []) or [] open_probs = _open_prob_lookup(events) by_coin: Dict[str, List[Dict[str, Any]]] = defaultdict(list) by_side: Dict[str, List[Dict[str, Any]]] = defaultdict(list) for t in closed: by_coin[str(t.get("coin", "unknown"))].append(t) by_side[str(t.get("side", "unknown")).lower()].append(t) per_coin = {coin: _summarize_basic(trades) for coin, trades in sorted(by_coin.items())} per_side = {side: _summarize_basic(trades) for side, trades in sorted(by_side.items())} report: Dict[str, Any] = { "state_samples": { "closed_trades": len(closed), "trade_events": len(events), }, "overall": _summarize_basic(closed), "per_coin": per_coin, "per_side": per_side, "directional_sanity": _directional_sanity(closed, open_probs), "brier_direction_aware": _direction_aware_brier(closed, open_probs), "calibration": { "long": _calibration_table(closed, open_probs, "long"), "short": _calibration_table(closed, open_probs, "short"), }, "runtime_status": status_payload, } # High-level warnings to speed up triage. warnings: List[str] = [] ds = report["directional_sanity"] lw = ds.get("long_winners_avg_p_long") ll = ds.get("long_losers_avg_p_long") sw = ds.get("short_winners_avg_p_long") sl = ds.get("short_losers_avg_p_long") if lw is not None and ll is not None and lw <= ll: warnings.append("Long directional signal is inverted or weak: long winners do not have higher p_long than long losers.") if sw is not None and sl is not None and sw >= sl: warnings.append("Short directional signal is inverted or weak: short winners do not have lower p_long than short losers.") overall_wr = report["overall"].get("win_rate", 0.0) overall_exp = report["overall"].get("expectancy_usd", 0.0) if len(closed) >= 50 and overall_wr < 0.45: warnings.append("Win rate below 45% after 50+ trades; model may be mis-specified or overtrading.") if len(closed) >= 50 and overall_exp < 0: warnings.append("Negative expectancy after 50+ trades; consider reducing exploration and revisiting labels.") report["warnings"] = warnings return report def print_report(report: Dict[str, Any]) -> None: print("=== Adaptive Bot ML Diagnostics ===") samples = report["state_samples"] print(f"Closed trades: {samples['closed_trades']}") print(f"Trade events: {samples['trade_events']}") overall = report["overall"] print( "Overall: " f"n={int(overall['trades'])}, " f"win_rate={overall['win_rate'] * 100:.2f}%, " f"total_pnl={overall['total_pnl_usd']:.2f}, " f"expectancy={overall['expectancy_usd']:.4f}" ) print("\nBy side:") for side, s in report["per_side"].items(): print( f" {side}: n={int(s['trades'])}, win_rate={s['win_rate'] * 100:.2f}%, " f"total_pnl={s['total_pnl_usd']:.2f}, expectancy={s['expectancy_usd']:.4f}" ) print("\nBy coin:") for coin, s in report["per_coin"].items(): print( f" {coin}: n={int(s['trades'])}, win_rate={s['win_rate'] * 100:.2f}%, " f"total_pnl={s['total_pnl_usd']:.2f}, expectancy={s['expectancy_usd']:.4f}" ) ds = report["directional_sanity"] print("\nDirectional sanity:") print( " Long winners avg p_long: " f"{ds['long_winners_avg_p_long'] if ds['long_winners_avg_p_long'] is not None else 'n/a'}" ) print( " Long losers avg p_long: " f"{ds['long_losers_avg_p_long'] if ds['long_losers_avg_p_long'] is not None else 'n/a'}" ) print( " Short winners avg p_long: " f"{ds['short_winners_avg_p_long'] if ds['short_winners_avg_p_long'] is not None else 'n/a'}" ) print( " Short losers avg p_long: " f"{ds['short_losers_avg_p_long'] if ds['short_losers_avg_p_long'] is not None else 'n/a'}" ) brier = report.get("brier_direction_aware") print(f"\nDirection-aware Brier (lower is better): {brier if brier is not None else 'n/a'}") warnings = report.get("warnings", []) if warnings: print("\nWarnings:") for w in warnings: print(f" - {w}") runtime = report.get("runtime_status") if runtime: print("\nLive status snapshot:") metrics = runtime.get("metrics", {}) ml_progress = runtime.get("ml_progress", {}) print( " runtime trades=" f"{int(_safe_float(metrics.get('trades', 0), 0))}, " f"win_rate={_safe_float(metrics.get('win_rate', 0.0), 0.0) * 100:.2f}%, " f"pf={_safe_float(metrics.get('profit_factor', 0.0), 0.0):.2f}" ) if ml_progress: print( " runtime brier_50=" f"{ml_progress.get('brier_rolling_50')}, " f"expectancy_30={ml_progress.get('expectancy_usd_30')}, " f"max_dd={ml_progress.get('max_drawdown_pct')}" ) def parse_args() -> argparse.Namespace: p = argparse.ArgumentParser(description="Diagnose adaptive bot ML learning quality") p.add_argument("--state-file", default="learning_state.joblib", help="Path to persisted bot state file") p.add_argument( "--status-url", default="http://127.0.0.1:7860/status/full", help="Optional bot status endpoint for runtime snapshot", ) p.add_argument("--skip-status", action="store_true", help="Skip HTTP status fetch") p.add_argument("--json", action="store_true", help="Print full report as JSON") return p.parse_args() def main() -> int: args = parse_args() if not os.path.exists(args.state_file): print(f"State file not found: {args.state_file}") return 1 payload = joblib.load(args.state_file) status_payload = None if args.skip_status else _fetch_status(args.status_url) report = build_report(payload, status_payload=status_payload) if args.json: print(json.dumps(report, indent=2)) else: print_report(report) return 0 if __name__ == "__main__": raise SystemExit(main()) " />
|
| 28 |
+
</PendingDiffInfo>
|
| 29 |
+
</value>
|
| 30 |
+
</entry>
|
| 31 |
+
</map>
|
| 32 |
+
</option>
|
| 33 |
+
</component>
|
| 34 |
+
</project>
|
.idea/workspace.xml
CHANGED
|
@@ -5,153 +5,7 @@
|
|
| 5 |
</component>
|
| 6 |
<component name="ChangeListManager">
|
| 7 |
<list default="true" id="4d8c1aed-8693-45b5-a8e3-cfb3e1553feb" name="Changes" comment="">
|
| 8 |
-
<change beforePath="$PROJECT_DIR$/.
|
| 9 |
-
<change beforePath="$PROJECT_DIR$/.gitattributes" beforeDir="false" />
|
| 10 |
-
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" />
|
| 11 |
-
<change beforePath="$PROJECT_DIR$/Dockerfile" beforeDir="false" afterPath="$PROJECT_DIR$/Dockerfile" afterDir="false" />
|
| 12 |
-
<change beforePath="$PROJECT_DIR$/GOOGLE_APPS_SCRIPT_FULL.js" beforeDir="false" />
|
| 13 |
-
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
| 14 |
-
<change beforePath="$PROJECT_DIR$/_backtest_hype.py" beforeDir="false" />
|
| 15 |
-
<change beforePath="$PROJECT_DIR$/_backtest_ml_auto.py" beforeDir="false" />
|
| 16 |
-
<change beforePath="$PROJECT_DIR$/_scan_coins.py" beforeDir="false" />
|
| 17 |
-
<change beforePath="$PROJECT_DIR$/autonomous_bot.py" beforeDir="false" />
|
| 18 |
-
<change beforePath="$PROJECT_DIR$/backtest.py" beforeDir="false" />
|
| 19 |
-
<change beforePath="$PROJECT_DIR$/bot_config.json" beforeDir="false" />
|
| 20 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_APT_h12_long.joblib" beforeDir="false" />
|
| 21 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_APT_h12_short.joblib" beforeDir="false" />
|
| 22 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_APT_h3_long.joblib" beforeDir="false" />
|
| 23 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_APT_h3_short.joblib" beforeDir="false" />
|
| 24 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_APT_h6_long.joblib" beforeDir="false" />
|
| 25 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_APT_h6_short.joblib" beforeDir="false" />
|
| 26 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ARB_h12_long.joblib" beforeDir="false" />
|
| 27 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ARB_h12_short.joblib" beforeDir="false" />
|
| 28 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ARB_h3_long.joblib" beforeDir="false" />
|
| 29 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ARB_h3_short.joblib" beforeDir="false" />
|
| 30 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ARB_h6_long.joblib" beforeDir="false" />
|
| 31 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ARB_h6_short.joblib" beforeDir="false" />
|
| 32 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_AVAX_h12_long.joblib" beforeDir="false" />
|
| 33 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_AVAX_h12_short.joblib" beforeDir="false" />
|
| 34 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_AVAX_h3_long.joblib" beforeDir="false" />
|
| 35 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_AVAX_h3_short.joblib" beforeDir="false" />
|
| 36 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_AVAX_h6_long.joblib" beforeDir="false" />
|
| 37 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_AVAX_h6_short.joblib" beforeDir="false" />
|
| 38 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_BTC_h12_long.joblib" beforeDir="false" />
|
| 39 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_BTC_h12_short.joblib" beforeDir="false" />
|
| 40 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_BTC_h3_long.joblib" beforeDir="false" />
|
| 41 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_BTC_h3_short.joblib" beforeDir="false" />
|
| 42 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_BTC_h6_long.joblib" beforeDir="false" />
|
| 43 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_BTC_h6_short.joblib" beforeDir="false" />
|
| 44 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_DOGE_h12_long.joblib" beforeDir="false" />
|
| 45 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_DOGE_h12_short.joblib" beforeDir="false" />
|
| 46 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_DOGE_h3_long.joblib" beforeDir="false" />
|
| 47 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_DOGE_h3_short.joblib" beforeDir="false" />
|
| 48 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_DOGE_h6_long.joblib" beforeDir="false" />
|
| 49 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_DOGE_h6_short.joblib" beforeDir="false" />
|
| 50 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ETH_h12_long.joblib" beforeDir="false" />
|
| 51 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ETH_h12_short.joblib" beforeDir="false" />
|
| 52 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ETH_h3_long.joblib" beforeDir="false" />
|
| 53 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ETH_h3_short.joblib" beforeDir="false" />
|
| 54 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ETH_h6_long.joblib" beforeDir="false" />
|
| 55 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_ETH_h6_short.joblib" beforeDir="false" />
|
| 56 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_INJ_h12_long.joblib" beforeDir="false" />
|
| 57 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_INJ_h12_short.joblib" beforeDir="false" />
|
| 58 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_INJ_h3_long.joblib" beforeDir="false" />
|
| 59 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_INJ_h3_short.joblib" beforeDir="false" />
|
| 60 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_INJ_h6_long.joblib" beforeDir="false" />
|
| 61 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_INJ_h6_short.joblib" beforeDir="false" />
|
| 62 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_LINK_h12_long.joblib" beforeDir="false" />
|
| 63 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_LINK_h12_short.joblib" beforeDir="false" />
|
| 64 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_LINK_h3_long.joblib" beforeDir="false" />
|
| 65 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_LINK_h3_short.joblib" beforeDir="false" />
|
| 66 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_LINK_h6_long.joblib" beforeDir="false" />
|
| 67 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_LINK_h6_short.joblib" beforeDir="false" />
|
| 68 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_NEAR_h12_long.joblib" beforeDir="false" />
|
| 69 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_NEAR_h12_short.joblib" beforeDir="false" />
|
| 70 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_NEAR_h3_long.joblib" beforeDir="false" />
|
| 71 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_NEAR_h3_short.joblib" beforeDir="false" />
|
| 72 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_NEAR_h6_long.joblib" beforeDir="false" />
|
| 73 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_NEAR_h6_short.joblib" beforeDir="false" />
|
| 74 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_OP_h12_long.joblib" beforeDir="false" />
|
| 75 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_OP_h12_short.joblib" beforeDir="false" />
|
| 76 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_OP_h3_long.joblib" beforeDir="false" />
|
| 77 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_OP_h3_short.joblib" beforeDir="false" />
|
| 78 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_OP_h6_long.joblib" beforeDir="false" />
|
| 79 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_OP_h6_short.joblib" beforeDir="false" />
|
| 80 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SOL_h12_long.joblib" beforeDir="false" />
|
| 81 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SOL_h12_short.joblib" beforeDir="false" />
|
| 82 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SOL_h3_long.joblib" beforeDir="false" />
|
| 83 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SOL_h3_short.joblib" beforeDir="false" />
|
| 84 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SOL_h6_long.joblib" beforeDir="false" />
|
| 85 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SOL_h6_short.joblib" beforeDir="false" />
|
| 86 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SUI_h12_long.joblib" beforeDir="false" />
|
| 87 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SUI_h12_short.joblib" beforeDir="false" />
|
| 88 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SUI_h3_long.joblib" beforeDir="false" />
|
| 89 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SUI_h3_short.joblib" beforeDir="false" />
|
| 90 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SUI_h6_long.joblib" beforeDir="false" />
|
| 91 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_SUI_h6_short.joblib" beforeDir="false" />
|
| 92 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_TIA_h12_long.joblib" beforeDir="false" />
|
| 93 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_TIA_h12_short.joblib" beforeDir="false" />
|
| 94 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_TIA_h3_long.joblib" beforeDir="false" />
|
| 95 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_TIA_h3_short.joblib" beforeDir="false" />
|
| 96 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_TIA_h6_long.joblib" beforeDir="false" />
|
| 97 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_TIA_h6_short.joblib" beforeDir="false" />
|
| 98 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_WIF_h12_long.joblib" beforeDir="false" />
|
| 99 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_WIF_h12_short.joblib" beforeDir="false" />
|
| 100 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_WIF_h3_long.joblib" beforeDir="false" />
|
| 101 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_WIF_h3_short.joblib" beforeDir="false" />
|
| 102 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_WIF_h6_long.joblib" beforeDir="false" />
|
| 103 |
-
<change beforePath="$PROJECT_DIR$/models/ml_auto_WIF_h6_short.joblib" beforeDir="false" />
|
| 104 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_APT_long.joblib" beforeDir="false" />
|
| 105 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_APT_short.joblib" beforeDir="false" />
|
| 106 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_ARB_long.joblib" beforeDir="false" />
|
| 107 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_ARB_short.joblib" beforeDir="false" />
|
| 108 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_AVAX_long.joblib" beforeDir="false" />
|
| 109 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_AVAX_short.joblib" beforeDir="false" />
|
| 110 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_BTC_long.joblib" beforeDir="false" />
|
| 111 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_BTC_short.joblib" beforeDir="false" />
|
| 112 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_DOGE_long.joblib" beforeDir="false" />
|
| 113 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_DOGE_short.joblib" beforeDir="false" />
|
| 114 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_DOT_long.joblib" beforeDir="false" />
|
| 115 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_DOT_short.joblib" beforeDir="false" />
|
| 116 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_ETH_long.joblib" beforeDir="false" />
|
| 117 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_ETH_short.joblib" beforeDir="false" />
|
| 118 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_INJ_long.joblib" beforeDir="false" />
|
| 119 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_INJ_short.joblib" beforeDir="false" />
|
| 120 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_LINK_long.joblib" beforeDir="false" />
|
| 121 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_LINK_short.joblib" beforeDir="false" />
|
| 122 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_NEAR_long.joblib" beforeDir="false" />
|
| 123 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_NEAR_short.joblib" beforeDir="false" />
|
| 124 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_OP_long.joblib" beforeDir="false" />
|
| 125 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_OP_short.joblib" beforeDir="false" />
|
| 126 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_SOL_long.joblib" beforeDir="false" />
|
| 127 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_SOL_short.joblib" beforeDir="false" />
|
| 128 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_SUI_long.joblib" beforeDir="false" />
|
| 129 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_SUI_short.joblib" beforeDir="false" />
|
| 130 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_TIA_long.joblib" beforeDir="false" />
|
| 131 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_TIA_short.joblib" beforeDir="false" />
|
| 132 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_WIF_long.joblib" beforeDir="false" />
|
| 133 |
-
<change beforePath="$PROJECT_DIR$/models/scalp_WIF_short.joblib" beforeDir="false" />
|
| 134 |
-
<change beforePath="$PROJECT_DIR$/optimize_ml_coins.py" beforeDir="false" />
|
| 135 |
-
<change beforePath="$PROJECT_DIR$/regime.py" beforeDir="false" />
|
| 136 |
-
<change beforePath="$PROJECT_DIR$/reports/.gitkeep" beforeDir="false" />
|
| 137 |
-
<change beforePath="$PROJECT_DIR$/reports/ai_entry_decisions.jsonl" beforeDir="false" />
|
| 138 |
-
<change beforePath="$PROJECT_DIR$/reports/autonomous_backtest_report.json" beforeDir="false" />
|
| 139 |
-
<change beforePath="$PROJECT_DIR$/reports/funding_arb.log" beforeDir="false" />
|
| 140 |
-
<change beforePath="$PROJECT_DIR$/reports/funding_monitor.log" beforeDir="false" />
|
| 141 |
-
<change beforePath="$PROJECT_DIR$/reports/ml_auto_backtest_seed.json" beforeDir="false" />
|
| 142 |
-
<change beforePath="$PROJECT_DIR$/reports/ml_auto_memory_BTC.jsonl" beforeDir="false" />
|
| 143 |
-
<change beforePath="$PROJECT_DIR$/run_all_backtests.py" beforeDir="false" />
|
| 144 |
-
<change beforePath="$PROJECT_DIR$/setup.sh" beforeDir="false" />
|
| 145 |
-
<change beforePath="$PROJECT_DIR$/strategy_bb_reversal.py" beforeDir="false" />
|
| 146 |
-
<change beforePath="$PROJECT_DIR$/strategy_conservative.py" beforeDir="false" />
|
| 147 |
-
<change beforePath="$PROJECT_DIR$/strategy_hype.py" beforeDir="false" />
|
| 148 |
-
<change beforePath="$PROJECT_DIR$/strategy_ml_auto.py" beforeDir="false" />
|
| 149 |
-
<change beforePath="$PROJECT_DIR$/strategy_scalp.py" beforeDir="false" />
|
| 150 |
-
<change beforePath="$PROJECT_DIR$/strategy_zlema.py" beforeDir="false" />
|
| 151 |
-
<change beforePath="$PROJECT_DIR$/tests/.gitkeep" beforeDir="false" />
|
| 152 |
-
<change beforePath="$PROJECT_DIR$/tests/test_autonomous_bot.py" beforeDir="false" />
|
| 153 |
-
<change beforePath="$PROJECT_DIR$/tests/test_hbot_copy_execution.py" beforeDir="false" />
|
| 154 |
-
<change beforePath="$PROJECT_DIR$/track_ml_learning.py" beforeDir="false" />
|
| 155 |
</list>
|
| 156 |
<option name="SHOW_DIALOG" value="false" />
|
| 157 |
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
|
|
|
| 5 |
</component>
|
| 6 |
<component name="ChangeListManager">
|
| 7 |
<list default="true" id="4d8c1aed-8693-45b5-a8e3-cfb3e1553feb" name="Changes" comment="">
|
| 8 |
+
<change beforePath="$PROJECT_DIR$/.idea/copilotDiffState.xml" beforeDir="false" />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
</list>
|
| 10 |
<option name="SHOW_DIALOG" value="false" />
|
| 11 |
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
README.md
CHANGED
|
@@ -33,6 +33,8 @@ This project now includes an adaptive ML bot that uses your existing Hyperliquid
|
|
| 33 |
- `hbot.py`: existing Hyperliquid API framework and order placement helpers
|
| 34 |
- `adaptive_bot.py`: new adaptive ML paper/live-gated trading bot
|
| 35 |
- `smoke_test.py`: offline persistence and learning smoke test
|
|
|
|
|
|
|
| 36 |
- `paper_report.json`: runtime status and metrics output (created at runtime)
|
| 37 |
|
| 38 |
## Health endpoints (for HF + UptimeRobot)
|
|
@@ -61,12 +63,36 @@ pip install -r requirements.txt
|
|
| 61 |
python smoke_test.py
|
| 62 |
```
|
| 63 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
3. Start the adaptive paper bot:
|
| 65 |
|
| 66 |
```bash
|
| 67 |
python adaptive_bot.py --interval 15m --poll-seconds 45
|
| 68 |
```
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
Optional knobs for early growth from scratch:
|
| 71 |
|
| 72 |
```bash
|
|
@@ -189,6 +215,21 @@ At a high level, this bot uses online learning so it improves continuously while
|
|
| 189 |
|
| 190 |
So each closed paper trade directly trains the next decision, and training survives restarts.
|
| 191 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
## Notes
|
| 193 |
|
| 194 |
- The consistency gate currently requires:
|
|
|
|
| 33 |
- `hbot.py`: existing Hyperliquid API framework and order placement helpers
|
| 34 |
- `adaptive_bot.py`: new adaptive ML paper/live-gated trading bot
|
| 35 |
- `smoke_test.py`: offline persistence and learning smoke test
|
| 36 |
+
- `ml_diagnostics.py`: offline + live learning diagnostics report
|
| 37 |
+
- `diagnostics_smoke_test.py`: quick smoke test for diagnostics logic
|
| 38 |
- `paper_report.json`: runtime status and metrics output (created at runtime)
|
| 39 |
|
| 40 |
## Health endpoints (for HF + UptimeRobot)
|
|
|
|
| 63 |
python smoke_test.py
|
| 64 |
```
|
| 65 |
|
| 66 |
+
Optional: run diagnostics smoke test:
|
| 67 |
+
|
| 68 |
+
```bash
|
| 69 |
+
python diagnostics_smoke_test.py
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
3. Start the adaptive paper bot:
|
| 73 |
|
| 74 |
```bash
|
| 75 |
python adaptive_bot.py --interval 15m --poll-seconds 45
|
| 76 |
```
|
| 77 |
|
| 78 |
+
4. Check ML learning quality from saved state:
|
| 79 |
+
|
| 80 |
+
```bash
|
| 81 |
+
python ml_diagnostics.py --state-file learning_state.joblib
|
| 82 |
+
```
|
| 83 |
+
|
| 84 |
+
If the bot is running locally, include live status snapshot:
|
| 85 |
+
|
| 86 |
+
```bash
|
| 87 |
+
python ml_diagnostics.py --state-file learning_state.joblib --status-url http://127.0.0.1:7860/status/full
|
| 88 |
+
```
|
| 89 |
+
|
| 90 |
+
JSON output (useful for storing artifacts in CI/HF workflows):
|
| 91 |
+
|
| 92 |
+
```bash
|
| 93 |
+
python ml_diagnostics.py --state-file learning_state.joblib --json
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
Optional knobs for early growth from scratch:
|
| 97 |
|
| 98 |
```bash
|
|
|
|
| 215 |
|
| 216 |
So each closed paper trade directly trains the next decision, and training survives restarts.
|
| 217 |
|
| 218 |
+
## ML diagnostics
|
| 219 |
+
|
| 220 |
+
`ml_diagnostics.py` helps answer whether the model signal is actually improving.
|
| 221 |
+
|
| 222 |
+
It reports:
|
| 223 |
+
|
| 224 |
+
- overall/per-coin/per-side win rate and expectancy
|
| 225 |
+
- directional sanity checks:
|
| 226 |
+
- long winners should have higher `p_long` than long losers
|
| 227 |
+
- short winners should have lower `p_long` than short losers
|
| 228 |
+
- direction-aware Brier score (lower is better)
|
| 229 |
+
- optional runtime snapshot from `/status/full` including rolling Brier/expectancy/drawdown
|
| 230 |
+
|
| 231 |
+
This is useful to catch label/specification issues where the model keeps repeating failing trade patterns.
|
| 232 |
+
|
| 233 |
## Notes
|
| 234 |
|
| 235 |
- The consistency gate currently requires:
|
__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
|
|
|
__pycache__/diagnostics_smoke_test.cpython-310.pyc
ADDED
|
Binary file (1.29 kB). View file
|
|
|
__pycache__/ml_diagnostics.cpython-310.pyc
ADDED
|
Binary file (10.4 kB). View file
|
|
|
diagnostics_smoke_test.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Smoke test for ml_diagnostics report builder."""
|
| 3 |
+
|
| 4 |
+
from __future__ import annotations
|
| 5 |
+
|
| 6 |
+
import tempfile
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import joblib
|
| 10 |
+
|
| 11 |
+
from ml_diagnostics import build_report
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main() -> None:
|
| 15 |
+
payload = {
|
| 16 |
+
"closed_trades": [
|
| 17 |
+
{
|
| 18 |
+
"coin": "BTC",
|
| 19 |
+
"side": "long",
|
| 20 |
+
"entry_ts": 1000,
|
| 21 |
+
"pnl_usd": 5.0,
|
| 22 |
+
"entry_p_long": 0.72,
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"coin": "BTC",
|
| 26 |
+
"side": "short",
|
| 27 |
+
"entry_ts": 2000,
|
| 28 |
+
"pnl_usd": -3.0,
|
| 29 |
+
"entry_p_long": 0.81,
|
| 30 |
+
},
|
| 31 |
+
{
|
| 32 |
+
"coin": "ETH",
|
| 33 |
+
"side": "short",
|
| 34 |
+
"entry_ts": 3000,
|
| 35 |
+
"pnl_usd": 4.0,
|
| 36 |
+
"entry_p_long": 0.22,
|
| 37 |
+
},
|
| 38 |
+
],
|
| 39 |
+
"trade_events": [
|
| 40 |
+
{"event": "open", "coin": "BTC", "entry_ts": 1000, "p_long": 0.72},
|
| 41 |
+
{"event": "open", "coin": "BTC", "entry_ts": 2000, "p_long": 0.81},
|
| 42 |
+
{"event": "open", "coin": "ETH", "entry_ts": 3000, "p_long": 0.22},
|
| 43 |
+
],
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
with tempfile.TemporaryDirectory() as td:
|
| 47 |
+
state_path = Path(td) / "learning_state.joblib"
|
| 48 |
+
joblib.dump(payload, state_path)
|
| 49 |
+
loaded = joblib.load(state_path)
|
| 50 |
+
|
| 51 |
+
report = build_report(loaded)
|
| 52 |
+
|
| 53 |
+
assert report["state_samples"]["closed_trades"] == 3
|
| 54 |
+
assert "overall" in report
|
| 55 |
+
assert "directional_sanity" in report
|
| 56 |
+
assert report["brier_direction_aware"] is not None
|
| 57 |
+
|
| 58 |
+
print("diagnostics_smoke_test: OK")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if __name__ == "__main__":
|
| 62 |
+
main()
|
| 63 |
+
|
ml_diagnostics.py
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Offline and live diagnostics for adaptive bot learning quality.
|
| 3 |
+
|
| 4 |
+
This script helps validate whether the model is learning directionally useful signals.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from __future__ import annotations
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import json
|
| 11 |
+
import math
|
| 12 |
+
import os
|
| 13 |
+
from collections import defaultdict
|
| 14 |
+
from typing import Any, Dict, List, Optional, Tuple
|
| 15 |
+
from urllib.error import URLError
|
| 16 |
+
from urllib.request import urlopen
|
| 17 |
+
|
| 18 |
+
import joblib
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def _safe_float(v: Any, default: float = 0.0) -> float:
|
| 22 |
+
try:
|
| 23 |
+
return float(v)
|
| 24 |
+
except Exception:
|
| 25 |
+
return default
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def _mean(values: List[float]) -> Optional[float]:
|
| 29 |
+
return (sum(values) / len(values)) if values else None
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _trade_p_long(trade: Dict[str, Any], open_probs: Dict[Tuple[str, int], float]) -> float:
|
| 33 |
+
if "entry_p_long" in trade and trade["entry_p_long"] is not None:
|
| 34 |
+
return float(max(0.0, min(1.0, _safe_float(trade["entry_p_long"], 0.5))))
|
| 35 |
+
key = (str(trade.get("coin", "")), int(_safe_float(trade.get("entry_ts", 0), 0)))
|
| 36 |
+
return float(max(0.0, min(1.0, open_probs.get(key, 0.5))))
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _summarize_basic(trades: List[Dict[str, Any]]) -> Dict[str, float]:
|
| 40 |
+
n = len(trades)
|
| 41 |
+
wins = sum(1 for t in trades if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0)
|
| 42 |
+
pnl = sum(_safe_float(t.get("pnl_usd", 0.0), 0.0) for t in trades)
|
| 43 |
+
return {
|
| 44 |
+
"trades": float(n),
|
| 45 |
+
"wins": float(wins),
|
| 46 |
+
"win_rate": (wins / n) if n else 0.0,
|
| 47 |
+
"total_pnl_usd": pnl,
|
| 48 |
+
"expectancy_usd": (pnl / n) if n else 0.0,
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def _directional_sanity(trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float]) -> Dict[str, Optional[float]]:
|
| 53 |
+
longs = [t for t in trades if str(t.get("side", "")).lower() == "long"]
|
| 54 |
+
shorts = [t for t in trades if str(t.get("side", "")).lower() == "short"]
|
| 55 |
+
|
| 56 |
+
long_win = [_trade_p_long(t, open_probs) for t in longs if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0]
|
| 57 |
+
long_lose = [_trade_p_long(t, open_probs) for t in longs if _safe_float(t.get("pnl_usd", 0.0), 0.0) <= 0]
|
| 58 |
+
short_win = [_trade_p_long(t, open_probs) for t in shorts if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0]
|
| 59 |
+
short_lose = [_trade_p_long(t, open_probs) for t in shorts if _safe_float(t.get("pnl_usd", 0.0), 0.0) <= 0]
|
| 60 |
+
|
| 61 |
+
# For a healthy long-probability model:
|
| 62 |
+
# - long winners should have higher p_long than long losers
|
| 63 |
+
# - short winners should have lower p_long than short losers
|
| 64 |
+
return {
|
| 65 |
+
"long_winners_avg_p_long": _mean(long_win),
|
| 66 |
+
"long_losers_avg_p_long": _mean(long_lose),
|
| 67 |
+
"short_winners_avg_p_long": _mean(short_win),
|
| 68 |
+
"short_losers_avg_p_long": _mean(short_lose),
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def _direction_aware_brier(trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float]) -> Optional[float]:
|
| 73 |
+
scores: List[float] = []
|
| 74 |
+
for t in trades:
|
| 75 |
+
side = str(t.get("side", "")).lower()
|
| 76 |
+
if side not in {"long", "short"}:
|
| 77 |
+
continue
|
| 78 |
+
p_long = _trade_p_long(t, open_probs)
|
| 79 |
+
won = 1.0 if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0 else 0.0
|
| 80 |
+
# Target for long probability:
|
| 81 |
+
# - long trade: y=1 if win
|
| 82 |
+
# - short trade: y=1 if short loses (long would've won)
|
| 83 |
+
y_long = won if side == "long" else (1.0 - won)
|
| 84 |
+
scores.append((p_long - y_long) ** 2)
|
| 85 |
+
return _mean(scores)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
def _calibration_table(
|
| 89 |
+
trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float], side: str
|
| 90 |
+
) -> List[Dict[str, Any]]:
|
| 91 |
+
bins: Dict[int, List[Dict[str, Any]]] = defaultdict(list)
|
| 92 |
+
for t in trades:
|
| 93 |
+
t_side = str(t.get("side", "")).lower()
|
| 94 |
+
if t_side != side:
|
| 95 |
+
continue
|
| 96 |
+
p = _trade_p_long(t, open_probs)
|
| 97 |
+
decile = min(9, max(0, int(math.floor(p * 10.0))))
|
| 98 |
+
bins[decile].append(t)
|
| 99 |
+
|
| 100 |
+
rows: List[Dict[str, Any]] = []
|
| 101 |
+
for decile in range(10):
|
| 102 |
+
arr = bins.get(decile, [])
|
| 103 |
+
if not arr:
|
| 104 |
+
continue
|
| 105 |
+
n = len(arr)
|
| 106 |
+
wins = sum(1 for t in arr if _safe_float(t.get("pnl_usd", 0.0), 0.0) > 0)
|
| 107 |
+
avg_p = _mean([_trade_p_long(t, open_probs) for t in arr])
|
| 108 |
+
rows.append(
|
| 109 |
+
{
|
| 110 |
+
"decile": decile,
|
| 111 |
+
"range": f"{decile / 10:.1f}-{(decile + 1) / 10:.1f}",
|
| 112 |
+
"trades": n,
|
| 113 |
+
"avg_p_long": avg_p,
|
| 114 |
+
"win_rate": (wins / n) if n else None,
|
| 115 |
+
}
|
| 116 |
+
)
|
| 117 |
+
return rows
|
| 118 |
+
|
| 119 |
+
|
| 120 |
+
def _open_prob_lookup(events: List[Dict[str, Any]]) -> Dict[Tuple[str, int], float]:
|
| 121 |
+
out: Dict[Tuple[str, int], float] = {}
|
| 122 |
+
for e in events:
|
| 123 |
+
if e.get("event") != "open":
|
| 124 |
+
continue
|
| 125 |
+
coin = str(e.get("coin", ""))
|
| 126 |
+
entry_ts = int(_safe_float(e.get("entry_ts", 0), 0))
|
| 127 |
+
p_long = float(max(0.0, min(1.0, _safe_float(e.get("p_long", 0.5), 0.5))))
|
| 128 |
+
out[(coin, entry_ts)] = p_long
|
| 129 |
+
return out
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _fetch_status(url: str) -> Optional[Dict[str, Any]]:
|
| 133 |
+
try:
|
| 134 |
+
with urlopen(url, timeout=3) as response:
|
| 135 |
+
if response.status != 200:
|
| 136 |
+
return None
|
| 137 |
+
body = response.read().decode("utf-8")
|
| 138 |
+
return json.loads(body)
|
| 139 |
+
except (URLError, TimeoutError, json.JSONDecodeError, ValueError):
|
| 140 |
+
return None
|
| 141 |
+
|
| 142 |
+
|
| 143 |
+
def build_report(payload: Dict[str, Any], status_payload: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
| 144 |
+
closed = payload.get("closed_trades", []) or []
|
| 145 |
+
events = payload.get("trade_events", []) or []
|
| 146 |
+
open_probs = _open_prob_lookup(events)
|
| 147 |
+
|
| 148 |
+
by_coin: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
| 149 |
+
by_side: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
| 150 |
+
for t in closed:
|
| 151 |
+
by_coin[str(t.get("coin", "unknown"))].append(t)
|
| 152 |
+
by_side[str(t.get("side", "unknown")).lower()].append(t)
|
| 153 |
+
|
| 154 |
+
per_coin = {coin: _summarize_basic(trades) for coin, trades in sorted(by_coin.items())}
|
| 155 |
+
per_side = {side: _summarize_basic(trades) for side, trades in sorted(by_side.items())}
|
| 156 |
+
|
| 157 |
+
report: Dict[str, Any] = {
|
| 158 |
+
"state_samples": {
|
| 159 |
+
"closed_trades": len(closed),
|
| 160 |
+
"trade_events": len(events),
|
| 161 |
+
},
|
| 162 |
+
"overall": _summarize_basic(closed),
|
| 163 |
+
"per_coin": per_coin,
|
| 164 |
+
"per_side": per_side,
|
| 165 |
+
"directional_sanity": _directional_sanity(closed, open_probs),
|
| 166 |
+
"brier_direction_aware": _direction_aware_brier(closed, open_probs),
|
| 167 |
+
"calibration": {
|
| 168 |
+
"long": _calibration_table(closed, open_probs, "long"),
|
| 169 |
+
"short": _calibration_table(closed, open_probs, "short"),
|
| 170 |
+
},
|
| 171 |
+
"runtime_status": status_payload,
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
# High-level warnings to speed up triage.
|
| 175 |
+
warnings: List[str] = []
|
| 176 |
+
ds = report["directional_sanity"]
|
| 177 |
+
lw = ds.get("long_winners_avg_p_long")
|
| 178 |
+
ll = ds.get("long_losers_avg_p_long")
|
| 179 |
+
sw = ds.get("short_winners_avg_p_long")
|
| 180 |
+
sl = ds.get("short_losers_avg_p_long")
|
| 181 |
+
|
| 182 |
+
if lw is not None and ll is not None and lw <= ll:
|
| 183 |
+
warnings.append("Long directional signal is inverted or weak: long winners do not have higher p_long than long losers.")
|
| 184 |
+
if sw is not None and sl is not None and sw >= sl:
|
| 185 |
+
warnings.append("Short directional signal is inverted or weak: short winners do not have lower p_long than short losers.")
|
| 186 |
+
|
| 187 |
+
overall_wr = report["overall"].get("win_rate", 0.0)
|
| 188 |
+
overall_exp = report["overall"].get("expectancy_usd", 0.0)
|
| 189 |
+
if len(closed) >= 50 and overall_wr < 0.45:
|
| 190 |
+
warnings.append("Win rate below 45% after 50+ trades; model may be mis-specified or overtrading.")
|
| 191 |
+
if len(closed) >= 50 and overall_exp < 0:
|
| 192 |
+
warnings.append("Negative expectancy after 50+ trades; consider reducing exploration and revisiting labels.")
|
| 193 |
+
|
| 194 |
+
report["warnings"] = warnings
|
| 195 |
+
return report
|
| 196 |
+
|
| 197 |
+
|
| 198 |
+
def print_report(report: Dict[str, Any]) -> None:
|
| 199 |
+
print("=== Adaptive Bot ML Diagnostics ===")
|
| 200 |
+
samples = report["state_samples"]
|
| 201 |
+
print(f"Closed trades: {samples['closed_trades']}")
|
| 202 |
+
print(f"Trade events: {samples['trade_events']}")
|
| 203 |
+
|
| 204 |
+
overall = report["overall"]
|
| 205 |
+
print(
|
| 206 |
+
"Overall: "
|
| 207 |
+
f"n={int(overall['trades'])}, "
|
| 208 |
+
f"win_rate={overall['win_rate'] * 100:.2f}%, "
|
| 209 |
+
f"total_pnl={overall['total_pnl_usd']:.2f}, "
|
| 210 |
+
f"expectancy={overall['expectancy_usd']:.4f}"
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
print("\nBy side:")
|
| 214 |
+
for side, s in report["per_side"].items():
|
| 215 |
+
print(
|
| 216 |
+
f" {side}: n={int(s['trades'])}, win_rate={s['win_rate'] * 100:.2f}%, "
|
| 217 |
+
f"total_pnl={s['total_pnl_usd']:.2f}, expectancy={s['expectancy_usd']:.4f}"
|
| 218 |
+
)
|
| 219 |
+
|
| 220 |
+
print("\nBy coin:")
|
| 221 |
+
for coin, s in report["per_coin"].items():
|
| 222 |
+
print(
|
| 223 |
+
f" {coin}: n={int(s['trades'])}, win_rate={s['win_rate'] * 100:.2f}%, "
|
| 224 |
+
f"total_pnl={s['total_pnl_usd']:.2f}, expectancy={s['expectancy_usd']:.4f}"
|
| 225 |
+
)
|
| 226 |
+
|
| 227 |
+
ds = report["directional_sanity"]
|
| 228 |
+
print("\nDirectional sanity:")
|
| 229 |
+
print(
|
| 230 |
+
" Long winners avg p_long: "
|
| 231 |
+
f"{ds['long_winners_avg_p_long'] if ds['long_winners_avg_p_long'] is not None else 'n/a'}"
|
| 232 |
+
)
|
| 233 |
+
print(
|
| 234 |
+
" Long losers avg p_long: "
|
| 235 |
+
f"{ds['long_losers_avg_p_long'] if ds['long_losers_avg_p_long'] is not None else 'n/a'}"
|
| 236 |
+
)
|
| 237 |
+
print(
|
| 238 |
+
" Short winners avg p_long: "
|
| 239 |
+
f"{ds['short_winners_avg_p_long'] if ds['short_winners_avg_p_long'] is not None else 'n/a'}"
|
| 240 |
+
)
|
| 241 |
+
print(
|
| 242 |
+
" Short losers avg p_long: "
|
| 243 |
+
f"{ds['short_losers_avg_p_long'] if ds['short_losers_avg_p_long'] is not None else 'n/a'}"
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
brier = report.get("brier_direction_aware")
|
| 247 |
+
print(f"\nDirection-aware Brier (lower is better): {brier if brier is not None else 'n/a'}")
|
| 248 |
+
|
| 249 |
+
warnings = report.get("warnings", [])
|
| 250 |
+
if warnings:
|
| 251 |
+
print("\nWarnings:")
|
| 252 |
+
for w in warnings:
|
| 253 |
+
print(f" - {w}")
|
| 254 |
+
|
| 255 |
+
runtime = report.get("runtime_status")
|
| 256 |
+
if runtime:
|
| 257 |
+
print("\nLive status snapshot:")
|
| 258 |
+
metrics = runtime.get("metrics", {})
|
| 259 |
+
ml_progress = runtime.get("ml_progress", {})
|
| 260 |
+
print(
|
| 261 |
+
" runtime trades="
|
| 262 |
+
f"{int(_safe_float(metrics.get('trades', 0), 0))}, "
|
| 263 |
+
f"win_rate={_safe_float(metrics.get('win_rate', 0.0), 0.0) * 100:.2f}%, "
|
| 264 |
+
f"pf={_safe_float(metrics.get('profit_factor', 0.0), 0.0):.2f}"
|
| 265 |
+
)
|
| 266 |
+
if ml_progress:
|
| 267 |
+
print(
|
| 268 |
+
" runtime brier_50="
|
| 269 |
+
f"{ml_progress.get('brier_rolling_50')}, "
|
| 270 |
+
f"expectancy_30={ml_progress.get('expectancy_usd_30')}, "
|
| 271 |
+
f"max_dd={ml_progress.get('max_drawdown_pct')}"
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
def parse_args() -> argparse.Namespace:
|
| 276 |
+
p = argparse.ArgumentParser(description="Diagnose adaptive bot ML learning quality")
|
| 277 |
+
p.add_argument("--state-file", default="learning_state.joblib", help="Path to persisted bot state file")
|
| 278 |
+
p.add_argument(
|
| 279 |
+
"--status-url",
|
| 280 |
+
default="http://127.0.0.1:7860/status/full",
|
| 281 |
+
help="Optional bot status endpoint for runtime snapshot",
|
| 282 |
+
)
|
| 283 |
+
p.add_argument("--skip-status", action="store_true", help="Skip HTTP status fetch")
|
| 284 |
+
p.add_argument("--json", action="store_true", help="Print full report as JSON")
|
| 285 |
+
return p.parse_args()
|
| 286 |
+
|
| 287 |
+
|
| 288 |
+
def main() -> int:
|
| 289 |
+
args = parse_args()
|
| 290 |
+
|
| 291 |
+
if not os.path.exists(args.state_file):
|
| 292 |
+
print(f"State file not found: {args.state_file}")
|
| 293 |
+
return 1
|
| 294 |
+
|
| 295 |
+
payload = joblib.load(args.state_file)
|
| 296 |
+
status_payload = None if args.skip_status else _fetch_status(args.status_url)
|
| 297 |
+
report = build_report(payload, status_payload=status_payload)
|
| 298 |
+
|
| 299 |
+
if args.json:
|
| 300 |
+
print(json.dumps(report, indent=2))
|
| 301 |
+
else:
|
| 302 |
+
print_report(report)
|
| 303 |
+
return 0
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
if __name__ == "__main__":
|
| 307 |
+
raise SystemExit(main())
|
| 308 |
+
|