95 lines
2.8 KiB
Python
95 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import psycopg2
|
|
from psycopg2.extras import RealDictCursor
|
|
|
|
|
|
AI_ENGINE_DIR = Path(__file__).resolve().parents[1]
|
|
if str(AI_ENGINE_DIR) not in sys.path:
|
|
sys.path.insert(0, str(AI_ENGINE_DIR))
|
|
|
|
from services.single_match_orchestrator import SingleMatchOrchestrator
|
|
|
|
|
|
def _resolve_dsn() -> str:
|
|
env_path = AI_ENGINE_DIR / ".env"
|
|
if env_path.exists():
|
|
for line in env_path.read_text(encoding="utf-8").splitlines():
|
|
if line.startswith("DATABASE_URL="):
|
|
return line.split("=", 1)[1].strip().split("?schema=")[0]
|
|
raise SystemExit("DATABASE_URL not found in ai-engine/.env")
|
|
|
|
|
|
def _fetch_matches(dsn: str, limit: int = 60) -> list[str]:
|
|
query = """
|
|
SELECT m.id
|
|
FROM matches m
|
|
WHERE m.status = 'FT'
|
|
AND m.sport = 'football'
|
|
AND m.score_home IS NOT NULL
|
|
AND m.score_away IS NOT NULL
|
|
ORDER BY m.mst_utc DESC
|
|
LIMIT %s
|
|
"""
|
|
with psycopg2.connect(dsn) as conn:
|
|
with conn.cursor(cursor_factory=RealDictCursor) as cur:
|
|
cur.execute(query, (limit,))
|
|
return [str(row["id"]) for row in cur.fetchall()]
|
|
|
|
|
|
def _score_prediction(package: dict) -> dict[str, float]:
|
|
rows = package.get("bet_summary", []) or []
|
|
playable = [row for row in rows if row.get("playable")]
|
|
return {
|
|
"playable_count": float(len(playable)),
|
|
"avg_edge": round(
|
|
sum(float(row.get("ev_edge", 0.0)) for row in playable) / len(playable),
|
|
4,
|
|
)
|
|
if playable
|
|
else 0.0,
|
|
"avg_confidence": round(
|
|
sum(float(row.get("calibrated_confidence", 0.0)) for row in playable)
|
|
/ len(playable),
|
|
2,
|
|
)
|
|
if playable
|
|
else 0.0,
|
|
}
|
|
|
|
|
|
def main() -> None:
|
|
dsn = _resolve_dsn()
|
|
match_ids = _fetch_matches(dsn)
|
|
orchestrator = SingleMatchOrchestrator()
|
|
|
|
results: list[dict[str, object]] = []
|
|
for match_id in match_ids:
|
|
orchestrator.engine_mode = "v25"
|
|
v25 = orchestrator.analyze_match(match_id)
|
|
orchestrator.engine_mode = "v26"
|
|
v26 = orchestrator.analyze_match(match_id)
|
|
if not v25 or not v26:
|
|
continue
|
|
results.append(
|
|
{
|
|
"match_id": match_id,
|
|
"v25": _score_prediction(v25),
|
|
"v26": _score_prediction(v26),
|
|
"v25_main": (v25.get("main_pick") or {}).get("pick"),
|
|
"v26_main": (v26.get("main_pick") or {}).get("pick"),
|
|
}
|
|
)
|
|
|
|
out_path = AI_ENGINE_DIR / "reports" / "backtest_v26_shadow.json"
|
|
out_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
|
|
print(f"[OK] Shadow backtest summary written to {out_path}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|