diff --git a/api_v1/__init__.py b/api_v1/__init__.py index 268fd62..2e2504a 100644 --- a/api_v1/__init__.py +++ b/api_v1/__init__.py @@ -22,6 +22,8 @@ Registers sub-blueprints: /api/v1/history — historique préd. ML (Free:7j, Premium:90j, Pro:illimité) /api/v1/org/ — organisations Pro (multi-compte, max 5 users) /api/v1/docs — Swagger UI (via flasgger, registered on app) + /api/v1/ml/feedback/run — trigger feedback loop ML (admin) + /api/v1/ml/feedback/stats — stats par stratégie (premium+) """ from flask import Blueprint @@ -38,6 +40,7 @@ from .routes.user import user_bp from .routes.user_tokens import user_tokens_bp from .routes.history import history_bp from .routes.org import org_bp +from .routes.ml_feedback import ml_feedback_bp # Master blueprint that aggregates all sub-routes under /api/v1 api_v1_bp = Blueprint("api_v1", __name__, url_prefix="/api/v1") @@ -57,3 +60,4 @@ def register_api_v1(app): app.register_blueprint(user_tokens_bp) app.register_blueprint(history_bp) app.register_blueprint(org_bp) + app.register_blueprint(ml_feedback_bp) diff --git a/api_v1/routes/ml_feedback.py b/api_v1/routes/ml_feedback.py new file mode 100644 index 0000000..7c4523f --- /dev/null +++ b/api_v1/routes/ml_feedback.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +""" +ml_feedback.py — API routes pour le feedback loop ML (turf_saas). + +Routes: + POST /api/v1/ml/feedback/run — Déclenche le feedback loop (admin uniquement) + GET /api/v1/ml/feedback/stats — Stats performances par stratégie + +Sécurité admin : token via variable d'environnement ML_ADMIN_TOKEN +ou plan "pro" en fallback pour les stats. +""" + +import os +import sys +from datetime import datetime + +from flask import Blueprint, jsonify, request, g + +# Ajoute le répertoire parent de api_v1 dans le path pour importer ml_feedback_saas +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(__file__)))) + +from api_v1.utils import get_db, internal_error, bad_request +from auth import jwt_required_middleware, plan_required + +ml_feedback_bp = Blueprint("v1_ml_feedback", __name__, url_prefix="/api/v1/ml/feedback") + +# Token admin interne — configurable via variable d'environnement +ML_ADMIN_TOKEN = os.environ.get("ML_ADMIN_TOKEN", "") + + +def _check_admin(req): + """Vérifie le token admin via header X-Admin-Token ou Authorization Bearer (plan pro).""" + # 1. Token interne (scheduler/cron) + admin_token = req.headers.get("X-Admin-Token", "").strip() + if ML_ADMIN_TOKEN and admin_token == ML_ADMIN_TOKEN: + return True, None + + # 2. Pas de token admin configuré → autoriser les utilisateurs "pro" authentifiés + user = getattr(g, "current_user", None) + if user and user.get("plan") == "pro": + return True, None + + return False, jsonify({"error": "Accès admin requis", "code": 403}), 403 + + +@ml_feedback_bp.route("/run", methods=["POST"]) +@jwt_required_middleware +def feedback_run(): + """ + Déclenche le feedback loop ML pour une date donnée. + --- + tags: + - ML Feedback + summary: Déclenche le feedback loop XGBoost (admin only) + security: + - Bearer: [] + - AdminToken: [] + parameters: + - name: body + in: body + schema: + type: object + properties: + date: + type: string + description: Date YYYY-MM-DD (défaut aujourd'hui) + example: "2026-04-25" + mode: + type: string + description: "run (défaut) ou backfill" + enum: [run, backfill] + example: run + responses: + 200: + description: Feedback loop exécuté avec succès + 400: + description: Paramètre invalide + 403: + description: Accès refusé + 500: + description: Erreur interne + """ + # Vérification admin + user = getattr(g, "current_user", None) + admin_token = request.headers.get("X-Admin-Token", "").strip() + is_admin = (ML_ADMIN_TOKEN and admin_token == ML_ADMIN_TOKEN) or ( + user and user.get("plan") == "pro" + ) + if not is_admin: + return jsonify({"error": "Accès admin requis", "code": 403}), 403 + + body = request.get_json(silent=True) or {} + date_str = body.get("date") or datetime.now().strftime("%Y-%m-%d") + mode = body.get("mode", "run") + + # Validation date + try: + datetime.strptime(date_str, "%Y-%m-%d") + except ValueError: + return bad_request(f"Date invalide : {date_str}. Format attendu : YYYY-MM-DD") + + if mode not in ("run", "backfill"): + return bad_request("mode doit être 'run' ou 'backfill'") + + try: + import ml_feedback_saas + + if mode == "backfill": + inseres, maj = ml_feedback_saas.backfill(date_str) + total_inseres = inseres + else: + result = ml_feedback_saas.run(date_str) + total_inseres = sum(result["inseres"].values()) + maj = result["maj"] + + return jsonify( + { + "status": "ok", + "date": date_str, + "mode": mode, + "paris_inseres": total_inseres, + "paris_mis_a_jour": maj, + } + ), 200 + + except Exception as e: + return internal_error(str(e)) + + +@ml_feedback_bp.route("/stats", methods=["GET"]) +@jwt_required_middleware +@plan_required("premium", "pro") +def feedback_stats(): + """ + Stats performances ML par stratégie. + --- + tags: + - ML Feedback + summary: Stats paris ML par stratégie (premium+) + security: + - Bearer: [] + parameters: + - name: date_debut + in: query + type: string + description: Date de début YYYY-MM-DD + - name: date_fin + in: query + type: string + description: Date de fin YYYY-MM-DD + responses: + 200: + description: Stats par stratégie + 401: + description: Token invalide + 403: + description: Plan insuffisant (premium ou pro requis) + """ + date_debut = request.args.get("date_debut") + date_fin = request.args.get("date_fin") + + # Validation optionnelle des dates + for d_str, label in [(date_debut, "date_debut"), (date_fin, "date_fin")]: + if d_str: + try: + datetime.strptime(d_str, "%Y-%m-%d") + except ValueError: + return bad_request(f"{label} invalide : {d_str}. Format : YYYY-MM-DD") + + conn = get_db() + try: + import ml_feedback_saas + + stats = ml_feedback_saas.get_feedback_stats(conn, date_debut, date_fin) + + return jsonify( + { + "status": "ok", + "strategies": stats, + "filters": { + "date_debut": date_debut, + "date_fin": date_fin, + }, + "total_strategies": len(stats), + } + ), 200 + + except Exception as e: + return internal_error(str(e)) + finally: + conn.close() diff --git a/ml_feedback_saas.py b/ml_feedback_saas.py new file mode 100644 index 0000000..2d2c0be --- /dev/null +++ b/ml_feedback_saas.py @@ -0,0 +1,600 @@ +#!/usr/bin/env python3 +""" +ml_feedback_saas.py — Feedback loop ML pour turf_saas. +Enregistre les paris virtuels XGBoost depuis ml_predictions_cache +et met à jour les résultats/dividendes depuis pmu_partants + pmu_rapports. + +DB cible : /home/h3r7/turf_saas/turf_saas.db + +Stratégies : + A) xgboost_sg : simple_gagnant — top1 ML par course, ml_score >= 70, mise 1€ + B) xgboost_value : simple_gagnant — is_value_bet = 1, mise 1€ + C) xgboost_sp : simple_place — top1 ML par course, ml_score >= 50, mise 1€ + D) xgboost_2sur4 : deux_sur_quatre — top4 ML par course, 6 combos x 1€ = mise 6€ + +Usage : + python3 ml_feedback_saas.py # Traite aujourd'hui + python3 ml_feedback_saas.py --backfill 2026-04-25 + python3 ml_feedback_saas.py --date 2026-04-25 +""" + +import sqlite3 +import sys +import logging +import os +from datetime import datetime, timedelta + +DB_PATH = "/home/h3r7/turf_saas/turf_saas.db" + +os.makedirs("/home/h3r7/turf_saas/logs", exist_ok=True) + +logging.basicConfig( + level=logging.INFO, + format="%(asctime)s [ml_feedback_saas] %(levelname)s %(message)s", + handlers=[ + logging.FileHandler("/home/h3r7/turf_saas/logs/ml_feedback_saas.log"), + logging.StreamHandler(), + ], +) +log = logging.getLogger(__name__) + + +# ───────────────────────────────────────────────────────── +# UTILITAIRES +# ───────────────────────────────────────────────────────── + + +def get_db(): + conn = sqlite3.connect(DB_PATH) + conn.row_factory = sqlite3.Row + return conn + + +def pari_existe(cursor, date, num_reunion, num_course, numero1, type_pari, source_reco): + """Vérifie si un pari identique existe déjà (idempotence).""" + cursor.execute( + """ + SELECT id FROM paris + WHERE date_course = ? AND source_reco = ? + AND type_pari = ? AND numero1 = ? + AND race_label = ? + """, + (date, source_reco, type_pari, numero1, f"R{num_reunion}C{num_course}"), + ) + return cursor.fetchone() is not None + + +def pari_2sur4_existe(cursor, date, num_reunion, num_course, source_reco): + """Vérifie si un pari 2sur4 existe déjà pour cette course.""" + cursor.execute( + """ + SELECT id FROM paris + WHERE date_course = ? AND source_reco = ? + AND race_label = ? + """, + (date, source_reco, f"R{num_reunion}C{num_course}"), + ) + return cursor.fetchone() is not None + + +def get_top_ml_par_course(cursor, date, n=4, min_score=0): + """Retourne les n meilleurs chevaux ML par course pour une date.""" + cursor.execute( + """ + SELECT num_reunion, num_course, horse_name, horse_number, + ml_score, odds, recommendation, is_value_bet, + race_label, race_name, hippodrome, heure, + discipline, distance + FROM ml_predictions_cache + WHERE date = ? + AND ml_score >= ? + ORDER BY num_reunion, num_course, ml_score DESC + """, + (date, min_score), + ) + rows = cursor.fetchall() + + courses = {} + for r in rows: + key = (r["num_reunion"], r["num_course"]) + if key not in courses: + courses[key] = [] + if len(courses[key]) < n: + courses[key].append(dict(r)) + return courses + + +# ───────────────────────────────────────────────────────── +# STRATÉGIE A — Simple Gagnant top1 ML (score >= 70) +# ───────────────────────────────────────────────────────── + + +def save_ml_paris_sg(conn, date): + """Insère 1 pari simple_gagnant par course : top1 ML avec ml_score >= 70.""" + cursor = conn.cursor() + courses = get_top_ml_par_course(cursor, date, n=1, min_score=70) + inseres = 0 + + for (num_reunion, num_course), chevaux in courses.items(): + cheval = chevaux[0] + if pari_existe( + cursor, + date, + num_reunion, + num_course, + cheval["horse_number"], + "simple_gagnant", + "xgboost_sg", + ): + continue + + cursor.execute( + """ + INSERT INTO paris + (date_pari, date_course, race_name, race_label, hippodrome, + type_pari, chevaux, cheval1, numero1, cote, mise, + statut, gain, source_reco, model_source) + VALUES (?, ?, ?, ?, ?, 'simple_gagnant', ?, ?, ?, ?, 1.0, + 'EN_ATTENTE', 0.0, 'xgboost_sg', 'xgboost_v1') + """, + ( + date, + date, + cheval.get("race_name") or "", + f"R{num_reunion}C{num_course}", + cheval.get("hippodrome") or "", + cheval["horse_name"], + cheval["horse_name"], + cheval["horse_number"], + cheval["odds"], + ), + ) + inseres += 1 + + conn.commit() + log.info(f"[SG] {date} → {inseres} paris simple_gagnant insérés (score>=70)") + return inseres + + +# ───────────────────────────────────────────────────────── +# STRATÉGIE B — Value Bet (is_value_bet = 1) +# ───────────────────────────────────────────────────────── + + +def save_ml_paris_value(conn, date): + """Insère 1 pari simple_gagnant pour chaque cheval is_value_bet=1.""" + cursor = conn.cursor() + cursor.execute( + """ + SELECT num_reunion, num_course, horse_name, horse_number, + ml_score, odds, race_label, race_name, hippodrome + FROM ml_predictions_cache + WHERE date = ? AND is_value_bet = 1 + ORDER BY num_reunion, num_course, ml_score DESC + """, + (date,), + ) + rows = [dict(r) for r in cursor.fetchall()] + inseres = 0 + + for r in rows: + if pari_existe( + cursor, + date, + r["num_reunion"], + r["num_course"], + r["horse_number"], + "simple_gagnant", + "xgboost_value", + ): + continue + + cursor.execute( + """ + INSERT INTO paris + (date_pari, date_course, race_name, race_label, hippodrome, + type_pari, chevaux, cheval1, numero1, cote, mise, + statut, gain, source_reco, model_source) + VALUES (?, ?, ?, ?, ?, 'simple_gagnant', ?, ?, ?, ?, 1.0, + 'EN_ATTENTE', 0.0, 'xgboost_value', 'xgboost_v1') + """, + ( + date, + date, + r.get("race_name") or "", + r.get("race_label") or f"R{r['num_reunion']}C{r['num_course']}", + r.get("hippodrome") or "", + r["horse_name"], + r["horse_name"], + r["horse_number"], + r["odds"], + ), + ) + inseres += 1 + + conn.commit() + log.info(f"[VALUE] {date} → {inseres} paris value_bet insérés") + return inseres + + +# ───────────────────────────────────────────────────────── +# STRATÉGIE C — Simple Placé top1 ML (score >= 50) +# ───────────────────────────────────────────────────────── + + +def save_ml_paris_sp(conn, date): + """Insère 1 pari simple_place par course : top1 ML avec ml_score >= 50.""" + cursor = conn.cursor() + courses = get_top_ml_par_course(cursor, date, n=1, min_score=50) + inseres = 0 + + for (num_reunion, num_course), chevaux in courses.items(): + cheval = chevaux[0] + if pari_existe( + cursor, + date, + num_reunion, + num_course, + cheval["horse_number"], + "simple_place", + "xgboost_sp", + ): + continue + + cursor.execute( + """ + INSERT INTO paris + (date_pari, date_course, race_name, race_label, hippodrome, + type_pari, chevaux, cheval1, numero1, cote, mise, + statut, gain, source_reco, model_source) + VALUES (?, ?, ?, ?, ?, 'simple_place', ?, ?, ?, ?, 1.0, + 'EN_ATTENTE', 0.0, 'xgboost_sp', 'xgboost_v1') + """, + ( + date, + date, + cheval.get("race_name") or "", + f"R{num_reunion}C{num_course}", + cheval.get("hippodrome") or "", + cheval["horse_name"], + cheval["horse_name"], + cheval["horse_number"], + cheval["odds"], + ), + ) + inseres += 1 + + conn.commit() + log.info(f"[SP] {date} → {inseres} paris simple_place insérés (score>=50)") + return inseres + + +# ───────────────────────────────────────────────────────── +# STRATÉGIE D — 2sur4 top4 ML (6 combinaisons x 1€) +# ───────────────────────────────────────────────────────── + + +def save_ml_paris_2sur4(conn, date): + """Insère 1 pari deux_sur_quatre par course : top4 ML, mise 6€.""" + cursor = conn.cursor() + courses = get_top_ml_par_course(cursor, date, n=4, min_score=0) + inseres = 0 + + for (num_reunion, num_course), chevaux in courses.items(): + if len(chevaux) < 4: + continue + if pari_2sur4_existe(cursor, date, num_reunion, num_course, "xgboost_2sur4"): + continue + + top4 = chevaux[:4] + nums = [str(c["horse_number"]) for c in top4] + noms = [c["horse_name"] for c in top4] + chevaux_str = "/".join(noms) + + cursor.execute( + """ + INSERT INTO paris + (date_pari, date_course, race_name, race_label, hippodrome, + type_pari, chevaux, cheval1, numero1, cote, mise, + statut, gain, source_reco, model_source, commentaire) + VALUES (?, ?, ?, ?, ?, 'deux_sur_quatre', ?, ?, ?, 0.0, 6.0, + 'EN_ATTENTE', 0.0, 'xgboost_2sur4', 'xgboost_v1', ?) + """, + ( + date, + date, + top4[0].get("race_name") or "", + f"R{num_reunion}C{num_course}", + top4[0].get("hippodrome") or "", + chevaux_str, + top4[0]["horse_name"], + top4[0]["horse_number"], + f"top4 ML: {'/'.join(nums)}", + ), + ) + inseres += 1 + + conn.commit() + log.info(f"[2S4] {date} → {inseres} paris deux_sur_quatre insérés") + return inseres + + +# ───────────────────────────────────────────────────────── +# UPDATE RÉSULTATS + DIVIDENDES +# ───────────────────────────────────────────────────────── + + +def update_ml_paris_results(conn, date): + """ + Met à jour statut + gain (dividende PMU réel) pour tous les paris ML EN_ATTENTE. + Sources: pmu_partants (ordre_arrivee) + pmu_rapports (dividende_euro). + """ + cursor = conn.cursor() + + cursor.execute( + """ + SELECT id, race_label, type_pari, numero1, chevaux, mise, source_reco, commentaire + FROM paris + WHERE date_course = ? AND statut = 'EN_ATTENTE' + AND source_reco LIKE 'xgboost%' + """, + (date,), + ) + paris = [dict(r) for r in cursor.fetchall()] + + if not paris: + log.info(f"[UPDATE] {date} → aucun pari ML EN_ATTENTE") + return 0 + + maj = 0 + for pari in paris: + pari_id = pari["id"] + race_label = pari["race_label"] or "" + type_pari = pari["type_pari"] + numero1 = pari["numero1"] + mise = pari["mise"] + + # Extraire num_reunion / num_course depuis le race_label "R{r}C{c}" + try: + parts = race_label.replace("R", "").split("C") + num_reunion = int(parts[0]) + num_course = int(parts[1]) + except Exception: + log.warning(f"[UPDATE] race_label invalide : {race_label}") + continue + + if type_pari == "simple_gagnant": + cursor.execute( + """ + SELECT ordre_arrivee FROM pmu_partants + WHERE date_programme = ? AND num_reunion = ? + AND num_course = ? AND num_pmu = ? + """, + (date, num_reunion, num_course, numero1), + ) + row = cursor.fetchone() + if not row or row["ordre_arrivee"] is None or row["ordre_arrivee"] == 0: + continue + + gagne = row["ordre_arrivee"] == 1 + gain = 0.0 + if gagne: + cursor.execute( + """ + SELECT dividende_euro FROM pmu_rapports + WHERE date_programme = ? AND num_reunion = ? + AND num_course = ? AND type_pari = 'SIMPLE_GAGNANT' + AND CAST(combinaison AS INTEGER) = ? + AND libelle NOT LIKE '%NP%' + """, + (date, num_reunion, num_course, numero1), + ) + div = cursor.fetchone() + gain = div["dividende_euro"] if div and div["dividende_euro"] else 0.0 + + cursor.execute( + "UPDATE paris SET statut=?, gain=? WHERE id=?", + ("GAGNE" if gagne else "PERDU", gain, pari_id), + ) + maj += 1 + + elif type_pari == "simple_place": + cursor.execute( + """ + SELECT ordre_arrivee FROM pmu_partants + WHERE date_programme = ? AND num_reunion = ? + AND num_course = ? AND num_pmu = ? + """, + (date, num_reunion, num_course, numero1), + ) + row = cursor.fetchone() + if not row or not row["ordre_arrivee"]: + continue + + gagne = 1 <= row["ordre_arrivee"] <= 3 + gain = 0.0 + if gagne: + cursor.execute( + """ + SELECT dividende_euro FROM pmu_rapports + WHERE date_programme = ? AND num_reunion = ? + AND num_course = ? AND type_pari = 'SIMPLE_PLACE' + AND CAST(combinaison AS INTEGER) = ? + AND libelle NOT LIKE '%NP%' + """, + (date, num_reunion, num_course, numero1), + ) + div = cursor.fetchone() + gain = div["dividende_euro"] if div and div["dividende_euro"] else 0.0 + + cursor.execute( + "UPDATE paris SET statut=?, gain=? WHERE id=?", + ("GAGNE" if gagne else "PERDU", gain, pari_id), + ) + maj += 1 + + elif type_pari == "deux_sur_quatre": + # Récupère les 4 numéros depuis commentaire "top4 ML: n1/n2/n3/n4" + try: + nums_str = ( + pari["commentaire"].split(": ")[1] + if pari.get("commentaire") + else "" + ) + nums_top4 = [int(n) for n in nums_str.split("/") if n.strip().isdigit()] + except Exception: + nums_top4 = [] + + if len(nums_top4) < 4: + # Fallback : reconstituer top4 depuis ml_predictions_cache + cursor.execute( + """ + SELECT horse_number FROM ml_predictions_cache + WHERE date = ? AND num_reunion = ? AND num_course = ? + ORDER BY ml_score DESC LIMIT 4 + """, + (date, num_reunion, num_course), + ) + nums_top4 = [r["horse_number"] for r in cursor.fetchall()] + + if len(nums_top4) < 2: + continue + + cursor.execute( + """ + SELECT combinaison, dividende_euro FROM pmu_rapports + WHERE date_programme = ? AND num_reunion = ? + AND num_course = ? AND type_pari = 'DEUX_SUR_QUATRE' + AND libelle NOT LIKE '%NP%' + """, + (date, num_reunion, num_course), + ) + rapports = [dict(r) for r in cursor.fetchall()] + gain_total = 0.0 + + for rap in rapports: + try: + n1, n2 = [int(x) for x in rap["combinaison"].split("-")] + except Exception: + continue + if n1 in nums_top4 and n2 in nums_top4: + gain_total += rap["dividende_euro"] + + gagne = gain_total > 0 + cursor.execute( + "UPDATE paris SET statut=?, gain=? WHERE id=?", + ("GAGNE" if gagne else "PERDU", round(gain_total, 2), pari_id), + ) + maj += 1 + + conn.commit() + log.info(f"[UPDATE] {date} → {maj}/{len(paris)} paris ML mis à jour") + return maj + + +# ───────────────────────────────────────────────────────── +# STATS PAR STRATÉGIE +# ───────────────────────────────────────────────────────── + + +def get_feedback_stats(conn, date_debut=None, date_fin=None): + """Stats performances ML par stratégie (source_reco).""" + cursor = conn.cursor() + cursor.execute( + """ + SELECT source_reco, + COUNT(*) as n_paris, + SUM(CASE WHEN statut='GAGNE' THEN 1 ELSE 0 END) as n_gagne, + SUM(CASE WHEN statut='PERDU' THEN 1 ELSE 0 END) as n_perdu, + SUM(CASE WHEN statut='EN_ATTENTE' THEN 1 ELSE 0 END) as n_attente, + ROUND(100.0 * SUM(CASE WHEN statut='GAGNE' THEN 1 ELSE 0 END) + / NULLIF(SUM(CASE WHEN statut IN ('GAGNE','PERDU') THEN 1 ELSE 0 END), 0), 1) as win_rate_pct, + ROUND(SUM(gain), 2) as gain_total, + ROUND(SUM(mise), 2) as mise_totale, + ROUND(SUM(gain) - SUM(mise), 2) as roi_net + FROM paris + WHERE source_reco LIKE 'xgboost%' + AND (:debut IS NULL OR date_course >= :debut) + AND (:fin IS NULL OR date_course <= :fin) + GROUP BY source_reco + ORDER BY source_reco + """, + {"debut": date_debut, "fin": date_fin}, + ) + return [dict(r) for r in cursor.fetchall()] + + +# ───────────────────────────────────────────────────────── +# PIPELINE COMPLET +# ───────────────────────────────────────────────────────── + + +def run(date): + """Enregistre les paris ML du jour + met à jour les résultats de J-1.""" + conn = get_db() + log.info(f"=== ml_feedback_saas.run({date}) ===") + + # 1. Enregistre les paris ML pour la date (depuis le cache du jour) + sg = save_ml_paris_sg(conn, date) + vb = save_ml_paris_value(conn, date) + sp = save_ml_paris_sp(conn, date) + s4 = save_ml_paris_2sur4(conn, date) + log.info(f"[SAVE] {date} → total insérés : SG={sg} VALUE={vb} SP={sp} 2S4={s4}") + + # 2. Met à jour les résultats de J-1 (résultats PMU disponibles) + yesterday = (datetime.strptime(date, "%Y-%m-%d") - timedelta(days=1)).strftime( + "%Y-%m-%d" + ) + maj = update_ml_paris_results(conn, yesterday) + log.info(f"[UPDATE] {yesterday} → {maj} paris mis à jour") + + conn.close() + return {"inseres": {"sg": sg, "value": vb, "sp": sp, "2sur4": s4}, "maj": maj} + + +def backfill(date): + """Backfill : insère ET met à jour les résultats pour une date passée.""" + conn = get_db() + log.info(f"=== ml_feedback_saas.backfill({date}) ===") + + sg = save_ml_paris_sg(conn, date) + vb = save_ml_paris_value(conn, date) + sp = save_ml_paris_sp(conn, date) + s4 = save_ml_paris_2sur4(conn, date) + log.info(f"[SAVE] {date} → SG={sg} VALUE={vb} SP={sp} 2S4={s4}") + + maj = update_ml_paris_results(conn, date) + log.info(f"[UPDATE] {date} → {maj} paris mis à jour") + + conn.close() + return sg + vb + sp + s4, maj + + +# ───────────────────────────────────────────────────────── +# MAIN +# ───────────────────────────────────────────────────────── + +if __name__ == "__main__": + if "--backfill" in sys.argv: + idx = sys.argv.index("--backfill") + date = sys.argv[idx + 1] if idx + 1 < len(sys.argv) else None + if not date: + print("Usage: python3 ml_feedback_saas.py --backfill YYYY-MM-DD") + sys.exit(1) + inseres, maj = backfill(date) + print(f"Backfill {date} : {inseres} paris insérés, {maj} mis à jour") + + elif "--date" in sys.argv: + idx = sys.argv.index("--date") + date = sys.argv[idx + 1] if idx + 1 < len(sys.argv) else None + if not date: + print("Usage: python3 ml_feedback_saas.py --date YYYY-MM-DD") + sys.exit(1) + result = run(date) + total = sum(result["inseres"].values()) + print(f"Run {date} : {total} paris insérés, {result['maj']} mis à jour") + + else: + result = run(datetime.now().strftime("%Y-%m-%d")) + total = sum(result["inseres"].values()) + print(f"Run today : {total} paris insérés, {result['maj']} mis à jour")