Daniel Alnawaz commited on
Commit
136aa53
·
unverified ·
1 Parent(s): 907fa0b

diagnostics

Browse files
.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="---&#10;title: Hyperliquid Adaptive Bot&#10;emoji: &quot;&quot;&#10;colorFrom: blue&#10;colorTo: purple&#10;sdk: docker&#10;pinned: false&#10;---&#10;&#10;# Hyperliquid Adaptive Bot&#10;&#10;This project now includes an adaptive ML bot that uses your existing Hyperliquid framework.&#10;&#10;## What it does&#10;&#10;- Trades **only** `BTC`, `ETH`, and `SOL`&#10;- Runs in **paper mode by default**&#10;- Learns from closed paper trades with an online ML model (`SGDClassifier`)&#10;- Persists everything learned to disk (`learning_state.joblib`) so restart resumes learning&#10;- Persists paper runtime state (equity, open positions, entry features) so restart does not break learning continuity&#10;- Uses a profitability gate before any live execution is allowed&#10;- Sends Telegram alerts on trade open/close and a midnight UTC summary&#10;- Sends a readiness alert when paper performance is strong enough for live trading&#10;- Optional startup live transaction test (tiny BUY then SELL) to verify live order execution path&#10;- Can mirror to live orders only when:&#10; - `--enable-live` is set&#10; - paper performance is consistently profitable&#10; - manual approval is granted (`ALLOW_LIVE_TRADING=1` or approval flag file exists)&#10; - `HL_PRIVATE_KEY` is configured&#10;&#10;## Files&#10;&#10;- `hbot.py`: existing Hyperliquid API framework and order placement helpers&#10;- `adaptive_bot.py`: new adaptive ML paper/live-gated trading bot&#10;- `smoke_test.py`: offline persistence and learning smoke test&#10;- `paper_report.json`: runtime status and metrics output (created at runtime)&#10;&#10;## Health endpoints (for HF + UptimeRobot)&#10;&#10;- `GET /` -&gt; lightweight status response (`200`) for uptime checks&#10;- `GET /healthz` -&gt; same as root (`200`)&#10;- `GET /status` -&gt; concise overview (equity, open position count, performance, live gate)&#10;- `GET /status/full` -&gt; full runtime JSON (equity, open positions, metrics, thresholds)&#10;- `GET /trades` -&gt; open positions list and count&#10;&#10;If your Space is `https://chigginrice-hyperliquid-bot.hf.space/`, your existing root monitor will work as-is.&#10;&#10;## Quick start&#10;&#10;1. Install dependencies:&#10;&#10;```bash&#10;python3 -m venv .venv&#10;source .venv/bin/activate&#10;pip install -r requirements.txt&#10;```&#10;&#10;2. Run the offline smoke test:&#10;&#10;```bash&#10;python smoke_test.py&#10;```&#10;&#10;3. Start the adaptive paper bot:&#10;&#10;```bash&#10;python adaptive_bot.py --interval 15m --poll-seconds 45&#10;```&#10;&#10;Optional knobs for early growth from scratch:&#10;&#10;```bash&#10;python adaptive_bot.py --interval 15m --exploration-rate 0.08&#10;```&#10;&#10;## Optional: allow live mirror only after paper consistency gate&#10;&#10;```bash&#10;export HL_PRIVATE_KEY=&quot;your_private_key&quot;&#10;# optional if using delegated account&#10;export HL_ACCOUNT_ADDRESS=&quot;0x...&quot;&#10;python adaptive_bot.py --interval 15m --enable-live&#10;```&#10;&#10;By default, live arm still requires manual approval even after the paper gate is passed:&#10;&#10;```bash&#10;export ALLOW_LIVE_TRADING=1&#10;# or: touch allow_live_trading.flag&#10;python adaptive_bot.py --interval 15m --enable-live&#10;```&#10;&#10;Strict two-step approval mode (recommended for safety):&#10;&#10;```bash&#10;export ALLOW_LIVE_TRADING=1&#10;touch allow_live_trading.flag&#10;python adaptive_bot.py --interval 15m --enable-live --live-approval-mode both&#10;```&#10;&#10;Optional startup live execution test (real tiny transaction):&#10;&#10;```bash&#10;python adaptive_bot.py --interval 15m --enable-live --live-startup-test --live-startup-test-coin BTC --live-startup-test-notional 12&#10;```&#10;&#10;## Docker (Hugging Face)&#10;&#10;`Dockerfile` is included for Docker-based hosting.&#10;&#10;Build locally:&#10;&#10;```bash&#10;docker build -t hyperliquid-adaptive-bot .&#10;```&#10;&#10;Run locally in paper mode:&#10;&#10;```bash&#10;docker run --rm \&#10; -e BOT_INTERVAL=15m \&#10; -e BOT_POLL_SECONDS=45 \&#10; -v &quot;$(pwd)/data:/data&quot; \&#10; hyperliquid-adaptive-bot&#10;```&#10;&#10;Run with live mode allowed after paper gate passes:&#10;&#10;```bash&#10;docker run --rm \&#10; -e BOT_ENABLE_LIVE=1 \&#10; -e HL_PRIVATE_KEY=&quot;your_private_key&quot; \&#10; -e HL_ACCOUNT_ADDRESS=&quot;0x...&quot; \&#10; -v &quot;$(pwd)/data:/data&quot; \&#10; hyperliquid-adaptive-bot&#10;```&#10;&#10;Environment variables used by the container:&#10;&#10;- `BOT_INTERVAL` (default `15m`)&#10;- `BOT_POLL_SECONDS` (default `45`)&#10;- `BOT_STATE_FILE` (default `/data/learning_state.joblib`)&#10;- `BOT_REPORT_FILE` (default `/data/paper_report.json`)&#10;- `BOT_ENABLE_LIVE` (set any non-empty value to append `--enable-live`)&#10;- `BOT_HEALTH_HOST` (default `0.0.0.0`)&#10;- `BOT_HEALTH_PORT` (default `7860`)&#10;- `PORT` (default `7860`, used by Hugging Face Spaces)&#10;- `ALLOW_LIVE_TRADING` (set to `1/true/yes` to manually approve live trading)&#10;- `BOT_LIVE_STARTUP_TEST` (set non-empty to add `--live-startup-test`)&#10;- `BOT_LIVE_STARTUP_TEST_COIN` (default `BTC`)&#10;- `BOT_LIVE_STARTUP_TEST_NOTIONAL` (default `12`)&#10;- `TELEGRAM_BOT_TOKEN` (required for Telegram notifications)&#10;- `TELEGRAM_CHAT_ID` (required for Telegram notifications)&#10;- `PROXY_URL` (required by `telegram_notifier.py` to forward messages)&#10;- `HL_PRIVATE_KEY` (required for live mirror)&#10;- `HL_ACCOUNT_ADDRESS` (optional)&#10;&#10;## Telegram notifications&#10;&#10;- Entry alert: sent when a paper position opens&#10;- Exit alert: sent when TP/SL closes a position&#10;- Midnight summary: sent once per day at `00:00 UTC`&#10;- Readiness alert: sent when profitability gate is reached and bot is ready to request live approval&#10;&#10;Summary includes last-24h trades, win rate, PnL, and overall cumulative stats.&#10;&#10;## How the ML learns&#10;&#10;At a high level, this bot uses online learning so it improves continuously while paper trading:&#10;&#10;1. For each coin (`BTC`, `ETH`, `SOL`), it builds a feature vector from latest candles:&#10; - short-horizon returns (`ret_1`, `ret_3`, `ret_8`)&#10; - trend/momentum (`macd`, scaled `rsi`)&#10; - volatility and activity (`atr14`, `vol_z`)&#10;2. `SGDClassifier(loss=&quot;log_loss&quot;)` predicts probability of a profitable long setup.&#10;3. Entry logic maps probability to actions:&#10; - `p_long &gt;= threshold` -&gt; long&#10; - `p_long &lt;= 1 - threshold` -&gt; short&#10; - otherwise hold&#10; - when model is cold, bootstrap mode uses weak momentum direction to generate first labels&#10; - optional exploration (`--exploration-rate`) continues collecting data even during neutral model states&#10;4. When a trade closes (TP/SL), outcome is the supervision label:&#10; - profitable -&gt; label `1`&#10; - losing -&gt; label `0`&#10;5. The model is updated immediately using `partial_fit(...)` on that single labeled example.&#10;6. The confidence threshold adapts from recent win rate (last 30 outcomes), becoming stricter or looser.&#10;7. Learned state is persisted to `learning_state.joblib` and reloaded on restart.&#10;8. Open paper positions and captured entry features are also persisted, so trades crossing restarts still train the model on close.&#10;&#10;So each closed paper trade directly trains the next decision, and training survives restarts.&#10;&#10;## Notes&#10;&#10;- The consistency gate currently requires:&#10; - at least 30 paper trades&#10; - win rate &gt;= 58%&#10; - profit factor &gt;= 1.25&#10;- State persistence file is configurable with `--state-file`&#10;- Runtime report file is configurable with `--report-file`&#10;- Live approval can be controlled with:&#10; - `--require-live-approval / --no-require-live-approval`&#10; - `--live-approval-mode either|both` (default `either`)&#10; - `--live-approval-file` (default `allow_live_trading.flag`)&#10;- Startup live order test can be controlled with:&#10; - `--live-startup-test / --no-live-startup-test`&#10; - `--live-startup-test-coin` (default `BTC`)&#10; - `--live-startup-test-notional` (default `12`)&#10;&#10;&#10;" />
11
+ <option name="updatedContent" value="---&#10;title: Hyperliquid Adaptive Bot&#10;emoji: &quot;&quot;&#10;colorFrom: blue&#10;colorTo: purple&#10;sdk: docker&#10;pinned: false&#10;---&#10;&#10;# Hyperliquid Adaptive Bot&#10;&#10;This project now includes an adaptive ML bot that uses your existing Hyperliquid framework.&#10;&#10;## What it does&#10;&#10;- Trades **only** `BTC`, `ETH`, and `SOL`&#10;- Runs in **paper mode by default**&#10;- Learns from closed paper trades with an online ML model (`SGDClassifier`)&#10;- Persists everything learned to disk (`learning_state.joblib`) so restart resumes learning&#10;- Persists paper runtime state (equity, open positions, entry features) so restart does not break learning continuity&#10;- Uses a profitability gate before any live execution is allowed&#10;- Sends Telegram alerts on trade open/close and a midnight UTC summary&#10;- Sends a readiness alert when paper performance is strong enough for live trading&#10;- Optional startup live transaction test (tiny BUY then SELL) to verify live order execution path&#10;- Can mirror to live orders only when:&#10; - `--enable-live` is set&#10; - paper performance is consistently profitable&#10; - manual approval is granted (`ALLOW_LIVE_TRADING=1` or approval flag file exists)&#10; - `HL_PRIVATE_KEY` is configured&#10;&#10;## Files&#10;&#10;- `hbot.py`: existing Hyperliquid API framework and order placement helpers&#10;- `adaptive_bot.py`: new adaptive ML paper/live-gated trading bot&#10;- `smoke_test.py`: offline persistence and learning smoke test&#10;- `ml_diagnostics.py`: offline + live learning diagnostics report&#10;- `diagnostics_smoke_test.py`: quick smoke test for diagnostics logic&#10;- `paper_report.json`: runtime status and metrics output (created at runtime)&#10;&#10;## Health endpoints (for HF + UptimeRobot)&#10;&#10;- `GET /` -&gt; lightweight status response (`200`) for uptime checks&#10;- `GET /healthz` -&gt; same as root (`200`)&#10;- `GET /status` -&gt; concise overview (equity, open position count, performance, live gate)&#10;- `GET /status/full` -&gt; full runtime JSON (equity, open positions, metrics, thresholds)&#10;- `GET /trades` -&gt; open positions list and count&#10;&#10;If your Space is `https://chigginrice-hyperliquid-bot.hf.space/`, your existing root monitor will work as-is.&#10;&#10;## Quick start&#10;&#10;1. Install dependencies:&#10;&#10;```bash&#10;python3 -m venv .venv&#10;source .venv/bin/activate&#10;pip install -r requirements.txt&#10;```&#10;&#10;2. Run the offline smoke test:&#10;&#10;```bash&#10;python smoke_test.py&#10;```&#10;&#10;Optional: run diagnostics smoke test:&#10;&#10;```bash&#10;python diagnostics_smoke_test.py&#10;```&#10;&#10;3. Start the adaptive paper bot:&#10;&#10;```bash&#10;python adaptive_bot.py --interval 15m --poll-seconds 45&#10;```&#10;&#10;4. Check ML learning quality from saved state:&#10;&#10;```bash&#10;python ml_diagnostics.py --state-file learning_state.joblib&#10;```&#10;&#10;If the bot is running locally, include live status snapshot:&#10;&#10;```bash&#10;python ml_diagnostics.py --state-file learning_state.joblib --status-url http://127.0.0.1:7860/status/full&#10;```&#10;&#10;JSON output (useful for storing artifacts in CI/HF workflows):&#10;&#10;```bash&#10;python ml_diagnostics.py --state-file learning_state.joblib --json&#10;```&#10;&#10;Optional knobs for early growth from scratch:&#10;&#10;```bash&#10;python adaptive_bot.py --interval 15m --exploration-rate 0.08&#10;```&#10;&#10;## Optional: allow live mirror only after paper consistency gate&#10;&#10;```bash&#10;export HL_PRIVATE_KEY=&quot;your_private_key&quot;&#10;# optional if using delegated account&#10;export HL_ACCOUNT_ADDRESS=&quot;0x...&quot;&#10;python adaptive_bot.py --interval 15m --enable-live&#10;```&#10;&#10;By default, live arm still requires manual approval even after the paper gate is passed:&#10;&#10;```bash&#10;export ALLOW_LIVE_TRADING=1&#10;# or: touch allow_live_trading.flag&#10;python adaptive_bot.py --interval 15m --enable-live&#10;```&#10;&#10;Strict two-step approval mode (recommended for safety):&#10;&#10;```bash&#10;export ALLOW_LIVE_TRADING=1&#10;touch allow_live_trading.flag&#10;python adaptive_bot.py --interval 15m --enable-live --live-approval-mode both&#10;```&#10;&#10;Optional startup live execution test (real tiny transaction):&#10;&#10;```bash&#10;python adaptive_bot.py --interval 15m --enable-live --live-startup-test --live-startup-test-coin BTC --live-startup-test-notional 12&#10;```&#10;&#10;## Docker (Hugging Face)&#10;&#10;`Dockerfile` is included for Docker-based hosting.&#10;&#10;Build locally:&#10;&#10;```bash&#10;docker build -t hyperliquid-adaptive-bot .&#10;```&#10;&#10;Run locally in paper mode:&#10;&#10;```bash&#10;docker run --rm \&#10; -e BOT_INTERVAL=15m \&#10; -e BOT_POLL_SECONDS=45 \&#10; -v &quot;$(pwd)/data:/data&quot; \&#10; hyperliquid-adaptive-bot&#10;```&#10;&#10;Run with live mode allowed after paper gate passes:&#10;&#10;```bash&#10;docker run --rm \&#10; -e BOT_ENABLE_LIVE=1 \&#10; -e HL_PRIVATE_KEY=&quot;your_private_key&quot; \&#10; -e HL_ACCOUNT_ADDRESS=&quot;0x...&quot; \&#10; -v &quot;$(pwd)/data:/data&quot; \&#10; hyperliquid-adaptive-bot&#10;```&#10;&#10;Environment variables used by the container:&#10;&#10;- `BOT_INTERVAL` (default `15m`)&#10;- `BOT_POLL_SECONDS` (default `45`)&#10;- `BOT_STATE_FILE` (default `/data/learning_state.joblib`)&#10;- `BOT_REPORT_FILE` (default `/data/paper_report.json`)&#10;- `BOT_ENABLE_LIVE` (set any non-empty value to append `--enable-live`)&#10;- `BOT_HEALTH_HOST` (default `0.0.0.0`)&#10;- `BOT_HEALTH_PORT` (default `7860`)&#10;- `PORT` (default `7860`, used by Hugging Face Spaces)&#10;- `ALLOW_LIVE_TRADING` (set to `1/true/yes` to manually approve live trading)&#10;- `BOT_LIVE_STARTUP_TEST` (set non-empty to add `--live-startup-test`)&#10;- `BOT_LIVE_STARTUP_TEST_COIN` (default `BTC`)&#10;- `BOT_LIVE_STARTUP_TEST_NOTIONAL` (default `12`)&#10;- `TELEGRAM_BOT_TOKEN` (required for Telegram notifications)&#10;- `TELEGRAM_CHAT_ID` (required for Telegram notifications)&#10;- `PROXY_URL` (required by `telegram_notifier.py` to forward messages)&#10;- `HL_PRIVATE_KEY` (required for live mirror)&#10;- `HL_ACCOUNT_ADDRESS` (optional)&#10;&#10;## Telegram notifications&#10;&#10;- Entry alert: sent when a paper position opens&#10;- Exit alert: sent when TP/SL closes a position&#10;- Midnight summary: sent once per day at `00:00 UTC`&#10;- Readiness alert: sent when profitability gate is reached and bot is ready to request live approval&#10;&#10;Summary includes last-24h trades, win rate, PnL, and overall cumulative stats.&#10;&#10;## How the ML learns&#10;&#10;At a high level, this bot uses online learning so it improves continuously while paper trading:&#10;&#10;1. For each coin (`BTC`, `ETH`, `SOL`), it builds a feature vector from latest candles:&#10; - short-horizon returns (`ret_1`, `ret_3`, `ret_8`)&#10; - trend/momentum (`macd`, scaled `rsi`)&#10; - volatility and activity (`atr14`, `vol_z`)&#10;2. `SGDClassifier(loss=&quot;log_loss&quot;)` predicts probability of a profitable long setup.&#10;3. Entry logic maps probability to actions:&#10; - `p_long &gt;= threshold` -&gt; long&#10; - `p_long &lt;= 1 - threshold` -&gt; short&#10; - otherwise hold&#10; - when model is cold, bootstrap mode uses weak momentum direction to generate first labels&#10; - optional exploration (`--exploration-rate`) continues collecting data even during neutral model states&#10;4. When a trade closes (TP/SL), outcome is the supervision label:&#10; - profitable -&gt; label `1`&#10; - losing -&gt; label `0`&#10;5. The model is updated immediately using `partial_fit(...)` on that single labeled example.&#10;6. The confidence threshold adapts from recent win rate (last 30 outcomes), becoming stricter or looser.&#10;7. Learned state is persisted to `learning_state.joblib` and reloaded on restart.&#10;8. Open paper positions and captured entry features are also persisted, so trades crossing restarts still train the model on close.&#10;&#10;So each closed paper trade directly trains the next decision, and training survives restarts.&#10;&#10;## ML diagnostics&#10;&#10;`ml_diagnostics.py` helps answer whether the model signal is actually improving.&#10;&#10;It reports:&#10;&#10;- overall/per-coin/per-side win rate and expectancy&#10;- directional sanity checks:&#10; - long winners should have higher `p_long` than long losers&#10; - short winners should have lower `p_long` than short losers&#10;- direction-aware Brier score (lower is better)&#10;- optional runtime snapshot from `/status/full` including rolling Brier/expectancy/drawdown&#10;&#10;This is useful to catch label/specification issues where the model keeps repeating failing trade patterns.&#10;&#10;## Notes&#10;&#10;- The consistency gate currently requires:&#10; - at least 30 paper trades&#10; - win rate &gt;= 58%&#10; - profit factor &gt;= 1.25&#10;- State persistence file is configurable with `--state-file`&#10;- Runtime report file is configurable with `--report-file`&#10;- Live approval can be controlled with:&#10; - `--require-live-approval / --no-require-live-approval`&#10; - `--live-approval-mode either|both` (default `either`)&#10; - `--live-approval-file` (default `allow_live_trading.flag`)&#10;- Startup live order test can be controlled with:&#10; - `--live-startup-test / --no-live-startup-test`&#10; - `--live-startup-test-coin` (default `BTC`)&#10; - `--live-startup-test-notional` (default `12`)&#10;&#10;&#10;" />
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&#10;&quot;&quot;&quot;Smoke test for ml_diagnostics report builder.&quot;&quot;&quot;&#10;&#10;from __future__ import annotations&#10;&#10;import tempfile&#10;from pathlib import Path&#10;&#10;import joblib&#10;&#10;from ml_diagnostics import build_report&#10;&#10;&#10;def main() -&gt; None:&#10; payload = {&#10; &quot;closed_trades&quot;: [&#10; {&#10; &quot;coin&quot;: &quot;BTC&quot;,&#10; &quot;side&quot;: &quot;long&quot;,&#10; &quot;entry_ts&quot;: 1000,&#10; &quot;pnl_usd&quot;: 5.0,&#10; &quot;entry_p_long&quot;: 0.72,&#10; },&#10; {&#10; &quot;coin&quot;: &quot;BTC&quot;,&#10; &quot;side&quot;: &quot;short&quot;,&#10; &quot;entry_ts&quot;: 2000,&#10; &quot;pnl_usd&quot;: -3.0,&#10; &quot;entry_p_long&quot;: 0.81,&#10; },&#10; {&#10; &quot;coin&quot;: &quot;ETH&quot;,&#10; &quot;side&quot;: &quot;short&quot;,&#10; &quot;entry_ts&quot;: 3000,&#10; &quot;pnl_usd&quot;: 4.0,&#10; &quot;entry_p_long&quot;: 0.22,&#10; },&#10; ],&#10; &quot;trade_events&quot;: [&#10; {&quot;event&quot;: &quot;open&quot;, &quot;coin&quot;: &quot;BTC&quot;, &quot;entry_ts&quot;: 1000, &quot;p_long&quot;: 0.72},&#10; {&quot;event&quot;: &quot;open&quot;, &quot;coin&quot;: &quot;BTC&quot;, &quot;entry_ts&quot;: 2000, &quot;p_long&quot;: 0.81},&#10; {&quot;event&quot;: &quot;open&quot;, &quot;coin&quot;: &quot;ETH&quot;, &quot;entry_ts&quot;: 3000, &quot;p_long&quot;: 0.22},&#10; ],&#10; }&#10;&#10; with tempfile.TemporaryDirectory() as td:&#10; state_path = Path(td) / &quot;learning_state.joblib&quot;&#10; joblib.dump(payload, state_path)&#10; loaded = joblib.load(state_path)&#10;&#10; report = build_report(loaded)&#10;&#10; assert report[&quot;state_samples&quot;][&quot;closed_trades&quot;] == 3&#10; assert &quot;overall&quot; in report&#10; assert &quot;directional_sanity&quot; in report&#10; assert report[&quot;brier_direction_aware&quot;] is not None&#10;&#10; print(&quot;diagnostics_smoke_test: OK&quot;)&#10;&#10;&#10;if __name__ == &quot;__main__&quot;:&#10; main()&#10;" />
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&#10;&quot;&quot;&quot;Offline and live diagnostics for adaptive bot learning quality.&#10;&#10;This script helps validate whether the model is learning directionally useful signals.&#10;&quot;&quot;&quot;&#10;&#10;from __future__ import annotations&#10;&#10;import argparse&#10;import json&#10;import math&#10;import os&#10;from collections import defaultdict&#10;from typing import Any, Dict, List, Optional, Tuple&#10;from urllib.error import URLError&#10;from urllib.request import urlopen&#10;&#10;import joblib&#10;&#10;&#10;def _safe_float(v: Any, default: float = 0.0) -&gt; float:&#10; try:&#10; return float(v)&#10; except Exception:&#10; return default&#10;&#10;&#10;def _mean(values: List[float]) -&gt; Optional[float]:&#10; return (sum(values) / len(values)) if values else None&#10;&#10;&#10;def _trade_p_long(trade: Dict[str, Any], open_probs: Dict[Tuple[str, int], float]) -&gt; float:&#10; if &quot;entry_p_long&quot; in trade and trade[&quot;entry_p_long&quot;] is not None:&#10; return float(max(0.0, min(1.0, _safe_float(trade[&quot;entry_p_long&quot;], 0.5))))&#10; key = (str(trade.get(&quot;coin&quot;, &quot;&quot;)), int(_safe_float(trade.get(&quot;entry_ts&quot;, 0), 0)))&#10; return float(max(0.0, min(1.0, open_probs.get(key, 0.5))))&#10;&#10;&#10;def _summarize_basic(trades: List[Dict[str, Any]]) -&gt; Dict[str, float]:&#10; n = len(trades)&#10; wins = sum(1 for t in trades if _safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) &gt; 0)&#10; pnl = sum(_safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) for t in trades)&#10; return {&#10; &quot;trades&quot;: float(n),&#10; &quot;wins&quot;: float(wins),&#10; &quot;win_rate&quot;: (wins / n) if n else 0.0,&#10; &quot;total_pnl_usd&quot;: pnl,&#10; &quot;expectancy_usd&quot;: (pnl / n) if n else 0.0,&#10; }&#10;&#10;&#10;def _directional_sanity(trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float]) -&gt; Dict[str, Optional[float]]:&#10; longs = [t for t in trades if str(t.get(&quot;side&quot;, &quot;&quot;)).lower() == &quot;long&quot;]&#10; shorts = [t for t in trades if str(t.get(&quot;side&quot;, &quot;&quot;)).lower() == &quot;short&quot;]&#10;&#10; long_win = [_trade_p_long(t, open_probs) for t in longs if _safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) &gt; 0]&#10; long_lose = [_trade_p_long(t, open_probs) for t in longs if _safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) &lt;= 0]&#10; short_win = [_trade_p_long(t, open_probs) for t in shorts if _safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) &gt; 0]&#10; short_lose = [_trade_p_long(t, open_probs) for t in shorts if _safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) &lt;= 0]&#10;&#10; # For a healthy long-probability model:&#10; # - long winners should have higher p_long than long losers&#10; # - short winners should have lower p_long than short losers&#10; return {&#10; &quot;long_winners_avg_p_long&quot;: _mean(long_win),&#10; &quot;long_losers_avg_p_long&quot;: _mean(long_lose),&#10; &quot;short_winners_avg_p_long&quot;: _mean(short_win),&#10; &quot;short_losers_avg_p_long&quot;: _mean(short_lose),&#10; }&#10;&#10;&#10;def _direction_aware_brier(trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float]) -&gt; Optional[float]:&#10; scores: List[float] = []&#10; for t in trades:&#10; side = str(t.get(&quot;side&quot;, &quot;&quot;)).lower()&#10; if side not in {&quot;long&quot;, &quot;short&quot;}:&#10; continue&#10; p_long = _trade_p_long(t, open_probs)&#10; won = 1.0 if _safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) &gt; 0 else 0.0&#10; # Target for long probability:&#10; # - long trade: y=1 if win&#10; # - short trade: y=1 if short loses (long would've won)&#10; y_long = won if side == &quot;long&quot; else (1.0 - won)&#10; scores.append((p_long - y_long) ** 2)&#10; return _mean(scores)&#10;&#10;&#10;def _calibration_table(&#10; trades: List[Dict[str, Any]], open_probs: Dict[Tuple[str, int], float], side: str&#10;) -&gt; List[Dict[str, Any]]:&#10; bins: Dict[int, List[Dict[str, Any]]] = defaultdict(list)&#10; for t in trades:&#10; t_side = str(t.get(&quot;side&quot;, &quot;&quot;)).lower()&#10; if t_side != side:&#10; continue&#10; p = _trade_p_long(t, open_probs)&#10; decile = min(9, max(0, int(math.floor(p * 10.0))))&#10; bins[decile].append(t)&#10;&#10; rows: List[Dict[str, Any]] = []&#10; for decile in range(10):&#10; arr = bins.get(decile, [])&#10; if not arr:&#10; continue&#10; n = len(arr)&#10; wins = sum(1 for t in arr if _safe_float(t.get(&quot;pnl_usd&quot;, 0.0), 0.0) &gt; 0)&#10; avg_p = _mean([_trade_p_long(t, open_probs) for t in arr])&#10; rows.append(&#10; {&#10; &quot;decile&quot;: decile,&#10; &quot;range&quot;: f&quot;{decile / 10:.1f}-{(decile + 1) / 10:.1f}&quot;,&#10; &quot;trades&quot;: n,&#10; &quot;avg_p_long&quot;: avg_p,&#10; &quot;win_rate&quot;: (wins / n) if n else None,&#10; }&#10; )&#10; return rows&#10;&#10;&#10;def _open_prob_lookup(events: List[Dict[str, Any]]) -&gt; Dict[Tuple[str, int], float]:&#10; out: Dict[Tuple[str, int], float] = {}&#10; for e in events:&#10; if e.get(&quot;event&quot;) != &quot;open&quot;:&#10; continue&#10; coin = str(e.get(&quot;coin&quot;, &quot;&quot;))&#10; entry_ts = int(_safe_float(e.get(&quot;entry_ts&quot;, 0), 0))&#10; p_long = float(max(0.0, min(1.0, _safe_float(e.get(&quot;p_long&quot;, 0.5), 0.5))))&#10; out[(coin, entry_ts)] = p_long&#10; return out&#10;&#10;&#10;def _fetch_status(url: str) -&gt; Optional[Dict[str, Any]]:&#10; try:&#10; with urlopen(url, timeout=3) as response:&#10; if response.status != 200:&#10; return None&#10; body = response.read().decode(&quot;utf-8&quot;)&#10; return json.loads(body)&#10; except (URLError, TimeoutError, json.JSONDecodeError, ValueError):&#10; return None&#10;&#10;&#10;def build_report(payload: Dict[str, Any], status_payload: Optional[Dict[str, Any]] = None) -&gt; Dict[str, Any]:&#10; closed = payload.get(&quot;closed_trades&quot;, []) or []&#10; events = payload.get(&quot;trade_events&quot;, []) or []&#10; open_probs = _open_prob_lookup(events)&#10;&#10; by_coin: Dict[str, List[Dict[str, Any]]] = defaultdict(list)&#10; by_side: Dict[str, List[Dict[str, Any]]] = defaultdict(list)&#10; for t in closed:&#10; by_coin[str(t.get(&quot;coin&quot;, &quot;unknown&quot;))].append(t)&#10; by_side[str(t.get(&quot;side&quot;, &quot;unknown&quot;)).lower()].append(t)&#10;&#10; per_coin = {coin: _summarize_basic(trades) for coin, trades in sorted(by_coin.items())}&#10; per_side = {side: _summarize_basic(trades) for side, trades in sorted(by_side.items())}&#10;&#10; report: Dict[str, Any] = {&#10; &quot;state_samples&quot;: {&#10; &quot;closed_trades&quot;: len(closed),&#10; &quot;trade_events&quot;: len(events),&#10; },&#10; &quot;overall&quot;: _summarize_basic(closed),&#10; &quot;per_coin&quot;: per_coin,&#10; &quot;per_side&quot;: per_side,&#10; &quot;directional_sanity&quot;: _directional_sanity(closed, open_probs),&#10; &quot;brier_direction_aware&quot;: _direction_aware_brier(closed, open_probs),&#10; &quot;calibration&quot;: {&#10; &quot;long&quot;: _calibration_table(closed, open_probs, &quot;long&quot;),&#10; &quot;short&quot;: _calibration_table(closed, open_probs, &quot;short&quot;),&#10; },&#10; &quot;runtime_status&quot;: status_payload,&#10; }&#10;&#10; # High-level warnings to speed up triage.&#10; warnings: List[str] = []&#10; ds = report[&quot;directional_sanity&quot;]&#10; lw = ds.get(&quot;long_winners_avg_p_long&quot;)&#10; ll = ds.get(&quot;long_losers_avg_p_long&quot;)&#10; sw = ds.get(&quot;short_winners_avg_p_long&quot;)&#10; sl = ds.get(&quot;short_losers_avg_p_long&quot;)&#10;&#10; if lw is not None and ll is not None and lw &lt;= ll:&#10; warnings.append(&quot;Long directional signal is inverted or weak: long winners do not have higher p_long than long losers.&quot;)&#10; if sw is not None and sl is not None and sw &gt;= sl:&#10; warnings.append(&quot;Short directional signal is inverted or weak: short winners do not have lower p_long than short losers.&quot;)&#10;&#10; overall_wr = report[&quot;overall&quot;].get(&quot;win_rate&quot;, 0.0)&#10; overall_exp = report[&quot;overall&quot;].get(&quot;expectancy_usd&quot;, 0.0)&#10; if len(closed) &gt;= 50 and overall_wr &lt; 0.45:&#10; warnings.append(&quot;Win rate below 45% after 50+ trades; model may be mis-specified or overtrading.&quot;)&#10; if len(closed) &gt;= 50 and overall_exp &lt; 0:&#10; warnings.append(&quot;Negative expectancy after 50+ trades; consider reducing exploration and revisiting labels.&quot;)&#10;&#10; report[&quot;warnings&quot;] = warnings&#10; return report&#10;&#10;&#10;def print_report(report: Dict[str, Any]) -&gt; None:&#10; print(&quot;=== Adaptive Bot ML Diagnostics ===&quot;)&#10; samples = report[&quot;state_samples&quot;]&#10; print(f&quot;Closed trades: {samples['closed_trades']}&quot;)&#10; print(f&quot;Trade events: {samples['trade_events']}&quot;)&#10;&#10; overall = report[&quot;overall&quot;]&#10; print(&#10; &quot;Overall: &quot;&#10; f&quot;n={int(overall['trades'])}, &quot;&#10; f&quot;win_rate={overall['win_rate'] * 100:.2f}%, &quot;&#10; f&quot;total_pnl={overall['total_pnl_usd']:.2f}, &quot;&#10; f&quot;expectancy={overall['expectancy_usd']:.4f}&quot;&#10; )&#10;&#10; print(&quot;\nBy side:&quot;)&#10; for side, s in report[&quot;per_side&quot;].items():&#10; print(&#10; f&quot; {side}: n={int(s['trades'])}, win_rate={s['win_rate'] * 100:.2f}%, &quot;&#10; f&quot;total_pnl={s['total_pnl_usd']:.2f}, expectancy={s['expectancy_usd']:.4f}&quot;&#10; )&#10;&#10; print(&quot;\nBy coin:&quot;)&#10; for coin, s in report[&quot;per_coin&quot;].items():&#10; print(&#10; f&quot; {coin}: n={int(s['trades'])}, win_rate={s['win_rate'] * 100:.2f}%, &quot;&#10; f&quot;total_pnl={s['total_pnl_usd']:.2f}, expectancy={s['expectancy_usd']:.4f}&quot;&#10; )&#10;&#10; ds = report[&quot;directional_sanity&quot;]&#10; print(&quot;\nDirectional sanity:&quot;)&#10; print(&#10; &quot; Long winners avg p_long: &quot;&#10; f&quot;{ds['long_winners_avg_p_long'] if ds['long_winners_avg_p_long'] is not None else 'n/a'}&quot;&#10; )&#10; print(&#10; &quot; Long losers avg p_long: &quot;&#10; f&quot;{ds['long_losers_avg_p_long'] if ds['long_losers_avg_p_long'] is not None else 'n/a'}&quot;&#10; )&#10; print(&#10; &quot; Short winners avg p_long: &quot;&#10; f&quot;{ds['short_winners_avg_p_long'] if ds['short_winners_avg_p_long'] is not None else 'n/a'}&quot;&#10; )&#10; print(&#10; &quot; Short losers avg p_long: &quot;&#10; f&quot;{ds['short_losers_avg_p_long'] if ds['short_losers_avg_p_long'] is not None else 'n/a'}&quot;&#10; )&#10;&#10; brier = report.get(&quot;brier_direction_aware&quot;)&#10; print(f&quot;\nDirection-aware Brier (lower is better): {brier if brier is not None else 'n/a'}&quot;)&#10;&#10; warnings = report.get(&quot;warnings&quot;, [])&#10; if warnings:&#10; print(&quot;\nWarnings:&quot;)&#10; for w in warnings:&#10; print(f&quot; - {w}&quot;)&#10;&#10; runtime = report.get(&quot;runtime_status&quot;)&#10; if runtime:&#10; print(&quot;\nLive status snapshot:&quot;)&#10; metrics = runtime.get(&quot;metrics&quot;, {})&#10; ml_progress = runtime.get(&quot;ml_progress&quot;, {})&#10; print(&#10; &quot; runtime trades=&quot;&#10; f&quot;{int(_safe_float(metrics.get('trades', 0), 0))}, &quot;&#10; f&quot;win_rate={_safe_float(metrics.get('win_rate', 0.0), 0.0) * 100:.2f}%, &quot;&#10; f&quot;pf={_safe_float(metrics.get('profit_factor', 0.0), 0.0):.2f}&quot;&#10; )&#10; if ml_progress:&#10; print(&#10; &quot; runtime brier_50=&quot;&#10; f&quot;{ml_progress.get('brier_rolling_50')}, &quot;&#10; f&quot;expectancy_30={ml_progress.get('expectancy_usd_30')}, &quot;&#10; f&quot;max_dd={ml_progress.get('max_drawdown_pct')}&quot;&#10; )&#10;&#10;&#10;def parse_args() -&gt; argparse.Namespace:&#10; p = argparse.ArgumentParser(description=&quot;Diagnose adaptive bot ML learning quality&quot;)&#10; p.add_argument(&quot;--state-file&quot;, default=&quot;learning_state.joblib&quot;, help=&quot;Path to persisted bot state file&quot;)&#10; p.add_argument(&#10; &quot;--status-url&quot;,&#10; default=&quot;http://127.0.0.1:7860/status/full&quot;,&#10; help=&quot;Optional bot status endpoint for runtime snapshot&quot;,&#10; )&#10; p.add_argument(&quot;--skip-status&quot;, action=&quot;store_true&quot;, help=&quot;Skip HTTP status fetch&quot;)&#10; p.add_argument(&quot;--json&quot;, action=&quot;store_true&quot;, help=&quot;Print full report as JSON&quot;)&#10; return p.parse_args()&#10;&#10;&#10;def main() -&gt; int:&#10; args = parse_args()&#10;&#10; if not os.path.exists(args.state_file):&#10; print(f&quot;State file not found: {args.state_file}&quot;)&#10; return 1&#10;&#10; payload = joblib.load(args.state_file)&#10; status_payload = None if args.skip_status else _fetch_status(args.status_url)&#10; report = build_report(payload, status_payload=status_payload)&#10;&#10; if args.json:&#10; print(json.dumps(report, indent=2))&#10; else:&#10; print_report(report)&#10; return 0&#10;&#10;&#10;if __name__ == &quot;__main__&quot;:&#10; raise SystemExit(main())&#10;" />
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$/.env.example" beforeDir="false" />
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
+