From 8520ea3c3e748b387b3da9261aed88cc5f54e69f Mon Sep 17 00:00:00 2001 From: wooo Date: Thu, 18 Jun 2026 15:49:25 +0800 Subject: [PATCH] fix: recalibrate saved recommendation snapshots --- .../app/analytics/daily_card_generator.py | 83 +++++++++++++++++++ platform/backend/app/main.py | 4 +- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/platform/backend/app/analytics/daily_card_generator.py b/platform/backend/app/analytics/daily_card_generator.py index 882b703..c60f924 100644 --- a/platform/backend/app/analytics/daily_card_generator.py +++ b/platform/backend/app/analytics/daily_card_generator.py @@ -451,6 +451,89 @@ def _finalize_confidence_score( return round(_clamp(adjusted, floor, ceiling), 1) + + +def _market_tier_from_item(item: dict[str, Any]) -> str: + market_type = str(item.get('market_type') or '') + risk_level = str(item.get('risk_level') or '') + if market_type == '跨場串關' or risk_level == 'parlay': + return 'parlay' + if market_type == '同場串關' or risk_level == 'sgp': + return 'sgp' + if market_type == '正確比分': + return 'exact_score' + if risk_level == 'speculative': + return 'speculative' + if market_type.startswith('大小球'): + return 'totals' + if market_type == '雙方進球': + return 'btts' + if market_type == '隊伍總進球': + return 'team_total' + return 'core' + + +def recalibrate_daily_card_confidence_payload(card_payload: dict[str, Any]) -> dict[str, Any]: + """Re-label stored pre-match snapshots with the current public confidence scale. + + This intentionally does not create, remove, or reorder picks. It only prevents + older saved snapshots from showing stale 0.0 or hard-capped template scores. + """ + + payload = dict(card_payload) + bucket_names = ('safe_singles', 'high_risk_singles', 'safe_parlays', 'sgp_lotteries') + changed_count = 0 + + for bucket_name in bucket_names: + next_group: list[dict[str, Any]] = [] + for raw_item in payload.get(bucket_name, []) or []: + if not isinstance(raw_item, dict): + continue + item = dict(raw_item) + try: + market_tier = _market_tier_from_item(item) + risk_level = str(item.get('risk_level') or ('parlay' if market_tier == 'parlay' else 'sgp' if market_tier == 'sgp' else 'core')) + has_market_odds = bool(item.get('has_market_odds')) + target_odds = _safe_float(item.get('target_odds'), 1.0) + true_prob = _safe_float(item.get('win_prob'), 0.0) / 100.0 + ev_percent = _safe_float(item.get('ev_percent'), 0.0) + edge_percent = _safe_float(item.get('edge_percent'), 0.0) + previous_score = _safe_float(item.get('confidence_score'), 0.0) + next_score = _finalize_confidence_score( + previous_score, + match_id=str(item.get('match_id') or ''), + market_type=str(item.get('market_type') or ''), + selection=str(item.get('selection') or ''), + recommendation=str(item.get('recommendation') or ''), + risk_level=risk_level, + market_tier=market_tier, + data_quality=str(item.get('data_quality') or 'observed'), + has_market_odds=has_market_odds, + odds_source_kind=str(item.get('odds_source_kind') or 'market'), + target_odds=target_odds, + true_prob=true_prob, + ev_percent=ev_percent, + edge_percent=edge_percent, + ) + if next_score != previous_score: + changed_count += 1 + item['confidence_score'] = next_score + item['confidence_band'] = _confidence_band(next_score, has_market_odds, risk_level) + factors = list(item.get('confidence_factors') or []) + if '信心分數已套用新版資料品質校準' not in factors: + factors.append('信心分數已套用新版資料品質校準') + item['confidence_factors'] = factors[:8] + except Exception: + pass + next_group.append(item) + payload[bucket_name] = next_group + + if changed_count: + quality_summary = dict(payload.get('data_quality_summary') or {}) + quality_summary['confidence_recalibrated_items'] = changed_count + payload['data_quality_summary'] = quality_summary + return payload + def _confidence_band(score: float, has_market_odds: bool, risk_level: str) -> str: if not has_market_odds: if score >= 64: diff --git a/platform/backend/app/main.py b/platform/backend/app/main.py index 70849fc..283d0d0 100644 --- a/platform/backend/app/main.py +++ b/platform/backend/app/main.py @@ -20,7 +20,7 @@ from pydantic import BaseModel, Field from redis.asyncio import Redis from .db.base import SessionFactory from .db.models import Bookmaker, DailyRecommendationSnapshot, Match, MatchStatus, OddsHistory, SmartMoneyFlow, Team, Venue -from .analytics.daily_card_generator import generate_daily_card +from .analytics.daily_card_generator import generate_daily_card, recalibrate_daily_card_confidence_payload from .analytics.localization import ( localize_city, localize_country, @@ -1815,7 +1815,7 @@ async def _read_daily_recommendation_snapshot_payload(target_date: str) -> dict[ snapshot = await session.get(DailyRecommendationSnapshot, snapshot_id) if snapshot is None or not snapshot.payload: return None - payload = dict(snapshot.payload) + payload = recalibrate_daily_card_confidence_payload(dict(snapshot.payload)) payload.setdefault('snapshot_status', 'saved_snapshot') return payload