- Blueprint Flask api_v1 avec prefix /api/v1/
- GET /api/v1/health — healthcheck public
- GET /api/v1/courses/today — courses du jour (paginé, filtré)
- GET /api/v1/courses/{id}/predictions — prédictions ML pour une course
- GET /api/v1/predictions/top3 — top 3 global (free tier)
- GET /api/v1/predictions/all — toutes prédictions (premium+)
- GET /api/v1/valuebets — value bets du jour (premium+)
- GET /api/v1/backtest — résultats backtest historiques (pro)
- GET /api/v1/export/csv — export CSV prédictions/paris (pro)
- GET /api/v1/metrics — métriques perf ML (premium+)
- Swagger/OpenAPI via flasgger à /api/v1/docs
- Erreurs uniformes {status, message, code}
- Pagination limit/offset sur toutes les listes
- 42 tests d'intégration passants
Co-Authored-By: Paperclip <noreply@paperclip.ing>
145 lines
5.0 KiB
Python
145 lines
5.0 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Metrics route for API v1.
|
|
|
|
GET /api/v1/metrics — Métriques performances ML (premium+)
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from api_v1.utils import (
|
|
get_db,
|
|
table_exists,
|
|
internal_error,
|
|
bad_request,
|
|
)
|
|
from auth import jwt_required_middleware, plan_required
|
|
|
|
metrics_bp = Blueprint("v1_metrics", __name__, url_prefix="/api/v1")
|
|
|
|
|
|
@metrics_bp.route("/metrics", methods=["GET"])
|
|
@jwt_required_middleware
|
|
@plan_required("premium", "pro")
|
|
def metrics():
|
|
"""
|
|
Métriques ML
|
|
---
|
|
tags:
|
|
- Métriques
|
|
summary: Métriques de performance du modèle ML (precision, ROI, top-3 rate) — premium+
|
|
security:
|
|
- Bearer: []
|
|
parameters:
|
|
- name: days
|
|
in: query
|
|
type: integer
|
|
default: 30
|
|
description: Nombre de jours à analyser (max 365)
|
|
responses:
|
|
200:
|
|
description: Métriques de performance ML
|
|
401:
|
|
description: Token invalide
|
|
403:
|
|
description: Plan insuffisant (premium ou pro requis)
|
|
"""
|
|
try:
|
|
days = int(request.args.get("days", 30))
|
|
except (ValueError, TypeError):
|
|
return bad_request("Paramètre 'days' doit être un entier")
|
|
|
|
days = max(1, min(days, 365))
|
|
end_date = datetime.now().strftime("%Y-%m-%d")
|
|
start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
|
|
|
conn = get_db()
|
|
try:
|
|
# ── Bet-level metrics from bet_results ──
|
|
bet_metrics = {
|
|
"available": False,
|
|
"period": {"start": start_date, "end": end_date, "days": days},
|
|
}
|
|
ml_metrics = {"available": False}
|
|
daily_stats = []
|
|
|
|
if table_exists(conn, "bet_results"):
|
|
row = conn.execute(
|
|
"""SELECT
|
|
COUNT(*) AS total,
|
|
SUM(CASE WHEN resultat='GAGNE' THEN 1 ELSE 0 END) AS gagne,
|
|
SUM(mise) AS mise,
|
|
SUM(gain) AS gain
|
|
FROM bet_results
|
|
WHERE date BETWEEN ? AND ?""",
|
|
(start_date, end_date),
|
|
).fetchone()
|
|
|
|
total = row["total"] or 0
|
|
gagne = row["gagne"] or 0
|
|
mise = float(row["mise"] or 0)
|
|
gain = float(row["gain"] or 0)
|
|
|
|
bet_metrics = {
|
|
"available": True,
|
|
"period": {"start": start_date, "end": end_date, "days": days},
|
|
"total_bets": total,
|
|
"precision_pct": round(gagne / total * 100, 2) if total > 0 else 0.0,
|
|
"roi_pct": round((gain - mise) / mise * 100, 2) if mise > 0 else 0.0,
|
|
"mise_totale": round(mise, 2),
|
|
"gain_total": round(gain, 2),
|
|
}
|
|
|
|
# ── ML predictions cache metrics ──
|
|
if table_exists(conn, "ml_predictions_cache"):
|
|
cache_row = conn.execute(
|
|
"""SELECT
|
|
COUNT(*) AS total,
|
|
SUM(is_value_bet) AS value_bets,
|
|
AVG(prob_top1) AS avg_prob_top1,
|
|
AVG(prob_top3) AS avg_prob_top3,
|
|
AVG(ml_score) AS avg_ml_score
|
|
FROM ml_predictions_cache
|
|
WHERE date BETWEEN ? AND ?""",
|
|
(start_date, end_date),
|
|
).fetchone()
|
|
|
|
if cache_row and cache_row["total"]:
|
|
ml_metrics = {
|
|
"available": True,
|
|
"total_predictions": cache_row["total"],
|
|
"value_bets": cache_row["value_bets"] or 0,
|
|
"avg_prob_top1": round(float(cache_row["avg_prob_top1"] or 0), 4),
|
|
"avg_prob_top3": round(float(cache_row["avg_prob_top3"] or 0), 4),
|
|
"avg_ml_score": round(float(cache_row["avg_ml_score"] or 0), 4),
|
|
}
|
|
|
|
# ── Daily breakdown ──
|
|
if table_exists(conn, "daily_stats"):
|
|
daily_rows = conn.execute(
|
|
"""SELECT date, total_bets, bets_gagne, precision_pct, roi_pct,
|
|
mise_totale, gain_total
|
|
FROM daily_stats
|
|
WHERE date BETWEEN ? AND ?
|
|
ORDER BY date DESC
|
|
LIMIT 60""",
|
|
(start_date, end_date),
|
|
).fetchall()
|
|
daily_stats = [dict(r) for r in daily_rows]
|
|
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"period": {"start": start_date, "end": end_date, "days": days},
|
|
"bet_metrics": bet_metrics,
|
|
"ml_metrics": ml_metrics,
|
|
"daily": daily_stats,
|
|
}
|
|
), 200
|
|
|
|
except Exception as e:
|
|
return internal_error(str(e))
|
|
finally:
|
|
conn.close()
|