fix: calibrate recommendation confidence scores
This commit is contained in:
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user