Merge pull request '[HRT-83] feat: Météo & terrain intégrés dans prédictions ML (Premium)' (#10) from feature/HRT-83-meteo-terrain-ml-predictions into master
Some checks failed
CD / Deploy → Staging (push) Has been cancelled
CD / Smoke Tests on Staging (push) Has been cancelled
CD / Deploy → Production (push) Has been cancelled
CD / Rollback Production (push) Has been cancelled

This commit was merged in pull request #10.
This commit is contained in:
2026-04-30 08:40:16 +02:00
3 changed files with 478 additions and 167 deletions

View File

@@ -22,8 +22,14 @@ from auth import jwt_required_middleware, plan_required, free_daily_limit_check
predictions_bp = Blueprint("v1_predictions", __name__, url_prefix="/api/v1/predictions") predictions_bp = Blueprint("v1_predictions", __name__, url_prefix="/api/v1/predictions")
def _fetch_ml_predictions(conn, date: str, limit: int = None, offset: int = 0): def _fetch_ml_predictions(
"""Shared helper — returns rows from ml_predictions_cache.""" conn, date: str, limit: int = None, offset: int = 0, include_weather: bool = False
):
"""Shared helper — returns rows from ml_predictions_cache.
include_weather=True adds terrain_condition and weather_impact columns
via LEFT JOIN on pmu_meteo (premium routes only).
"""
if not table_exists(conn, "ml_predictions_cache"): if not table_exists(conn, "ml_predictions_cache"):
return [], 0 return [], 0
@@ -33,13 +39,35 @@ def _fetch_ml_predictions(conn, date: str, limit: int = None, offset: int = 0):
).fetchone() ).fetchone()
total = count_row["cnt"] if count_row else 0 total = count_row["cnt"] if count_row else 0
sql = """SELECT if (
race_label, hippodrome, discipline, distance, heure, include_weather
horse_name, horse_number, odds, prob_top1, prob_top3, and table_exists(conn, "pmu_meteo")
ml_score, recommendation, is_value_bet, risque_label, risque_score and table_exists(conn, "pmu_courses")
FROM ml_predictions_cache ):
WHERE date = ? sql = """SELECT
ORDER BY ml_score DESC""" m.race_label, m.hippodrome, m.discipline, m.distance, m.heure,
m.horse_name, m.horse_number, m.odds, m.prob_top1, m.prob_top3,
m.ml_score, m.recommendation, m.is_value_bet, m.risque_label, m.risque_score,
c.penetrometre_intitule,
mt.nebulositecode, mt.nebulosite_court, mt.temperature, mt.force_vent
FROM ml_predictions_cache m
LEFT JOIN pmu_courses c
ON c.date_programme = m.date
AND c.num_reunion = m.num_reunion
AND c.num_course = m.num_course
LEFT JOIN pmu_meteo mt
ON mt.date_programme = m.date
AND mt.num_reunion = m.num_reunion
WHERE m.date = ?
ORDER BY m.ml_score DESC"""
else:
sql = """SELECT
race_label, hippodrome, discipline, distance, heure,
horse_name, horse_number, odds, prob_top1, prob_top3,
ml_score, recommendation, is_value_bet, risque_label, risque_score
FROM ml_predictions_cache
WHERE date = ?
ORDER BY ml_score DESC"""
params = [date] params = [date]
if limit is not None: if limit is not None:
@@ -47,7 +75,42 @@ def _fetch_ml_predictions(conn, date: str, limit: int = None, offset: int = 0):
params += [limit, offset] params += [limit, offset]
rows = conn.execute(sql, params).fetchall() rows = conn.execute(sql, params).fetchall()
return [dict(r) for r in rows], total
results = []
for r in rows:
row_dict = dict(r)
if include_weather:
# Compute derived fields from raw columns
penetrometre = row_dict.pop("penetrometre_intitule", None) or ""
# Import inline to avoid circular dependency at module level
from scoring_v2 import get_terrain_condition, compute_weather_impact
terrain_condition = (
get_terrain_condition(penetrometre) if penetrometre else "inconnu"
)
weather_data = None
if (
row_dict.get("nebulositecode") is not None
or row_dict.get("temperature") is not None
):
weather_data = {
"nebulositecode": row_dict.pop("nebulositecode", None),
"nebulosite_court": row_dict.pop("nebulosite_court", None),
"temperature": row_dict.pop("temperature", None),
"force_vent": row_dict.pop("force_vent", None),
}
else:
# Remove raw meteo columns even if NULL
row_dict.pop("nebulositecode", None)
row_dict.pop("nebulosite_court", None)
row_dict.pop("temperature", None)
row_dict.pop("force_vent", None)
weather_impact = compute_weather_impact(weather_data, terrain_condition)
row_dict["terrain_condition"] = terrain_condition
row_dict["weather_impact"] = weather_impact
results.append(row_dict)
return results, total
# ────────────────────────────────────────────────────────────── # ──────────────────────────────────────────────────────────────
@@ -145,7 +208,7 @@ def predictions_all():
conn = get_db() conn = get_db()
try: try:
predictions, total = _fetch_ml_predictions( predictions, total = _fetch_ml_predictions(
conn, date_param, limit=limit, offset=offset conn, date_param, limit=limit, offset=offset, include_weather=True
) )
pagination = paginate_query(predictions, total, limit, offset) pagination = paginate_query(predictions, total, limit, offset)

View File

@@ -53,7 +53,7 @@ def valuebets():
default: 0 default: 0
responses: responses:
200: 200:
description: Value bets du jour description: Value bets du jour avec météo et terrain (HRT-83)
401: 401:
description: Token invalide description: Token invalide
403: 403:
@@ -69,7 +69,7 @@ def valuebets():
conn = get_db() conn = get_db()
try: try:
rows = [] rows_raw = []
total = 0 total = 0
if table_exists(conn, "ml_predictions_cache"): if table_exists(conn, "ml_predictions_cache"):
@@ -81,18 +81,73 @@ def valuebets():
).fetchone() ).fetchone()
total = count_row["cnt"] if count_row else 0 total = count_row["cnt"] if count_row else 0
rows = conn.execute( # LEFT JOIN pmu_courses (terrain) + pmu_meteo (météo) — HRT-83
"""SELECT race_label, hippodrome, discipline, distance, heure, has_courses = table_exists(conn, "pmu_courses")
horse_name, horse_number, odds, prob_top1, prob_top3, has_meteo = table_exists(conn, "pmu_meteo")
ml_score, recommendation, risque_label, risque_score
FROM ml_predictions_cache if has_courses and has_meteo:
WHERE date = ? AND is_value_bet = 1 AND odds >= ? rows_raw = conn.execute(
ORDER BY ml_score DESC """SELECT m.race_label, m.hippodrome, m.discipline, m.distance, m.heure,
LIMIT ? OFFSET ?""", m.horse_name, m.horse_number, m.odds, m.prob_top1, m.prob_top3,
(date_param, min_odds, limit, offset), m.ml_score, m.recommendation, m.risque_label, m.risque_score,
).fetchall() c.penetrometre_intitule,
mt.nebulositecode, mt.nebulosite_court,
mt.temperature, mt.force_vent
FROM ml_predictions_cache m
LEFT JOIN pmu_courses c
ON c.date_programme = m.date
AND c.num_reunion = m.num_reunion
AND c.num_course = m.num_course
LEFT JOIN pmu_meteo mt
ON mt.date_programme = m.date
AND mt.num_reunion = m.num_reunion
WHERE m.date = ? AND m.is_value_bet = 1 AND m.odds >= ?
ORDER BY m.ml_score DESC
LIMIT ? OFFSET ?""",
(date_param, min_odds, limit, offset),
).fetchall()
else:
rows_raw = conn.execute(
"""SELECT race_label, hippodrome, discipline, distance, heure,
horse_name, horse_number, odds, prob_top1, prob_top3,
ml_score, recommendation, risque_label, risque_score
FROM ml_predictions_cache
WHERE date = ? AND is_value_bet = 1 AND odds >= ?
ORDER BY ml_score DESC
LIMIT ? OFFSET ?""",
(date_param, min_odds, limit, offset),
).fetchall()
from scoring_v2 import get_terrain_condition, compute_weather_impact
valuebets_list = []
for r in rows_raw:
row_dict = dict(r)
penetrometre = row_dict.pop("penetrometre_intitule", None) or ""
terrain_condition = (
get_terrain_condition(penetrometre) if penetrometre else "inconnu"
)
weather_data = None
if (
row_dict.get("nebulositecode") is not None
or row_dict.get("temperature") is not None
):
weather_data = {
"nebulositecode": row_dict.pop("nebulositecode", None),
"nebulosite_court": row_dict.pop("nebulosite_court", None),
"temperature": row_dict.pop("temperature", None),
"force_vent": row_dict.pop("force_vent", None),
}
else:
row_dict.pop("nebulositecode", None)
row_dict.pop("nebulosite_court", None)
row_dict.pop("temperature", None)
row_dict.pop("force_vent", None)
weather_impact = compute_weather_impact(weather_data, terrain_condition)
row_dict["terrain_condition"] = terrain_condition
row_dict["weather_impact"] = weather_impact
valuebets_list.append(row_dict)
valuebets_list = [dict(r) for r in rows]
pagination = paginate_query(valuebets_list, total, limit, offset) pagination = paginate_query(valuebets_list, total, limit, offset)
return jsonify( return jsonify(

View File

@@ -11,29 +11,34 @@ import re
from datetime import datetime from datetime import datetime
DB_PATH = "/home/h3r7/turf_saas/turf_saas.db" DB_PATH = "/home/h3r7/turf_saas/turf_saas.db"
HEADERS = {'User-Agent': 'Mozilla/5.0', 'Accept': 'application/json'} HEADERS = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
def get_cote_from_db(horse_name, date_course): def get_cote_from_db(horse_name, date_course):
"""Recupere la cote depuis la table predictions (plus recente et non nulle)""" """Recupere la cote depuis la table predictions (plus recente et non nulle)"""
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
c = conn.execute(""" c = conn.execute(
"""
SELECT odds FROM predictions SELECT odds FROM predictions
WHERE date=? AND horse_name LIKE ? AND odds > 0 WHERE date=? AND horse_name LIKE ? AND odds > 0
ORDER BY created_at DESC LIMIT 1 ORDER BY created_at DESC LIMIT 1
""", (date_course, f"%{horse_name}%")) """,
(date_course, f"%{horse_name}%"),
)
r = c.fetchone() r = c.fetchone()
conn.close() conn.close()
return r['odds'] if r else 0 return r["odds"] if r else 0
def parse_musique(musique): def parse_musique(musique):
if not musique: if not musique:
return {} return {}
clean = re.sub(r'\(\d+\)', '', musique) clean = re.sub(r"\(\d+\)", "", musique)
resultats = re.findall(r'(\d+|D|0)([amphsc]?)', clean) resultats = re.findall(r"(\d+|D|0)([amphsc]?)", clean)
positions = [] positions = []
for pos, disc in resultats[:10]: for pos, disc in resultats[:10]:
positions.append(99 if pos == 'D' else int(pos)) positions.append(99 if pos == "D" else int(pos))
if not positions: if not positions:
return {} return {}
nb_courses = len(positions) nb_courses = len(positions)
@@ -41,222 +46,385 @@ def parse_musique(musique):
nb_places = sum(1 for p in positions if 1 <= p <= 3) nb_places = sum(1 for p in positions if 1 <= p <= 3)
recentes = [p for p in positions[:3] if p != 99] recentes = [p for p in positions[:3] if p != 99]
forme_recente = sum(recentes) / len(recentes) if recentes else 99 forme_recente = sum(recentes) / len(recentes) if recentes else 99
tendance = (sum(positions[-4:]) / 4 - sum(positions[:4]) / 4) if len(positions) >= 4 else 0 tendance = (
(sum(positions[-4:]) / 4 - sum(positions[:4]) / 4) if len(positions) >= 4 else 0
)
return { return {
'forme_recente': round(forme_recente, 1), "forme_recente": round(forme_recente, 1),
'tendance': round(tendance, 1), "tendance": round(tendance, 1),
'tx_victoire': round(nb_victoires / nb_courses * 100, 1) if nb_courses else 0, "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, "tx_place": round(nb_places / nb_courses * 100, 1) if nb_courses else 0,
} }
def score_cheval_v2(p, all_participants, today):
def get_terrain_condition(penetrometre_intitule: str | None) -> str:
"""Normalise le pénétromètre PMU en condition terrain standardisée."""
if not penetrometre_intitule:
return "inconnu"
val = penetrometre_intitule.upper()
if any(k in val for k in ("TRES BON", "TRÈS BON", "FERME", "FIRM")):
return "bon"
if any(k in val for k in ("BON", "GOOD", "STANDARD")):
return "bon"
if any(k in val for k in ("SOUPLE", "YIELDING", "COLLANT")):
return "souple"
if any(k in val for k in ("LOURD", "HEAVY", "TRES SOUPLE", "TRÈS SOUPLE")):
return "lourd"
if any(k in val for k in ("SOFT", "MOU")):
return "souple"
return "inconnu"
def compute_weather_impact(weather_data: dict | None, terrain_condition: str) -> float:
"""
Calcule un score d'impact météo/terrain sur [5, +5].
weather_data keys attendues : nebulositecode, temperature, force_vent
terrain_condition : 'bon' | 'souple' | 'lourd' | 'inconnu'
Retourne un delta de score ML (positif = favorable, négatif = défavorable).
"""
if not weather_data:
return 0.0
delta = 0.0
# Terrain
if terrain_condition == "lourd":
delta -= 3.0
elif terrain_condition == "souple":
delta -= 1.5
elif terrain_condition == "bon":
delta += 1.0
# inconnu → 0
# Vent
force_vent = weather_data.get("force_vent") or 0
try:
force_vent = float(force_vent)
except (TypeError, ValueError):
force_vent = 0.0
if force_vent >= 50:
delta -= 2.0
elif force_vent >= 30:
delta -= 1.0
# Températures extrêmes
temperature = weather_data.get("temperature")
try:
temperature = float(temperature) if temperature is not None else None
except (TypeError, ValueError):
temperature = None
if temperature is not None:
if temperature <= 0:
delta -= 1.0
elif temperature >= 35:
delta -= 1.0
return round(max(-5.0, min(5.0, delta)), 2)
def score_cheval_v2(p, all_participants, today, weather_data=None):
"""
Score un cheval pour le modèle V2.
weather_data (optionnel) : dict issu de pmu_meteo pour cette réunion.
Backward-compatible : weather_data=None → comportement identique à avant HRT-83.
"""
score = 0 score = 0
details = {} details = {}
# 1. COTE - Essaye PMU API, sinon DB # 1. COTE - Essaye PMU API, sinon DB
horse_name = p.get('nom', '') horse_name = p.get("nom", "")
cote = 0 cote = 0
# Essayer d'abord depuis l'API PMU # Essayer d'abord depuis l'API PMU
rapport = p.get('dernierRapportDirect', {}) rapport = p.get("dernierRapportDirect", {})
if rapport: if rapport:
cote = rapport.get('rapport', 0) cote = rapport.get("rapport", 0)
if not cote: if not cote:
rapport_ref = p.get('dernierRapportReference', {}) rapport_ref = p.get("dernierRapportReference", {})
cote = rapport_ref.get('rapport', 0) if rapport_ref else 0 cote = rapport_ref.get("rapport", 0) if rapport_ref else 0
# Fallback: aller chercher dans la DB # Fallback: aller chercher dans la DB
if not cote or cote == 0: if not cote or cote == 0:
cote = get_cote_from_db(horse_name, today) cote = get_cote_from_db(horse_name, today)
# Si toujours pas de cote, utiliser 99 comme valeur par defaut # Si toujours pas de cote, utiliser 99 comme valeur par defaut
if not cote or cote == 0: if not cote or cote == 0:
cote = 99.0 cote = 99.0
score_cote = max(2, min(10, 20 / (1 + cote * 0.15))) if cote > 0 else 2 score_cote = max(2, min(10, 20 / (1 + cote * 0.15))) if cote > 0 else 2
score += score_cote score += score_cote
details['cote'] = round(cote, 1) details["cote"] = round(cote, 1)
details['score_cote'] = round(score_cote, 1) details["score_cote"] = round(score_cote, 1)
# 2. FORME - AUGMENTE a 30 pts # 2. FORME - AUGMENTE a 30 pts
musique_stats = parse_musique(p.get('musique', '')) musique_stats = parse_musique(p.get("musique", ""))
forme = musique_stats.get('forme_recente', 99) forme = musique_stats.get("forme_recente", 99)
score_forme = 30 if forme <= 1 else 25 if forme <= 2 else 20 if forme <= 3 else 15 if forme <= 5 else 8 if forme <= 8 else 0 score_forme = (
30
if forme <= 1
else 25
if forme <= 2
else 20
if forme <= 3
else 15
if forme <= 5
else 8
if forme <= 8
else 0
)
score += score_forme score += score_forme
details['forme_recente'] = forme details["forme_recente"] = forme
details['score_forme'] = score_forme details["score_forme"] = score_forme
# 3. TAUX VICTOIRE (15 pts) # 3. TAUX VICTOIRE (15 pts)
nb_courses_total = p.get('nombreCourses', 0) nb_courses_total = p.get("nombreCourses", 0)
nb_victoires_total = p.get('nombreVictoires', 0) nb_victoires_total = p.get("nombreVictoires", 0)
tx_vic = (nb_victoires_total / nb_courses_total * 100) if nb_courses_total else 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_vic = min(15, tx_vic * 0.5)
score += score_vic score += score_vic
details['tx_victoire'] = round(tx_vic, 1) details["tx_victoire"] = round(tx_vic, 1)
details['score_victoire'] = round(score_vic, 1) details["score_victoire"] = round(score_vic, 1)
# 4. TAUX PLACE (15 pts) # 4. TAUX PLACE (15 pts)
nb_places_total = p.get('nombrePlaces', 0) nb_places_total = p.get("nombrePlaces", 0)
tx_place = (nb_places_total / nb_courses_total * 100) if nb_courses_total else 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_place = min(15, tx_place * 0.2)
score += score_place score += score_place
details['tx_place'] = round(tx_place, 1) details["tx_place"] = round(tx_place, 1)
details['score_place'] = round(score_place, 1) details["score_place"] = round(score_place, 1)
# 5. REDUCTION KM (10 pts) # 5. REDUCTION KM (10 pts)
rk = p.get('reductionKilometrique', 0) rk = p.get("reductionKilometrique", 0)
all_rk = [x.get('reductionKilometrique', 0) for x in all_participants if x.get('reductionKilometrique', 0) > 0] all_rk = [
x.get("reductionKilometrique", 0)
for x in all_participants
if x.get("reductionKilometrique", 0) > 0
]
if rk > 0 and all_rk: if rk > 0 and all_rk:
score_rk = 10 * (1 - (rk - min(all_rk)) / (max(all_rk) - min(all_rk))) if max(all_rk) > min(all_rk) else 5 score_rk = (
10 * (1 - (rk - min(all_rk)) / (max(all_rk) - min(all_rk)))
if max(all_rk) > min(all_rk)
else 5
)
else: else:
score_rk = 0 score_rk = 0
score += score_rk score += score_rk
details['rk'] = rk details["rk"] = rk
details['score_rk'] = round(score_rk, 1) details["score_rk"] = round(score_rk, 1)
# 6. TENDANCE (10 pts) # 6. TENDANCE (10 pts)
tendance = musique_stats.get('tendance', 0) tendance = musique_stats.get("tendance", 0)
score_tendance = min(10, max(0, 5 + tendance)) score_tendance = min(10, max(0, 5 + tendance))
score += score_tendance score += score_tendance
details['tendance'] = tendance details["tendance"] = tendance
details['score_tendance'] = round(score_tendance, 1) details["score_tendance"] = round(score_tendance, 1)
# 7. AVIS ENTRAINEUR (5 pts) # 7. AVIS ENTRAINEUR (5 pts)
avis = p.get('avisEntraineur', 'NEUTRE') avis = p.get("avisEntraineur", "NEUTRE")
score_avis = {'POSITIF': 5, 'TRES_POSITIF': 5, 'NEUTRE': 2, 'NEGATIF': 0, 'TRES_NEGATIF': 0}.get(avis, 2) score_avis = {
"POSITIF": 5,
"TRES_POSITIF": 5,
"NEUTRE": 2,
"NEGATIF": 0,
"TRES_NEGATIF": 0,
}.get(avis, 2)
score += score_avis score += score_avis
details['avis_entraineur'] = avis details["avis_entraineur"] = avis
details['score_avis'] = score_avis details["score_avis"] = score_avis
# 8. BONUS OUTSIDER (5 pts) # 8. BONUS OUTSIDER (5 pts)
bonus_outsider = 5 if forme <= 3 and cote >= 10 else 0 bonus_outsider = 5 if forme <= 3 and cote >= 10 else 0
score += bonus_outsider score += bonus_outsider
details['bonus_outsider'] = bonus_outsider details["bonus_outsider"] = bonus_outsider
# Driver change penalty # Driver change penalty
if p.get('driverChange', False): if p.get("driverChange", False):
score -= 3 score -= 3
details['driver_change'] = True details["driver_change"] = True
details['score_total'] = round(score, 1) # 9. METEO & TERRAIN (HRT-83) — premium feature, weather_data=None → skip
details['musique'] = p.get('musique', '') penetrometre = p.get("penetrometre_intitule", "") or ""
details['nb_victoires'] = nb_victoires_total terrain_condition = (
details['nb_places'] = nb_places_total get_terrain_condition(penetrometre) if penetrometre else "inconnu"
details['nb_courses'] = nb_courses_total )
weather_impact = 0.0
if weather_data is not None:
weather_impact = compute_weather_impact(weather_data, terrain_condition)
score += weather_impact
details["terrain_condition"] = terrain_condition
details["weather_impact"] = weather_impact
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 return round(score, 1), details
def get_ze2sur4_combinaisons(top4): def get_ze2sur4_combinaisons(top4):
combinaisons = [] combinaisons = []
for i in range(4): for i in range(4):
for j in range(i+1, 4): for j in range(i + 1, 4):
c1 = top4[i] c1 = top4[i]
c2 = top4[j] c2 = top4[j]
combinaisons.append({ combinaisons.append(
'cheval1': c1['nom'], {
'numero1': c1['numero'], "cheval1": c1["nom"],
'cheval2': c2['nom'], "numero1": c1["numero"],
'numero2': c2['numero'], "cheval2": c2["nom"],
'mise': 1.0, "numero2": c2["numero"],
}) "mise": 1.0,
}
)
return combinaisons return combinaisons
def build_recommendations_v2(scored_horses): def build_recommendations_v2(scored_horses):
ranked = sorted(scored_horses, key=lambda x: x['score'], reverse=True) ranked = sorted(scored_horses, key=lambda x: x["score"], reverse=True)
if len(ranked) < 4: if len(ranked) < 4:
return None return None
top1, top2, top3, top4 = ranked[0], ranked[1], ranked[2], ranked[3] top1, top2, top3, top4 = ranked[0], ranked[1], ranked[2], ranked[3]
top4_list = ranked[:4] top4_list = ranked[:4]
def confiance(s): def confiance(s):
return "FORTE" if s >= 55 else "BONNE" if s >= 45 else "MOYENNE" if s >= 35 else "FAIBLE" return (
"FORTE"
if s >= 55
else "BONNE"
if s >= 45
else "MOYENNE"
if s >= 35
else "FAIBLE"
)
ze2_combinaisons = get_ze2sur4_combinaisons(top4_list) ze2_combinaisons = get_ze2sur4_combinaisons(top4_list)
mise_ze2 = len(ze2_combinaisons) * 1.0 mise_ze2 = len(ze2_combinaisons) * 1.0
return { return {
'simple_gagnant': { "simple_gagnant": {
'cheval': top1['nom'], 'numero': top1['numero'], 'cote': top1['details']['cote'], "cheval": top1["nom"],
'score': top1['score'], 'confiance': confiance(top1['score']), "numero": top1["numero"],
'mise_suggeree': 2.0, 'gain_potentiel': round(2.0 * top1['details']['cote'], 2) "cote": top1["details"]["cote"],
"score": top1["score"],
"confiance": confiance(top1["score"]),
"mise_suggeree": 2.0,
"gain_potentiel": round(2.0 * top1["details"]["cote"], 2),
}, },
'ze2_sur_4': { "ze2_sur_4": {
'top4': [{'nom': h['nom'], 'numero': h['numero']} for h in top4_list], "top4": [{"nom": h["nom"], "numero": h["numero"]} for h in top4_list],
'combinaisons': ze2_combinaisons, "combinaisons": ze2_combinaisons,
'mise_totale': mise_ze2, "mise_totale": mise_ze2,
'nb_combinaisons': len(ze2_combinaisons), "nb_combinaisons": len(ze2_combinaisons),
'confiance': confiance((top1['score'] + top2['score'] + top3['score'] + top4['score']) / 4), "confiance": confiance(
'explication': 'Jouer les 6 combinaisons de 2 chevaux parmi les 4 premiers' (top1["score"] + top2["score"] + top3["score"] + top4["score"]) / 4
),
"explication": "Jouer les 6 combinaisons de 2 chevaux parmi les 4 premiers",
}, },
'outsider': _find_outsider(ranked), "outsider": _find_outsider(ranked),
'budget_total': 2.0 + mise_ze2, "budget_total": 2.0 + mise_ze2,
} }
def _find_outsider(ranked): def _find_outsider(ranked):
for h in ranked[3:7]: for h in ranked[3:7]:
d = h['details'] d = h["details"]
if d['cote'] >= 12 and d['forme_recente'] <= 4 and d['bonus_outsider'] == 5: if d["cote"] >= 12 and d["forme_recente"] <= 4 and d["bonus_outsider"] == 5:
return { return {
'cheval': h['nom'], 'numero': h['numero'], 'cote': d['cote'], "cheval": h["nom"],
'mise_suggeree': 1.0, 'gain_potentiel': round(1.0 * d['cote'], 2) "numero": h["numero"],
"cote": d["cote"],
"mise_suggeree": 1.0,
"gain_potentiel": round(1.0 * d["cote"], 2),
} }
return None return None
def save_to_db(scored_horses, date_course, hippodrome, libelle): def save_to_db(scored_horses, date_course, hippodrome, libelle):
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute("DELETE FROM scoring WHERE date = ?", (date_course,)) cursor.execute("DELETE FROM scoring WHERE date = ?", (date_course,))
for i, h in enumerate(scored_horses, 1): for i, h in enumerate(scored_horses, 1):
d = h['details'] d = h["details"]
cursor.execute(""" cursor.execute(
"""
INSERT INTO scoring (date, race_name, horse_number, horse_name, score, INSERT INTO scoring (date, race_name, horse_number, horse_name, score,
score_cote, score_forme, score_victoire, score_place, score_rk, score_cote, score_forme, score_victoire, score_place, score_rk,
score_tendance, score_avis, cote, forme_recente, tx_victoire, tx_place, score_tendance, score_avis, cote, forme_recente, tx_victoire, tx_place,
avis_entraineur, musique, rang_scoring, scoring_version) avis_entraineur, musique, rang_scoring, scoring_version)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'v2') VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 'v2')
""", (date_course, libelle, h['numero'], h['nom'], h['score'], """,
d.get('score_cote', 0), d.get('score_forme', 0), d.get('score_victoire', 0), (
d.get('score_place', 0), d.get('score_rk', 0), d.get('score_tendance', 0), date_course,
d.get('score_avis', 0), d.get('cote', 0), d.get('forme_recente', 0), libelle,
d.get('tx_victoire', 0), d.get('tx_place', 0), d.get('avis_entraineur', ''), h["numero"],
d.get('musique', ''), i)) h["nom"],
h["score"],
d.get("score_cote", 0),
d.get("score_forme", 0),
d.get("score_victoire", 0),
d.get("score_place", 0),
d.get("score_rk", 0),
d.get("score_tendance", 0),
d.get("score_avis", 0),
d.get("cote", 0),
d.get("forme_recente", 0),
d.get("tx_victoire", 0),
d.get("tx_place", 0),
d.get("avis_entraineur", ""),
d.get("musique", ""),
i,
),
)
conn.commit() conn.commit()
conn.close() conn.close()
print(f"💾 {len(scored_horses)} scores enregistres en BDD pour {date_course}") print(f"💾 {len(scored_horses)} scores enregistres en BDD pour {date_course}")
def main(): def main():
today = datetime.now().strftime('%Y-%m-%d') today = datetime.now().strftime("%Y-%m-%d")
date_pmu = datetime.now().strftime('%d%m%Y') date_pmu = datetime.now().strftime("%d%m%Y")
print(f"=== SCORING V2 - ZE2 SUR4 OPTIMISE === {datetime.now().strftime('%d/%m/%Y %H:%M')} ===") print(
f"=== SCORING V2 - ZE2 SUR4 OPTIMISE === {datetime.now().strftime('%d/%m/%Y %H:%M')} ==="
)
try: try:
url = f"https://turfinfo.api.pmu.fr/rest/client/1/programme/{date_pmu}/reunions" url = f"https://turfinfo.api.pmu.fr/rest/client/1/programme/{date_pmu}/reunions"
r = requests.get(url, headers=HEADERS, timeout=15) r = requests.get(url, headers=HEADERS, timeout=15)
reunions = r.json().get('programme', {}).get('reunions', []) reunions = r.json().get("programme", {}).get("reunions", [])
except Exception as e: except Exception as e:
print(f"Erreur: {e}") print(f"Erreur: {e}")
return return
quinte = None quinte = None
for reunion in reunions: for reunion in reunions:
for course in reunion.get('courses', []): for course in reunion.get("courses", []):
paris_types = [p["typePari"] for p in course.get("paris", [])] paris_types = [p["typePari"] for p in course.get("paris", [])]
if any("QUINTE" in p for p in paris_types) or "PARIS-TURF" in course.get('libelle', ''): if any("QUINTE" in p for p in paris_types) or "PARIS-TURF" in course.get(
quinte = (reunion['numOfficiel'], course['numOrdre'], course.get('libelle', ''), "libelle", ""
reunion['hippodrome']['libelleCourt'], course.get('heureDepart', 0)) ):
quinte = (
reunion["numOfficiel"],
course["numOrdre"],
course.get("libelle", ""),
reunion["hippodrome"]["libelleCourt"],
course.get("heureDepart", 0),
)
break break
if quinte: if quinte:
break break
if not quinte: if not quinte:
# Fallback: utiliser la premiere reunion francaise avec predictions # Fallback: utiliser la premiere reunion francaise avec predictions
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
conn.row_factory = sqlite3.Row conn.row_factory = sqlite3.Row
r = conn.execute(""" r = conn.execute(
"""
SELECT r.num_reunion, r.hippodrome_court, c.num_course, c.libelle SELECT r.num_reunion, r.hippodrome_court, c.num_course, c.libelle
FROM pmu_courses c FROM pmu_courses c
JOIN pmu_reunions r ON r.date_programme=c.date_programme AND r.num_reunion=c.num_reunion JOIN pmu_reunions r ON r.date_programme=c.date_programme AND r.num_reunion=c.num_reunion
@@ -264,57 +432,82 @@ def main():
AND EXISTS (SELECT 1 FROM predictions p WHERE p.date=? AND p.source='canalturf_partants' AND EXISTS (SELECT 1 FROM predictions p WHERE p.date=? AND p.source='canalturf_partants'
AND p.race_name LIKE '%' || c.libelle || '%') AND p.race_name LIKE '%' || c.libelle || '%')
ORDER BY c.heure_depart_str ASC LIMIT 1 ORDER BY c.heure_depart_str ASC LIMIT 1
""", (today, today)).fetchone() """,
(today, today),
).fetchone()
conn.close() conn.close()
if r: if r:
quinte = (r['num_reunion'], r['num_course'], r['libelle'], r['hippodrome_court'], 0) quinte = (
r["num_reunion"],
r["num_course"],
r["libelle"],
r["hippodrome_court"],
0,
)
else: else:
print("Aucune course trouvee") print("Aucune course trouvee")
return return
num_r, num_c, libelle, hippodrome, heure_ts = quinte num_r, num_c, libelle, hippodrome, heure_ts = quinte
heure = datetime.fromtimestamp(heure_ts/1000).strftime('%H:%M') if heure_ts else '13:55' heure = (
datetime.fromtimestamp(heure_ts / 1000).strftime("%H:%M")
if heure_ts
else "13:55"
)
print(f"Course: {libelle} - {hippodrome} {heure}") print(f"Course: {libelle} - {hippodrome} {heure}")
try: try:
url = f"https://turfinfo.api.pmu.fr/rest/client/1/programme/{date_pmu}/R{num_r}/C{num_c}/participants" 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) r = requests.get(url, headers=HEADERS, timeout=15)
participants = [p for p in r.json().get('participants', []) if p.get('statut') == 'PARTANT'] participants = [
p for p in r.json().get("participants", []) if p.get("statut") == "PARTANT"
]
except Exception as e: except Exception as e:
print(f"Erreur: {e}") print(f"Erreur: {e}")
return return
scored_horses = [] scored_horses = []
for p in participants: for p in participants:
score, details = score_cheval_v2(p, participants, today) score, details = score_cheval_v2(p, participants, today)
scored_horses.append({'nom': p['nom'], 'numero': p['numPmu'], 'score': score, 'details': details}) scored_horses.append(
{"nom": p["nom"], "numero": p["numPmu"], "score": score, "details": details}
ranked = sorted(scored_horses, key=lambda x: x['score'], reverse=True) )
ranked = sorted(scored_horses, key=lambda x: x["score"], reverse=True)
print(f"\n=== TOP 4 ===") print(f"\n=== TOP 4 ===")
for i, h in enumerate(ranked[:4], 1): for i, h in enumerate(ranked[:4], 1):
d = h['details'] d = h["details"]
print(f"{i}. #{h['numero']:>2} {h['nom']:<20} Score:{h['score']:.1f} Cote:{d['cote']:.1f}") print(
f"{i}. #{h['numero']:>2} {h['nom']:<20} Score:{h['score']:.1f} Cote:{d['cote']:.1f}"
)
save_to_db(ranked, today, hippodrome, libelle) save_to_db(ranked, today, hippodrome, libelle)
reco = build_recommendations_v2(scored_horses) reco = build_recommendations_v2(scored_horses)
if reco: if reco:
print(f"\n=== RECOMMANDATIONS ===") print(f"\n=== RECOMMANDATIONS ===")
sg = reco['simple_gagnant'] sg = reco["simple_gagnant"]
print(f"\n🎯 SIMPLE GAGNANT:") print(f"\n🎯 SIMPLE GAGNANT:")
print(f" #{sg['numero']} {sg['cheval']} @ {sg['cote']}/1 (mise {sg['mise_suggeree']}EUR)") print(
f" #{sg['numero']} {sg['cheval']} @ {sg['cote']}/1 (mise {sg['mise_suggeree']}EUR)"
ze2 = reco['ze2_sur_4'] )
ze2 = reco["ze2_sur_4"]
print(f"\n🎰 ZE 2 SUR 4 (TOP 4: {', '.join([h['nom'] for h in ze2['top4']])}") print(f"\n🎰 ZE 2 SUR 4 (TOP 4: {', '.join([h['nom'] for h in ze2['top4']])}")
print(f" Mise totale: {ze2['mise_totale']}EUR ({ze2['nb_combinaisons']} combis x 1EUR)") print(
f" Mise totale: {ze2['mise_totale']}EUR ({ze2['nb_combinaisons']} combis x 1EUR)"
)
print(f" Confiance: {ze2['confiance']}") print(f" Confiance: {ze2['confiance']}")
print(f" Combinaisons:") print(f" Combinaisons:")
for c in ze2['combinaisons']: for c in ze2["combinaisons"]:
print(f" {c['numero1']}-{c['cheval1']} + {c['numero2']}-{c['cheval2']}") print(
f" {c['numero1']}-{c['cheval1']} + {c['numero2']}-{c['cheval2']}"
)
print(f"\n💰 BUDGET TOTAL: {reco['budget_total']}EUR") print(f"\n💰 BUDGET TOTAL: {reco['budget_total']}EUR")
print(f" - Simple Gagnant: 2EUR") print(f" - Simple Gagnant: 2EUR")
print(f" - ZE 2 sur 4: {ze2['mise_totale']}EUR") print(f" - ZE 2 sur 4: {ze2['mise_totale']}EUR")
if __name__ == "__main__": if __name__ == "__main__":
main() main()