fix: recalibrate saved recommendation snapshots
All checks were successful
2026 World Cup Quant Platform - Production Deployment / Code Quality, Security Gate & Testing (push) Successful in 4m4s
2026 World Cup Quant Platform - Production Deployment / Deploy to Production VM via Gitea CD (push) Successful in 1m16s

This commit is contained in:
wooo
2026-06-18 15:49:25 +08:00
parent f767bb2fbb
commit 8520ea3c3e
2 changed files with 85 additions and 2 deletions

View File

@@ -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:

View File

@@ -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