- 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>
167 lines
5.9 KiB
Python
167 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Value bets route for API v1.
|
|
|
|
GET /api/v1/valuebets — Value bets du jour (premium+)
|
|
"""
|
|
|
|
from datetime import datetime
|
|
from flask import Blueprint, jsonify, request
|
|
|
|
from api_v1.utils import (
|
|
get_db,
|
|
table_exists,
|
|
internal_error,
|
|
get_pagination_params,
|
|
paginate_query,
|
|
)
|
|
from auth import jwt_required_middleware, plan_required
|
|
|
|
valuebets_bp = Blueprint("v1_valuebets", __name__, url_prefix="/api/v1")
|
|
|
|
|
|
@valuebets_bp.route("/valuebets", methods=["GET"])
|
|
@jwt_required_middleware
|
|
@plan_required("premium", "pro")
|
|
def valuebets():
|
|
"""
|
|
Value bets du jour
|
|
---
|
|
tags:
|
|
- Value Bets
|
|
summary: Value bets du jour — chevaux à cote surévaluée par le marché (premium+)
|
|
security:
|
|
- Bearer: []
|
|
parameters:
|
|
- name: date
|
|
in: query
|
|
type: string
|
|
format: date
|
|
description: Date YYYY-MM-DD (défaut aujourd'hui)
|
|
- name: min_odds
|
|
in: query
|
|
type: number
|
|
default: 2.0
|
|
description: Cote minimale pour filtrer les value bets
|
|
- name: limit
|
|
in: query
|
|
type: integer
|
|
default: 20
|
|
- name: offset
|
|
in: query
|
|
type: integer
|
|
default: 0
|
|
responses:
|
|
200:
|
|
description: Value bets du jour avec météo et terrain (HRT-83)
|
|
401:
|
|
description: Token invalide
|
|
403:
|
|
description: Plan insuffisant (premium ou pro requis)
|
|
"""
|
|
date_param = request.args.get("date", datetime.now().strftime("%Y-%m-%d"))
|
|
limit, offset = get_pagination_params(default_limit=20, max_limit=100)
|
|
|
|
try:
|
|
min_odds = float(request.args.get("min_odds", 2.0))
|
|
except (ValueError, TypeError):
|
|
min_odds = 2.0
|
|
|
|
conn = get_db()
|
|
try:
|
|
rows_raw = []
|
|
total = 0
|
|
|
|
if table_exists(conn, "ml_predictions_cache"):
|
|
count_row = conn.execute(
|
|
"""SELECT COUNT(*) as cnt
|
|
FROM ml_predictions_cache
|
|
WHERE date = ? AND is_value_bet = 1 AND odds >= ?""",
|
|
(date_param, min_odds),
|
|
).fetchone()
|
|
total = count_row["cnt"] if count_row else 0
|
|
|
|
# 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)
|
|
|
|
pagination = paginate_query(valuebets_list, total, limit, offset)
|
|
|
|
return jsonify(
|
|
{
|
|
"status": "ok",
|
|
"date": date_param,
|
|
"min_odds": min_odds,
|
|
"valuebets": valuebets_list,
|
|
**pagination,
|
|
}
|
|
), 200
|
|
|
|
except Exception as e:
|
|
return internal_error(str(e))
|
|
finally:
|
|
conn.close()
|