- 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>
196 lines
6.1 KiB
Python
196 lines
6.1 KiB
Python
#!/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()
|