#!/usr/bin/env python3 """Combined API - Turf + Ideas""" from flask import Flask, jsonify, request, send_file, send_from_directory from flask_cors import CORS import sqlite3 import json import os import requests from datetime import datetime, timedelta app = Flask(__name__) CORS(app, origins=["*"], methods=["GET", "POST", "OPTIONS"]) DB_PATH = "/home/h3r7/turf_saas/turf_saas.db" IDEAS_FILE = "/home/h3r7/boite_a_idees/idees.json" # Basic Auth decorator def require_auth(f): from functools import wraps from flask import request @wraps(f) def decorated(*args, **kwargs): from flask import make_response import base64 auth = request.headers.get("Authorization", "") expected = "Basic " + base64.b64encode("h3r7:h3r7".encode("utf-8")).decode( "utf-8" ) if not auth or auth != expected: return make_response("Unauthorized", 401) return f(*args, **kwargs) return decorated LLM_API_KEY = os.environ.get( "OPENROUTER_API_KEY", os.environ.get("OPENAI_API_KEY", os.environ.get("1MINAI_API_KEY", "")), ) LLM_BASE_URL = os.environ.get("LLM_BASE_URL", "https://openrouter.ai/v1") LLM_MODEL = os.environ.get("LLM_MODEL", "liquid/lfm-2.5-1.2b-instruct:free") # === API KEYS === RESEND_API_KEY = os.environ.get("RESEND_API", "") BRAVE_SEARCH_API_KEY = os.environ.get("BRAVE_SEARCH_API", "") SQL_SCHEMA = """ Tables: - pmu_partants: date_programme, num_reunion, num_course, num_pmu, id_cheval, nom, age, sexe, race, robe, pays, place_corde, nom_pere, nom_mere, nom_pere_mere, driver, driver_change, entraineur, proprietaire, eleveur, oeilleres, supplement, handicap_valeur, handicap_poids, musique, nombre_courses, nombre_victoires, nombre_places, cote_direct, cote_reference, tendance_cote, favoris, ordre_arrivee, tx_victoire, tx_place, forme_recente - pmu_courses: date_programme, num_reunion, num_course, libelle, discipline, distance, hippodrome,px_type Tables relations: - pmu_partants.date_programme = pmu_courses.date_programme - pmu_partants.num_reunion = pmu_courses.num_reunion - pmu_partants.num_course = pmu_courses.num_course Key fields: - ordre_arrivee: 1=winner, 2=2nd, 3=3rd, 0=not finished - favoris: 1 if favorite (cote < 5), 0 otherwise - date_programme: YYYY-MM-DD format - discipline: 'Plat', 'Trot', 'Attele', 'Monte' """ def generate_sql_with_llm(question: str) -> str: """Generate SQL query from natural language using LLM""" if not LLM_API_KEY: return None try: import litellm litellm.drop_params = True system_prompt = f"""Tu es un expert SQL. Génère SEULEMENT la requête SQL (pas d'explication). Schéma STRICT: - Table pmu_partants: date_programme, num_reunion, num_course, nom, driver, entraineur, cote_direct, favoris, ordre_arrivee, tx_victoire, tx_place, forme_recente - Table pmu_courses: date_programme, num_reunion, num_course, libelle, discipline, distance, hippodrome Règles OBLIGATOIRES: - Utilise les noms de colonnes EXACTS ci-dessus - Utilise date('now', '-X days') pour les dates relatives - Pour "aujourd'hui", utilise date('now') - Pour "hier", utilise date('now', '-1 day') - Pour "cette semaine", utilise date('now', '-7 days') - Pour "ce mois", utilise date('now', '-30 days') - JOIN requis entre pmu_partants et pmu_courses - Limite à 15 résultats - Only SELECT queries allowed Question: {question} SQL:""" response = litellm.completion( model=f"openrouter/{LLM_MODEL}", messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": question}, ], api_key=LLM_API_KEY, max_tokens=300, ) sql = response.choices[0].message.content.strip() if sql.upper().startswith("SQL:"): sql = sql[4:].strip() if sql.upper().startswith("```SQL"): sql = sql[6:].strip() if sql.upper().startswith("```"): sql = sql[3:].strip() if sql.endswith("```"): sql = sql[:-3].strip() return sql if sql.upper().startswith("SELECT") else None except Exception as e: print(f"LLM SQL generation error: {e}") return None # === TURF API === def get_db(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row return conn @app.route("/") def index(): return send_file("/home/h3r7/turf_saas/dashboard.html") @app.route("/dashboard") def dashboard(): return send_file("/home/h3r7/turf_saas/dashboard.html") @app.route("/n8n-chat") @app.route("/turf/n8n-chat") def n8n_chat(): return send_file("/home/h3r7/turf_saas/n8n-chat.html") @app.route("/turf/api/n8n-proxy", methods=["GET", "POST", "OPTIONS"]) @app.route("/api/n8n-proxy", methods=["GET", "POST", "OPTIONS"]) def n8n_proxy(): """Proxy pour éviter CORS vers n8n interne""" if request.method == "OPTIONS": response = make_response("", 200) response.headers["Access-Control-Allow-Origin"] = "*" response.headers["Access-Control-Allow-Methods"] = "GET, POST, OPTIONS" response.headers["Access-Control-Allow-Headers"] = "Content-Type" return response data = request.json or {} try: r = requests.post( "http://10.0.1.7:5678/webhook/openclaw", json=data, timeout=30, headers={"Content-Type": "application/json"}, ) try: return jsonify(r.json()), 200 except: return jsonify({"message": r.text}), 200 except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/api") @app.route("/turf/api") @app.route("/api/today") def api_today(): conn = get_db() today = datetime.now().strftime("%Y-%m-%d") yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") # Parametre de selection des courses race_filter = request.args.get("race", "") # Nom de la course ou vide pour toutes data = { "date": today, "races": [], "predictions": {}, "results": [], "scores": {}, "weather": {}, } # Construire la condition de filtre - si un nom de course est fourni, l'utiliser directement if race_filter: race_condition = "AND race_name = ?" # Utilise le nom exact de la course race_params = (race_filter,) else: race_condition = "" race_params = () # Filtre France uniquement - matcher avec LIKE sur nom hippodrome (désactivé car pas de données aujourd'hui) # france_condition = "AND EXISTS (SELECT 1 FROM pmu_reunions r WHERE r.pays_code='FRA' AND r.date_programme=date AND (r.hippodrome_long LIKE '%' || race_hippodrome || '%' OR r.hippodrome_court LIKE '%' || race_hippodrome || '%' OR r.hippodrome_code LIKE '%' || race_hippodrome || '%'))" france_condition = "" # Filtre données suffisantes (min 10 partants) min_partants_condition = "AND (SELECT COUNT(*) FROM predictions p2 WHERE p2.race_name=predictions.race_name AND p2.date=predictions.date AND p2.source='canalturf_partants') >= 10" # Recuperer toutes les courses du jour try: # Get full race name format: R1 ANGERS - C1 Grand prix ... (13:55) full_race_name = get_full_race_name(conn, today) c = conn.execute( f""" SELECT DISTINCT c.num_reunion, c.num_course, c.libelle, c.libelle_court, c.heure_depart_str, r.hippodrome_court, r.hippodrome_long, r.nature FROM pmu_courses c JOIN pmu_reunions r ON r.date_programme = c.date_programme AND r.num_reunion = c.num_reunion WHERE c.date_programme=? {france_condition} ORDER BY c.num_reunion ASC, c.num_course ASC """, (today,), ) races = c.fetchall() if not races: c = conn.execute( f""" SELECT DISTINCT race_name, race_hippodrome, race_time FROM predictions WHERE date=? AND source='canalturf_partants' {france_condition} ORDER BY race_time ASC """, (today,), ) races = c.fetchall() def infer_race_type(libelle="", libelle_court="", nature=""): text = f"{libelle or ''} {libelle_court or ''} {nature or ''}".upper() if "QUINTE" in text: return "Quinté+" if "TROT" in text: return "Trot" if "PLAT" in text: return "Plat" return (nature or "").strip().title() def fmt_race(row): if "num_reunion" not in row.keys(): race_name = row["race_name"] or "" hippo = row["race_hippodrome"] or "" time = row["race_time"] or "" race_type = infer_race_type(race_name) display = f"{hippo} - {race_name} - {time}".strip() if race_type: display = f"{display} : {race_type}" return { "name": race_name, "filter": race_name, "display_label": display, "hippodrome": hippo, "time": time, "num_reunion": None, "num_course": None, "race_type": "", } num_reunion = row["num_reunion"] num_course = row["num_course"] hippo = row["hippodrome_court"] or row["hippodrome_long"] or "" course = row["libelle"] or row["libelle_court"] or "" time = row["heure_depart_str"] or "" race_type = infer_race_type( row["libelle"], row["libelle_court"], row["nature"] ) display = f"R{num_reunion} {hippo} - {course} - {time}".strip(" -") if race_type: display = f"{display} : {race_type}" return { "name": row["libelle"] or course, "filter": row["libelle"] or course, "display_label": display, "hippodrome": hippo, "time": time, "num_reunion": num_reunion, "num_course": num_course, "race_type": row["nature"] or "", } data["races"] = [fmt_race(r) for r in races] # Première course comme course principale, ou course correspondant au filtre if races: selected_race = None if race_filter: import re race_filter_name = race_filter match = re.search(r"-\s+C\d+\s+(.+?)(?:\s*\(|$)", race_filter) if match: race_filter_name = match.group(1).strip() selected_race = next( ( fmt_race(r) for r in races if (r["libelle"] or "") == race_filter_name or (r["libelle_court"] or "") == race_filter_name ), None, ) first_race = selected_race or fmt_race(races[0]) data["race"] = { "name": first_race["display_label"] or full_race_name, "display_label": first_race["display_label"] or full_race_name, "filter_name": first_race["filter"], "hippodrome": first_race["hippodrome"], "time": first_race["time"], "race_type": first_race["race_type"], } except Exception as e: print(f"Erreur races: {e}") data["race"] = {} # Partants avec cotes — 1 ligne par cheval (dédoublonnage) try: if race_filter: # Extract just the race name part (after time) for better matching race_name_part = race_filter # Try to extract the actual race name (after " - HH:MM ") import re match = re.search(r"-\s+\d{1,2}:\d{2}\s+(.+)$", race_filter) if match: race_name_part = match.group(1).strip() c = conn.execute( """ SELECT horse_name, horse_number, AVG(odds) as odds, prediction_rank, jockey FROM predictions WHERE date=? AND source='canalturf_partants' AND odds > 0 AND (race_name LIKE ? OR ? LIKE '%' || race_name || '%') GROUP BY horse_name ORDER BY odds ASC """, (today, "%" + race_name_part + "%", race_filter), ) else: c = conn.execute( """ SELECT horse_name, horse_number, AVG(odds) as odds, prediction_rank, jockey FROM predictions WHERE date=? AND source='canalturf_partants' AND odds > 0 GROUP BY horse_name ORDER BY odds ASC """, (today,), ) data["predictions"]["partants"] = [dict(r) for r in c.fetchall()] except: data["predictions"]["partants"] = [] # Pronostic bases/chances/outsiders - aussi filtré France for cat, src in [ ("bases", "canalturf_prono_bases"), ("chances", "canalturf_prono_chances"), ("outsiders", "canalturf_prono_outsiders"), ]: try: if race_filter: # Extract just the race name part (after time) for better matching import re match = re.search(r"-\s+\d{1,2}:\d{2}\s+(.+)$", race_filter) race_name_part = race_filter if match: race_name_part = match.group(1).strip() c = conn.execute( """ SELECT horse_name, horse_number, MIN(prediction_rank) as prediction_rank FROM predictions WHERE date=? AND source=? AND (race_name LIKE ? OR ? LIKE '%' || race_name || '%') GROUP BY horse_name ORDER BY prediction_rank """, (today, src, "%" + race_name_part + "%", race_filter), ) else: c = conn.execute( """ SELECT horse_name, horse_number, MIN(prediction_rank) as prediction_rank FROM predictions WHERE date=? AND source=? GROUP BY horse_name ORDER BY prediction_rank """, (today, src), ) data["predictions"][cat] = [dict(r) for r in c.fetchall()] except: data["predictions"][cat] = [] # Resultats du jour - depuis pmu_partants (toutes les courses France terminées) try: # Add race filter if provided race_condition = "" race_params = [today] if race_filter: import re match = re.search(r"-\s+\d{1,2}:\d{2}\s+(.+)$", race_filter) race_name_part = race_filter if match: race_name_part = match.group(1).strip() race_condition = "AND (c.libelle LIKE ? OR ? LIKE '%' || c.libelle || '%')" race_params.append("%" + race_name_part + "%") race_params.append(race_filter) c = conn.execute( f""" SELECT pp.nom as horse_name, pp.ordre_arrivee as position, pp.cote_direct as odds, pr.num_reunion, pr.hippodrome_court, c.heure_depart_str, c.libelle as race_name, c.num_course FROM pmu_partants pp JOIN pmu_reunions pr ON pr.date_programme=pp.date_programme AND pr.num_reunion=pp.num_reunion JOIN pmu_courses c ON c.date_programme=pp.date_programme AND c.num_reunion=pp.num_reunion AND c.num_course=pp.num_course WHERE pp.date_programme=? AND pp.ordre_arrivee > 0 AND pp.ordre_arrivee <= 5 AND pr.pays_code='FRA' {race_condition} ORDER BY c.heure_depart_str, pp.ordre_arrivee """, race_params, ) results = c.fetchall() if results: # Grouper par reunion current_reunion = None grouped = [] for r in results: rd = dict(r) reunion_key = f"R{rd['num_reunion']} {rd['hippodrome_court']}" rd["race_label"] = reunion_key rd["race_type"] = rd["race_name"][:30] if rd["race_name"] else "" grouped.append(rd) data["results"] = grouped else: c = conn.execute( "SELECT horse_name, position, odds FROM results WHERE date=? ORDER BY position LIMIT 5", (today,), ) data["results"] = [dict(r) for r in c.fetchall()] except Exception as e: print(f"Erreur résultats: {e}") data["results"] = [] # Fallback: utiliser v_resultats_complets si results table est vide if not data["results"]: try: c = conn.execute( """ SELECT date_programme as date, course as race_name, cheval as horse_name, position_finale as position, cote_direct as odds, num_reunion, num_course FROM v_resultats_complets WHERE date_programme=? AND position_finale IS NOT NULL AND position_finale > 0 ORDER BY course, position_finale """, (today,), ) rows = c.fetchall() if rows: data["results"] = [dict(r) for r in rows] except Exception as e: print(f"Erreur fallback résultats: {e}") # Fallback 2: afficher les résultats d'hier si pas de résultats aujourd'hui if not data["results"]: try: c = conn.execute( """ SELECT date_programme as date, course as race_name, cheval as horse_name, position_finale as position, cote_direct as odds, num_reunion, num_course FROM v_resultats_complets WHERE date_programme=? AND position_finale IS NOT NULL AND position_finale > 0 ORDER BY course, position_finale """, (yesterday,), ) rows = c.fetchall() if rows: data["results"] = [dict(r) for r in rows] except: pass # Score hier try: c = conn.execute( "SELECT horse_name FROM results WHERE date=? AND position<=3", (yesterday,) ) result_names = [r[0] for r in c.fetchall()] c = conn.execute( "SELECT DISTINCT horse_name FROM predictions WHERE date=? AND source='canalturf_prono_bases'", (yesterday,), ) bases_yest = [r[0] for r in c.fetchall()] score = sum( 1 for p in bases_yest if any( p.upper() in r.upper() or r.upper() in p.upper() for r in result_names ) ) data["scores"] = { "bases": f"{score}/{len(bases_yest)}" if bases_yest else "-", "date": yesterday, } except: data["scores"] = {} # Weather try: c = conn.execute("SELECT * FROM weather ORDER BY id DESC LIMIT 1") w = c.fetchone() if w: data["weather"] = dict(w) except: pass conn.close() return jsonify(data) @app.route("/api/races") @app.route("/turf/api/races") def api_races(): """Liste des courses du jour avec selection""" conn = get_db() today = datetime.now().strftime("%Y-%m-%d") race_filter = request.args.get( "filter", "quinte" ) # 'quinte', 'all', 'trot', 'plat' if race_filter == "quinte": condition = "AND (race_name LIKE '%Quinte%' OR race_name LIKE '%Quinté%')" elif race_filter == "trot": condition = "AND race_name LIKE '%TROT%'" elif race_filter == "plat": condition = "AND race_name LIKE '%PLAT%'" else: condition = "" try: c = conn.execute( f""" SELECT DISTINCT race_name, race_hippodrome, race_time FROM predictions WHERE date=? AND source='canalturf_partants' {condition} ORDER BY race_time ASC """, (today,), ) races = [] for r in c.fetchall(): # Compter les partants c2 = conn.execute( """ SELECT COUNT(DISTINCT horse_name) FROM predictions WHERE date=? AND race_name=? AND source='canalturf_partants' """, (today, r[0]), ) partants = c2.fetchone()[0] races.append( { "name": r[0], "hippodrome": r[1], "time": r[2], "partants": partants, "is_quinte": "Quinte" in (r[0] or ""), } ) return jsonify({"date": today, "races": races, "filter": race_filter}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() # === PREDICTIONS PAR COURSE === @app.route("/api/race/") @app.route("/turf/api/race/") def api_race_predictions(race_name): """Predictions pour une course specifique""" conn = get_db() today = datetime.now().strftime("%Y-%m-%d") yesterday = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") # Decoder le nom de la course import urllib.parse race_name = urllib.parse.unquote(race_name) data = { "date": today, "race": {"name": race_name}, "predictions": {}, "results": [], "scores": {}, } # Infos de la course try: c = conn.execute( """ SELECT DISTINCT race_hippodrome, race_time FROM predictions WHERE date=? AND race_name=? AND source='canalturf_partants' """, (today, race_name), ) row = c.fetchone() if row: data["race"]["hippodrome"] = row[0] data["race"]["time"] = row[1] except: pass # Partants try: c = conn.execute( """ SELECT horse_name, horse_number, AVG(odds) as odds, prediction_rank, jockey FROM predictions WHERE date=? AND race_name=? AND source='canalturf_partants' AND odds > 0 GROUP BY horse_name ORDER BY odds ASC """, (today, race_name), ) data["predictions"]["partants"] = [dict(r) for r in c.fetchall()] except: data["predictions"]["partants"] = [] # Pronostics for cat, src in [ ("bases", "canalturf_prono_bases"), ("chances", "canalturf_prono_chances"), ("outsiders", "canalturf_prono_outsiders"), ]: try: c = conn.execute( """ SELECT horse_name, horse_number, MIN(prediction_rank) as prediction_rank FROM predictions WHERE date=? AND race_name=? AND source=? GROUP BY horse_name ORDER BY prediction_rank """, (today, race_name, src), ) data["predictions"][cat] = [dict(r) for r in c.fetchall()] except: data["predictions"][cat] = [] # Resultats try: c = conn.execute( """ SELECT horse_name, position, odds FROM results WHERE date=? AND race_name LIKE ? ORDER BY position """, (today, f"%{race_name[:20]}%"), ) data["results"] = [dict(r) for r in c.fetchall()] except: data["results"] = [] conn.close() return jsonify(data) # Partants avec cotes — 1 ligne par cheval (dédoublonnage) try: c = conn.execute( """ SELECT horse_name, horse_number, AVG(odds) as odds, prediction_rank, jockey FROM predictions WHERE date=? AND source='canalturf_partants' AND odds > 0 GROUP BY horse_name ORDER BY odds ASC """, (today,), ) data["predictions"]["partants"] = [dict(r) for r in c.fetchall()] except: data["predictions"]["partants"] = [] # Pronostic bases/chances/outsiders — 1 ligne par cheval for cat, src in [ ("bases", "canalturf_prono_bases"), ("chances", "canalturf_prono_chances"), ("outsiders", "canalturf_prono_outsiders"), ]: try: c = conn.execute( """ SELECT horse_name, horse_number, MIN(prediction_rank) as prediction_rank FROM predictions WHERE date=? AND source=? GROUP BY horse_name ORDER BY prediction_rank """, (today, src), ) data["predictions"][cat] = [dict(r) for r in c.fetchall()] except: data["predictions"][cat] = [] # Legacy ours (compatibilité) try: c = conn.execute( "SELECT horse_name, odds, prediction_rank FROM predictions WHERE date=?", (today,), ) data["predictions"]["ours"] = [ {"horse_name": r[0], "odds": r[1], "prediction_rank": r[2]} for r in c.fetchall() ] except: data["predictions"]["ours"] = [] # Résultats du jour try: c = conn.execute( "SELECT horse_name, position, odds FROM results WHERE date=? ORDER BY position LIMIT 5", (today,), ) data["results"] = [dict(r) for r in c.fetchall()] except: data["results"] = [] # Score hier try: c = conn.execute( "SELECT horse_name FROM results WHERE date=? AND position<=3", (yesterday,) ) result_names = [r[0] for r in c.fetchall()] c = conn.execute( "SELECT DISTINCT horse_name FROM predictions WHERE date=? AND source='canalturf_prono_bases'", (yesterday,), ) bases_yest = [r[0] for r in c.fetchall()] score = sum( 1 for p in bases_yest if any( p.upper() in r.upper() or r.upper() in p.upper() for r in result_names ) ) data["scores"] = { "bases": f"{score}/{len(bases_yest)}" if bases_yest else "-", "date": yesterday, } except: data["scores"] = {} # Weather try: c = conn.execute("SELECT * FROM weather ORDER BY id DESC LIMIT 1") w = c.fetchone() if w: data["weather"] = dict(w) except: pass conn.close() return jsonify(data) @app.route("/api/odds_history") @app.route("/turf/api/odds_history") def api_odds_history(): conn = get_db() today = datetime.now().strftime("%Y-%m-%d") try: c = conn.execute( """ SELECT horse_name, horse_number, odds, MIN(scraped_at) as scraped_at FROM odds_history WHERE date=? GROUP BY horse_name, DATE(scraped_at), SUBSTR(scraped_at, 12, 5) ORDER BY horse_name, scraped_at ASC """, (today,), ) rows = c.fetchall() except: conn.close() return jsonify({"date": today, "horses": []}) horses = {} for row in rows: h = row["horse_name"] if h not in horses: horses[h] = { "horse_name": h, "horse_number": row["horse_number"], "snapshots": [], } horses[h]["snapshots"].append( {"odds": row["odds"], "time": row["scraped_at"][11:16]} ) result = [] for h, data in horses.items(): snaps = data["snapshots"] debut = snaps[0]["odds"] if snaps else 0 actuel = snaps[-1]["odds"] if snaps else 0 evol = ( round(((actuel - debut) / debut) * 100, 1) if debut > 0 and len(snaps) > 1 else 0 ) result.append( { "horse_name": h, "horse_number": data["horse_number"], "odds_debut": debut, "odds_actuel": actuel, "evol_pct": evol, "nb_snapshots": len(snaps), "snapshots": snaps, "tendance": "baisse" if evol < -5 else "hausse" if evol > 5 else "stable", } ) result.sort(key=lambda x: x["odds_actuel"]) conn.close() return jsonify({"date": today, "horses": result}) @app.route("/api/weather") def api_weather(): conn = get_db() c = conn.execute("SELECT * FROM weather ORDER BY id DESC LIMIT 4") weather = [dict(row) for row in c.fetchall()] conn.close() return jsonify(weather) # === IDEAS API === def load_ideas(): if not os.path.exists(IDEAS_FILE): os.makedirs(os.path.dirname(IDEAS_FILE), exist_ok=True) default = { "categories": [ {"id": "tech", "name": "Tech & IA", "color": "#7b2cbf"}, {"id": "saas", "name": "SaaS", "color": "#2ec4b6"}, {"id": "service", "name": "Service", "color": "#e94560"}, {"id": "produit", "name": "Produit", "color": "#ff9f1c"}, {"id": "invest", "name": "Investissement", "color": "#00d9ff"}, ], "ideas": [], "next_id": 1, } with open(IDEAS_FILE, "w") as f: json.dump(default, f, indent=2) return default with open(IDEAS_FILE, "r") as f: return json.load(f) def save_ideas(data): os.makedirs(os.path.dirname(IDEAS_FILE), exist_ok=True) with open(IDEAS_FILE, "w") as f: json.dump(data, f, indent=2) @app.route("/api/ideas") def get_ideas(): return jsonify(load_ideas()) @app.route("/api/ideas", methods=["POST"]) def add_idea(): data = load_ideas() new_idea = request.json new_idea["id"] = data["next_id"] new_idea["created"] = datetime.now().strftime("%Y-%m-%d") data["ideas"].append(new_idea) data["next_id"] += 1 save_ideas(data) return jsonify({"success": True, "id": new_idea["id"]}) @app.route("/api/ideas/", methods=["GET"]) def get_idea(idea_id): data = load_ideas() for i in data["ideas"]: if i["id"] == idea_id: return jsonify(i) return jsonify({"error": "Not found"}), 404 @app.route("/api/ideas/", methods=["PUT"]) def update_idea(idea_id): data = load_ideas() for i, idea in enumerate(data["ideas"]): if idea["id"] == idea_id: data["ideas"][i].update(request.json) save_ideas(data) return jsonify({"success": True}) return jsonify({"error": "Not found"}), 404 @app.route("/api/ideas/", methods=["DELETE"]) def delete_idea(idea_id): data = load_ideas() data["ideas"] = [i for i in data["ideas"] if i["id"] != idea_id] save_ideas(data) return jsonify({"success": True}) @app.route("/idees") @app.route("/idees/") def idees_page(): return send_from_directory("/home/h3r7/turf_saas", "idees_final.html") @app.route("/H3R7Tech_logo.png") @app.route("/turf/H3R7Tech_logo.png") def serve_logo(): return send_from_directory("/home/h3r7/turf_saas", "H3R7Tech_logo.png") @app.route("/turf/") def serve_static_turf(filename): if filename.endswith((".html", ".md", ".png", ".svg", ".jpg", ".css", ".js")): return send_from_directory("/home/h3r7/turf_saas", filename) return "Not found", 404 @app.route("/") def serve_static(filename): if filename.endswith((".html", ".md", ".png", ".svg", ".jpg")): return send_from_directory("/home/h3r7/turf_saas", filename) return "Not found", 404 def ensure_scoring_tables(conn): conn.execute(""" CREATE TABLE IF NOT EXISTS scoring ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, race_name TEXT, horse_name TEXT, horse_number INTEGER, score REAL, score_cote REAL, score_forme REAL, score_victoire REAL, score_place REAL, score_rk REAL, score_tendance REAL, score_avis REAL, cote REAL, forme_recente REAL, tx_victoire REAL, tx_place REAL, avis_entraineur TEXT, musique TEXT, rang_scoring INTEGER, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) conn.execute(""" CREATE TABLE IF NOT EXISTS recommendations ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT NOT NULL, race_name TEXT, type_pari TEXT, cheval1 TEXT, numero1 INTEGER, cheval2 TEXT, numero2 INTEGER, cote REAL, mise REAL, gain_potentiel REAL, confiance TEXT, justification TEXT, resultat TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """) def build_scoring_fallback(conn, date_str): try: import sys if "/home/h3r7/turf_saas" not in sys.path: sys.path.insert(0, "/home/h3r7/turf_saas") from scoring import build_recommendations, score_cheval except Exception: return [], {}, "R1C1" def find_race(for_date): return conn.execute( """ SELECT c.num_reunion, c.num_course, c.libelle, c.libelle_court, c.heure_depart_str FROM pmu_courses c LEFT JOIN pmu_reunions r ON r.date_programme = c.date_programme AND r.num_reunion = c.num_reunion WHERE c.date_programme = ? AND COALESCE(r.pays_code, 'FRA') = 'FRA' ORDER BY CASE WHEN UPPER(COALESCE(c.libelle, '')) LIKE '%QUINTE%' THEN 0 ELSE 1 END, c.num_reunion, c.num_course LIMIT 1 """, (for_date,), ).fetchone() race = find_race(date_str) if not race: fallback_date = conn.execute( "SELECT MAX(date_programme) FROM pmu_courses" ).fetchone()[0] if not fallback_date: fallback_date = conn.execute( "SELECT MAX(date_programme) FROM pmu_partants" ).fetchone()[0] if fallback_date: date_str = fallback_date race = find_race(date_str) if not race: return [], {}, "R1C1" race_label = f"R{race['num_reunion']}C{race['num_course']}" race_name = race["libelle"] or race["libelle_court"] or race_label rows = conn.execute( """ SELECT p.num_pmu, p.nom, p.musique, p.nombre_courses, p.nombre_victoires, p.nombre_places, p.cote_direct, p.cote_reference, p.driver_change, p.entraineur, p.tx_victoire, p.tx_place, p.forme_recente, p.oeilleres, c.distance, c.discipline, c.nb_declares_partants FROM pmu_partants p LEFT JOIN pmu_courses c ON c.date_programme = p.date_programme AND c.num_reunion = p.num_reunion AND c.num_course = p.num_course WHERE p.date_programme = ? AND p.num_reunion = ? AND p.num_course = ? ORDER BY p.num_pmu """, (date_str, race["num_reunion"], race["num_course"]), ).fetchall() participants = [] for row in rows: r = dict(row) participants.append( { "nom": r["nom"], "numero": r["num_pmu"], "musique": r.get("musique", ""), "nombreCourses": r.get("nombre_courses", 0) or 0, "nombreVictoires": r.get("nombre_victoires", 0) or 0, "nombrePlaces": r.get("nombre_places", 0) or 0, "reductionKilometrique": 0, "avisEntraineur": "NEUTRE", "driverChange": bool(r.get("driver_change", 0)), "dernierRapportDirect": {"rapport": r.get("cote_direct", 0) or 0}, "dernierRapportReference": { "rapport": r.get("cote_reference", 0) or r.get("cote_direct", 0) or 0 }, "partants": r.get("nb_declares_partants", 0) or 0, "distance": r.get("distance", 0) or 0, "discipline": r.get("discipline", "PLAT") or "PLAT", } ) if not participants: return [], {}, race_label scored_horses = [] for p in participants: score, details = score_cheval(p, participants) scored_horses.append( { "nom": p["nom"], "numero": p["numero"], "score": score, "details": details, } ) scored_horses = sorted(scored_horses, key=lambda x: x["score"], reverse=True) recos = build_recommendations(scored_horses) scores = [] for rank, h in enumerate(scored_horses, start=1): d = h["details"] scores.append( { "race_label": race_label, "horse_name": h["nom"], "horse_number": h["numero"], "score": h["score"], "cote": d.get("cote", 0), "forme_recente": d.get("forme_recente", 0), "tx_victoire": d.get("tx_victoire", 0), "avis_entraineur": d.get("avis_entraineur", "NEUTRE"), "musique": d.get("musique", ""), "rang_scoring": rank, } ) return scores, recos, race_name @app.route("/api/scoring") @app.route("/turf/api/scoring") def api_scoring(): conn = get_db() today = datetime.now().strftime("%Y-%m-%d") race_filter = request.args.get("race", "").strip() ensure_scoring_tables(conn) def infer_race_type(libelle="", libelle_court="", nature=""): text = f"{libelle or ''} {libelle_court or ''} {nature or ''}".upper() if "QUINTE" in text: return "Quinté+" if "TROT" in text: return "Trot" if "PLAT" in text: return "Plat" return (nature or "").strip().title() def format_race_label(row): reunion = row["num_reunion"] course = row["num_course"] hippodrome = row["hippodrome_court"] or row["hippodrome_long"] or "" name = row["libelle"] or row["libelle_court"] or "" time = row["heure_depart_str"] or "" race_type = infer_race_type(row["libelle"], row["libelle_court"], row["nature"]) label = f"R{reunion} {hippodrome} - {name} - {time}".strip(" -") if race_type: label = f"{label} : {race_type}" return { "race_label": f"R{reunion}C{course}", "full_race_name": label, "race_type": race_type, "race_time": time, "hippodrome": hippodrome, "name": name, } race_info = None try: if race_filter: race_info = conn.execute( """ SELECT c.num_reunion, c.num_course, c.libelle, c.libelle_court, c.heure_depart_str, r.hippodrome_court, r.hippodrome_long, r.nature FROM pmu_courses c JOIN pmu_reunions r ON r.date_programme=c.date_programme AND r.num_reunion=c.num_reunion WHERE c.date_programme=? AND ( c.libelle = ? OR c.libelle_court = ? OR c.libelle LIKE ? OR c.libelle_court LIKE ? ) ORDER BY c.heure_depart_str ASC, c.num_reunion ASC, c.num_course ASC LIMIT 1 """, ( today, race_filter, race_filter, f"%{race_filter}%", f"%{race_filter}%", ), ).fetchone() if not race_info: # Essayer de trouver la course qui correspond au scoring du jour scored_race = conn.execute( "SELECT race_name FROM scoring WHERE date=? LIMIT 1", (today,) ).fetchone() if scored_race and scored_race["race_name"]: race_name_db = scored_race["race_name"] race_info = conn.execute( """ SELECT c.num_reunion, c.num_course, c.libelle, c.libelle_court, c.heure_depart_str, r.hippodrome_court, r.hippodrome_long, r.nature FROM pmu_courses c JOIN pmu_reunions r ON r.date_programme=c.date_programme AND r.num_reunion=c.num_reunion WHERE c.date_programme=? AND ( c.libelle = ? OR c.libelle LIKE ? OR c.libelle_court LIKE ? ) LIMIT 1 """, (today, race_name_db, f"%{race_name_db}%", f"%{race_name_db}%"), ).fetchone() if not race_info: race_info = conn.execute( """ SELECT c.num_reunion, c.num_course, c.libelle, c.libelle_court, c.heure_depart_str, r.hippodrome_court, r.hippodrome_long, r.nature FROM pmu_courses c JOIN pmu_reunions r ON r.date_programme=c.date_programme AND r.num_reunion=c.num_reunion WHERE c.date_programme=? AND COALESCE(r.pays_code, 'FRA') = 'FRA' ORDER BY c.heure_depart_str ASC, c.num_reunion ASC, c.num_course ASC LIMIT 1 """, (today,), ).fetchone() except Exception: race_info = None if race_info: race_meta = format_race_label(race_info) race_label = race_meta["race_label"] full_race_name = race_meta["full_race_name"] else: race_label = "R1C1" full_race_name = "R1C1" # Scores du jour try: c = conn.execute( """ SELECT horse_name, horse_number, score, COALESCE(cote,0) as cote, forme_recente, tx_victoire, avis_entraineur, musique, rang_scoring FROM scoring WHERE date=? ORDER BY rang_scoring ASC """, (today,), ) scores = [dict(r) for r in c.fetchall()] # Add race label and full race name to each score for s in scores: s["race_label"] = race_label s["full_race_name"] = full_race_name except: scores = [] if not scores: scores, recos, fallback_race_name = build_scoring_fallback(conn, today) if scores and fallback_race_name: for s in scores: s["race_label"] = fallback_race_name s["full_race_name"] = ( full_race_name if full_race_name != "R1C1" else fallback_race_name ) else: # Recommandations du jour - Focus B4/B3 (ZE5) recos = generate_ze5_recommendations(conn, today) # Sauvegarder les recommandations ZE5 dans la DB save_ze5_recommendations(conn, today, recos) # Ajouter les recommandations depuis la table recommendations try: c = conn.execute( """ SELECT type_pari, cheval1, numero1, cheval2, numero2, cote, mise, gain_potentiel, confiance, justification FROM recommendations WHERE date=? ORDER BY type_pari """, (today,), ) for r in c.fetchall(): recos[r["type_pari"]] = dict(r) except: pass conn.close() return jsonify( { "date": today, "scores": scores, "recommendations": recos, "full_race_name": full_race_name, "race_label": race_label, } ) def generate_ze5_recommendations(conn, today): """ Génère des recommandations ZE5 avec focus B4/B3 Stratégie: Bases + Chances + 1 Outsider = bonne couverture pour B4/B3 """ recos = {} try: # Récupérer nos prédictions bases = [ r["horse_name"] for r in conn.execute( """ SELECT DISTINCT horse_name FROM predictions WHERE date=? AND source='canalturf_prono_bases' ORDER BY prediction_rank LIMIT 2 """, (today,), ).fetchall() ] chances = [ r["horse_name"] for r in conn.execute( """ SELECT DISTINCT horse_name FROM predictions WHERE date=? AND source='canalturf_prono_chances' ORDER BY prediction_rank LIMIT 3 """, (today,), ).fetchall() ] outsiders = [ r["horse_name"] for r in conn.execute( """ SELECT DISTINCT horse_name FROM predictions WHERE date=? AND source='canalturf_prono_outsiders' ORDER BY prediction_rank LIMIT 2 """, (today,), ).fetchall() ] if not bases or not chances: return recos # Get horse numbers too def get_horse_num(name): row = conn.execute( "SELECT horse_number FROM predictions WHERE date=? AND horse_name=? LIMIT 1", (today, name), ).fetchone() return str(row[0]) if row else "" # Format: "NUMERO NOM" def format_horse(name): num = get_horse_num(name) return f"{num} {name}" if num else name # Stratégie B4/B3: 4-5 chevaux couvrant bases + chances all_horses = bases[:2] + chances[:2] if outsiders: all_horses.append(outsiders[0]) if len(all_horses) >= 4: combo = "-".join([format_horse(h) for h in all_horses[:5]]) recos["ze5_b4b3"] = { "type_pari": "ZE5 B4/B3", "cheval1": combo, "cote": 15.0, "mise": 3, "gain_potentiel": 45, "confiance": "haute", "justification": "Bases + Chances + Outsider. Optimise pour B4/B3", "strategy": "b4b3", } # ZE5 Conservateur: Top 2 bases + Top 2 chances if len(bases) >= 1 and len(chances) >= 2: combo_list = [ format_horse(bases[0]), format_horse(chances[0]), format_horse(chances[1]), ] if len(bases) > 1: combo_list.append(format_horse(bases[1])) else: combo_list.append(format_horse(chances[0])) combo_conservative = "-".join(combo_list) recos["ze5_conservateur"] = { "type_pari": "ZE5 Conservateur", "cheval1": combo_conservative, "cote": 8.0, "mise": 3, "gain_potentiel": 24, "confiance": "moyenne-haute", "justification": "Favori + 2 Chances - Plus securise", "strategy": "conservateur", } # ZE5 Audacieux: outsiders + 1 base if len(outsiders) >= 2 and bases: combo_audacieux_list = [ format_horse(bases[0]), format_horse(outsiders[0]), format_horse(outsiders[1]), ] if chances: combo_audacieux_list.append(format_horse(chances[0])) combo_audacieux = "-".join(combo_audacieux_list) recos["ze5_audacieux"] = { "type_pari": "ZE5 Audacieux", "cheval1": combo_audacieux, "cote": 50.0, "mise": 1, "gain_potentiel": 50, "confiance": "basse", "justification": "Base + 2 Outsiders + Chance. Rapport élevé mais moins probable", "strategy": "audacieux", } # ZE4 (équivalent Multi): Top 4 bases + chances # Trouver les 4 premiers if len(bases) >= 2 and len(chances) >= 2: ze4_horses = [ format_horse(bases[0]), format_horse(bases[1]), format_horse(chances[0]), format_horse(chances[1]), ] combo_ze4 = "-".join(ze4_horses) recos["ze4"] = { "type_pari": "ZE4 (Multi 4)", "cheval1": combo_ze4, "cote": 20.0, "mise": 3, "gain_potentiel": 60, "confiance": "haute", "justification": "Top 2 Bases + Top 2 Chances. Trouver 4 premiers", "strategy": "ze4", } except Exception as e: print(f"Erreur génération recommandations: {e}") return recos def get_full_race_name(conn, date): """Construit le nom complet de la course: R1 MARSEILLE BORELY - 13h55 Grand National du Trot - Prix ...""" # Try to get from pmu_reunions first r = conn.execute( """ SELECT r.num_reunion, r.hippodrome_court, c.heure_depart_str, c.libelle FROM pmu_courses c JOIN pmu_reunions r ON r.date_programme=c.date_programme AND r.num_reunion=c.num_reunion WHERE c.date_programme=? AND r.pays_code='FRA' AND c.num_course=1 ORDER BY r.num_reunion LIMIT 1 """, (date,), ).fetchone() if r: return f"R{r[0]} {r[1]} - {r[2]} {r[3]}" # Fallback: get from predictions table r2 = conn.execute( """ SELECT DISTINCT race_name, race_hippodrome, race_time FROM predictions WHERE date=? AND source='canalturf_partants' ORDER BY race_time ASC LIMIT 1 """, (date,), ).fetchone() if r2: return f"{r2[1]} - {r2[2]} {r2[0]}" return "Course du jour" def save_ze5_recommendations(conn, today, recos): """Sauvegarde les recommandations ZE5 dans la base de données""" try: race_name = get_full_race_name(conn, today) saved = 0 for key, r in recos.items(): if not (key.startswith("ze5") or key == "ze4"): continue conn.execute( """ INSERT OR REPLACE INTO recommendations (date, race_name, type_pari, cheval1, numero1, cote, mise, gain_potentiel, confiance, justification, scoring_version) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( today, race_name, r["type_pari"], r["cheval1"], 0, r.get("cote", 0), r.get("mise", 0), r.get("gain_potentiel", 0), r.get("confiance", ""), r.get("justification", ""), key, ), ) saved += 1 conn.commit() print( f"✅ {len([k for k in recos if k.startswith('ze5')])} recommandations ZE5 sauvegardées" ) except Exception as e: print(f"Erreur sauvegarde ZE5: {e}") # === ML PREDICTIONS === import pickle import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder ml_models = None MODEL_PATH = "/home/h3r7/turf_saas/xgboost_models.pkl" def load_models(): global ml_models if ml_models is None and os.path.exists(MODEL_PATH): try: with open(MODEL_PATH, "rb") as f: ml_models = pickle.load(f) except: ml_models = False return ml_models def table_exists(conn, table_name): c = conn.execute( "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", (table_name,) ) return c.fetchone() is not None def ensure_analytics_tables(): conn = sqlite3.connect(DB_PATH) tables = [ ( "bet_results", """ CREATE TABLE IF NOT EXISTS bet_results ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT, race_name TEXT, type_pari TEXT, horse_name TEXT, horse_number INTEGER, cote REAL, mise REAL, resultat TEXT, gain REAL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """, ), ( "daily_stats", """ CREATE TABLE IF NOT EXISTS daily_stats ( date TEXT PRIMARY KEY, total_bets INTEGER, bets_gagne INTEGER, bets_perdu INTEGER, mise_totale REAL, gain_total REAL, precision_pct REAL, roi_pct REAL ) """, ), ( "stats_by_type", """ CREATE TABLE IF NOT EXISTS stats_by_type ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT, type_pari TEXT, total_bets INTEGER, gagne INTEGER, perdu INTEGER, mise_totale REAL, gain_total REAL, precision_pct REAL, roi_pct REAL ) """, ), ( "race_scores", """ CREATE TABLE IF NOT EXISTS race_scores ( id INTEGER PRIMARY KEY AUTOINCREMENT, date TEXT, race_name TEXT, race_time TEXT, hippodrome TEXT, top5_cotes TEXT, top5_cotes_hits INTEGER, top5_bc TEXT, top5_bc_hits INTEGER, top5_bo TEXT, top5_bo_hits INTEGER, results_top5 TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """, ), ( "paris", """ CREATE TABLE IF NOT EXISTS paris ( id INTEGER PRIMARY KEY AUTOINCREMENT, date_pari TEXT NOT NULL, date_course TEXT NOT NULL, race_name TEXT, race_label TEXT, hippodrome TEXT, type_pari TEXT, chevaux TEXT, cheval1 TEXT, numero1 INTEGER, cheval2 TEXT, numero2 INTEGER, cheval3 TEXT, numero3 INTEGER, cote REAL, mise REAL DEFAULT 1.0, statut TEXT DEFAULT 'EN_ATTENTE', gain REAL DEFAULT 0, commentaire TEXT, source_reco TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) """, ), ] for name, ddl in tables: conn.execute(ddl) conn.close() ensure_analytics_tables() def load_ml_horses(conn, today): """Load horses for ML predictions, with a fallback when historical_data is absent.""" course_info = {} c = conn.execute( """ SELECT num_reunion, num_course, libelle, libelle_court, discipline, distance, heure_depart_str FROM pmu_courses WHERE date_programme = ? ORDER BY num_reunion, num_course """, (today,), ) for row in c.fetchall(): course_info[f"{row['num_reunion']}_{row['num_course']}"] = dict(row) if table_exists(conn, "historical_data"): c = conn.execute( """ SELECT DISTINCT p.horse_name, p.horse_number, p.odds, h.age, h.sexe, h.nb_courses, h.nb_victoires, h.nb_places, h.tx_victoire, h.tx_place, h.forme_recente, h.reduction_km, h.gains_annee, h.cote_directe, h.distance, h.discipline, h.avis_entraineur, h.oeilleres, h.deferre, h.nb_partants, h.rang_cote, h.ratio_cote_field, h.musique FROM predictions p LEFT JOIN historical_data h ON h.horse_name = p.horse_name WHERE p.date = ? AND p.source = 'canalturf_partants' AND p.odds > 0 """, (today,), ) horses = [dict(row) for row in c.fetchall()] return today, horses, course_info def fetch_fallback(date_str): c = conn.execute( """ SELECT p.date_programme AS date, p.num_reunion, p.num_course, p.num_pmu AS horse_number, p.nom AS horse_name, p.age, p.sexe, p.musique, p.nombre_courses AS nb_courses, p.nombre_victoires AS nb_victoires, p.nombre_places AS nb_places, p.gains_annee_en_cours AS gains_annee, COALESCE(p.cote_direct, 0) AS cote_directe, COALESCE(c.distance, 0) AS distance, COALESCE(c.discipline, 'PLAT') AS discipline, COALESCE(c.nb_declares_partants, 0) AS nb_partants, COALESCE(p.oeilleres, 'SANS_OEILLERES') AS oeilleres, COALESCE(p.tx_victoire, 0) AS tx_victoire, COALESCE(p.tx_place, 0) AS tx_place, COALESCE(p.forme_recente, 0) AS forme_recente, 0 AS reduction_km, 'NEUTRE' AS avis_entraineur, 'NON' AS deferre, 0 AS rang_cote, 0 AS ratio_cote_field FROM pmu_partants p LEFT JOIN pmu_courses c ON c.date_programme = p.date_programme AND c.num_reunion = p.num_reunion AND c.num_course = p.num_course WHERE p.date_programme = ? ORDER BY p.num_reunion, p.num_course, p.num_pmu """, (date_str,), ) horses = [dict(row) for row in c.fetchall()] return horses horses = fetch_fallback(today) if horses: return today, horses, course_info c = conn.execute("SELECT MAX(date_programme) FROM pmu_partants") fallback_date = c.fetchone()[0] if fallback_date: return fallback_date, fetch_fallback(fallback_date), {} return today, [], {} def enrich_ml_horses(horses): """Fill missing ML fields and derive odds-based features.""" races = {} for horse in horses: race_key = ( horse.get("date") or horse.get("date_programme"), horse.get("num_reunion"), horse.get("num_course"), ) races.setdefault(race_key, []).append(horse) for group in races.values(): odds_values = [] for horse in group: raw_odds = horse.get("odds", horse.get("cote_directe", 0)) try: odds = float(raw_odds or 0) except (TypeError, ValueError): odds = 0.0 horse["odds"] = odds horse["cote_directe"] = float(horse.get("cote_directe", odds) or odds or 0) if odds > 0: odds_values.append(odds) avg_odds = sum(odds_values) / len(odds_values) if odds_values else 0 ranked = sorted( group, key=lambda h: h.get("odds", h.get("cote_directe", 0)) or 999999 ) for idx, horse in enumerate(ranked, start=1): horse.setdefault("horse_number", horse.get("num_pmu")) horse.setdefault("horse_name", horse.get("nom")) horse.setdefault("age", 0) horse.setdefault("sexe", "U") horse.setdefault("nb_courses", 0) horse.setdefault("nb_victoires", 0) horse.setdefault("nb_places", 0) horse.setdefault("tx_victoire", 0) horse.setdefault("tx_place", 0) horse.setdefault("forme_recente", 0) horse.setdefault("reduction_km", 0) horse.setdefault("gains_annee", 0) horse.setdefault("distance", 0) horse.setdefault("discipline", "PLAT") horse.setdefault("avis_entraineur", "NEUTRE") horse.setdefault("oeilleres", "SANS") horse.setdefault("deferre", "NON") horse.setdefault("nb_partants", len(group)) horse.setdefault("musique", "") horse.setdefault("rang_cote", idx) if not horse.get("ratio_cote_field"): horse["ratio_cote_field"] = ( round(horse.get("odds", 0) / avg_odds, 3) if avg_odds > 0 else 0 ) return horses @app.route("/api/ml_predictions") @app.route("/turf/api/ml_predictions") def api_ml_predictions(): models = load_models() if not models: return jsonify({"error": "Models not loaded"}) conn = get_db() today = datetime.now().strftime("%Y-%m-%d") date_used, horses, course_info = load_ml_horses(conn, today) horses = enrich_ml_horses(horses) if not horses: conn.close() return jsonify( { "date": date_used, "predictions": [], "message": "No predictions available", } ) feature_cols = [ "age", "sexe_enc", "nb_courses", "nb_victoires", "nb_places", "tx_victoire", "tx_place", "forme_recente", "reduction_km", "gains_annee", "cote_directe", "distance", "nb_partants", "discipline_enc", "avis_enc", "oeilleres_enc", "deferre_enc", "form_1", "form_2", "form_3", "form_4", "form_5", "form_avg", "win_rate_adj", "place_rate_adj", "implied_prob", "victories_per_race", "places_per_race", "earnings_per_race", "age_win_interact", "distance_cat", "is_favorite", "rang_cote", "ratio_cote_field", ] all_sexes = set(h.get("sexe", "U") or "U" for h in horses) all_avis = set(h.get("avis_entraineur", "NEUTRE") or "NEUTRE" for h in horses) all_oeilleres = set(h.get("oeilleres", "SANS") or "SANS" for h in horses) all_deferre = set(h.get("deferre", "NON") or "NON" for h in horses) all_discipline = set(h.get("discipline", "PLAT") or "PLAT" for h in horses) le_sexe = LabelEncoder() le_sexe.fit(list(all_sexes) + ["U"]) le_avis = LabelEncoder() le_avis.fit(list(all_avis) + ["NEUTRE"]) le_oeilleres = LabelEncoder() le_oeilleres.fit(list(all_oeilleres) + ["SANS"]) le_deferre = LabelEncoder() le_deferre.fit(list(all_deferre) + ["NON"]) le_discipline = LabelEncoder() le_discipline.fit(list(all_discipline) + ["PLAT"]) predictions = [] import re for horse in horses: features = {} for col in [ "age", "nb_courses", "nb_victoires", "nb_places", "tx_victoire", "tx_place", "forme_recente", "reduction_km", "gains_annee", "cote_directe", "distance", "nb_partants", "rang_cote", "ratio_cote_field", ]: features[col] = float(horse.get(col, 0) or 0) features["sexe_enc"] = le_sexe.transform([horse.get("sexe", "U") or "U"])[0] features["avis_enc"] = le_avis.transform( [horse.get("avis_entraineur", "NEUTRE") or "NEUTRE"] )[0] features["oeilleres_enc"] = le_oeilleres.transform( [horse.get("oeilleres", "SANS") or "SANS"] )[0] features["deferre_enc"] = le_deferre.transform( [horse.get("deferre", "NON") or "NON"] )[0] features["discipline_enc"] = le_discipline.transform( [horse.get("discipline", "PLAT") or "PLAT"] )[0] musique = horse.get("musique", "") form_nums = re.findall(r"\d+", str(musique))[:5] for i, fn in enumerate(form_nums): features[f"form_{i + 1}"] = float(fn) for i in range(len(form_nums) + 1, 6): features[f"form_{i}"] = 0.0 features["form_avg"] = sum(features[f"form_{i}"] for i in range(1, 6)) / 5 features["implied_prob"] = ( 1 / features["cote_directe"] if features["cote_directe"] > 0 else 0 ) features["win_rate_adj"] = features["tx_victoire"] * np.log1p( features["nb_courses"] ) features["place_rate_adj"] = features["tx_place"] * np.log1p( features["nb_courses"] ) features["victories_per_race"] = features["nb_victoires"] / max( features["nb_courses"], 1 ) features["places_per_race"] = features["nb_places"] / max( features["nb_courses"], 1 ) features["earnings_per_race"] = features["gains_annee"] / max( features["nb_courses"], 1 ) features["age_win_interact"] = features["age"] * features["tx_victoire"] features["distance_cat"] = ( 2.0 if 1500 < features["distance"] <= 2000 else (3.0 if 2000 < features["distance"] <= 2500 else 1.0) ) features["is_favorite"] = 1 if features["cote_directe"] < 5 else 0 try: X = pd.DataFrame([features])[feature_cols] X = X.fillna(0) prob_top1 = float(models["model_top1"].predict_proba(X)[0][1]) prob_top3 = float(models["model_top3"].predict_proba(X)[0][1]) predictions.append( { "horse_name": horse["horse_name"], "horse_number": horse["horse_number"], "odds": float(horse["odds"]), "prob_top1": round(prob_top1 * 100, 1), "prob_top3": round(prob_top3 * 100, 1), "ml_score": round((prob_top1 * 0.6 + prob_top3 * 0.4) * 100, 1), "recommendation": "top1" if prob_top1 > 0.15 else ("top3" if prob_top3 > 0.35 else "pass"), "is_value_bet": 1 if (prob_top3 > 0.35 and float(horse.get("odds", 0)) > 10) else 0, "is_outlier": 1 if ( float(horse.get("odds", 0)) <= 5 and (prob_top1 < 0.1 and prob_top3 < 0.25) ) else 0, "num_reunion": horse.get("num_reunion"), "num_course": horse.get("num_course"), } ) except Exception as e: predictions.append( { "horse_name": horse["horse_name"], "horse_number": horse["horse_number"], "odds": float(horse["odds"]), "error": str(e), } ) predictions.sort(key=lambda x: x.get("ml_score", 0), reverse=True) for pred in predictions: course_key = f"{pred.get('num_reunion', 1)}_{pred.get('num_course', 1)}" if course_key in course_info: cinfo = course_info[course_key] pred["race_label"] = ( f"R{pred.get('num_reunion', 1)}C{pred.get('num_course', 1)}" ) pred["race_name"] = cinfo.get("libelle", "") pred["hippodrome"] = cinfo.get("libelle_court", "") pred["discipline"] = cinfo.get("discipline", "") pred["distance"] = cinfo.get("distance", 0) pred["heure"] = cinfo.get("heure_depart_str", "") conn.close() return jsonify( { "date": date_used, "model_version": "xgboost_v1", "predictions": predictions, "courses": course_info, } ) # === VITESSE === @app.route("/api/vitesse") @app.route("/turf/api/vitesse") def api_vitesse(): try: conn = get_db() today = datetime.now().strftime("%Y-%m-%d") categories = ["bases", "chances", "outsiders"] result = {"date": today, "predictions": {}} for category in categories: c = conn.execute( f""" SELECT horse_name, horse_number FROM predictions WHERE date=? AND source=? AND horse_name IS NOT NULL GROUP BY horse_name ORDER BY MIN(prediction_rank) """, (today, f"canalturf_prono_{category}"), ) horses = [] for row in c.fetchall(): horse_name = row[0] horse_number = row[1] latest = conn.execute( """ SELECT odds, odds_prev, jockey, created_at FROM predictions WHERE date = ? AND horse_name = ? AND source = 'canalturf_partants' ORDER BY created_at DESC LIMIT 1 """, (today, horse_name), ).fetchone() pmu = conn.execute( """ SELECT forme_recente, tx_victoire, tx_place, nombre_courses FROM pmu_partants WHERE nom LIKE ? || '%' ORDER BY date_programme DESC LIMIT 1 """, (horse_name.split()[0],), ).fetchone() if latest: odds_change = "" if latest[1] and latest[0]: diff = latest[0] - latest[1] if diff < 0: odds_change = " 🔻" elif diff > 0: odds_change = " 🔺" if pmu and pmu[0] is not None: forme_str = f"{pmu[0]:.1f}" if pmu[0] else "N/A" speed_info = { "avg_time_ms": None, "races": pmu[3] or 0, "avg_time_formatted": f"F:{forme_str} | {pmu[1]:.0f}%V/{pmu[2]:.0f}%P", "odds": latest[0], "odds_prev": latest[1], "jockey": latest[2] or "", "forme": pmu[0], "tx_victoire": pmu[1], "tx_place": pmu[2], "source": "predictions+pmu", } else: trend = ( f" ({latest[1]:.1f}→{latest[0]:.1f})" if latest[1] else "" ) speed_info = { "avg_time_ms": None, "races": 0, "avg_time_formatted": f"🚴 {latest[2][:12] if latest[2] else ''} | {latest[0]:.1f}{odds_change}", "odds": latest[0], "odds_prev": latest[1], "jockey": latest[2] or "", "source": "predictions", } else: speed_info = { "avg_time_ms": None, "races": 0, "avg_time_formatted": "—", "source": "none", } horses.append( { "horse_name": horse_name, "horse_number": horse_number, "speed_info": speed_info, } ) result["predictions"][category] = horses conn.close() return jsonify(result) except Exception as e: return jsonify({"error": str(e), "status": "error"}), 500 # === BACKTEST === from datetime import timedelta def calculate_backtest_data(start_date=None, end_date=None): if not start_date: start_date = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") if not end_date: end_date = datetime.now().strftime("%Y-%m-%d") conn = get_db() query = """ SELECT r.date, r.race_name, r.type_pari, r.cheval1, r.cote, r.mise, r.resultat FROM recommendations r WHERE r.resultat IS NOT NULL AND r.resultat != '' AND r.date BETWEEN ? AND ? """ try: cursor = conn.execute(query, (start_date, end_date)) rows = cursor.fetchall() except sqlite3.OperationalError as e: conn.close() return { "summary": { "total_bets": 0, "message": "Données backtest indisponibles", "error": str(e), } } if not rows: conn.close() return {"summary": {"total_bets": 0, "message": "Aucune donnée"}} stats = { "total_bets": len(rows), "gagne": 0, "perdu": 0, "mise_totale": 0, "gain_total": 0, } details = [] by_type = {} for date, race_name, type_pari, cheval1, cote, mise, resultat in rows: mise = float(mise or 1) cote = float(cote or 1) stats["mise_totale"] += mise if type_pari not in by_type: by_type[type_pari] = {"count": 0, "gagne": 0, "mise": 0, "gain": 0} by_type[type_pari]["count"] += 1 by_type[type_pari]["mise"] += mise if resultat == "GAGNE": stats["gagne"] += 1 stats["gain_total"] += mise * cote by_type[type_pari]["gagne"] += 1 by_type[type_pari]["gain"] += mise * cote else: stats["perdu"] += 1 details.append( { "date": date, "race_name": (race_name or "")[:30], "type_pari": type_pari, "cheval": cheval1, "cote": cote, "mise": mise, "resultat": resultat, "gain": mise * cote if resultat == "GAGNE" else 0, } ) roi = ( ((stats["gain_total"] - stats["mise_totale"]) / stats["mise_totale"] * 100) if stats["mise_totale"] > 0 else 0 ) precision = ( (stats["gagne"] / stats["total_bets"] * 100) if stats["total_bets"] > 0 else 0 ) # Par type by_type_results = {} for pari_type, data in by_type.items(): pari_roi = ( ((data["gain"] - data["mise"]) / data["mise"] * 100) if data["mise"] > 0 else 0 ) pari_precision = ( (data["gagne"] / data["count"] * 100) if data["count"] > 0 else 0 ) by_type_results[pari_type] = { "count": data["count"], "gagne": data["gagne"], "mise": round(data["mise"], 2), "gain": round(data["gain"], 2), "roi": round(pari_roi, 1), "precision": round(pari_precision, 1), } conn.close() return { "period": {"start": start_date, "end": end_date}, "summary": { "total_bets": stats["total_bets"], "gagne": stats["gagne"], "perdu": stats["perdu"], "precision": round(precision, 1), "mise_totale": round(stats["mise_totale"], 2), "gain_total": round(stats["gain_total"], 2), "roi": round(roi, 1), }, "by_type": by_type_results, "details": details[-50:], } @app.route("/api/backtest") @app.route("/turf/api/backtest") def api_backtest(): """Backtest - lit depuis la base de données""" start = request.args.get("start") end = request.args.get("end") if not start: start = (datetime.now() - timedelta(days=30)).strftime("%Y-%m-%d") if not end: end = datetime.now().strftime("%Y-%m-%d") conn = get_db() # Récupérer les détails depuis bet_results cursor = conn.execute( """ SELECT b.date, b.race_name, b.type_pari, b.horse_name, b.horse_number, COALESCE(b.cote, 0) as cote, b.mise, b.resultat, b.gain FROM bet_results b WHERE b.date BETWEEN ? AND ? ORDER BY b.date DESC, b.id DESC LIMIT 50 """, (start, end), ) rows = cursor.fetchall() # Add race_label to each row details = [] import re for r in rows: d = dict(r) race_label = "R1C1" if d.get("race_name") and "R" in str(d["race_name"]): match = re.search(r"R(\d+)C(\d+)", str(d["race_name"])) if match: race_label = f"R{match.group(1)}C{match.group(2)}" d["race_label"] = race_label details.append(d) if not details: conn.close() return jsonify({"summary": {"total_bets": 0, "message": "Aucune donnée"}}) # Calculer résumé depuis la DB cursor = 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), ) row = cursor.fetchone() total, gagne, mise, gain = row roi = ((gain - mise) / mise * 100) if mise > 0 else 0 precision = (gagne / total * 100) if total > 0 else 0 # Par type cursor = 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), ) by_type = {} for row in cursor.fetchall(): type_pari, total, gagne, mise, gain = row pari_roi = ((gain - mise) / mise * 100) if mise > 0 else 0 pari_precision = (gagne / total * 100) if total > 0 else 0 by_type[type_pari] = { "count": total, "gagne": gagne, "mise": round(mise or 0, 2), "gain": round(gain or 0, 2), "roi": round(pari_roi, 1), "precision": round(pari_precision, 1), } # Details - already prepared above with race_label conn.close() return jsonify( { "period": {"start": start, "end": end}, "summary": { "total_bets": total, "gagne": gagne, "perdu": total - gagne, "precision": round(precision, 1), "mise_totale": round(mise or 0, 2), "gain_total": round(gain or 0, 2), "roi": round(roi, 1), }, "by_type": by_type, "details": details, } ) @app.route("/api/stats") @app.route("/turf/api/stats") def api_stats(): """Statistiques quotidiennes - lit depuis la base""" days = int(request.args.get("days", 30)) end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") conn = get_db() # Lire depuis daily_stats if not table_exists(conn, "daily_stats"): conn.close() return jsonify({"daily": [], "period": {"start": start_date, "end": end_date}}) cursor = conn.execute( """ SELECT date, total_bets, bets_gagne, bets_perdu, mise_totale, gain_total, precision_pct, roi_pct FROM daily_stats WHERE date BETWEEN ? AND ? ORDER BY date DESC """, (start_date, end_date), ) rows = cursor.fetchall() daily = [] for row in rows: daily.append( { "date": row[0], "total_bets": row[1], "gagne": row[2], "perdu": row[3], "precision": row[6], "mises": row[4], "gains": row[5], "roi": row[7], } ) conn.close() return jsonify({"daily": daily, "period": {"start": start_date, "end": end_date}}) def generate_sql_from_question(question): """Generate SQL query from natural language question - keyword fallback first, then LLM""" q = question.lower() # Keyword-based queries - prioritize common questions keyword_patterns = [ ("taux", "favori"), ("taux", "victoire"), ("taux", "reussite"), ("jockey",), ("driver",), ("entraineur",), ("aujourd",), ("hier",), ("programme",), ("resultat",), ("resultats",), ("arrivee",), ("gagn", "victoire", "vainqueur"), ("cote", "evolu"), ("cote", "chang"), ("cote", "vari"), ("roi",), ("profit",), ("gain",), ("distance",), ("vincennes",), ("performances",), ("statistiques",), ("historique",), ] # Check if question matches keyword patterns is_keyword_query = any( all(kw in q for kw in pattern) for pattern in keyword_patterns ) # Try keyword first for common questions if is_keyword_query: sql = generate_sql_from_keywords(question, q) if sql: return sql # Try LLM only for complex/niche questions sql = generate_sql_with_llm(question) if sql: return sql # Final fallback to keyword return generate_sql_from_keywords(question, q) def generate_sql_from_keywords(question, q): """Generate SQL from keywords - extracted from generate_sql_from_question""" # Today's races # Today's races (or most recent if no today) if "aujourd" in q or "aujourd'hui" in q or "ce jour" in q or "programme" in q: return """SELECT c.libelle as course, p.nom as cheval, p.cote_direct as cote, p.favoris, c.discipline, p.date_programme FROM pmu_partants p JOIN pmu_courses c ON p.date_programme = c.date_programme AND p.num_reunion = c.num_reunion AND p.num_course = c.num_course WHERE p.date_programme >= date('now', '-1 day') ORDER BY p.date_programme DESC, c.num_course, p.cote_direct LIMIT 15""" # Today's results (arrived horses) if "resultat" in q or "résultat" in q or "arrivee" in q: return """SELECT c.libelle as course, c.discipline, c.distance, p.nom as cheval, p.ordre_arrivee as position, p.cote_direct as cote, p.driver FROM pmu_partants p JOIN pmu_courses c ON p.date_programme = c.date_programme AND p.num_reunion = c.num_reunion AND p.num_course = c.num_course WHERE p.ordre_arrivee > 0 AND p.date_programme >= date('now', '-2 days') ORDER BY p.date_programme DESC, c.num_course, p.ordre_arrivee LIMIT 20""" # Yesterday's results if "hier" in q and ("resultat" in q or "résultat" in q): return """SELECT c.libelle as course, p.nom as cheval, p.ordre_arrivee as position, p.cote_direct as cote, c.discipline FROM pmu_partants p JOIN pmu_courses c ON p.date_programme = c.date_programme AND p.num_reunion = c.num_reunion AND p.num_course = c.num_course WHERE p.date_programme = date('now', '-1 day') AND p.ordre_arrivee > 0 AND p.ordre_arrivee <= 3 ORDER BY c.num_course, p.ordre_arrivee LIMIT 15""" # Winners today if "gagn" in q or "victoire" in q or "vainqueur" in q: return """SELECT c.libelle as course, p.nom as cheval, p.cote_direct as cote FROM pmu_partants p JOIN pmu_courses c ON p.date_programme = c.date_programme AND p.num_reunion = c.num_reunion AND p.num_course = c.num_course WHERE p.date_programme >= date('now', '-3 days') AND p.ordre_arrivee = 1 ORDER BY p.date_programme DESC, c.num_course LIMIT 10""" # Favorites win rate if "taux" in q and ("favori" in q or "favoris" in q): return """SELECT COUNT(*) as total_races, SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) as wins, ROUND(CAST(SUM(CASE WHEN ordre_arrivee <= 3 THEN 1 ELSE 0 END) AS FLOAT) / NULLIF(SUM(CASE WHEN favoris = 1 THEN 1 ELSE 0 END), 0) * 100, 1) as podium_rate, ROUND(CAST(SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) AS FLOAT) / NULLIF(SUM(CASE WHEN favoris = 1 THEN 1 ELSE 0 END), 0) * 100, 1) as win_rate FROM pmu_partants WHERE favoris = 1 AND ordre_arrivee > 0 AND date_programme >= date('now', '-7 days')""" # Odds changes if "cote" in q and ("evolu" in q or "chang" in q or "vari" in q): return """SELECT date_programme, nom as cheval, cote_direct, cote_reference, tendance_cote FROM pmu_partants WHERE date_programme >= date('now', '-2 days') ORDER BY date_programme DESC, ABS(cote_direct - cote_reference) DESC LIMIT 10""" if "taux" in q and "favori" in q: return """SELECT COUNT(*) as total_races, SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) as wins, ROUND(CAST(SUM(CASE WHEN ordre_arrivee <= 3 THEN 1 ELSE 0 END) AS FLOAT) / NULLIF(SUM(CASE WHEN favoris = 1 THEN 1 ELSE 0 END), 0) * 100, 1) as podium_rate, ROUND(CAST(SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) AS FLOAT) / NULLIF(SUM(CASE WHEN favoris = 1 THEN 1 ELSE 0 END), 0) * 100, 1) as win_rate FROM pmu_partants WHERE favoris = 1 AND ordre_arrivee > 0 AND date_programme >= date('now', '-7 days')""" # ROI calculation if "roi" in q or "profit" in q or "gain" in q: return """SELECT date_programme, COUNT(*) as nb_partants, SUM(CASE WHEN ordre_arrivee = 1 THEN (cote_direct - 1) ELSE -1 END) as profit_total, ROUND(AVG(CASE WHEN ordre_arrivee = 1 THEN (cote_direct - 1) ELSE -1 END), 2) as profit_moyen, SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) as nb_victoires, ROUND(CAST(SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) * 100, 1) as tx_victoire FROM pmu_partants WHERE ordre_arrivee > 0 AND date_programme >= date('now', '-30 days') GROUP BY date_programme ORDER BY date_programme DESC LIMIT 15""" if "jockey" in q or "driver" in q or "driver" in q: return """SELECT driver as jockey, COUNT(*) as total_races, SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) as victories, ROUND(CAST(SUM(CASE WHEN ordre_arrivee <= 3 THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) * 100, 1) as podium_rate FROM pmu_partants WHERE driver IS NOT NULL AND driver != '' AND ordre_arrivee > 0 GROUP BY driver ORDER BY victories DESC LIMIT 10""" if "entraineur" in q: return """SELECT entraineur, COUNT(*) as total_races, SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) as victories, ROUND(CAST(SUM(CASE WHEN ordre_arrivee <= 3 THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) * 100, 1) as podium_rate FROM pmu_partants WHERE entraineur IS NOT NULL AND entraineur != '' AND ordre_arrivee > 0 GROUP BY entraineur ORDER BY victories DESC LIMIT 10""" if "distance" in q and "vincennes" in q: return """SELECT c.discipline, COUNT(*) as races, ROUND(AVG(c.distance), 0) as avg_distance FROM pmu_courses c WHERE c.libelle LIKE '%Vincennes%' GROUP BY c.discipline ORDER BY races DESC""" # Horse name extraction - try various patterns if any( kw in q for kw in ["performances", "statistiques", "historique", "cheval", "stats"] ): import re # Match sequences of uppercase words (horse names are all caps in DB) # Looking for patterns like "TORTISAMBERT" or "TORTISAMBERT Z" match = re.search(r"\b([A-Z]{4,}(?:\s+[A-Z]{4,})?)\b", question) if match: horse = match.group(1).strip() return ( """SELECT nom as cheval, COUNT(*) as total_races, SUM(CASE WHEN ordre_arrivee = 1 THEN 1 ELSE 0 END) as victoires, ROUND(AVG(cote_direct), 1) as cote_moyenne, ROUND(CAST(SUM(CASE WHEN ordre_arrivee <= 3 THEN 1 ELSE 0 END) AS FLOAT) / COUNT(*) * 100, 1) as taux_place FROM pmu_partants WHERE nom LIKE ? AND ordre_arrivee > 0 GROUP BY nom""", (f"%{horse}%",), ) # Default: recent races with results return """SELECT c.libelle as course, c.discipline, p.nom as cheval, p.cote_direct as cote, p.favoris, p.ordre_arrivee as position FROM pmu_partants p JOIN pmu_courses c ON p.date_programme = c.date_programme AND p.num_reunion = c.num_reunion AND p.num_course = c.num_course WHERE p.date_programme >= date('now', '-3 days') ORDER BY p.date_programme DESC, c.num_course, p.ordre_arrivee LIMIT 15""" @app.route("/turf/api/execute-sql", methods=["POST", "OPTIONS"]) @app.route("/api/execute-sql", methods=["POST", "OPTIONS"]) @app.route("/turf/api/ask", methods=["GET"]) @app.route("/api/ask", methods=["GET"]) def execute_sql(): """Endpoint pour exécuter des requêtes SQL ou recevoir des questions via GET""" if request.method == "OPTIONS": return "", 200 # Pour GET, générer la requête SQL depuis la question if request.method == "GET": question = request.args.get("question", "") result = generate_sql_from_question(question) if not result: return jsonify({"error": "No question provided"}), 400 if isinstance(result, tuple): query, sql_params = result else: query, sql_params = result, () else: data = request.get_json() query = data.get("query", "") if data else "" sql_params = () if not query: return jsonify({"error": "No query provided"}), 400 # Limiter les requêtes SELECT uniquement pour la sécurité query_upper = query.strip().upper() if not query_upper.startswith("SELECT"): return jsonify({"error": "Only SELECT queries allowed"}), 403 try: conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(query, sql_params) rows = cursor.fetchall() results = [dict(row) for row in rows] conn.close() return jsonify({"results": results, "count": len(results)}) except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/call-workflow", methods=["POST"]) def call_workflow(): """Endpoint pour exécuter un workflow n8n via MCP""" import requests data = request.get_json() workflow_id = data.get("workflow_id", "sHVEK4hwyUmAww3F") question = data.get("question", "") if not question: return jsonify({"error": "No question provided"}), 400 try: # Call n8n MCP server mcp_url = "https://kolifee.duckdns.org/mcp-server/http" mcp_token = os.environ.get("MCP_TOKEN", "n8n_mcp_token_2025") response = requests.post( mcp_url, json={ "json": { "tool": "execute_workflow", "arguments": { "workflowId": workflow_id, "data": {"question": question}, }, } }, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {mcp_token}", }, timeout=60, ) if response.status_code == 200: result = response.json() return jsonify( {"response": result.get("text", result.get("output", str(result)))} ) else: return jsonify( {"error": f"MCP error: {response.status_code}"} ), response.status_code except Exception as e: return jsonify({"error": str(e)}), 500 # === NOUVEAUX ENDPOINTS LLM & ANALYTICS === try: from prompts_llm import ( get_system_prompt, get_sql_prompt, get_keyword_sql, get_suggestions_prompt, ) from llm_cache import get_llm_cache, get_sql_cache HAS_LLM_MODULES = True except ImportError: HAS_LLM_MODULES = False try: from analytics_reports import ( get_daily_report, get_weekly_report, get_monthly_report, format_report_markdown, ) HAS_ANALYTICS = True except ImportError: HAS_ANALYTICS = False @app.route("/turf/api/report/daily", methods=["GET"]) @app.route("/api/report/daily", methods=["GET"]) def daily_report(): """Rapport quotidien de performance""" if not HAS_ANALYTICS: return jsonify({"error": "analytics_reports module not available"}), 500 date = request.args.get("date") report = get_daily_report(date) return jsonify(report) @app.route("/turf/api/report/weekly", methods=["GET"]) @app.route("/api/report/weekly", methods=["GET"]) def weekly_report(): """Rapport hebdomadaire de performance""" if not HAS_ANALYTICS: return jsonify({"error": "analytics_reports module not available"}), 500 start_date = request.args.get("start") end_date = request.args.get("end") report = get_weekly_report(start_date, end_date) return jsonify(report) @app.route("/turf/api/report/monthly", methods=["GET"]) @app.route("/api/report/monthly", methods=["GET"]) def monthly_report(): """Rapport mensuel de performance""" if not HAS_ANALYTICS: return jsonify({"error": "analytics_reports module not available"}), 500 year = request.args.get("year", type=int) month = request.args.get("month", type=int) report = get_monthly_report(year, month) return jsonify(report) @app.route("/turf/api/report/markdown/", methods=["GET"]) def report_markdown(report_type): """Retourne le rapport en format Markdown""" if not HAS_ANALYTICS: return jsonify({"error": "analytics_reports module not available"}), 500 if report_type == "daily": date = request.args.get("date") report = get_daily_report(date) markdown = format_report_markdown(report, "daily") elif report_type == "weekly": start = request.args.get("start") end = request.args.get("end") report = get_weekly_report(start, end) markdown = format_report_markdown(report, "weekly") elif report_type == "monthly": year = request.args.get("year", type=int) month = request.args.get("month", type=int) report = get_monthly_report(year, month) markdown = format_report_markdown(report, "monthly") else: return jsonify({"error": "Invalid report type"}), 400 return jsonify({"markdown": markdown, "type": report_type}) @app.route("/turf/api/suggestions", methods=["GET"]) @app.route("/api/suggestions", methods=["GET"]) def suggestions(): """Retourne des suggestions de questions basées sur les données""" if not HAS_LLM_MODULES: return jsonify( { "suggestions": [ "Quel est mon taux de réussite cette semaine?", "Liste les 5 meilleurs jockeys", "Performances du cheval TORTISAMBERT", "Évolution des cotes", "Résultats d'hier", ] } ) conn = get_db() c = conn.cursor() suggestions = [] try: c.execute( "SELECT COUNT(*) as cnt FROM pmu_partants WHERE date_programme >= date('now', '-7 days')" ) recent = c.fetchone()["cnt"] if recent > 0: suggestions = [ "Quel est mon taux de réussite cette semaine?", "Liste les 5 meilleurs jockeys", "Quel est le ROI du mois?", "Résultats d'hier", "Programme du jour", ] else: suggestions = [ "Derniers gagnants", "Meilleurs entraîneurs", "Performances à Vincennes", "Évolution des cotes", ] except: suggestions = [ "Quel est le taux de réussite des favoris?", "Liste les meilleurs jockeys", "Résultats d'hier", ] finally: conn.close() return jsonify({"suggestions": suggestions}) @app.route("/turf/api/ask-enhanced", methods=["GET", "POST"]) def ask_enhanced(): """Version améliorée du endpoint /ask avec cache et fallback keywords""" if not HAS_LLM_MODULES: return jsonify({"error": "prompts_llm module not available"}), 500 if request.method == "GET": question = request.args.get("question", "") else: data = request.get_json() question = data.get("question", "") if data else "" if not question: return jsonify({"error": "No question provided"}), 400 sql_cache = get_sql_cache() cached_sql = sql_cache.get_sql(question) if cached_sql: query = cached_sql else: query = get_keyword_sql(question) if not query and LLM_API_KEY: try: import litellm litellm.drop_params = True prompt = get_sql_prompt(question) response = litellm.completion( model=f"openrouter/{LLM_MODEL}", messages=[ {"role": "system", "content": get_system_prompt()}, {"role": "user", "content": prompt}, ], api_key=LLM_API_KEY, max_tokens=300, ) sql = response.choices[0].message.content.strip() if sql.upper().startswith("SELECT"): query = sql sql_cache.set_sql(question, sql, success=True) except Exception as e: print(f"LLM error: {e}") if not query: return jsonify({"error": "Could not generate SQL for this question"}), 400 if not query.upper().startswith("SELECT"): return jsonify({"error": "Invalid query generated"}), 400 try: conn = get_db() c = conn.cursor() c.execute(query) rows = c.fetchall() results = [dict(row) for row in rows] conn.close() return jsonify( { "question": question, "sql": query, "results": results, "count": len(results), } ) except Exception as e: return jsonify({"error": str(e), "sql": query}), 500 @app.route("/turf/api/scores", methods=["POST"]) @require_auth def save_race_scores(): data = request.json if not data: return jsonify({"error": "No data provided"}), 400 conn = get_db() try: conn.execute( """ INSERT OR REPLACE INTO race_scores (date, race_name, race_time, hippodrome, top5_cotes, top5_cotes_hits, top5_bc, top5_bc_hits, top5_bo, top5_bo_hits, results_top5) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( data.get("date"), data.get("race_name"), data.get("race_time"), data.get("hippodrome"), json.dumps(data.get("top5_cotes", [])), data.get("top5_cotes_hits", 0), json.dumps(data.get("top5_bc", [])), data.get("top5_bc_hits", 0), json.dumps(data.get("top5_bo", [])), data.get("top5_bo_hits", 0), json.dumps(data.get("results_top5", [])), ), ) conn.commit() return jsonify({"status": "ok", "message": "Scores saved"}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @app.route("/turf/api/scores/history", methods=["GET"]) @require_auth def get_scores_history(): limit = request.args.get("limit", 30) conn = get_db() try: if not table_exists(conn, "race_scores"): return jsonify({"results": [], "count": 0}) c = conn.execute( """ SELECT date, race_name, race_time, hippodrome, top5_cotes, top5_cotes_hits, top5_bc, top5_bc_hits, top5_bo, top5_bo_hits, results_top5 FROM race_scores ORDER BY date DESC, race_time DESC LIMIT ? """, (limit,), ) rows = c.fetchall() results = [] for r in rows: d = dict(r) d["top5_cotes"] = json.loads(d["top5_cotes"]) if d.get("top5_cotes") else [] d["top5_bc"] = json.loads(d["top5_bc"]) if d.get("top5_bc") else [] d["top5_bo"] = json.loads(d["top5_bo"]) if d.get("top5_bo") else [] d["results_top5"] = ( json.loads(d["results_top5"]) if d.get("results_top5") else [] ) results.append(d) return jsonify({"results": results, "count": len(results)}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() # ============================================================ # PARIS / ROI - Gestion des paris manuels # ============================================================ @app.route("/api/parisroi", methods=["GET"]) @app.route("/turf/api/parisroi", methods=["GET"]) @require_auth def api_parisroi(): """Dashboard complet Paris/ROI""" conn = get_db() today = datetime.now().strftime("%Y-%m-%d") try: # Resume global depuis table paris stats = conn.execute(""" SELECT COUNT(*) as total_bets, SUM(CASE WHEN statut='GAGNE' THEN 1 ELSE 0 END) as gagne, SUM(CASE WHEN statut='PERDU' THEN 1 ELSE 0 END) as perdu, COALESCE(SUM(mise),0) as mise_totale, COALESCE(SUM(gain),0) as gain_total FROM paris WHERE statut != 'EN_ATTENTE' """).fetchone() stats = ( dict(stats) if stats else { "total_bets": 0, "gagne": 0, "perdu": 0, "mise_totale": 0, "gain_total": 0, } ) mise = stats["mise_totale"] or 0 gain = stats["gain_total"] or 0 roi = ((gain - mise) / mise * 100) if mise > 0 else 0 precision = ( (stats["gagne"] / stats["total_bets"] * 100) if stats["total_bets"] > 0 else 0 ) stats["roi"] = round(roi, 1) stats["precision"] = round(precision, 1) # Paris du jour (EN_ATTENTE) paris_jour = conn.execute(""" SELECT * FROM paris WHERE statut = 'EN_ATTENTE' ORDER BY created_at DESC """).fetchall() paris_jour = [dict(r) for r in paris_jour] # Historique des 30 derniers paris historique = conn.execute(""" SELECT * FROM paris WHERE statut != 'EN_ATTENTE' ORDER BY created_at DESC LIMIT 30 """).fetchall() historique = [dict(r) for r in historique] # Stats par type by_type = conn.execute(""" SELECT type_pari, COUNT(*) as total, SUM(CASE WHEN statut='GAGNE' THEN 1 ELSE 0 END) as gagne, SUM(mise) as mise, SUM(gain) as gain FROM paris WHERE statut != 'EN_ATTENTE' GROUP BY type_pari """).fetchall() by_type = { r["type_pari"]: { "total": r["total"], "gagne": r["gagne"], "mise": round(r["mise"] or 0, 2), "gain": round(r["gain"] or 0, 2), "roi": round( ((r["gain"] or 0) - (r["mise"] or 0)) / (r["mise"] or 1) * 100, 1 ), } for r in by_type } # ROI curve (30 derniers jours) roi_curve = conn.execute(""" SELECT date, roi_pct, gain_total, mise_totale FROM daily_stats ORDER BY date DESC LIMIT 30 """).fetchall() roi_curve = [dict(r) for r in roi_curve] return jsonify( { "summary": stats, "paris_jour": paris_jour, "historique": historique, "by_type": by_type, "roi_curve": roi_curve, } ) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @app.route("/api/paris", methods=["GET"]) @app.route("/turf/api/paris", methods=["GET"]) @require_auth def api_get_paris(): """Liste des paris avec filtres""" conn = get_db() date = request.args.get("date") statut = request.args.get("statut") try: query = "SELECT * FROM paris WHERE 1=1" params = [] if date: query += " AND date_pari = ?" params.append(date) if statut: query += " AND statut = ?" params.append(statut) query += " ORDER BY created_at DESC" rows = conn.execute(query, params).fetchall() return jsonify({"paris": [dict(r) for r in rows], "count": len(rows)}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @app.route("/api/paris", methods=["POST"]) @app.route("/turf/api/paris", methods=["POST"]) @require_auth def api_add_pari(): """Ajouter un nouveau pari""" data = request.json if not data: return jsonify({"error": "Donnees requises"}), 400 conn = get_db() try: today = datetime.now().strftime("%Y-%m-%d") chevaux = data.get("chevaux", "") c = conn.execute( """ INSERT INTO paris (date_pari, date_course, race_name, race_label, hippodrome, type_pari, chevaux, cheval1, numero1, cheval2, numero2, cheval3, numero3, cote, mise, statut, commentaire, source_reco) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'EN_ATTENTE', ?, ?) """, ( today, data.get("date_course", today), data.get("race_name", ""), data.get("race_label", ""), data.get("hippodrome", ""), data.get("type_pari", "tierce"), chevaux, data.get("cheval1", ""), data.get("numero1"), data.get("cheval2", ""), data.get("numero2"), data.get("cheval3", ""), data.get("numero3"), data.get("cote", 0), data.get("mise", 1.0), data.get("commentaire", ""), data.get("source_reco", "manuel"), ), ) conn.commit() return jsonify({"status": "ok", "id": c.lastrowid}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @app.route("/api/paris/", methods=["PUT"]) @app.route("/turf/api/paris/", methods=["PUT"]) @require_auth def api_update_pari(pari_id): """Mettre a jour un pari (resultat/gain)""" data = request.json if not data: return jsonify({"error": "Donnees requises"}), 400 conn = get_db() try: statut = data.get("statut", "EN_ATTENTE") gain = data.get("gain", 0) commentaire = data.get("commentaire", "") conn.execute( """ UPDATE paris SET statut = ?, gain = ?, commentaire = ? WHERE id = ? """, (statut, gain, commentaire, pari_id), ) conn.commit() # Mettre a jour bet_results en cascade pari = conn.execute("SELECT * FROM paris WHERE id = ?", (pari_id,)).fetchone() if pari and statut != "EN_ATTENTE": conn.execute( """ INSERT INTO bet_results (date, race_name, type_pari, horse_name, horse_number, cote, mise, resultat, gain) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( pari["date_pari"], pari["race_name"], pari["type_pari"], pari["cheval1"], pari["numero1"], pari["cote"], pari["mise"], statut, gain, ), ) conn.commit() return jsonify({"status": "ok"}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @app.route("/api/paris/", methods=["DELETE"]) @app.route("/turf/api/paris/", methods=["DELETE"]) @require_auth def api_delete_pari(pari_id): """Supprimer un pari""" conn = get_db() try: conn.execute( "DELETE FROM paris WHERE id = ? AND statut = 'EN_ATTENTE'", (pari_id,) ) conn.commit() return jsonify({"status": "ok"}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() @app.route("/api/paris/auto-settle", methods=["POST"]) @app.route("/turf/api/paris/auto-settle", methods=["POST"]) @require_auth def api_auto_settle(): """Regler automatiquement les paris du jour via resultats PMU""" conn = get_db() today = datetime.now().strftime("%Y-%m-%d") try: paris = conn.execute( """ SELECT * FROM paris WHERE date_course = ? AND statut = 'EN_ATTENTE' """, (today,), ).fetchall() settled = 0 for pari in paris: p = dict(pari) chevaux_paris = [p.get("numero1"), p.get("numero2"), p.get("numero3")] chevaux_paris = [c for c in chevaux_paris if c] if not chevaux_paris: continue # Recuperer resultats results = conn.execute( """ SELECT nom, ordre_arrivee FROM pmu_partants WHERE date_programme = ? AND ordre_arrivee > 0 ORDER BY ordre_arrivee ASC """, (today,), ).fetchall() top3 = [ r["ordre_arrivee"] for r in results if r["ordre_arrivee"] in [1, 2, 3] ] if not top3: continue # Verifier si les numeros paris correspondent # Simplifie: on verifie si tous les numeros sont dans le top3 is_gagne = all(n in top3 for n in chevaux_paris) statut = "GAGNE" if is_gagne else "PERDU" gain = 0 if is_gagne and p.get("cote"): gain = p["mise"] * p["cote"] conn.execute( """ UPDATE paris SET statut = ?, gain = ? WHERE id = ? """, (statut, gain, p["id"]), ) settled += 1 conn.commit() return jsonify({"status": "ok", "settled": settled}) except Exception as e: return jsonify({"error": str(e)}), 500 finally: conn.close() # === EMAIL SENDING (Resend) === @app.route("/api/send-email", methods=["POST", "OPTIONS"]) @app.route("/turf/api/send-email", methods=["POST", "OPTIONS"]) def send_email(): """Envoyer un email via Resend API""" if request.method == "OPTIONS": return "", 200 if not RESEND_API_KEY: return jsonify({"error": "RESEND_API key not configured in environment"}), 500 data = request.get_json() if not data: return jsonify({"error": "JSON body required"}), 400 to = data.get("to") subject = data.get("subject") html = data.get("html") text = data.get("text") from_addr = data.get("from", "H3R7Tech ") if not to or not subject: return jsonify({"error": "Missing required fields: to, subject"}), 400 if not html and not text: return jsonify({"error": "Missing email body: provide html or text"}), 400 try: payload = { "from": from_addr, "to": [to] if isinstance(to, str) else to, "subject": subject, } if html: payload["html"] = html if text: payload["text"] = text resp = requests.post( "https://api.resend.com/emails", headers={ "Authorization": f"Bearer {RESEND_API_KEY}", "Content-Type": "application/json", }, json=payload, timeout=15, ) if resp.status_code in (200, 201): result = resp.json() return jsonify( { "success": True, "id": result.get("id"), "message": "Email sent successfully", } ) else: return jsonify( {"error": f"Resend API error: {resp.status_code}", "details": resp.text} ), resp.status_code except Exception as e: return jsonify({"error": str(e)}), 500 # === BRAVE SEARCH === @app.route("/api/brave-search", methods=["GET", "POST", "OPTIONS"]) @app.route("/turf/api/brave-search", methods=["GET", "POST", "OPTIONS"]) def brave_search(): """Recherche web via Brave Search API""" if request.method == "OPTIONS": return "", 200 if not BRAVE_SEARCH_API_KEY: return jsonify( {"error": "BRAVE_SEARCH_API key not configured in environment"} ), 500 if request.method == "GET": q = request.args.get("q", "") count = int(request.args.get("count", 10)) offset = int(request.args.get("offset", 0)) search_type = request.args.get("type", "web") else: data = request.get_json() or {} q = data.get("q", data.get("query", "")) count = int(data.get("count", 10)) offset = int(data.get("offset", 0)) search_type = data.get("type", "web") if not q: return jsonify({"error": "Missing query parameter: q"}), 400 count = min(count, 20) # Limit to 20 results max try: # Choose endpoint based on type if search_type == "news": url = "https://api.search.brave.com/res/v1/news/search" else: url = "https://api.search.brave.com/res/v1/web/search" resp = requests.get( url, headers={ "X-Subscription-Token": BRAVE_SEARCH_API_KEY, "Accept": "application/json", }, params={ "q": q, "count": count, "offset": offset, "search_lang": "fr", }, timeout=10, ) if resp.status_code == 200: result = resp.json() # Normalize response if search_type == "news": items = result.get("results", []) normalized = [ { "title": r.get("title", ""), "url": r.get("url", ""), "description": r.get("description", ""), "age": r.get("age", ""), "source": r.get("source", {}).get("title", ""), } for r in items ] else: items = result.get("web", {}).get("results", []) normalized = [ { "title": r.get("title", ""), "url": r.get("url", ""), "description": r.get("description", ""), "age": r.get("age", ""), "language": r.get("language", ""), } for r in items ] return jsonify( { "query": q, "type": search_type, "count": len(normalized), "results": normalized, } ) else: return jsonify( { "error": f"Brave Search API error: {resp.status_code}", "details": resp.text, } ), resp.status_code except Exception as e: return jsonify({"error": str(e)}), 500 @app.route("/turf/api/predictions_analysis", methods=["GET"]) def api_predictions_analysis(): """Analyse des predictions vs resultats reels""" from datetime import datetime, timedelta days = int(request.args.get("days", 30)) end_date = datetime.now().strftime("%Y-%m-%d") start_date = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") conn = get_db() cursor = conn.cursor() stats = { "canalturf": {"total": 0, "top1_pct": 0, "top3_pct": 0, "top5_pct": 0, "ze2_pct": 0}, "scoring": {"total": 0, "top1_pct": 0, "top3_pct": 0, "top5_pct": 0, "ze2_pct": 0}, } for source in ["canalturf", "scoring"]: pred_table = "predictions" if source == "canalturf" else "scoring" pred_col = "predicted_1" if source == "canalturf" else "horse_number" try: cursor.execute( f""" SELECT c.libelle, c.ordre_arrivee, c.numero, p.{pred_col} FROM pmu_courses c LEFT JOIN {pred_table} p ON c.date_programme = p.date_programme AND c.num_course = p.num_course WHERE c.date_course BETWEEN ? AND ? AND c.ordre_arrivee IS NOT NULL ORDER BY c.date_course DESC """, (start_date, end_date), ) races = {} for row in cursor.fetchall(): race, ordre, numero, pred = row if race not in races: races[race] = {"actual": [], "predicted": []} if ordre and ordre > 0: races[race]["actual"].append(str(numero)) if pred: races[race]["predicted"].append(str(pred)) top1_hit = top3_hit = 0 total = len(races) for race, data in races.items(): actual = set(data["actual"][:3]) pred_top1 = data["predicted"][0] if data["predicted"] else None actual_top1 = data["actual"][0] if data["actual"] else None if pred_top1 and actual_top1 and pred_top1 == actual_top1: top1_hit += 1 if len(set(data["predicted"][:3]) & actual) >= 1: top3_hit += 1 if total > 0: stats[source]["total"] = total stats[source]["top1_pct"] = round(top1_hit / total * 100, 1) stats[source]["top3_pct"] = round(top3_hit / total * 100, 1) except Exception as e: print(f"Erreur {source}: {e}") conn.close() return jsonify({"stats": stats, "period": {"start": start_date, "end": end_date}}) if __name__ == "__main__": app.run(host="0.0.0.0", port=8790, debug=False)