Spaces:
Running
Running
Daniel Alnawaz commited on
Added API
Browse files- _scan_coins.py +9 -9
- autonomous_bot.py +101 -9
- bot_config.json +5 -11
- from +0 -0
- investigate_config.py +57 -0
- optimize_ml_coins.py +230 -0
- reports/ml_auto_backtest_seed.json +66 -66
- run_all_backtests.py +165 -0
- track_ml_learning.py +193 -0
_scan_coins.py
CHANGED
|
@@ -45,18 +45,18 @@ COINS = ['TIA', 'LINK', 'NEAR', 'INJ', 'ARB', 'AVAX', 'SOL', 'BTC', 'ETH',
|
|
| 45 |
|
| 46 |
# ββ Qualification thresholds (minimum to be considered at all) ββββββββββββββββ
|
| 47 |
Q = {
|
| 48 |
-
'strategy_conservative': dict(min_ret=
|
| 49 |
-
'strategy_hype': dict(min_ret=
|
| 50 |
-
'strategy_scalp': dict(min_ret=
|
| 51 |
-
'strategy_ml_auto': dict(min_ret=
|
| 52 |
}
|
| 53 |
|
| 54 |
# ββ Capital allocation per strategy (must sum to 1.0) ββββββββββββββββββββββββ
|
| 55 |
CAPITAL = {
|
| 56 |
-
'strategy_hype': 0.
|
| 57 |
-
'strategy_conservative': 0.
|
| 58 |
-
'strategy_scalp': 0.
|
| 59 |
-
'strategy_ml_auto': 0.
|
| 60 |
}
|
| 61 |
|
| 62 |
|
|
@@ -254,7 +254,7 @@ if not slots:
|
|
| 254 |
# ML slots share equally from the ML budget.
|
| 255 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 256 |
ml_slot_count = sum(1 for s in slots if s['strategy'] == 'strategy_ml_auto')
|
| 257 |
-
ml_per_slot = round(CAPITAL['strategy_ml_auto'], 4)
|
| 258 |
|
| 259 |
final_slots = []
|
| 260 |
for s in slots:
|
|
|
|
| 45 |
|
| 46 |
# ββ Qualification thresholds (minimum to be considered at all) ββββββββββββββββ
|
| 47 |
Q = {
|
| 48 |
+
'strategy_conservative': dict(min_ret=8.0, min_pf=1.40, max_dd=12.0),
|
| 49 |
+
'strategy_hype': dict(min_ret=8.0, min_pf=1.30, max_dd=99.0),
|
| 50 |
+
'strategy_scalp': dict(min_ret=5.0, min_pf=1.20, max_dd=12.0),
|
| 51 |
+
'strategy_ml_auto': dict(min_ret=8.0, min_pf=1.50, max_dd=15.0),
|
| 52 |
}
|
| 53 |
|
| 54 |
# ββ Capital allocation per strategy (must sum to 1.0) ββββββββββββββββββββββββ
|
| 55 |
CAPITAL = {
|
| 56 |
+
'strategy_hype': 0.40,
|
| 57 |
+
'strategy_conservative': 0.40,
|
| 58 |
+
'strategy_scalp': 0.00, # Disabled (poor recent performance)
|
| 59 |
+
'strategy_ml_auto': 0.20, # 2 slots = 0.10 each
|
| 60 |
}
|
| 61 |
|
| 62 |
|
|
|
|
| 254 |
# ML slots share equally from the ML budget.
|
| 255 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 256 |
ml_slot_count = sum(1 for s in slots if s['strategy'] == 'strategy_ml_auto')
|
| 257 |
+
ml_per_slot = round(CAPITAL['strategy_ml_auto'] / max(1, ml_slot_count), 4)
|
| 258 |
|
| 259 |
final_slots = []
|
| 260 |
for s in slots:
|
autonomous_bot.py
CHANGED
|
@@ -19,6 +19,7 @@ from collections import deque
|
|
| 19 |
from dataclasses import dataclass, field
|
| 20 |
from datetime import datetime, timedelta, timezone
|
| 21 |
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
|
|
| 22 |
from typing import Any, Dict, List, Optional
|
| 23 |
|
| 24 |
import pandas as pd
|
|
@@ -1133,10 +1134,11 @@ def main():
|
|
| 1133 |
|
| 1134 |
bot = AutonomousTradingBot(bot_config, slots)
|
| 1135 |
|
| 1136 |
-
# ββ Health-check server (required by Hugging Face Spaces) βββββββββββββ
|
| 1137 |
# HF Docker Spaces expect an HTTP listener on port 7860 or the
|
| 1138 |
# workload is killed after 30 min as "unhealthy".
|
| 1139 |
-
|
|
|
|
| 1140 |
|
| 1141 |
if args.live:
|
| 1142 |
pk = os.getenv("HYPERLIQUID_PRIVATE_KEY")
|
|
@@ -1166,8 +1168,8 @@ def main():
|
|
| 1166 |
f"{r.total_return_pct:>7.2f} | {r.profit_factor:>4.2f} | {r.max_drawdown_pct:>6.2f}")
|
| 1167 |
|
| 1168 |
|
| 1169 |
-
def _start_health_server(port: int = 7860):
|
| 1170 |
-
"""Start
|
| 1171 |
_start_time = datetime.now(timezone.utc)
|
| 1172 |
|
| 1173 |
def _health_body() -> bytes:
|
|
@@ -1178,10 +1180,96 @@ def _start_health_server(port: int = 7860):
|
|
| 1178 |
"ts": datetime.now(timezone.utc).isoformat(),
|
| 1179 |
}).encode()
|
| 1180 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1181 |
class _Handler(BaseHTTPRequestHandler):
|
| 1182 |
def do_GET(self):
|
| 1183 |
-
|
| 1184 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1185 |
self.send_header("Content-Type", "application/json")
|
| 1186 |
self.send_header("Content-Length", str(len(body)))
|
| 1187 |
self.end_headers()
|
|
@@ -1200,12 +1288,16 @@ def _start_health_server(port: int = 7860):
|
|
| 1200 |
def _serve():
|
| 1201 |
try:
|
| 1202 |
server = HTTPServer(("0.0.0.0", port), _Handler)
|
| 1203 |
-
logger.info(f"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1204 |
server.serve_forever()
|
| 1205 |
except Exception as e:
|
| 1206 |
-
logger.warning(f"
|
| 1207 |
|
| 1208 |
-
t = threading.Thread(target=_serve, daemon=True, name="
|
| 1209 |
t.start()
|
| 1210 |
|
| 1211 |
|
|
|
|
| 19 |
from dataclasses import dataclass, field
|
| 20 |
from datetime import datetime, timedelta, timezone
|
| 21 |
from http.server import BaseHTTPRequestHandler, HTTPServer
|
| 22 |
+
from pathlib import Path
|
| 23 |
from typing import Any, Dict, List, Optional
|
| 24 |
|
| 25 |
import pandas as pd
|
|
|
|
| 1134 |
|
| 1135 |
bot = AutonomousTradingBot(bot_config, slots)
|
| 1136 |
|
| 1137 |
+
# ββ API + Health-check server (required by Hugging Face Spaces) βββββββββββββ
|
| 1138 |
# HF Docker Spaces expect an HTTP listener on port 7860 or the
|
| 1139 |
# workload is killed after 30 min as "unhealthy".
|
| 1140 |
+
# Also serves API endpoints for monitoring from Telegram
|
| 1141 |
+
_start_health_server(bot_instance=bot)
|
| 1142 |
|
| 1143 |
if args.live:
|
| 1144 |
pk = os.getenv("HYPERLIQUID_PRIVATE_KEY")
|
|
|
|
| 1168 |
f"{r.total_return_pct:>7.2f} | {r.profit_factor:>4.2f} | {r.max_drawdown_pct:>6.2f}")
|
| 1169 |
|
| 1170 |
|
| 1171 |
+
def _start_health_server(port: int = 7860, bot_instance: Optional[AutonomousBot] = None):
|
| 1172 |
+
"""Start HTTP server with health check + API endpoints (for HuggingFace Spaces)."""
|
| 1173 |
_start_time = datetime.now(timezone.utc)
|
| 1174 |
|
| 1175 |
def _health_body() -> bytes:
|
|
|
|
| 1180 |
"ts": datetime.now(timezone.utc).isoformat(),
|
| 1181 |
}).encode()
|
| 1182 |
|
| 1183 |
+
def _get_api_response(path: str) -> tuple[int, bytes]:
|
| 1184 |
+
"""Route API requests and return (status_code, response_body)."""
|
| 1185 |
+
if path == "/api/status":
|
| 1186 |
+
try:
|
| 1187 |
+
data = {
|
| 1188 |
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
| 1189 |
+
"bot_active": True,
|
| 1190 |
+
"slots": []
|
| 1191 |
+
}
|
| 1192 |
+
if bot_instance:
|
| 1193 |
+
for slot in bot_instance.slots:
|
| 1194 |
+
data["slots"].append({
|
| 1195 |
+
"strategy": slot.strategy_name,
|
| 1196 |
+
"coin": slot.coin,
|
| 1197 |
+
"timeframe": slot.timeframe,
|
| 1198 |
+
"capital_fraction": slot.capital_frac,
|
| 1199 |
+
})
|
| 1200 |
+
return 200, json.dumps(data).encode()
|
| 1201 |
+
except Exception as e:
|
| 1202 |
+
return 500, json.dumps({"error": str(e)}).encode()
|
| 1203 |
+
|
| 1204 |
+
elif path == "/api/ml/progress":
|
| 1205 |
+
try:
|
| 1206 |
+
from pathlib import Path
|
| 1207 |
+
memory_dir = Path(__file__).parent / "reports"
|
| 1208 |
+
data = {"coins": {}, "summary": {"total_trades": 0, "coins_tracked": 0}}
|
| 1209 |
+
|
| 1210 |
+
for path_obj in memory_dir.glob("ml_auto_memory_*.jsonl"):
|
| 1211 |
+
coin = path_obj.stem.replace("ml_auto_memory_", "")
|
| 1212 |
+
try:
|
| 1213 |
+
with open(path_obj) as f:
|
| 1214 |
+
records = [json.loads(line) for line in f if line.strip()]
|
| 1215 |
+
if not records:
|
| 1216 |
+
continue
|
| 1217 |
+
|
| 1218 |
+
trades = len(records)
|
| 1219 |
+
wins = sum(1 for r in records if r.get("won"))
|
| 1220 |
+
win_rate = wins / trades if trades else 0
|
| 1221 |
+
|
| 1222 |
+
recent = records[-20:] if len(records) > 20 else records
|
| 1223 |
+
recent_wins = sum(1 for r in recent if r.get("won"))
|
| 1224 |
+
recent_wr = recent_wins / len(recent) if recent else 0
|
| 1225 |
+
|
| 1226 |
+
if recent_wr > win_rate + 0.05:
|
| 1227 |
+
trend = "π’ IMPROVING"
|
| 1228 |
+
elif abs(recent_wr - win_rate) <= 0.05:
|
| 1229 |
+
trend = "π‘ STABLE"
|
| 1230 |
+
else:
|
| 1231 |
+
trend = "π΄ DEGRADING"
|
| 1232 |
+
|
| 1233 |
+
data["coins"][coin] = {
|
| 1234 |
+
"total_trades": trades,
|
| 1235 |
+
"win_rate": round(win_rate * 100, 1),
|
| 1236 |
+
"recent_win_rate": round(recent_wr * 100, 1),
|
| 1237 |
+
"trend": trend,
|
| 1238 |
+
}
|
| 1239 |
+
data["summary"]["total_trades"] += trades
|
| 1240 |
+
data["summary"]["coins_tracked"] += 1
|
| 1241 |
+
except Exception as e:
|
| 1242 |
+
logger.debug(f"Error analyzing {coin}: {e}")
|
| 1243 |
+
|
| 1244 |
+
return 200, json.dumps(data).encode()
|
| 1245 |
+
except Exception as e:
|
| 1246 |
+
return 500, json.dumps({"error": str(e)}).encode()
|
| 1247 |
+
|
| 1248 |
+
elif path == "/api/config":
|
| 1249 |
+
try:
|
| 1250 |
+
config_path = Path(__file__).parent / "bot_config.json"
|
| 1251 |
+
if config_path.exists():
|
| 1252 |
+
with open(config_path) as f:
|
| 1253 |
+
config = json.load(f)
|
| 1254 |
+
return 200, json.dumps(config).encode()
|
| 1255 |
+
return 404, json.dumps({"error": "Config not found"}).encode()
|
| 1256 |
+
except Exception as e:
|
| 1257 |
+
return 500, json.dumps({"error": str(e)}).encode()
|
| 1258 |
+
|
| 1259 |
+
else:
|
| 1260 |
+
return 404, json.dumps({"error": "Endpoint not found"}).encode()
|
| 1261 |
+
|
| 1262 |
class _Handler(BaseHTTPRequestHandler):
|
| 1263 |
def do_GET(self):
|
| 1264 |
+
path = self.path.split("?")[0] # Remove query string
|
| 1265 |
+
|
| 1266 |
+
if path.startswith("/api/"):
|
| 1267 |
+
status, body = _get_api_response(path)
|
| 1268 |
+
else:
|
| 1269 |
+
# Health check for root path
|
| 1270 |
+
status, body = 200, _health_body()
|
| 1271 |
+
|
| 1272 |
+
self.send_response(status)
|
| 1273 |
self.send_header("Content-Type", "application/json")
|
| 1274 |
self.send_header("Content-Length", str(len(body)))
|
| 1275 |
self.end_headers()
|
|
|
|
| 1288 |
def _serve():
|
| 1289 |
try:
|
| 1290 |
server = HTTPServer(("0.0.0.0", port), _Handler)
|
| 1291 |
+
logger.info(f"Bot API server listening on port {port}")
|
| 1292 |
+
logger.info(f" / - Health check")
|
| 1293 |
+
logger.info(f" /api/status - Bot status & slots")
|
| 1294 |
+
logger.info(f" /api/ml/progress - ML learning stats")
|
| 1295 |
+
logger.info(f" /api/config - Bot configuration")
|
| 1296 |
server.serve_forever()
|
| 1297 |
except Exception as e:
|
| 1298 |
+
logger.warning(f"API server failed: {e}")
|
| 1299 |
|
| 1300 |
+
t = threading.Thread(target=_serve, daemon=True, name="api-server")
|
| 1301 |
t.start()
|
| 1302 |
|
| 1303 |
|
bot_config.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"generated_at": "2026-05-
|
| 3 |
"scan_days": 180,
|
| 4 |
"ml_backtest_seed_path": "/Users/daniel.a/hyperliquid-bot/reports/ml_auto_backtest_seed.json",
|
| 5 |
"slots": [
|
|
@@ -7,31 +7,25 @@
|
|
| 7 |
"strategy": "strategy_hype",
|
| 8 |
"coin": "TIA",
|
| 9 |
"timeframe": "1h",
|
| 10 |
-
"capital_frac": 0.
|
| 11 |
},
|
| 12 |
{
|
| 13 |
"strategy": "strategy_conservative",
|
| 14 |
"coin": "WIF",
|
| 15 |
"timeframe": "1h",
|
| 16 |
-
"capital_frac": 0.
|
| 17 |
-
},
|
| 18 |
-
{
|
| 19 |
-
"strategy": "strategy_scalp",
|
| 20 |
-
"coin": "ARB",
|
| 21 |
-
"timeframe": "30m",
|
| 22 |
-
"capital_frac": 0.17
|
| 23 |
},
|
| 24 |
{
|
| 25 |
"strategy": "strategy_ml_auto",
|
| 26 |
"coin": "SUI",
|
| 27 |
"timeframe": "1h",
|
| 28 |
-
"capital_frac": 0.
|
| 29 |
},
|
| 30 |
{
|
| 31 |
"strategy": "strategy_ml_auto",
|
| 32 |
"coin": "NEAR",
|
| 33 |
"timeframe": "1h",
|
| 34 |
-
"capital_frac": 0.
|
| 35 |
}
|
| 36 |
]
|
| 37 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"generated_at": "2026-05-28T02:26:19.496078+00:00",
|
| 3 |
"scan_days": 180,
|
| 4 |
"ml_backtest_seed_path": "/Users/daniel.a/hyperliquid-bot/reports/ml_auto_backtest_seed.json",
|
| 5 |
"slots": [
|
|
|
|
| 7 |
"strategy": "strategy_hype",
|
| 8 |
"coin": "TIA",
|
| 9 |
"timeframe": "1h",
|
| 10 |
+
"capital_frac": 0.4
|
| 11 |
},
|
| 12 |
{
|
| 13 |
"strategy": "strategy_conservative",
|
| 14 |
"coin": "WIF",
|
| 15 |
"timeframe": "1h",
|
| 16 |
+
"capital_frac": 0.4
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
},
|
| 18 |
{
|
| 19 |
"strategy": "strategy_ml_auto",
|
| 20 |
"coin": "SUI",
|
| 21 |
"timeframe": "1h",
|
| 22 |
+
"capital_frac": 0.1
|
| 23 |
},
|
| 24 |
{
|
| 25 |
"strategy": "strategy_ml_auto",
|
| 26 |
"coin": "NEAR",
|
| 27 |
"timeframe": "1h",
|
| 28 |
+
"capital_frac": 0.1
|
| 29 |
}
|
| 30 |
]
|
| 31 |
}
|
from
DELETED
|
File without changes
|
investigate_config.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Investigate what changed with scalp and ml_auto strategies."""
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import subprocess
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
# Load current config
|
| 9 |
+
config_path = Path("bot_config.json")
|
| 10 |
+
with open(config_path) as f:
|
| 11 |
+
current_config = json.load(f)
|
| 12 |
+
|
| 13 |
+
print("=" * 70)
|
| 14 |
+
print("CURRENT BOT CONFIG")
|
| 15 |
+
print("=" * 70)
|
| 16 |
+
print(json.dumps(current_config, indent=2))
|
| 17 |
+
|
| 18 |
+
print("\n" + "=" * 70)
|
| 19 |
+
print("GIT HISTORY - Recent backtest runs")
|
| 20 |
+
print("=" * 70)
|
| 21 |
+
|
| 22 |
+
# Get recent commits that touch bot_config
|
| 23 |
+
result = subprocess.run(
|
| 24 |
+
["git", "log", "--oneline", "-10", "bot_config.json"],
|
| 25 |
+
capture_output=True, text=True
|
| 26 |
+
)
|
| 27 |
+
print(result.stdout)
|
| 28 |
+
|
| 29 |
+
print("\n" + "=" * 70)
|
| 30 |
+
print("Configuration from 2 commits ago (before recent changes)")
|
| 31 |
+
print("=" * 70)
|
| 32 |
+
|
| 33 |
+
result = subprocess.run(
|
| 34 |
+
["git", "show", "HEAD~2:bot_config.json"],
|
| 35 |
+
capture_output=True, text=True
|
| 36 |
+
)
|
| 37 |
+
if result.returncode == 0:
|
| 38 |
+
print(result.stdout)
|
| 39 |
+
else:
|
| 40 |
+
print("Could not retrieve historical config")
|
| 41 |
+
|
| 42 |
+
print("\n" + "=" * 70)
|
| 43 |
+
print("Analysis: Why did scalp and ml_auto appear in current config?")
|
| 44 |
+
print("=" * 70)
|
| 45 |
+
print("""
|
| 46 |
+
Based on the conversation summary:
|
| 47 |
+
- Scalp was removed in commit 0936622 ("removing scalp")
|
| 48 |
+
- But now it's back in the config with ARB 30m
|
| 49 |
+
|
| 50 |
+
Possible explanations:
|
| 51 |
+
1. _scan_coins.py was run and re-added scalp with ARB
|
| 52 |
+
2. User manually added slots back
|
| 53 |
+
3. Configuration was reset
|
| 54 |
+
|
| 55 |
+
Let's check _scan_coins.py to see if it adds scalp...
|
| 56 |
+
""")
|
| 57 |
+
|
optimize_ml_coins.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
ML Auto Coin Optimizer - Find the best coins for ML strategy.
|
| 4 |
+
|
| 5 |
+
Backtests ML Auto on many coins to find:
|
| 6 |
+
β’ Highest return
|
| 7 |
+
β’ Best risk-adjusted (Sharpe)
|
| 8 |
+
β’ Most consistent (lowest drawdown)
|
| 9 |
+
β’ Best fit for ensemble learning
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import sys
|
| 13 |
+
import importlib
|
| 14 |
+
import json
|
| 15 |
+
from datetime import datetime, timedelta, timezone
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
import pandas as pd
|
| 19 |
+
from hbot import HyperliquidAPI
|
| 20 |
+
from backtest import StrategyBacktester
|
| 21 |
+
|
| 22 |
+
api = HyperliquidAPI(verify_ssl=False)
|
| 23 |
+
|
| 24 |
+
# Test on these coins (sorted by 180d volume/liquidity)
|
| 25 |
+
TEST_COINS = [
|
| 26 |
+
'BTC', 'ETH', 'SOL', # Majors
|
| 27 |
+
'TIA', 'ARB', 'AVAX', 'OP', # L1s/L2s
|
| 28 |
+
'DOGE', 'WIF', 'LINK', # Alts with good volume
|
| 29 |
+
'INJ', 'SUI', 'NEAR', 'APT', # High-beta ecosystem
|
| 30 |
+
'DOGE', 'LINK', 'AVAX', # Diversified
|
| 31 |
+
]
|
| 32 |
+
|
| 33 |
+
def fetch_candles(coin: str, timeframe: str = "1h", days: int = 180) -> pd.DataFrame:
|
| 34 |
+
"""Fetch historical candles."""
|
| 35 |
+
try:
|
| 36 |
+
start_ms = int((datetime.now(timezone.utc) - timedelta(days=days)).timestamp() * 1000)
|
| 37 |
+
raw = api.get_candles(coin, timeframe, start_ms)
|
| 38 |
+
|
| 39 |
+
if not raw:
|
| 40 |
+
return pd.DataFrame()
|
| 41 |
+
|
| 42 |
+
rows = [
|
| 43 |
+
{
|
| 44 |
+
"timestamp": pd.to_datetime(int(x["t"]), unit="ms", utc=True),
|
| 45 |
+
"open": float(x["o"]),
|
| 46 |
+
"high": float(x["h"]),
|
| 47 |
+
"low": float(x["l"]),
|
| 48 |
+
"close": float(x["c"]),
|
| 49 |
+
"volume": float(x["v"]),
|
| 50 |
+
}
|
| 51 |
+
for x in raw
|
| 52 |
+
]
|
| 53 |
+
df = (
|
| 54 |
+
pd.DataFrame(rows)
|
| 55 |
+
.drop_duplicates("timestamp")
|
| 56 |
+
.sort_values("timestamp")
|
| 57 |
+
.set_index("timestamp")
|
| 58 |
+
)
|
| 59 |
+
return df
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f" Error fetching {coin}: {e}")
|
| 62 |
+
return pd.DataFrame()
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
def backtest_ml_on_coin(coin: str, days: int = 180) -> dict:
|
| 66 |
+
"""Backtest ML Auto strategy on a single coin."""
|
| 67 |
+
|
| 68 |
+
# Import fresh
|
| 69 |
+
for mod in ["strategy_ml_auto", "backtest", "strategy"]:
|
| 70 |
+
sys.modules.pop(mod, None)
|
| 71 |
+
|
| 72 |
+
import backtest as bt_mod
|
| 73 |
+
from strategy_ml_auto import StrategyEngine
|
| 74 |
+
|
| 75 |
+
# Fetch data
|
| 76 |
+
df = fetch_candles(coin, "1h", days)
|
| 77 |
+
if df.empty or len(df) < 100:
|
| 78 |
+
return None
|
| 79 |
+
|
| 80 |
+
# Backtest
|
| 81 |
+
try:
|
| 82 |
+
strategy = StrategyEngine({"use_regime_filter": False, "coin": coin})
|
| 83 |
+
tester = StrategyBacktester(
|
| 84 |
+
strategy=strategy,
|
| 85 |
+
starting_capital=1000.0,
|
| 86 |
+
fee_bps=4.32,
|
| 87 |
+
slippage_bps=1.0,
|
| 88 |
+
position_risk_fraction=0.005,
|
| 89 |
+
strategy_name="strategy_ml_auto",
|
| 90 |
+
)
|
| 91 |
+
result = tester.backtest(df, coin, "1h")
|
| 92 |
+
return {
|
| 93 |
+
"coin": coin,
|
| 94 |
+
"trades": result.trades,
|
| 95 |
+
"win_rate": result.win_rate * 100,
|
| 96 |
+
"return_pct": result.total_return_pct,
|
| 97 |
+
"profit_factor": result.profit_factor,
|
| 98 |
+
"sharpe": result.sharpe_ratio,
|
| 99 |
+
"max_dd": result.max_drawdown_pct,
|
| 100 |
+
"fees": result.total_fees_paid,
|
| 101 |
+
}
|
| 102 |
+
except Exception as e:
|
| 103 |
+
print(f" Backtest failed for {coin}: {e}")
|
| 104 |
+
return None
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
def main():
|
| 108 |
+
print("\n" + "="*90)
|
| 109 |
+
print(" ML AUTO COIN OPTIMIZER β Finding Best ML Coins")
|
| 110 |
+
print("="*90)
|
| 111 |
+
print(" Testing 180-day backtest on 1h timeframe across multiple coins...\n")
|
| 112 |
+
|
| 113 |
+
results = []
|
| 114 |
+
|
| 115 |
+
for i, coin in enumerate(sorted(set(TEST_COINS)), 1):
|
| 116 |
+
print(f" [{i:2d}/{len(set(TEST_COINS))}] Testing {coin:6s} ... ", end="", flush=True)
|
| 117 |
+
|
| 118 |
+
result = backtest_ml_on_coin(coin, days=180)
|
| 119 |
+
|
| 120 |
+
if result is None or result["trades"] == 0:
|
| 121 |
+
print("β (no trades)")
|
| 122 |
+
continue
|
| 123 |
+
|
| 124 |
+
print(f"β {result['trades']:3d} trades | Return: {result['return_pct']:+6.2f}% | "
|
| 125 |
+
f"WR: {result['win_rate']:5.1f}% | Sharpe: {result['sharpe']:6.2f}")
|
| 126 |
+
|
| 127 |
+
results.append(result)
|
| 128 |
+
|
| 129 |
+
if not results:
|
| 130 |
+
print("\n β οΈ No profitable strategies found!\n")
|
| 131 |
+
return
|
| 132 |
+
|
| 133 |
+
# Sort by composite score
|
| 134 |
+
results_sorted = sorted(
|
| 135 |
+
results,
|
| 136 |
+
key=lambda x: x["return_pct"] * 0.5 + x["sharpe"] * 20 - x["max_dd"] * 0.3,
|
| 137 |
+
reverse=True
|
| 138 |
+
)
|
| 139 |
+
|
| 140 |
+
print("\n" + "="*90)
|
| 141 |
+
print(" TOP ML AUTO COINS (sorted by composite score)")
|
| 142 |
+
print("="*90)
|
| 143 |
+
print(f"{'Rank':<6} {'Coin':<6} {'Trades':<8} {'Return%':<10} {'Win%':<8} "
|
| 144 |
+
f"{'PF':<6} {'Sharpe':<8} {'DD%':<7} {'Score':<8}")
|
| 145 |
+
print("β" * 90)
|
| 146 |
+
|
| 147 |
+
for i, r in enumerate(results_sorted[:15], 1):
|
| 148 |
+
score = r["return_pct"] * 0.5 + r["sharpe"] * 20 - r["max_dd"] * 0.3
|
| 149 |
+
|
| 150 |
+
# Rating
|
| 151 |
+
if r["return_pct"] >= 15:
|
| 152 |
+
rating = "π₯"
|
| 153 |
+
elif r["return_pct"] >= 8:
|
| 154 |
+
rating = "π₯"
|
| 155 |
+
elif r["return_pct"] >= 3:
|
| 156 |
+
rating = "π₯"
|
| 157 |
+
else:
|
| 158 |
+
rating = " "
|
| 159 |
+
|
| 160 |
+
print(f"{i:<6} {r['coin']:<6} {r['trades']:<8} {r['return_pct']:>8.2f}% "
|
| 161 |
+
f"{r['win_rate']:>6.1f}% {r['profit_factor']:>5.2f} {r['sharpe']:>7.2f} "
|
| 162 |
+
f"{r['max_dd']:>6.2f}% {score:>7.2f} {rating}")
|
| 163 |
+
|
| 164 |
+
# Analysis
|
| 165 |
+
print("\n" + "="*90)
|
| 166 |
+
print(" ANALYSIS")
|
| 167 |
+
print("="*90)
|
| 168 |
+
|
| 169 |
+
profitable = [r for r in results if r["return_pct"] > 8]
|
| 170 |
+
high_sharpe = [r for r in results if r["sharpe"] > 1.5]
|
| 171 |
+
low_dd = [r for r in results if r["max_dd"] < 8]
|
| 172 |
+
|
| 173 |
+
print(f"\n Profitable (>8%): {len(profitable)} coins")
|
| 174 |
+
if profitable:
|
| 175 |
+
print(f" {', '.join(r['coin'] for r in profitable)}")
|
| 176 |
+
|
| 177 |
+
print(f"\n High Sharpe (>1.5): {len(high_sharpe)} coins")
|
| 178 |
+
if high_sharpe:
|
| 179 |
+
print(f" {', '.join(r['coin'] for r in high_sharpe)}")
|
| 180 |
+
|
| 181 |
+
print(f"\n Low DD (<8%): {len(low_dd)} coins")
|
| 182 |
+
if low_dd:
|
| 183 |
+
print(f" {', '.join(r['coin'] for r in low_dd)}")
|
| 184 |
+
|
| 185 |
+
# Current holdings vs best
|
| 186 |
+
current = ["SUI", "NEAR"]
|
| 187 |
+
print(f"\n Current ML coins: {', '.join(current)}")
|
| 188 |
+
|
| 189 |
+
current_results = {r["coin"]: r for r in results}
|
| 190 |
+
if any(c in current_results for c in current):
|
| 191 |
+
print(f"\n Current performance:")
|
| 192 |
+
for coin in current:
|
| 193 |
+
if coin in current_results:
|
| 194 |
+
r = current_results[coin]
|
| 195 |
+
print(f" {coin}: {r['return_pct']:+6.2f}% | {r['trades']:3d} trades | "
|
| 196 |
+
f"Sharpe {r['sharpe']:6.2f}")
|
| 197 |
+
|
| 198 |
+
best = results_sorted[0]
|
| 199 |
+
print(f"\n Best alternative:")
|
| 200 |
+
print(f" {best['coin']}: {best['return_pct']:+6.2f}% | {best['trades']:3d} trades | "
|
| 201 |
+
f"Sharpe {best['sharpe']:6.2f}")
|
| 202 |
+
|
| 203 |
+
improvement = best["return_pct"] - current_results[current[0]]["return_pct"]
|
| 204 |
+
print(f"\n Potential improvement: {improvement:+.2f}% by swapping to {best['coin']}")
|
| 205 |
+
|
| 206 |
+
print("\n" + "="*90 + "\n")
|
| 207 |
+
|
| 208 |
+
# Generate recommendations
|
| 209 |
+
print(" π‘ RECOMMENDATIONS:\n")
|
| 210 |
+
|
| 211 |
+
if len(profitable) >= 2:
|
| 212 |
+
top_2 = results_sorted[:2]
|
| 213 |
+
print(f" β
Replace SUI+NEAR with top 2 performers:")
|
| 214 |
+
print(f" β’ {top_2[0]['coin']}: {top_2[0]['return_pct']:+.2f}%")
|
| 215 |
+
print(f" β’ {top_2[1]['coin']}: {top_2[1]['return_pct']:+.2f}%")
|
| 216 |
+
print(f" Expected blended return: {(top_2[0]['return_pct'] + top_2[1]['return_pct'])/2:+.2f}%\n")
|
| 217 |
+
|
| 218 |
+
print(" β οΈ Current SUI+NEAR performance:")
|
| 219 |
+
for coin in current:
|
| 220 |
+
if coin in current_results:
|
| 221 |
+
r = current_results[coin]
|
| 222 |
+
status = "π" if r['return_pct'] > 0 else "π"
|
| 223 |
+
print(f" {status} {coin}: {r['return_pct']:+6.2f}% ({r['trades']} trades)")
|
| 224 |
+
|
| 225 |
+
print("\n")
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
if __name__ == "__main__":
|
| 229 |
+
main()
|
| 230 |
+
|
reports/ml_auto_backtest_seed.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
{
|
| 2 |
-
"generated_at": "2026-05-
|
| 3 |
"days": 180,
|
| 4 |
"coins": {
|
| 5 |
"INJ_1h": {
|
|
@@ -17,23 +17,23 @@
|
|
| 17 |
"coin": "SUI",
|
| 18 |
"timeframe": "1h",
|
| 19 |
"days": 180,
|
| 20 |
-
"trades":
|
| 21 |
-
"win_rate": 0.
|
| 22 |
-
"total_return_pct":
|
| 23 |
-
"profit_factor":
|
| 24 |
-
"max_drawdown_pct":
|
| 25 |
-
"sharpe_ratio": 1.
|
| 26 |
},
|
| 27 |
"WIF_1h": {
|
| 28 |
"coin": "WIF",
|
| 29 |
"timeframe": "1h",
|
| 30 |
"days": 180,
|
| 31 |
-
"trades":
|
| 32 |
-
"win_rate": 0.
|
| 33 |
-
"total_return_pct":
|
| 34 |
-
"profit_factor": 1.
|
| 35 |
-
"max_drawdown_pct":
|
| 36 |
-
"sharpe_ratio": 0.
|
| 37 |
},
|
| 38 |
"NEAR_1h": {
|
| 39 |
"coin": "NEAR",
|
|
@@ -50,89 +50,89 @@
|
|
| 50 |
"coin": "ARB",
|
| 51 |
"timeframe": "1h",
|
| 52 |
"days": 180,
|
| 53 |
-
"trades":
|
| 54 |
-
"win_rate": 0.
|
| 55 |
-
"total_return_pct": -
|
| 56 |
-
"profit_factor":
|
| 57 |
"max_drawdown_pct": 9.0079,
|
| 58 |
-
"sharpe_ratio": 0.
|
| 59 |
},
|
| 60 |
"TIA_1h": {
|
| 61 |
"coin": "TIA",
|
| 62 |
"timeframe": "1h",
|
| 63 |
"days": 180,
|
| 64 |
-
"trades":
|
| 65 |
-
"win_rate": 0.
|
| 66 |
-
"total_return_pct": -
|
| 67 |
-
"profit_factor": 0.
|
| 68 |
-
"max_drawdown_pct":
|
| 69 |
-
"sharpe_ratio": -
|
| 70 |
},
|
| 71 |
"ETH_1h": {
|
| 72 |
"coin": "ETH",
|
| 73 |
"timeframe": "1h",
|
| 74 |
"days": 180,
|
| 75 |
-
"trades":
|
| 76 |
-
"win_rate": 0.
|
| 77 |
-
"total_return_pct":
|
| 78 |
-
"profit_factor":
|
| 79 |
-
"max_drawdown_pct":
|
| 80 |
-
"sharpe_ratio": 0.
|
| 81 |
},
|
| 82 |
"AVAX_1h": {
|
| 83 |
"coin": "AVAX",
|
| 84 |
"timeframe": "1h",
|
| 85 |
"days": 180,
|
| 86 |
-
"trades":
|
| 87 |
-
"win_rate": 0.
|
| 88 |
-
"total_return_pct": -
|
| 89 |
-
"profit_factor": 0.
|
| 90 |
-
"max_drawdown_pct":
|
| 91 |
-
"sharpe_ratio": -0.
|
| 92 |
},
|
| 93 |
"LINK_1h": {
|
| 94 |
"coin": "LINK",
|
| 95 |
"timeframe": "1h",
|
| 96 |
"days": 180,
|
| 97 |
-
"trades":
|
| 98 |
-
"win_rate": 0.
|
| 99 |
-
"total_return_pct": -
|
| 100 |
-
"profit_factor": 0.
|
| 101 |
-
"max_drawdown_pct": 13.
|
| 102 |
-
"sharpe_ratio": -0.
|
| 103 |
},
|
| 104 |
"OP_1h": {
|
| 105 |
"coin": "OP",
|
| 106 |
"timeframe": "1h",
|
| 107 |
"days": 180,
|
| 108 |
-
"trades":
|
| 109 |
-
"win_rate": 0.
|
| 110 |
-
"total_return_pct": -
|
| 111 |
-
"profit_factor": 0.
|
| 112 |
-
"max_drawdown_pct":
|
| 113 |
-
"sharpe_ratio": -0.
|
| 114 |
},
|
| 115 |
"DOGE_1h": {
|
| 116 |
"coin": "DOGE",
|
| 117 |
"timeframe": "1h",
|
| 118 |
"days": 180,
|
| 119 |
-
"trades":
|
| 120 |
-
"win_rate": 0.
|
| 121 |
-
"total_return_pct":
|
| 122 |
-
"profit_factor":
|
| 123 |
-
"max_drawdown_pct":
|
| 124 |
-
"sharpe_ratio":
|
| 125 |
},
|
| 126 |
"APT_1h": {
|
| 127 |
"coin": "APT",
|
| 128 |
"timeframe": "1h",
|
| 129 |
"days": 180,
|
| 130 |
-
"trades":
|
| 131 |
-
"win_rate": 0.
|
| 132 |
-
"total_return_pct": -
|
| 133 |
-
"profit_factor": 0.
|
| 134 |
-
"max_drawdown_pct":
|
| 135 |
-
"sharpe_ratio": -
|
| 136 |
},
|
| 137 |
"BTC_1h": {
|
| 138 |
"coin": "BTC",
|
|
@@ -149,12 +149,12 @@
|
|
| 149 |
"coin": "SOL",
|
| 150 |
"timeframe": "1h",
|
| 151 |
"days": 180,
|
| 152 |
-
"trades":
|
| 153 |
-
"win_rate": 0.
|
| 154 |
-
"total_return_pct": -
|
| 155 |
-
"profit_factor": 0.
|
| 156 |
-
"max_drawdown_pct":
|
| 157 |
-
"sharpe_ratio": -
|
| 158 |
}
|
| 159 |
}
|
| 160 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"generated_at": "2026-05-28T02:01:22.311732+00:00",
|
| 3 |
"days": 180,
|
| 4 |
"coins": {
|
| 5 |
"INJ_1h": {
|
|
|
|
| 17 |
"coin": "SUI",
|
| 18 |
"timeframe": "1h",
|
| 19 |
"days": 180,
|
| 20 |
+
"trades": 28,
|
| 21 |
+
"win_rate": 0.3929,
|
| 22 |
+
"total_return_pct": 18.9368,
|
| 23 |
+
"profit_factor": 2.0416,
|
| 24 |
+
"max_drawdown_pct": 5.2638,
|
| 25 |
+
"sharpe_ratio": 1.683
|
| 26 |
},
|
| 27 |
"WIF_1h": {
|
| 28 |
"coin": "WIF",
|
| 29 |
"timeframe": "1h",
|
| 30 |
"days": 180,
|
| 31 |
+
"trades": 23,
|
| 32 |
+
"win_rate": 0.2609,
|
| 33 |
+
"total_return_pct": 1.8033,
|
| 34 |
+
"profit_factor": 1.129,
|
| 35 |
+
"max_drawdown_pct": 5.9185,
|
| 36 |
+
"sharpe_ratio": 0.2863
|
| 37 |
},
|
| 38 |
"NEAR_1h": {
|
| 39 |
"coin": "NEAR",
|
|
|
|
| 50 |
"coin": "ARB",
|
| 51 |
"timeframe": "1h",
|
| 52 |
"days": 180,
|
| 53 |
+
"trades": 49,
|
| 54 |
+
"win_rate": 0.2857,
|
| 55 |
+
"total_return_pct": -2.7523,
|
| 56 |
+
"profit_factor": 0.9535,
|
| 57 |
"max_drawdown_pct": 9.0079,
|
| 58 |
+
"sharpe_ratio": -0.0933
|
| 59 |
},
|
| 60 |
"TIA_1h": {
|
| 61 |
"coin": "TIA",
|
| 62 |
"timeframe": "1h",
|
| 63 |
"days": 180,
|
| 64 |
+
"trades": 25,
|
| 65 |
+
"win_rate": 0.08,
|
| 66 |
+
"total_return_pct": -16.0572,
|
| 67 |
+
"profit_factor": 0.2677,
|
| 68 |
+
"max_drawdown_pct": 16.0572,
|
| 69 |
+
"sharpe_ratio": -2.8536
|
| 70 |
},
|
| 71 |
"ETH_1h": {
|
| 72 |
"coin": "ETH",
|
| 73 |
"timeframe": "1h",
|
| 74 |
"days": 180,
|
| 75 |
+
"trades": 37,
|
| 76 |
+
"win_rate": 0.2432,
|
| 77 |
+
"total_return_pct": -2.038,
|
| 78 |
+
"profit_factor": 0.9889,
|
| 79 |
+
"max_drawdown_pct": 11.5952,
|
| 80 |
+
"sharpe_ratio": 0.0324
|
| 81 |
},
|
| 82 |
"AVAX_1h": {
|
| 83 |
"coin": "AVAX",
|
| 84 |
"timeframe": "1h",
|
| 85 |
"days": 180,
|
| 86 |
+
"trades": 83,
|
| 87 |
+
"win_rate": 0.253,
|
| 88 |
+
"total_return_pct": -14.6898,
|
| 89 |
+
"profit_factor": 0.8073,
|
| 90 |
+
"max_drawdown_pct": 18.1576,
|
| 91 |
+
"sharpe_ratio": -0.868
|
| 92 |
},
|
| 93 |
"LINK_1h": {
|
| 94 |
"coin": "LINK",
|
| 95 |
"timeframe": "1h",
|
| 96 |
"days": 180,
|
| 97 |
+
"trades": 67,
|
| 98 |
+
"win_rate": 0.2836,
|
| 99 |
+
"total_return_pct": -5.4177,
|
| 100 |
+
"profit_factor": 0.9354,
|
| 101 |
+
"max_drawdown_pct": 13.8572,
|
| 102 |
+
"sharpe_ratio": -0.1937
|
| 103 |
},
|
| 104 |
"OP_1h": {
|
| 105 |
"coin": "OP",
|
| 106 |
"timeframe": "1h",
|
| 107 |
"days": 180,
|
| 108 |
+
"trades": 60,
|
| 109 |
+
"win_rate": 0.2667,
|
| 110 |
+
"total_return_pct": -7.1059,
|
| 111 |
+
"profit_factor": 0.8779,
|
| 112 |
+
"max_drawdown_pct": 16.1324,
|
| 113 |
+
"sharpe_ratio": -0.438
|
| 114 |
},
|
| 115 |
"DOGE_1h": {
|
| 116 |
"coin": "DOGE",
|
| 117 |
"timeframe": "1h",
|
| 118 |
"days": 180,
|
| 119 |
+
"trades": 79,
|
| 120 |
+
"win_rate": 0.3165,
|
| 121 |
+
"total_return_pct": 2.1745,
|
| 122 |
+
"profit_factor": 1.0875,
|
| 123 |
+
"max_drawdown_pct": 16.9499,
|
| 124 |
+
"sharpe_ratio": 0.4138
|
| 125 |
},
|
| 126 |
"APT_1h": {
|
| 127 |
"coin": "APT",
|
| 128 |
"timeframe": "1h",
|
| 129 |
"days": 180,
|
| 130 |
+
"trades": 84,
|
| 131 |
+
"win_rate": 0.2024,
|
| 132 |
+
"total_return_pct": -25.4033,
|
| 133 |
+
"profit_factor": 0.618,
|
| 134 |
+
"max_drawdown_pct": 26.6465,
|
| 135 |
+
"sharpe_ratio": -2.0437
|
| 136 |
},
|
| 137 |
"BTC_1h": {
|
| 138 |
"coin": "BTC",
|
|
|
|
| 149 |
"coin": "SOL",
|
| 150 |
"timeframe": "1h",
|
| 151 |
"days": 180,
|
| 152 |
+
"trades": 50,
|
| 153 |
+
"win_rate": 0.2,
|
| 154 |
+
"total_return_pct": -10.6837,
|
| 155 |
+
"profit_factor": 0.7761,
|
| 156 |
+
"max_drawdown_pct": 17.0197,
|
| 157 |
+
"sharpe_ratio": -0.6932
|
| 158 |
}
|
| 159 |
}
|
| 160 |
}
|
run_all_backtests.py
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Comprehensive backtest runner for all active strategies in bot config."""
|
| 3 |
+
|
| 4 |
+
import json
|
| 5 |
+
import sys
|
| 6 |
+
from datetime import datetime, timedelta, timezone
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
from hbot import HyperliquidAPI
|
| 11 |
+
from backtest import StrategyBacktester
|
| 12 |
+
from strategy_conservative import StrategyEngine as ConservativeEngine
|
| 13 |
+
from strategy_hype import StrategyEngine as HypeEngine
|
| 14 |
+
from strategy_ml_auto import StrategyEngine as MLAutoEngine
|
| 15 |
+
from strategy_scalp import StrategyEngine as ScalpEngine
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# ββ Initialize API with SSL verification disabled ββββββββββββββββββββ
|
| 19 |
+
api = HyperliquidAPI(verify_ssl=False)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
# ββ Load historical data ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 23 |
+
def fetch_candles(coin: str, timeframe: str, days: int = 180) -> pd.DataFrame:
|
| 24 |
+
"""Fetch candles from Hyperliquid API."""
|
| 25 |
+
|
| 26 |
+
interval_map = {
|
| 27 |
+
"1m": "1m", "5m": "5m", "15m": "15m", "30m": "30m",
|
| 28 |
+
"1h": "1h", "4h": "4h", "1d": "1d"
|
| 29 |
+
}
|
| 30 |
+
interval = interval_map.get(timeframe, "1h")
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
start_ms = int((datetime.now(timezone.utc) - timedelta(days=days)).timestamp() * 1000)
|
| 34 |
+
raw = api.get_candles(coin, interval, start_ms)
|
| 35 |
+
|
| 36 |
+
if not raw:
|
| 37 |
+
print(f" β οΈ No data for {coin} {interval}")
|
| 38 |
+
return pd.DataFrame()
|
| 39 |
+
|
| 40 |
+
rows = [
|
| 41 |
+
{
|
| 42 |
+
"timestamp": pd.to_datetime(int(x["t"]), unit="ms", utc=True),
|
| 43 |
+
"open": float(x["o"]),
|
| 44 |
+
"high": float(x["h"]),
|
| 45 |
+
"low": float(x["l"]),
|
| 46 |
+
"close": float(x["c"]),
|
| 47 |
+
"volume": float(x["v"]),
|
| 48 |
+
}
|
| 49 |
+
for x in raw
|
| 50 |
+
]
|
| 51 |
+
df = pd.DataFrame(rows).drop_duplicates("timestamp").sort_values("timestamp").set_index("timestamp")
|
| 52 |
+
return df
|
| 53 |
+
|
| 54 |
+
except Exception as e:
|
| 55 |
+
print(f" β Error fetching {coin} {timeframe}: {e}")
|
| 56 |
+
return pd.DataFrame()
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
# ββ Main backtest runner ββββββββββββββββββββββββββββββββββββββββββββββ
|
| 60 |
+
def main():
|
| 61 |
+
"""Run backtest for all strategies in bot config."""
|
| 62 |
+
|
| 63 |
+
config_path = Path(__file__).parent / "bot_config.json"
|
| 64 |
+
with open(config_path) as f:
|
| 65 |
+
config = json.load(f)
|
| 66 |
+
|
| 67 |
+
print(f"\n{'='*70}")
|
| 68 |
+
print(f" BACKTEST REPORT - All Active Strategies")
|
| 69 |
+
print(f" Generated: {datetime.utcnow().isoformat()}")
|
| 70 |
+
print(f"{'='*70}\n")
|
| 71 |
+
|
| 72 |
+
all_results = []
|
| 73 |
+
|
| 74 |
+
for slot in config["slots"]:
|
| 75 |
+
strategy_name = slot["strategy"]
|
| 76 |
+
coin = slot["coin"]
|
| 77 |
+
timeframe = slot["timeframe"]
|
| 78 |
+
capital_frac = slot["capital_frac"]
|
| 79 |
+
|
| 80 |
+
print(f"\nπ Testing {strategy_name:25} | {coin} {timeframe} (allocation: {capital_frac:.1%})")
|
| 81 |
+
print(f" {'β'*65}")
|
| 82 |
+
|
| 83 |
+
# Fetch data
|
| 84 |
+
df = fetch_candles(coin, timeframe, days=180)
|
| 85 |
+
if df.empty:
|
| 86 |
+
print(f" β SKIPPED (no data)")
|
| 87 |
+
continue
|
| 88 |
+
|
| 89 |
+
print(f" β Loaded {len(df)} candles")
|
| 90 |
+
|
| 91 |
+
# Instantiate strategy
|
| 92 |
+
if strategy_name == "strategy_hype":
|
| 93 |
+
strategy = HypeEngine({"use_regime_filter": False})
|
| 94 |
+
elif strategy_name == "strategy_conservative":
|
| 95 |
+
strategy = ConservativeEngine({"use_regime_filter": False})
|
| 96 |
+
elif strategy_name == "strategy_scalp":
|
| 97 |
+
strategy = ScalpEngine({"use_regime_filter": False})
|
| 98 |
+
elif strategy_name == "strategy_ml_auto":
|
| 99 |
+
strategy = MLAutoEngine({"use_regime_filter": False, "coin": coin})
|
| 100 |
+
else:
|
| 101 |
+
print(f" β Unknown strategy: {strategy_name}")
|
| 102 |
+
continue
|
| 103 |
+
|
| 104 |
+
# Run backtest
|
| 105 |
+
tester = StrategyBacktester(
|
| 106 |
+
strategy=strategy,
|
| 107 |
+
starting_capital=1000.0,
|
| 108 |
+
fee_bps=3.0,
|
| 109 |
+
slippage_bps=2.0,
|
| 110 |
+
position_risk_fraction=0.02,
|
| 111 |
+
strategy_name=strategy_name,
|
| 112 |
+
)
|
| 113 |
+
|
| 114 |
+
result = tester.backtest(df, coin, timeframe)
|
| 115 |
+
all_results.append({
|
| 116 |
+
"slot": f"{strategy_name:25}",
|
| 117 |
+
"coin": coin,
|
| 118 |
+
"tf": timeframe,
|
| 119 |
+
"alloc": f"{capital_frac:.1%}",
|
| 120 |
+
"trades": result.trades,
|
| 121 |
+
"win%": f"{result.win_rate*100:.1f}",
|
| 122 |
+
"ret%": f"{result.total_return_pct:+.2f}",
|
| 123 |
+
"pf": f"{result.profit_factor:.2f}",
|
| 124 |
+
"dd%": f"{result.max_drawdown_pct:.2f}",
|
| 125 |
+
"sharpe": f"{result.sharpe_ratio:.2f}",
|
| 126 |
+
"fees": f"${result.total_fees_paid:.2f}",
|
| 127 |
+
})
|
| 128 |
+
|
| 129 |
+
status = "β" if result.total_return_pct >= 5.0 else "β" if result.total_return_pct < 0 else "~"
|
| 130 |
+
print(f" {status} Trades: {result.trades:3d} | Win: {result.win_rate*100:5.1f}% | "
|
| 131 |
+
f"Return: {result.total_return_pct:+6.2f}% | PF: {result.profit_factor:5.2f} | "
|
| 132 |
+
f"DD: {result.max_drawdown_pct:5.2f}% | Sharpe: {result.sharpe_ratio:5.2f}")
|
| 133 |
+
print(f" Fees paid: ${result.total_fees_paid:.2f}")
|
| 134 |
+
|
| 135 |
+
# Print summary table
|
| 136 |
+
print(f"\n{'='*70}")
|
| 137 |
+
print(f" SUMMARY TABLE (180-day backtest)")
|
| 138 |
+
print(f"{'='*70}\n")
|
| 139 |
+
|
| 140 |
+
header = f"{'Strategy':<27} {'Coin':<6} {'TF':<5} {'Alloc':<7} {'Trades':<8} {'Win%':<8} {'Return%':<10} {'PF':<6} {'DD%':<7} {'Sharpe':<8} {'Fees':<10}"
|
| 141 |
+
print(header)
|
| 142 |
+
print("β" * len(header))
|
| 143 |
+
|
| 144 |
+
total_return = 0.0
|
| 145 |
+
for r in all_results:
|
| 146 |
+
ret_pct = float(r["ret%"])
|
| 147 |
+
total_return += ret_pct * float(r["alloc"].strip("%")) / 100.0
|
| 148 |
+
|
| 149 |
+
print(f"{r['slot']:<27} {r['coin']:<6} {r['tf']:<5} {r['alloc']:<7} "
|
| 150 |
+
f"{r['trades']:<8} {r['win%']:<8} {r['ret%']:<10} {r['pf']:<6} "
|
| 151 |
+
f"{r['dd%']:<7} {r['sharpe']:<8} {r['fees']:<10}")
|
| 152 |
+
|
| 153 |
+
print("β" * len(header))
|
| 154 |
+
print(f"{'PORTFOLIO BLENDED':<27} {'':6} {'':5} {'100.0%':<7} "
|
| 155 |
+
f"{'':8} {'':8} {total_return:+10.2f}% {'':6} {'':7} {'':8}")
|
| 156 |
+
print(f"\n{'='*70}\n")
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
if __name__ == "__main__":
|
| 160 |
+
main()
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
|
track_ml_learning.py
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
ML Auto Performance Tracker - Monitor model learning over time.
|
| 4 |
+
|
| 5 |
+
Shows:
|
| 6 |
+
β’ Live win rate vs baseline rule-only performance
|
| 7 |
+
β’ Training examples collected per coin
|
| 8 |
+
β’ Model confidence trends
|
| 9 |
+
β’ Shadow mode status
|
| 10 |
+
β’ Recommendation on when to rotate coins
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import json
|
| 14 |
+
import os
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
from datetime import datetime
|
| 17 |
+
from collections import defaultdict
|
| 18 |
+
|
| 19 |
+
MEMORY_DIR = Path(__file__).parent / "reports"
|
| 20 |
+
|
| 21 |
+
def analyze_memory_file(coin: str):
|
| 22 |
+
"""Analyze a single coin's memory ledger."""
|
| 23 |
+
path = MEMORY_DIR / f"ml_auto_memory_{coin}.jsonl"
|
| 24 |
+
|
| 25 |
+
if not path.exists():
|
| 26 |
+
return None
|
| 27 |
+
|
| 28 |
+
records = []
|
| 29 |
+
try:
|
| 30 |
+
with open(path) as f:
|
| 31 |
+
for line in f:
|
| 32 |
+
line = line.strip()
|
| 33 |
+
if line:
|
| 34 |
+
records.append(json.loads(line))
|
| 35 |
+
except Exception as e:
|
| 36 |
+
print(f" Error reading {coin}: {e}")
|
| 37 |
+
return None
|
| 38 |
+
|
| 39 |
+
if not records:
|
| 40 |
+
return None
|
| 41 |
+
|
| 42 |
+
# Calculate stats
|
| 43 |
+
total_trades = len(records)
|
| 44 |
+
wins = sum(1 for r in records if r.get("won"))
|
| 45 |
+
win_rate = wins / total_trades if total_trades else 0
|
| 46 |
+
|
| 47 |
+
# Recent performance (last 20 trades)
|
| 48 |
+
recent = records[-20:]
|
| 49 |
+
recent_wins = sum(1 for r in recent if r.get("won"))
|
| 50 |
+
recent_wr = recent_wins / len(recent) if recent else 0
|
| 51 |
+
|
| 52 |
+
# Historical performance (before last 20)
|
| 53 |
+
historical = records[:-20] if len(records) > 20 else []
|
| 54 |
+
historical_wins = sum(1 for r in historical if r.get("won"))
|
| 55 |
+
historical_wr = historical_wins / len(historical) if historical else 0
|
| 56 |
+
|
| 57 |
+
# Side distribution
|
| 58 |
+
long_trades = sum(1 for r in records if r.get("side") == 1)
|
| 59 |
+
short_trades = sum(1 for r in records if r.get("side") == -1)
|
| 60 |
+
|
| 61 |
+
# Calculate PnL (rough estimate)
|
| 62 |
+
total_pnl = 0
|
| 63 |
+
for r in records:
|
| 64 |
+
entry = float(r.get("entry", 0))
|
| 65 |
+
exit_px = float(r.get("exit", 0))
|
| 66 |
+
side = int(r.get("side", 1))
|
| 67 |
+
if entry and exit_px:
|
| 68 |
+
pnl = (exit_px - entry) * side * abs(exit_px) / 100 # Rough estimate
|
| 69 |
+
total_pnl += pnl if r.get("won") else pnl * -0.2
|
| 70 |
+
|
| 71 |
+
# Newest and oldest trades
|
| 72 |
+
oldest_ts = records[0].get("ts", "unknown")
|
| 73 |
+
newest_ts = records[-1].get("ts", "unknown")
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
"coin": coin,
|
| 77 |
+
"total_trades": total_trades,
|
| 78 |
+
"win_rate": win_rate,
|
| 79 |
+
"recent_wr": recent_wr,
|
| 80 |
+
"historical_wr": historical_wr,
|
| 81 |
+
"improvement": recent_wr - historical_wr if historical else 0,
|
| 82 |
+
"long_trades": long_trades,
|
| 83 |
+
"short_trades": short_trades,
|
| 84 |
+
"oldest_ts": oldest_ts,
|
| 85 |
+
"newest_ts": newest_ts,
|
| 86 |
+
"memory_file_size": path.stat().st_size,
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
def main():
|
| 91 |
+
print("\n" + "="*80)
|
| 92 |
+
print(" ML AUTO LEARNING TRACKER β Performance Over Time")
|
| 93 |
+
print("="*80 + "\n")
|
| 94 |
+
|
| 95 |
+
# Find all memory files
|
| 96 |
+
memory_files = list(MEMORY_DIR.glob("ml_auto_memory_*.jsonl"))
|
| 97 |
+
|
| 98 |
+
if not memory_files:
|
| 99 |
+
print(" β οΈ No ML memory files found yet. Start trading to collect data!\n")
|
| 100 |
+
return
|
| 101 |
+
|
| 102 |
+
results = []
|
| 103 |
+
for path in sorted(memory_files):
|
| 104 |
+
coin = path.stem.replace("ml_auto_memory_", "")
|
| 105 |
+
analysis = analyze_memory_file(coin)
|
| 106 |
+
if analysis:
|
| 107 |
+
results.append(analysis)
|
| 108 |
+
|
| 109 |
+
if not results:
|
| 110 |
+
print(" β οΈ Could not analyze any memory files.\n")
|
| 111 |
+
return
|
| 112 |
+
|
| 113 |
+
# Print detailed report
|
| 114 |
+
print(f"{'Coin':<8} {'Trades':<8} {'Win%':<8} {'Recent%':<10} {'Trend':<12} {'Long:Short':<12} {'Status':<15}")
|
| 115 |
+
print("β" * 80)
|
| 116 |
+
|
| 117 |
+
for r in sorted(results, key=lambda x: x["win_rate"], reverse=True):
|
| 118 |
+
win_pct = r["win_rate"] * 100
|
| 119 |
+
recent_pct = r["recent_wr"] * 100
|
| 120 |
+
improvement = r["improvement"] * 100
|
| 121 |
+
|
| 122 |
+
# Trend indicator
|
| 123 |
+
if improvement > 5:
|
| 124 |
+
trend = f"βοΈ +{improvement:.1f}pp"
|
| 125 |
+
status = "π’ IMPROVING"
|
| 126 |
+
elif improvement > 0:
|
| 127 |
+
trend = f"β +{improvement:.1f}pp"
|
| 128 |
+
status = "π‘ STABLE"
|
| 129 |
+
else:
|
| 130 |
+
trend = f"β {improvement:.1f}pp"
|
| 131 |
+
status = "π΄ DEGRADING"
|
| 132 |
+
|
| 133 |
+
side_ratio = f"{r['long_trades']}:{r['short_trades']}"
|
| 134 |
+
|
| 135 |
+
print(f"{r['coin']:<8} {r['total_trades']:<8} {win_pct:>6.1f}% {recent_pct:>8.1f}% "
|
| 136 |
+
f"{trend:<12} {side_ratio:<12} {status:<15}")
|
| 137 |
+
|
| 138 |
+
print("\n" + "="*80)
|
| 139 |
+
print(" INTERPRETATION GUIDE")
|
| 140 |
+
print("="*80 + """
|
| 141 |
+
π’ IMPROVING (Recent% > Historical%)
|
| 142 |
+
β Model is learning! Confidence in predictions is rising.
|
| 143 |
+
β Keep coin active, monitor for stability.
|
| 144 |
+
|
| 145 |
+
π‘ STABLE (Recent% β Historical%)
|
| 146 |
+
β Model has plateaued. Good consistent baseline.
|
| 147 |
+
β Performance is predictable and repeatable.
|
| 148 |
+
|
| 149 |
+
π΄ DEGRADING (Recent% < Historical%)
|
| 150 |
+
β Model fit to old market regime. Performance drifting.
|
| 151 |
+
β Consider retraining or rotating to new coin.
|
| 152 |
+
β If stays negative >30 trades, rotate out.
|
| 153 |
+
|
| 154 |
+
Win Rate Interpretation:
|
| 155 |
+
β’ >50% = Excellent (2:1 RR makes this highly profitable)
|
| 156 |
+
β’ >40% = Good
|
| 157 |
+
β’ >30% = Acceptable (2.5:1 RR needed)
|
| 158 |
+
β’ <30% = Poor (not sustainable with fixed RR)
|
| 159 |
+
|
| 160 |
+
Trades per Coin:
|
| 161 |
+
β’ <10 trades = too early to judge, keep collecting data
|
| 162 |
+
β’ 10-30 trades = initial verdict forming
|
| 163 |
+
β’ 30+ trades = reliable signal of model quality
|
| 164 |
+
""" + "="*80 + "\n")
|
| 165 |
+
|
| 166 |
+
# Summary stats
|
| 167 |
+
total_all = sum(r["total_trades"] for r in results)
|
| 168 |
+
avg_wr = sum(r["win_rate"] for r in results) / len(results) if results else 0
|
| 169 |
+
|
| 170 |
+
print(f" π SUMMARY")
|
| 171 |
+
print(f" Total trades across all coins: {total_all}")
|
| 172 |
+
print(f" Average win rate: {avg_wr*100:.1f}%")
|
| 173 |
+
print(f" Coins tracked: {len(results)}")
|
| 174 |
+
|
| 175 |
+
# Recommendations
|
| 176 |
+
print(f"\n π‘ RECOMMENDATIONS")
|
| 177 |
+
improving = [r for r in results if r["improvement"] > 0.05]
|
| 178 |
+
degrading = [r for r in results if r["improvement"] < -0.05 and r["total_trades"] > 30]
|
| 179 |
+
early = [r for r in results if r["total_trades"] < 10]
|
| 180 |
+
|
| 181 |
+
if improving:
|
| 182 |
+
print(f" β
Keep active: {', '.join(r['coin'] for r in improving)} (learning)")
|
| 183 |
+
if degrading:
|
| 184 |
+
print(f" β οΈ Consider rotating: {', '.join(r['coin'] for r in degrading)} (degrading)")
|
| 185 |
+
if early:
|
| 186 |
+
print(f" β³ Collect more data: {', '.join(r['coin'] for r in early)} (<10 trades)")
|
| 187 |
+
|
| 188 |
+
print()
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
if __name__ == "__main__":
|
| 192 |
+
main()
|
| 193 |
+
|