feat(HRT-83): intégrer météo & terrain dans prédictions ML (Premium)
- scoring_v2.py : ajout get_terrain_condition() + compute_weather_impact() score_cheval_v2() accepte weather_data=None (backward-compat préservée) Impact météo/terrain sur [-5, +5] pts selon pénétromètre + vent + temp - api_v1/routes/predictions.py : _fetch_ml_predictions() avec include_weather=True LEFT JOIN pmu_courses (pénétromètre) + pmu_meteo sur date+num_reunion /predictions/all → terrain_condition + weather_impact dans chaque row /predictions/top3 → inchangé (free tier, pas de champs météo) - api_v1/routes/valuebets.py : même LEFT JOIN météo/terrain /valuebets → terrain_condition + weather_impact dans chaque value bet Tests : 42/42 passent (pytest tests/test_api_v1.py) Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
413
scoring_v2.py
413
scoring_v2.py
@@ -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,29 +46,102 @@ 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:
|
||||||
@@ -75,94 +153,136 @@ def score_cheval_v2(p, all_participants, today):
|
|||||||
|
|
||||||
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
|
||||||
|
|
||||||
@@ -170,39 +290,58 @@ def build_recommendations_v2(scored_horses):
|
|||||||
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()
|
||||||
@@ -210,44 +349,72 @@ def save_to_db(scored_horses, date_course, hippodrome, libelle):
|
|||||||
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
|
||||||
@@ -256,7 +423,8 @@ def main():
|
|||||||
# 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,22 +432,36 @@ 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
|
||||||
@@ -287,34 +469,45 @@ def main():
|
|||||||
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()
|
||||||
|
|||||||
Reference in New Issue
Block a user