diff --git a/api_v1/routes/predictions.py b/api_v1/routes/predictions.py index eb0c7a3..6911cb5 100644 --- a/api_v1/routes/predictions.py +++ b/api_v1/routes/predictions.py @@ -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") -def _fetch_ml_predictions(conn, date: str, limit: int = None, offset: int = 0): - """Shared helper — returns rows from ml_predictions_cache.""" +def _fetch_ml_predictions( + 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"): return [], 0 @@ -33,13 +39,35 @@ def _fetch_ml_predictions(conn, date: str, limit: int = None, offset: int = 0): ).fetchone() total = count_row["cnt"] if count_row else 0 - 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""" + if ( + include_weather + and table_exists(conn, "pmu_meteo") + and table_exists(conn, "pmu_courses") + ): + sql = """SELECT + 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] 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] 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() try: 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) diff --git a/api_v1/routes/valuebets.py b/api_v1/routes/valuebets.py index e3ca889..0080ed9 100644 --- a/api_v1/routes/valuebets.py +++ b/api_v1/routes/valuebets.py @@ -53,7 +53,7 @@ def valuebets(): default: 0 responses: 200: - description: Value bets du jour + description: Value bets du jour avec météo et terrain (HRT-83) 401: description: Token invalide 403: @@ -69,7 +69,7 @@ def valuebets(): conn = get_db() try: - rows = [] + rows_raw = [] total = 0 if table_exists(conn, "ml_predictions_cache"): @@ -81,18 +81,73 @@ def valuebets(): ).fetchone() total = count_row["cnt"] if count_row else 0 - rows = 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() + # LEFT JOIN pmu_courses (terrain) + pmu_meteo (météo) — HRT-83 + has_courses = table_exists(conn, "pmu_courses") + has_meteo = table_exists(conn, "pmu_meteo") + + if has_courses and has_meteo: + rows_raw = conn.execute( + """SELECT 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.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 = ? 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) return jsonify( diff --git a/scoring_v2.py b/scoring_v2.py index 0fd72f6..69e4235 100755 --- a/scoring_v2.py +++ b/scoring_v2.py @@ -11,29 +11,34 @@ import re from datetime import datetime 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): """Recupere la cote depuis la table predictions (plus recente et non nulle)""" conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row - c = conn.execute(""" + c = conn.execute( + """ SELECT odds FROM predictions WHERE date=? AND horse_name LIKE ? AND odds > 0 ORDER BY created_at DESC LIMIT 1 - """, (date_course, f"%{horse_name}%")) + """, + (date_course, f"%{horse_name}%"), + ) r = c.fetchone() conn.close() - return r['odds'] if r else 0 + return r["odds"] if r else 0 + def parse_musique(musique): if not musique: return {} - clean = re.sub(r'\(\d+\)', '', musique) - resultats = re.findall(r'(\d+|D|0)([amphsc]?)', clean) + clean = re.sub(r"\(\d+\)", "", musique) + resultats = re.findall(r"(\d+|D|0)([amphsc]?)", clean) positions = [] 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: return {} nb_courses = len(positions) @@ -41,222 +46,385 @@ def parse_musique(musique): nb_places = sum(1 for p in positions if 1 <= p <= 3) recentes = [p for p in positions[:3] if p != 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 { - '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, + "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, } -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 details = {} - + # 1. COTE - Essaye PMU API, sinon DB - horse_name = p.get('nom', '') + horse_name = p.get("nom", "") cote = 0 - + # Essayer d'abord depuis l'API PMU - rapport = p.get('dernierRapportDirect', {}) + rapport = p.get("dernierRapportDirect", {}) if rapport: - cote = rapport.get('rapport', 0) + cote = rapport.get("rapport", 0) if not cote: - rapport_ref = p.get('dernierRapportReference', {}) - cote = rapport_ref.get('rapport', 0) if rapport_ref else 0 - + rapport_ref = p.get("dernierRapportReference", {}) + cote = rapport_ref.get("rapport", 0) if rapport_ref else 0 + # Fallback: aller chercher dans la DB if not cote or cote == 0: cote = get_cote_from_db(horse_name, today) - + # Si toujours pas de cote, utiliser 99 comme valeur par defaut if not cote or cote == 0: cote = 99.0 - + score_cote = max(2, min(10, 20 / (1 + cote * 0.15))) if cote > 0 else 2 score += score_cote - details['cote'] = round(cote, 1) - details['score_cote'] = round(score_cote, 1) - + details["cote"] = round(cote, 1) + details["score_cote"] = round(score_cote, 1) + # 2. FORME - AUGMENTE a 30 pts - musique_stats = parse_musique(p.get('musique', '')) - 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 + musique_stats = parse_musique(p.get("musique", "")) + 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 += score_forme - details['forme_recente'] = forme - details['score_forme'] = score_forme - + details["forme_recente"] = forme + details["score_forme"] = score_forme + # 3. TAUX VICTOIRE (15 pts) - nb_courses_total = p.get('nombreCourses', 0) - nb_victoires_total = p.get('nombreVictoires', 0) + 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) - + details["tx_victoire"] = round(tx_vic, 1) + details["score_victoire"] = round(score_vic, 1) + # 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 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) - + details["tx_place"] = round(tx_place, 1) + details["score_place"] = round(score_place, 1) + # 5. REDUCTION KM (10 pts) - rk = p.get('reductionKilometrique', 0) - all_rk = [x.get('reductionKilometrique', 0) for x in all_participants if x.get('reductionKilometrique', 0) > 0] + rk = p.get("reductionKilometrique", 0) + all_rk = [ + x.get("reductionKilometrique", 0) + for x in all_participants + if x.get("reductionKilometrique", 0) > 0 + ] 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: score_rk = 0 score += score_rk - details['rk'] = rk - details['score_rk'] = round(score_rk, 1) - + details["rk"] = rk + details["score_rk"] = round(score_rk, 1) + # 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 += score_tendance - details['tendance'] = tendance - details['score_tendance'] = round(score_tendance, 1) - + details["tendance"] = tendance + details["score_tendance"] = round(score_tendance, 1) + # 7. AVIS ENTRAINEUR (5 pts) - avis = p.get('avisEntraineur', 'NEUTRE') - score_avis = {'POSITIF': 5, 'TRES_POSITIF': 5, 'NEUTRE': 2, 'NEGATIF': 0, 'TRES_NEGATIF': 0}.get(avis, 2) + 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 - + details["avis_entraineur"] = avis + details["score_avis"] = score_avis + # 8. BONUS OUTSIDER (5 pts) bonus_outsider = 5 if forme <= 3 and cote >= 10 else 0 score += bonus_outsider - details['bonus_outsider'] = bonus_outsider - + details["bonus_outsider"] = bonus_outsider + # Driver change penalty - if p.get('driverChange', False): + 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 - + details["driver_change"] = True + + # 9. METEO & TERRAIN (HRT-83) — premium feature, weather_data=None → skip + penetrometre = p.get("penetrometre_intitule", "") or "" + terrain_condition = ( + get_terrain_condition(penetrometre) if penetrometre else "inconnu" + ) + 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 + def get_ze2sur4_combinaisons(top4): combinaisons = [] for i in range(4): - for j in range(i+1, 4): + for j in range(i + 1, 4): c1 = top4[i] c2 = top4[j] - combinaisons.append({ - 'cheval1': c1['nom'], - 'numero1': c1['numero'], - 'cheval2': c2['nom'], - 'numero2': c2['numero'], - 'mise': 1.0, - }) + combinaisons.append( + { + "cheval1": c1["nom"], + "numero1": c1["numero"], + "cheval2": c2["nom"], + "numero2": c2["numero"], + "mise": 1.0, + } + ) return combinaisons + 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: return None - + top1, top2, top3, top4 = ranked[0], ranked[1], ranked[2], ranked[3] top4_list = ranked[:4] - + 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) mise_ze2 = len(ze2_combinaisons) * 1.0 - + return { - '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) + "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), }, - 'ze2_sur_4': { - 'top4': [{'nom': h['nom'], 'numero': h['numero']} for h in top4_list], - 'combinaisons': ze2_combinaisons, - 'mise_totale': mise_ze2, - 'nb_combinaisons': len(ze2_combinaisons), - 'confiance': confiance((top1['score'] + top2['score'] + top3['score'] + top4['score']) / 4), - 'explication': 'Jouer les 6 combinaisons de 2 chevaux parmi les 4 premiers' + "ze2_sur_4": { + "top4": [{"nom": h["nom"], "numero": h["numero"]} for h in top4_list], + "combinaisons": ze2_combinaisons, + "mise_totale": mise_ze2, + "nb_combinaisons": len(ze2_combinaisons), + "confiance": confiance( + (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), - 'budget_total': 2.0 + mise_ze2, + "outsider": _find_outsider(ranked), + "budget_total": 2.0 + mise_ze2, } + def _find_outsider(ranked): for h in ranked[3:7]: - d = h['details'] - if d['cote'] >= 12 and d['forme_recente'] <= 4 and d['bonus_outsider'] == 5: + d = h["details"] + if d["cote"] >= 12 and d["forme_recente"] <= 4 and d["bonus_outsider"] == 5: return { - 'cheval': h['nom'], 'numero': h['numero'], 'cote': d['cote'], - 'mise_suggeree': 1.0, 'gain_potentiel': round(1.0 * d['cote'], 2) + "cheval": h["nom"], + "numero": h["numero"], + "cote": d["cote"], + "mise_suggeree": 1.0, + "gain_potentiel": round(1.0 * d["cote"], 2), } return None + def save_to_db(scored_horses, date_course, hippodrome, libelle): conn = sqlite3.connect(DB_PATH) cursor = conn.cursor() - + cursor.execute("DELETE FROM scoring WHERE date = ?", (date_course,)) - + for i, h in enumerate(scored_horses, 1): - d = h['details'] - cursor.execute(""" + d = h["details"] + cursor.execute( + """ INSERT INTO scoring (date, race_name, horse_number, horse_name, 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, scoring_version) 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), - 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)) - + """, + ( + 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), + 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.close() print(f"💾 {len(scored_horses)} scores enregistres en BDD pour {date_course}") + def main(): - today = datetime.now().strftime('%Y-%m-%d') - date_pmu = datetime.now().strftime('%d%m%Y') - print(f"=== SCORING V2 - ZE2 SUR4 OPTIMISE === {datetime.now().strftime('%d/%m/%Y %H:%M')} ===") - + today = datetime.now().strftime("%Y-%m-%d") + date_pmu = datetime.now().strftime("%d%m%Y") + print( + f"=== SCORING V2 - ZE2 SUR4 OPTIMISE === {datetime.now().strftime('%d/%m/%Y %H:%M')} ===" + ) + 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', []) + reunions = r.json().get("programme", {}).get("reunions", []) except Exception as e: print(f"Erreur: {e}") return - + quinte = None 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", [])] - if any("QUINTE" in p for p in paris_types) or "PARIS-TURF" in course.get('libelle', ''): - quinte = (reunion['numOfficiel'], course['numOrdre'], course.get('libelle', ''), - reunion['hippodrome']['libelleCourt'], course.get('heureDepart', 0)) + if any("QUINTE" in p for p in paris_types) or "PARIS-TURF" in course.get( + "libelle", "" + ): + quinte = ( + reunion["numOfficiel"], + course["numOrdre"], + course.get("libelle", ""), + reunion["hippodrome"]["libelleCourt"], + course.get("heureDepart", 0), + ) break if quinte: break - + if not quinte: # Fallback: utiliser la premiere reunion francaise avec predictions conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row - r = conn.execute(""" + r = conn.execute( + """ SELECT r.num_reunion, r.hippodrome_court, c.num_course, c.libelle FROM pmu_courses c 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 p.race_name LIKE '%' || c.libelle || '%') ORDER BY c.heure_depart_str ASC LIMIT 1 - """, (today, today)).fetchone() + """, + (today, today), + ).fetchone() conn.close() 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: print("Aucune course trouvee") 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' + heure = ( + datetime.fromtimestamp(heure_ts / 1000).strftime("%H:%M") + if heure_ts + else "13:55" + ) print(f"Course: {libelle} - {hippodrome} {heure}") - + 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'] + participants = [ + p for p in r.json().get("participants", []) if p.get("statut") == "PARTANT" + ] except Exception as e: print(f"Erreur: {e}") return - + scored_horses = [] for p in participants: score, details = score_cheval_v2(p, participants, today) - scored_horses.append({'nom': p['nom'], 'numero': p['numPmu'], 'score': score, 'details': details}) - - ranked = sorted(scored_horses, key=lambda x: x['score'], reverse=True) + scored_horses.append( + {"nom": p["nom"], "numero": p["numPmu"], "score": score, "details": details} + ) + + ranked = sorted(scored_horses, key=lambda x: x["score"], reverse=True) print(f"\n=== TOP 4 ===") for i, h in enumerate(ranked[:4], 1): - d = h['details'] - print(f"{i}. #{h['numero']:>2} {h['nom']:<20} Score:{h['score']:.1f} Cote:{d['cote']:.1f}") - + d = h["details"] + print( + f"{i}. #{h['numero']:>2} {h['nom']:<20} Score:{h['score']:.1f} Cote:{d['cote']:.1f}" + ) + save_to_db(ranked, today, hippodrome, libelle) - + reco = build_recommendations_v2(scored_horses) if reco: print(f"\n=== RECOMMANDATIONS ===") - sg = reco['simple_gagnant'] + sg = reco["simple_gagnant"] print(f"\n🎯 SIMPLE GAGNANT:") - print(f" #{sg['numero']} {sg['cheval']} @ {sg['cote']}/1 (mise {sg['mise_suggeree']}EUR)") - - ze2 = reco['ze2_sur_4'] + print( + f" #{sg['numero']} {sg['cheval']} @ {sg['cote']}/1 (mise {sg['mise_suggeree']}EUR)" + ) + + ze2 = reco["ze2_sur_4"] 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" Combinaisons:") - for c in ze2['combinaisons']: - print(f" {c['numero1']}-{c['cheval1']} + {c['numero2']}-{c['cheval2']}") - + for c in ze2["combinaisons"]: + print( + f" {c['numero1']}-{c['cheval1']} + {c['numero2']}-{c['cheval2']}" + ) + print(f"\n💰 BUDGET TOTAL: {reco['budget_total']}EUR") print(f" - Simple Gagnant: 2EUR") print(f" - ZE 2 sur 4: {ze2['mise_totale']}EUR") + if __name__ == "__main__": main()