Files
turf_saas/scoring.py
2026-04-25 17:18:43 +02:00

604 lines
22 KiB
Python
Executable File

#!/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} {'':<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()