604 lines
22 KiB
Python
Executable File
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} {'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()
|