diff --git a/platform/backend/app/analytics/daily_card_generator.py b/platform/backend/app/analytics/daily_card_generator.py index 2865727..882b703 100644 --- a/platform/backend/app/analytics/daily_card_generator.py +++ b/platform/backend/app/analytics/daily_card_generator.py @@ -354,6 +354,103 @@ def _confidence_score( return round(_clamp(raw, 0, 96), 1) + + +def _stable_confidence_jitter(*parts: object) -> float: + """Deterministic tie-breaker so equal model priors do not all display the same score.""" + + seed = '|'.join(str(part or '') for part in parts) + checksum = sum((index + 1) * ord(char) for index, char in enumerate(seed)) + return ((checksum % 49) - 24) / 10.0 + + +def _finalize_confidence_score( + score: float, + *, + match_id: str, + market_type: str, + selection: str, + recommendation: str, + risk_level: str, + market_tier: str, + data_quality: str, + has_market_odds: bool, + odds_source_kind: str, + target_odds: float, + true_prob: float, + ev_percent: float, + edge_percent: float, +) -> float: + """Turn raw model score into a public-facing confidence rating. + + The score is not a hit probability. It is a review priority that must reflect + source quality, market availability, play volatility, and model/value strength. + """ + + adjusted = float(score) + + if odds_source_kind == 'conditional_threshold': + adjusted -= 3.2 + elif odds_source_kind == 'reference_market': + adjusted -= 1.7 + elif odds_source_kind in {'mixed_market_source', 'single_provider_market'}: + adjusted -= 2.4 + elif odds_source_kind in {'market', 'multi_book_market'}: + adjusted += 2.0 + + if data_quality == 'fallback_prior': + adjusted -= 4.5 + elif data_quality == 'rank_elo_prior': + adjusted -= 1.6 + elif data_quality == 'observed': + adjusted += 1.5 + + if not has_market_odds: + adjusted -= 1.8 + if risk_level in {'speculative', 'sgp'} or market_tier in {'exact_score', 'speculative', 'sgp'}: + adjusted -= 3.5 + elif risk_level == 'parlay' or market_tier == 'parlay': + adjusted -= 1.8 + + adjusted -= _clamp(max(target_odds - 3.2, 0.0) * 1.4, 0.0, 6.0) + adjusted += _clamp((true_prob - 0.56) * 10.0, -2.5, 3.0) + adjusted += _clamp((ev_percent - 3.0) * 0.10, -1.2, 2.6) + adjusted += _clamp((edge_percent - 2.0) * 0.08, -1.0, 2.4) + adjusted += _stable_confidence_jitter(match_id, market_type, selection, recommendation, target_odds) + + if market_tier in {'exact_score', 'speculative', 'sgp'} or risk_level in {'speculative', 'sgp'}: + floor = 12.0 + elif risk_level == 'parlay' or market_tier == 'parlay': + floor = 18.0 + else: + floor = 24.0 + + if has_market_odds: + ceiling = 92.0 + elif odds_source_kind == 'reference_market': + ceiling = 66.5 + elif odds_source_kind == 'conditional_threshold': + ceiling = 61.5 + else: + ceiling = 64.0 + + if data_quality == 'fallback_prior': + ceiling = min(ceiling, 56.0) + elif data_quality == 'rank_elo_prior': + ceiling = min(ceiling, 66.0) + + if market_tier in {'exact_score', 'speculative'}: + ceiling = min(ceiling, 46.0) + elif market_tier == 'sgp': + ceiling = min(ceiling, 44.0) + elif market_tier == 'parlay': + ceiling = min(ceiling, 54.0 if not has_market_odds else 68.0) + + if ceiling < floor: + floor = max(8.0, ceiling - 8.0) + + return round(_clamp(adjusted, floor, ceiling), 1) + def _confidence_band(score: float, has_market_odds: bool, risk_level: str) -> str: if not has_market_odds: if score >= 64: @@ -584,6 +681,22 @@ def _build_pick( has_market_odds=has_market_odds, market_tier=market_tier, ) + confidence = _finalize_confidence_score( + confidence, + match_id=match_id, + market_type=market_type, + selection=selection, + recommendation=recommendation, + risk_level=risk_level, + market_tier=market_tier, + data_quality=data_quality, + has_market_odds=has_market_odds, + odds_source_kind=odds_source_kind, + target_odds=odds, + true_prob=true_prob, + ev_percent=ev, + edge_percent=edge, + ) implied = (1.0 / odds) * 100 fair_note = '' enriched_checks = list(data_checks) @@ -691,6 +804,22 @@ def _build_conditional_pick( has_market_odds=has_market_odds, market_tier=market_tier, ) + confidence = _finalize_confidence_score( + confidence, + match_id=match_id, + market_type=market_type, + selection=selection, + recommendation=recommendation, + risk_level=risk_level, + market_tier=market_tier, + data_quality=data_quality, + has_market_odds=has_market_odds, + odds_source_kind=odds_source_kind, + target_odds=target_odds, + true_prob=true_prob, + ev_percent=min_ev, + edge_percent=edge, + ) quality_note = _quality_note(data_quality, has_market_odds) confidence_band = _confidence_band(confidence, has_market_odds, risk_level) confidence_factors = _confidence_factors( @@ -830,6 +959,22 @@ def _build_cross_match_parlays(safe_singles: list[dict[str, Any]]) -> list[dict[ has_market_odds=combo_has_market_odds, market_tier='parlay', ) + confidence = _finalize_confidence_score( + confidence, + match_id=f'PARLAY-CROSS-MATCH-{size}-{len(parlays) + 1}', + market_type='跨場串關', + selection=f'{size} 串 1', + recommendation='SAFE_PARLAY', + risk_level='parlay', + market_tier='parlay', + data_quality=combo_data_quality, + has_market_odds=combo_has_market_odds, + odds_source_kind=combo_source_kind, + target_odds=target_combo_odds, + true_prob=combined_prob, + ev_percent=ev, + edge_percent=edge, + ) parlays.append( { 'match_id': f'PARLAY-CROSS-MATCH-{size}-{len(parlays) + 1}', @@ -939,6 +1084,22 @@ def _build_same_game_parlays(picks_by_match: dict[str, list[dict[str, Any]]]) -> has_market_odds=sgp_has_market_odds, market_tier='sgp', ) + confidence = _finalize_confidence_score( + confidence, + match_id=match_id, + market_type='同場串關', + selection=f"{first['selection']} + {second['selection']}", + recommendation='SGP_LOTTERY', + risk_level='sgp', + market_tier='sgp', + data_quality=sgp_data_quality, + has_market_odds=sgp_has_market_odds, + odds_source_kind=sgp_source_kind, + target_odds=target_combo_odds, + true_prob=combo_prob, + ev_percent=ev, + edge_percent=edge, + ) sgps.append( {