feat: league tier system + retrained V25 models (48 quality leagues)
Deploy Iddaai Backend / build-and-deploy (push) Failing after 3m56s

- Add LeagueTier DB model and Prisma schema
- Add league-tiers service (CRUD, sync, retrain trigger)
- Add league-tiers controller with admin API endpoints
- Add /v1/admin/retrain endpoint in AI engine (extract→train→reload pipeline)
- Retrain V25 Pro with 48 quality leagues (MS accuracy: 26.9%→51.4%)
- Update qualified_leagues.json (443→48 leagues)
- Include V25 model files in repo for Docker deployment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-20 21:57:15 +03:00
parent e001ce9ab5
commit 21e05148c8
58 changed files with 112323 additions and 897 deletions
+127
View File
@@ -7,11 +7,14 @@ import time
from contextlib import asynccontextmanager
from typing import Any
from datetime import datetime
import uvicorn
from dotenv import load_dotenv
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
import subprocess
from pydantic import BaseModel
try:
@@ -38,6 +41,23 @@ class CouponRequest(BaseModel):
min_confidence: float | None = None
class RetrainRequest(BaseModel):
reason: str | None = "manual"
markets: str | None = None # comma-separated, e.g. "MS,OU25,BTTS"
trials: int | None = 50
# ─── Retrain state tracking ──────────────────────────────────
_retrain_state: dict[str, Any] = {
"running": False,
"last_started": None,
"last_completed": None,
"last_status": None,
"last_error": None,
"pid": None,
}
@asynccontextmanager
async def lifespan(_: FastAPI):
try:
@@ -115,6 +135,8 @@ def read_root() -> dict[str, Any]:
"GET /v20plus/reversal-watchlist",
"POST /v20plus/coupon",
"GET /v20plus/daily-banker",
"POST /v1/admin/retrain",
"GET /v1/admin/retrain/status",
],
}
@@ -298,6 +320,111 @@ async def get_reversal_watchlist_v20plus(
)
# ─── ADMIN: Retrain Pipeline ─────────────────────────────────
def _run_retrain_pipeline(markets: str | None, trials: int):
"""Background function: extract data → train model → reload."""
global _retrain_state
ai_dir = os.path.dirname(os.path.abspath(__file__))
scripts_dir = os.path.join(ai_dir, "scripts")
python = os.path.join(ai_dir, "venv", "bin", "python3")
if not os.path.exists(python):
python = sys.executable # fallback
try:
# Step 1: Extract training data
print("🔄 [RETRAIN] Step 1/3: Extracting training data...", flush=True)
result = subprocess.run(
[python, os.path.join(scripts_dir, "extract_training_data.py")],
capture_output=True, text=True, timeout=600, cwd=ai_dir,
)
if result.returncode != 0:
raise RuntimeError(f"Extract failed:\n{result.stderr[-500:]}")
print(f"✅ [RETRAIN] Extract done", flush=True)
# Step 2: Train V25 Pro
print("🔄 [RETRAIN] Step 2/3: Training V25 Pro model...", flush=True)
train_cmd = [python, os.path.join(scripts_dir, "train_v25_pro.py")]
if markets:
train_cmd += ["--markets", markets]
train_cmd += ["--trials", str(trials)]
result = subprocess.run(
train_cmd, capture_output=True, text=True, timeout=3600, cwd=ai_dir,
)
if result.returncode != 0:
raise RuntimeError(f"Training failed:\n{result.stderr[-500:]}")
print(f"✅ [RETRAIN] Training done", flush=True)
# Step 3: Reload models in memory
print("🔄 [RETRAIN] Step 3/3: Reloading models...", flush=True)
try:
orchestrator = get_single_match_orchestrator()
v25 = orchestrator._get_v25_predictor()
v25._loaded = False
v25.load_models()
print("✅ [RETRAIN] Models reloaded in memory", flush=True)
except Exception as reload_err:
print(f"⚠️ [RETRAIN] Hot reload failed (restart needed): {reload_err}", flush=True)
_retrain_state.update({
"running": False,
"last_completed": datetime.now().isoformat(),
"last_status": "success",
"last_error": None,
})
print("🎉 [RETRAIN] Pipeline complete!", flush=True)
except Exception as err:
_retrain_state.update({
"running": False,
"last_completed": datetime.now().isoformat(),
"last_status": "failed",
"last_error": str(err),
})
print(f"❌ [RETRAIN] Pipeline failed: {err}", flush=True)
@app.post("/v1/admin/retrain")
async def admin_retrain(request: RetrainRequest) -> dict[str, Any]:
"""Trigger full retrain pipeline: extract → train → reload."""
if _retrain_state["running"]:
return {
"status": "already_running",
"message": f"Retrain in progress since {_retrain_state['last_started']}",
}
_retrain_state.update({
"running": True,
"last_started": datetime.now().isoformat(),
"last_status": "running",
"last_error": None,
})
# Run in background thread
import threading
thread = threading.Thread(
target=_run_retrain_pipeline,
args=(request.markets, request.trials or 50),
daemon=True,
)
thread.start()
return {
"status": "triggered",
"message": "Retrain pipeline started in background",
"reason": request.reason,
"markets": request.markets or "all",
"trials": request.trials or 50,
}
@app.get("/v1/admin/retrain/status")
async def admin_retrain_status() -> dict[str, Any]:
"""Check retrain pipeline status."""
return {**_retrain_state}
if __name__ == "__main__":
port = int(os.getenv("PORT", "8000"))
uvicorn.run("main:app", host="0.0.0.0", port=port, reload=True)