#!/usr/bin/env python3 """ Scoring Engine - Analyse complète des partants via API PMU Calcule un score composite et recommande Simple Gagnant, Simple Placé, Couplé Sauvegarde en BDD + JSON + rapport Telegram """ import requests import sqlite3 import json import re from datetime import datetime import os; DB_PATH = os.environ.get("DB_PATH", "/home/h3r7/turf_scraper/turf.db") HEADERS = {'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'} # ============================================================ # DÉCODEUR MUSIQUE # ============================================================ def parse_musique(musique): """ Décode la musique PMU : ex "1a0a(25)0a4a2a" Retourne des stats sur les 10 dernières courses (hors recul) Position 1 = course la plus récente """ if not musique: return {} # Supprimer les indices de recul (25), (24) etc. clean = re.sub(r'\(\d+\)', '', musique) # Extraire les résultats : chiffre + lettre discipline # a=attelé, m=monté, p=plat, h=haies, s=steeple, c=cross resultats = re.findall(r'(\d+|D|0)([amphsc]?)', clean) positions = [] for pos, disc in resultats[:10]: # 10 dernières if pos == 'D': positions.append(99) # Disqualifié else: positions.append(int(pos)) if not positions: return {} # Stats nb_courses = len(positions) nb_victoires = positions.count(1) nb_places = sum(1 for p in positions if 1 <= p <= 3) nb_top5 = sum(1 for p in positions if 1 <= p <= 5) nb_disq = positions.count(99) # Forme récente (3 dernières) recentes = [p for p in positions[:3] if p != 99] forme_recente = sum(recentes) / len(recentes) if recentes else 99 # Tendance : amélioration ou dégradation if len(positions) >= 4: debut = sum(positions[-4:]) / 4 # anciennes fin = sum(positions[:4]) / 4 # récentes tendance = debut - fin # positif = amélioration else: tendance = 0 return { 'positions': positions, 'nb_courses': nb_courses, 'nb_victoires': nb_victoires, 'nb_places': nb_places, 'nb_top5': nb_top5, 'nb_disq': nb_disq, 'forme_recente': round(forme_recente, 1), 'tendance': round(tendance, 1), 'tx_victoire': round(nb_victoires / nb_courses * 100, 1) if nb_courses else 0, 'tx_place': round(nb_places / nb_courses * 100, 1) if nb_courses else 0, } # ============================================================ # SCORING COMPOSITE # ============================================================ def score_cheval(p, all_participants): """ Calcule un score composite 0-100 pour un cheval. Critères pondérés : - Cote (20%) : inverse de la cote = plus c'est bas, mieux c'est - Forme récente (25%) : positions des 3 dernières courses - Taux victoire carrière (15%) - Taux placé carrière (15%) - Réduction kilométrique (10%) : vitesse de référence - Tendance (10%) : amélioration récente - Avis entraîneur (5%) """ score = 0 details = {} # 1. COTE (20 pts) — plus la cote est basse, plus le cheval est favori cote = 0 rapport = p.get('dernierRapportDirect', {}) if rapport: cote = rapport.get('rapport', 0) if not cote: rapport_ref = p.get('dernierRapportReference', {}) cote = rapport_ref.get('rapport', 99) if rapport_ref else 99 # Normaliser : cote 1 = 20pts, cote 10 = 10pts, cote 50+ = 2pts if cote > 0: score_cote = max(2, min(20, 20 / (1 + cote * 0.15))) else: score_cote = 2 score += score_cote details['cote'] = round(cote, 1) details['score_cote'] = round(score_cote, 1) # 2. FORME RÉCENTE (25 pts) musique_stats = parse_musique(p.get('musique', '')) forme = musique_stats.get('forme_recente', 99) if forme <= 1: score_forme = 25 elif forme <= 2: score_forme = 20 elif forme <= 3: score_forme = 15 elif forme <= 5: score_forme = 10 elif forme <= 8: score_forme = 5 else: score_forme = 0 score += score_forme details['forme_recente'] = forme details['score_forme'] = score_forme # 3. TAUX VICTOIRE CARRIÈRE (15 pts) nb_courses_total = p.get('nombreCourses', 0) nb_victoires_total = p.get('nombreVictoires', 0) tx_vic = (nb_victoires_total / nb_courses_total * 100) if nb_courses_total else 0 score_vic = min(15, tx_vic * 0.5) score += score_vic details['tx_victoire'] = round(tx_vic, 1) details['score_victoire'] = round(score_vic, 1) # 4. TAUX PLACÉ CARRIÈRE (15 pts) nb_places_total = p.get('nombrePlaces', 0) tx_place = (nb_places_total / nb_courses_total * 100) if nb_courses_total else 0 score_place = min(15, tx_place * 0.2) score += score_place details['tx_place'] = round(tx_place, 1) details['score_place'] = round(score_place, 1) # 5. RÉDUCTION KILOMÉTRIQUE (10 pts) — plus c'est bas, plus c'est rapide rk = p.get('reductionKilometrique', 0) if rk > 0: # Normaliser : RK 72000 = excellent, RK 78000 = moyen all_rk = [x.get('reductionKilometrique', 0) for x in all_participants if x.get('reductionKilometrique', 0) > 0] if all_rk: min_rk = min(all_rk) max_rk = max(all_rk) if max_rk > min_rk: score_rk = 10 * (1 - (rk - min_rk) / (max_rk - min_rk)) else: score_rk = 5 else: score_rk = 5 else: score_rk = 0 score += score_rk details['rk'] = rk details['score_rk'] = round(score_rk, 1) # 6. TENDANCE (10 pts) — amélioration récente tendance = musique_stats.get('tendance', 0) score_tendance = min(10, max(0, 5 + tendance)) score += score_tendance details['tendance'] = tendance details['score_tendance'] = round(score_tendance, 1) # 7. AVIS ENTRAÎNEUR (5 pts) avis = p.get('avisEntraineur', 'NEUTRE') score_avis = {'POSITIF': 5, 'TRES_POSITIF': 5, 'NEUTRE': 2, 'NEGATIF': 0, 'TRES_NEGATIF': 0}.get(avis, 2) score += score_avis details['avis_entraineur'] = avis details['score_avis'] = score_avis # Bonus driver change négatif if p.get('driverChange', False): score -= 3 details['driver_change'] = True details['score_total'] = round(score, 1) details['musique'] = p.get('musique', '') details['nb_victoires'] = nb_victoires_total details['nb_places'] = nb_places_total details['nb_courses'] = nb_courses_total return round(score, 1), details # ============================================================ # RECOMMANDATIONS PARIS # ============================================================ def build_recommendations(scored_horses): """ Construit les recommandations de paris basées sur le scoring. """ # Trier par score décroissant ranked = sorted(scored_horses, key=lambda x: x['score'], reverse=True) top1 = ranked[0] top2 = ranked[1] top3 = ranked[2] # Calcul de la confiance def confiance(score): if score >= 55: return "🔥 FORTE" elif score >= 45: return "✅ BONNE" elif score >= 35: return "⚠️ MOYENNE" else: return "❓ FAIBLE" reco = { 'simple_gagnant': { 'cheval': top1['nom'], 'numero': top1['numero'], 'cote': top1['details']['cote'], 'score': top1['score'], 'confiance': confiance(top1['score']), 'mise_suggeree': 2.0, 'gain_potentiel': round(2.0 * top1['details']['cote'], 2), 'justification': _justif_gagnant(top1), }, 'simple_place': { 'cheval': top1['nom'], 'numero': top1['numero'], 'cote': round(top1['details']['cote'] / 4, 2), # Cote placé ≈ cote/4 'score': top1['score'], 'confiance': confiance(top1['score'] + 10), # Placé plus facile 'mise_suggeree': 3.0, 'gain_potentiel': round(3.0 * (top1['details']['cote'] / 4), 2), 'justification': 'Même cheval qu\'en Simple Gagnant, pari sécurisé', }, 'couple_gagnant': { 'cheval1': top1['nom'], 'numero1': top1['numero'], 'cheval2': top2['nom'], 'numero2': top2['numero'], 'score_combo': round((top1['score'] + top2['score']) / 2, 1), 'confiance': confiance((top1['score'] + top2['score']) / 2 - 5), 'mise_suggeree': 2.0, 'justification': _justif_couple(top1, top2), }, 'couple_place': { 'cheval1': top1['nom'], 'numero1': top1['numero'], 'cheval2': top3['nom'], 'numero2': top3['numero'], 'score_combo': round((top1['score'] + top3['score']) / 2, 1), 'confiance': confiance((top1['score'] + top3['score']) / 2), 'mise_suggeree': 2.0, 'justification': 'Combinaison favorite + outsider solide', }, 'top3_scores': [ {'rang': i+1, 'nom': h['nom'], 'numero': h['numero'], 'score': h['score'], 'cote': h['details']['cote'], 'forme': h['details']['forme_recente'], 'tx_vic': h['details']['tx_victoire'], 'avis': h['details']['avis_entraineur'], 'musique': h['details']['musique']} for i, h in enumerate(ranked[:5]) ], 'budget_total': 9.0, 'generated_at': datetime.now().isoformat(), } return reco def _justif_gagnant(horse): parts = [] d = horse['details'] if d['forme_recente'] <= 2: parts.append(f"forme excellente ({d['forme_recente']} de moy.)") if d['tx_victoire'] >= 20: parts.append(f"bon taux de victoire ({d['tx_victoire']}%)") if d['avis_entraineur'] == 'POSITIF': parts.append("avis entraîneur positif") if d['tendance'] > 2: parts.append("en progression") if d['cote'] <= 5: parts.append(f"grand favori ({d['cote']}/1)") return " • ".join(parts) if parts else "Meilleur score composite" def _justif_couple(h1, h2): return f"{h1['nom']} (score {h1['score']}) + {h2['nom']} (score {h2['score']})" # ============================================================ # BASE DE DONNÉES # ============================================================ def init_scoring_table(): conn = sqlite3.connect(DB_PATH) c = conn.cursor() c.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 ) ''') c.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 ) ''') conn.commit() conn.close() def save_scoring(date, race_name, scored_horses, recommendations): conn = sqlite3.connect(DB_PATH) c = conn.cursor() # Supprimer les anciens scores du jour c.execute("DELETE FROM scoring WHERE date=? AND race_name=?", (date, race_name)) c.execute("DELETE FROM recommendations WHERE date=? AND race_name=?", (date, race_name)) # Sauvegarder les scores ranked = sorted(scored_horses, key=lambda x: x['score'], reverse=True) for rang, h in enumerate(ranked, 1): d = h['details'] c.execute(''' INSERT INTO scoring (date, race_name, horse_name, horse_number, score, score_cote, score_forme, score_victoire, score_place, score_rk, score_tendance, score_avis, cote, forme_recente, tx_victoire, tx_place, avis_entraineur, musique, rang_scoring) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) ''', (date, race_name, h['nom'], h['numero'], h['score'], d['score_cote'], d['score_forme'], d['score_victoire'], d['score_place'], d['score_rk'], d['score_tendance'], d['score_avis'], d['cote'], d['forme_recente'], d['tx_victoire'], d['tx_place'], d['avis_entraineur'], d['musique'], rang)) # Sauvegarder les recommandations paris = [ ('simple_gagnant', recommendations['simple_gagnant']), ('simple_place', recommendations['simple_place']), ('couple_gagnant', recommendations['couple_gagnant']), ('couple_place', recommendations['couple_place']), ] for type_pari, reco in paris: c1 = reco.get('cheval', reco.get('cheval1', '')) n1 = reco.get('numero', reco.get('numero1', 0)) c2 = reco.get('cheval2', '') n2 = reco.get('numero2', 0) c.execute(''' INSERT INTO recommendations (date, race_name, type_pari, cheval1, numero1, cheval2, numero2, cote, mise, gain_potentiel, confiance, justification) VALUES (?,?,?,?,?,?,?,?,?,?,?,?) ''', (date, race_name, type_pari, c1, n1, c2, n2, reco.get('cote', 0), reco.get('mise_suggeree', 2), reco.get('gain_potentiel', 0), reco.get('confiance', ''), reco.get('justification', ''))) conn.commit() conn.close() # ============================================================ # RAPPORT CONSOLE + JSON # ============================================================ def print_report(scored_horses, recommendations, race_info): ranked = sorted(scored_horses, key=lambda x: x['score'], reverse=True) race_name = race_info.get('nom', 'Quinté+') hippodrome = race_info.get('hippodrome', '') heure = race_info.get('heure', '') print(f"\n{'='*65}") print(f"🏇 SCORING — {race_name}") print(f" {hippodrome} — {heure}") print(f"{'='*65}") print(f" {'RANG':<5} {'N°':<4} {'CHEVAL':<25} {'SCORE':<7} {'COTE':<7} {'FORME':<7} {'TX-V':<6} {'AVIS'}") print(f"{'─'*65}") for rang, h in enumerate(ranked, 1): d = h['details'] star = '⭐' if rang <= 3 else ' ' print(f" {star}{rang:<4} {h['numero']:<4} {h['nom']:<25} {h['score']:<7} {d['cote']:<7} {d['forme_recente']:<7} {d['tx_victoire']:<6}% {d['avis_entraineur']}") print(f"\n{'='*65}") print(f"💰 RECOMMANDATIONS PARIS") print(f"{'='*65}") sg = recommendations['simple_gagnant'] print(f"\n 🎯 SIMPLE GAGNANT") print(f" ➤ N°{sg['numero']} {sg['cheval']} @ {sg['cote']}/1") print(f" Mise : {sg['mise_suggeree']}€ | Gain potentiel : {sg['gain_potentiel']}€") print(f" Confiance : {sg['confiance']}") print(f" {sg['justification']}") sp = recommendations['simple_place'] print(f"\n 🛡️ SIMPLE PLACÉ") print(f" ➤ N°{sp['numero']} {sp['cheval']} @ {sp['cote']}/1") print(f" Mise : {sp['mise_suggeree']}€ | Gain potentiel : {sp['gain_potentiel']}€") print(f" Confiance : {sp['confiance']}") cg = recommendations['couple_gagnant'] print(f"\n 🔗 COUPLÉ GAGNANT") print(f" ➤ N°{cg['numero1']} {cg['cheval1']} + N°{cg['numero2']} {cg['cheval2']}") print(f" Mise : {cg['mise_suggeree']}€ | Confiance : {cg['confiance']}") print(f" {cg['justification']}") cp = recommendations['couple_place'] print(f"\n 🔗 COUPLÉ PLACÉ") print(f" ➤ N°{cp['numero1']} {cp['cheval1']} + N°{cp['numero2']} {cp['cheval2']}") print(f" Mise : {cp['mise_suggeree']}€ | Confiance : {cp['confiance']}") print(f"\n 💼 Budget total suggéré : {recommendations['budget_total']}€") print(f"{'='*65}\n") def save_json(data, date): path = f"{os.environ.get('TURF_DIR', '/home/h3r7/turf_scraper')}/scoring_{date.replace('-','')}.json" with open(path, 'w', encoding='utf-8') as f: json.dump(data, f, indent=2, ensure_ascii=False) return path def format_telegram(recommendations, race_info): sg = recommendations['simple_gagnant'] sp = recommendations['simple_place'] cg = recommendations['couple_gagnant'] cp = recommendations['couple_place'] top3 = recommendations['top3_scores'] msg = f"""🏇 *QUINTÉ+ — {race_info.get('hippodrome','?')} {race_info.get('heure','?')}* _{race_info.get('nom','')}_ 📊 *TOP 3 SCORING* """ for h in top3[:3]: msg += f" {h['rang']}. N°{h['numero']} *{h['nom']}* — score {h['score']} | cote {h['cote']} | forme {h['forme']}\n" msg += f""" 💰 *RECOMMANDATIONS* 🎯 *Simple Gagnant* : N°{sg['numero']} {sg['cheval']} @ {sg['cote']}/1 Mise {sg['mise_suggeree']}€ → gain potentiel {sg['gain_potentiel']}€ {sg['confiance']} | {sg['justification']} 🛡️ *Simple Placé* : N°{sp['numero']} {sp['cheval']} Mise {sp['mise_suggeree']}€ | {sp['confiance']} 🔗 *Couplé Gagnant* : {cg['numero1']}-{cg['numero2']} ({cg['cheval1']} / {cg['cheval2']}) Mise {cg['mise_suggeree']}€ | {cg['confiance']} 🔗 *Couplé Placé* : {cp['numero1']}-{cp['numero2']} ({cp['cheval1']} / {cp['cheval2']}) Mise {cp['mise_suggeree']}€ | {cp['confiance']} 💼 Budget total : {recommendations['budget_total']}€ """ return msg # ============================================================ # MAIN # ============================================================ def main(): today = datetime.now().strftime('%Y-%m-%d') date_pmu = datetime.now().strftime('%d%m%Y') print(f"\n{'='*65}") print(f"🧠 SCORING ENGINE — {datetime.now().strftime('%d/%m/%Y %H:%M')}") print(f"{'='*65}\n") init_scoring_table() # Récupérer le programme print("📡 Récupération du programme PMU...") try: url = f"https://turfinfo.api.pmu.fr/rest/client/1/programme/{date_pmu}/reunions" r = requests.get(url, headers=HEADERS, timeout=15) reunions = r.json().get('programme', {}).get('reunions', []) print(f" ✅ {len(reunions)} réunion(s)") except Exception as e: print(f" ❌ {e}") return # Trouver le Quinté+ quinte = None for reunion in reunions: for course in reunion.get('courses', []): libelle = course.get('libelle', '') paris_types = [p["typePari"] for p in course.get("paris", [])] if any("QUINTE" in p for p in paris_types) or "PARIS-TURF" in libelle: quinte = (reunion['numOfficiel'], course['numOrdre'], libelle, reunion['hippodrome']['libelleCourt'], course.get('heureDepart', 0)) break if quinte: break if not quinte: print("❌ Quinté+ non trouvé") return num_r, num_c, libelle, hippodrome, heure_ts = quinte heure = datetime.fromtimestamp(heure_ts/1000).strftime('%H:%M') if heure_ts else '13:55' race_info = {'nom': libelle, 'hippodrome': hippodrome, 'heure': heure} print(f" 🏇 {libelle} — {hippodrome} {heure}") # Récupérer les participants print(f"\n📡 Récupération des participants R{num_r}C{num_c}...") try: url = f"https://turfinfo.api.pmu.fr/rest/client/1/programme/{date_pmu}/R{num_r}/C{num_c}/participants" r = requests.get(url, headers=HEADERS, timeout=15) participants = [p for p in r.json().get('participants', []) if p.get('statut') == 'PARTANT'] print(f" ✅ {len(participants)} partants") except Exception as e: print(f" ❌ {e}") return # Calculer le scoring print(f"\n🧠 Calcul du scoring composite...") scored_horses = [] for p in participants: score, details = score_cheval(p, participants) scored_horses.append({ 'nom': p['nom'], 'numero': p['numPmu'], 'score': score, 'details': details, }) # Construire les recommandations recommendations = build_recommendations(scored_horses) # Afficher le rapport print_report(scored_horses, recommendations, race_info) # Sauvegarder en BDD save_scoring(today, libelle, scored_horses, recommendations) print(f"💾 Scoring sauvegardé en BDD") # Sauvegarder en JSON output = { 'date': today, 'race': race_info, 'scored_horses': sorted(scored_horses, key=lambda x: x['score'], reverse=True), 'recommendations': recommendations, } json_path = save_json(output, today) print(f"📁 JSON : {json_path}") # Message Telegram telegram_msg = format_telegram(recommendations, race_info) telegram_path = f"{os.environ.get('TURF_DIR', '/home/h3r7/turf_scraper')}/telegram_scoring_{today.replace('-','')}.txt" with open(telegram_path, 'w', encoding='utf-8') as f: f.write(telegram_msg) print(f"📱 Message Telegram : {telegram_path}") print(f"\n{'─'*65}") print(telegram_msg) if __name__ == "__main__": main()