feat: Sprint 3-4 — Refacto API /v1/ (HRT-29)
- 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>
This commit is contained in:
195
api_v1/routes/backtest.py
Normal file
195
api_v1/routes/backtest.py
Normal file
@@ -0,0 +1,195 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Backtest route for API v1.
|
||||
|
||||
GET /api/v1/backtest — Résultats backtest historiques (pro)
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from api_v1.utils import (
|
||||
get_db,
|
||||
table_exists,
|
||||
internal_error,
|
||||
bad_request,
|
||||
get_pagination_params,
|
||||
paginate_query,
|
||||
)
|
||||
from auth import jwt_required_middleware, plan_required
|
||||
|
||||
backtest_bp = Blueprint("v1_backtest", __name__, url_prefix="/api/v1")
|
||||
|
||||
|
||||
@backtest_bp.route("/backtest", methods=["GET"])
|
||||
@jwt_required_middleware
|
||||
@plan_required("pro")
|
||||
def backtest():
|
||||
"""
|
||||
Backtest historique
|
||||
---
|
||||
tags:
|
||||
- Backtest
|
||||
summary: Résultats backtest historiques des paris simulés — accès pro uniquement
|
||||
security:
|
||||
- Bearer: []
|
||||
parameters:
|
||||
- name: start
|
||||
in: query
|
||||
type: string
|
||||
format: date
|
||||
description: Date de début (YYYY-MM-DD), défaut = -30j
|
||||
- name: end
|
||||
in: query
|
||||
type: string
|
||||
format: date
|
||||
description: Date de fin (YYYY-MM-DD), défaut = aujourd'hui
|
||||
- name: limit
|
||||
in: query
|
||||
type: integer
|
||||
default: 50
|
||||
- name: offset
|
||||
in: query
|
||||
type: integer
|
||||
default: 0
|
||||
responses:
|
||||
200:
|
||||
description: Résultats backtest
|
||||
401:
|
||||
description: Token invalide
|
||||
403:
|
||||
description: Plan insuffisant (pro requis)
|
||||
"""
|
||||
start = request.args.get("start")
|
||||
end = request.args.get("end")
|
||||
|
||||
# Validate date formats
|
||||
for label, val in [("start", start), ("end", end)]:
|
||||
if val:
|
||||
try:
|
||||
datetime.strptime(val, "%Y-%m-%d")
|
||||
except ValueError:
|
||||
return bad_request(
|
||||
f"Paramètre '{label}' invalide, format attendu: YYYY-MM-DD"
|
||||
)
|
||||
|
||||
if not start:
|
||||
start = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d")
|
||||
if not end:
|
||||
end = datetime.now().strftime("%Y-%m-%d")
|
||||
|
||||
limit, offset = get_pagination_params(default_limit=50, max_limit=200)
|
||||
|
||||
conn = get_db()
|
||||
try:
|
||||
if not table_exists(conn, "bet_results"):
|
||||
return jsonify(
|
||||
{
|
||||
"status": "ok",
|
||||
"period": {"start": start, "end": end},
|
||||
"summary": {
|
||||
"total_bets": 0,
|
||||
"message": "Aucune donnée bet_results",
|
||||
},
|
||||
"by_type": {},
|
||||
"details": [],
|
||||
"pagination": {
|
||||
"total": 0,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
"has_more": False,
|
||||
},
|
||||
}
|
||||
), 200
|
||||
|
||||
# Summary
|
||||
summary_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, end),
|
||||
).fetchone()
|
||||
|
||||
total_bets = summary_row["total"] or 0
|
||||
gagne = summary_row["gagne"] or 0
|
||||
mise = float(summary_row["mise"] or 0)
|
||||
gain = float(summary_row["gain"] or 0)
|
||||
roi = round((gain - mise) / mise * 100, 1) if mise > 0 else 0.0
|
||||
precision = round(gagne / total_bets * 100, 1) if total_bets > 0 else 0.0
|
||||
|
||||
# By type
|
||||
by_type_rows = conn.execute(
|
||||
"""SELECT
|
||||
type_pari,
|
||||
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 ?
|
||||
GROUP BY type_pari""",
|
||||
(start, end),
|
||||
).fetchall()
|
||||
|
||||
by_type = {}
|
||||
for row in by_type_rows:
|
||||
t = row["total"] or 0
|
||||
g = row["gagne"] or 0
|
||||
m = float(row["mise"] or 0)
|
||||
gn = float(row["gain"] or 0)
|
||||
by_type[row["type_pari"]] = {
|
||||
"count": t,
|
||||
"gagne": g,
|
||||
"mise": round(m, 2),
|
||||
"gain": round(gn, 2),
|
||||
"roi": round((gn - m) / m * 100, 1) if m > 0 else 0.0,
|
||||
"precision": round(g / t * 100, 1) if t > 0 else 0.0,
|
||||
}
|
||||
|
||||
# Paginated details
|
||||
count_row = conn.execute(
|
||||
"SELECT COUNT(*) AS cnt FROM bet_results WHERE date BETWEEN ? AND ?",
|
||||
(start, end),
|
||||
).fetchone()
|
||||
detail_total = count_row["cnt"] if count_row else 0
|
||||
|
||||
detail_rows = conn.execute(
|
||||
"""SELECT date, race_name, type_pari, horse_name, horse_number,
|
||||
COALESCE(cote, 0) AS cote, mise, resultat, gain
|
||||
FROM bet_results
|
||||
WHERE date BETWEEN ? AND ?
|
||||
ORDER BY date DESC, id DESC
|
||||
LIMIT ? OFFSET ?""",
|
||||
(start, end, limit, offset),
|
||||
).fetchall()
|
||||
|
||||
details = [dict(r) for r in detail_rows]
|
||||
pagination = paginate_query(details, detail_total, limit, offset)
|
||||
|
||||
return jsonify(
|
||||
{
|
||||
"status": "ok",
|
||||
"period": {"start": start, "end": end},
|
||||
"summary": {
|
||||
"total_bets": total_bets,
|
||||
"gagne": gagne,
|
||||
"perdu": total_bets - gagne,
|
||||
"precision": precision,
|
||||
"mise_totale": round(mise, 2),
|
||||
"gain_total": round(gain, 2),
|
||||
"roi": roi,
|
||||
},
|
||||
"by_type": by_type,
|
||||
"details": details,
|
||||
**pagination,
|
||||
}
|
||||
), 200
|
||||
|
||||
except Exception as e:
|
||||
return internal_error(str(e))
|
||||
finally:
|
||||
conn.close()
|
||||
Reference in New Issue
Block a user