diff --git a/app/db.py b/app/db.py index 394b66a00..b0c222866 100644 --- a/app/db.py +++ b/app/db.py @@ -1123,7 +1123,8 @@ def _backtest_theories(winners): return {"error": "Not enough data for backtesting"} theories = ["base_rate", "markov_1", "markov_2", "recent_20", "streak", "combined"] - correct = {t: 0 for t in theories} + full_hits = {t: 0 for t in theories} + semi_hits = {t: 0 for t in theories} total_tested = 0 rolling = {t: [] for t in theories} # rolling accuracy over last 200 @@ -1135,24 +1136,24 @@ def _backtest_theories(winners): total_h = len(history) # Base rate base = {c: history.count(c) / total_h for c in CHAIR_LABELS} - base_pick = max(CHAIR_LABELS, key=lambda c: base[c]) + base_ranked = sorted(CHAIR_LABELS, key=lambda c: base[c], reverse=True) # Markov-1 m1, _ = _markov_matrix_1(history) last = history[-1] m1_probs = m1.get(last, {c: 1 / 3 for c in CHAIR_LABELS}) - m1_pick = max(CHAIR_LABELS, key=lambda c: m1_probs.get(c, 0)) + m1_ranked = sorted(CHAIR_LABELS, key=lambda c: m1_probs.get(c, 0), reverse=True) # Markov-2 m2, _ = _markov_matrix_2(history) key2 = f"{history[-2]}{history[-1]}" m2_probs = m2.get(key2, {c: 1 / 3 for c in CHAIR_LABELS}) - m2_pick = max(CHAIR_LABELS, key=lambda c: m2_probs.get(c, 0)) + m2_ranked = sorted(CHAIR_LABELS, key=lambda c: m2_probs.get(c, 0), reverse=True) # Recent-20 recent = history[-20:] if len(history) >= 20 else history rec = {c: recent.count(c) / len(recent) for c in CHAIR_LABELS} - rec_pick = max(CHAIR_LABELS, key=lambda c: rec[c]) + rec_ranked = sorted(CHAIR_LABELS, key=lambda c: rec[c], reverse=True) # Streak streak_chair = history[-1] @@ -1173,7 +1174,7 @@ def _backtest_theories(winners): streak_probs = {c: streak_probs[c] / s_total for c in CHAIR_LABELS} else: streak_probs = {c: 1 / 3 for c in CHAIR_LABELS} - streak_pick = max(CHAIR_LABELS, key=lambda c: streak_probs[c]) + streak_ranked = sorted(CHAIR_LABELS, key=lambda c: streak_probs[c], reverse=True) # Combined Bayesian combined = {c: 0 for c in CHAIR_LABELS} @@ -1182,19 +1183,28 @@ def _backtest_theories(winners): for sig_name, weight in weights.items(): for c in CHAIR_LABELS: combined[c] += weight * signals[sig_name].get(c, 1 / 3) - combined_pick = max(CHAIR_LABELS, key=lambda c: combined[c]) + combined_ranked = sorted(CHAIR_LABELS, key=lambda c: combined[c], reverse=True) - picks = { - "base_rate": base_pick, "markov_1": m1_pick, "markov_2": m2_pick, - "recent_20": rec_pick, "streak": streak_pick, "combined": combined_pick, + ranked = { + "base_rate": base_ranked, "markov_1": m1_ranked, "markov_2": m2_ranked, + "recent_20": rec_ranked, "streak": streak_ranked, "combined": combined_ranked, } for t in theories: - hit = 1 if picks[t] == actual else 0 - if picks[t] == actual: - correct[t] += 1 - rolling[t].append(hit) + pick = ranked[t][0] + second = ranked[t][1] + if pick == actual: + full_hits[t] += 1 + rolling[t].append(1.0) + elif second == actual: + semi_hits[t] += 1 + rolling[t].append(0.5) + else: + rolling[t].append(0.0) - accuracy = {t: round(correct[t] / total_tested * 100, 2) if total_tested else 0 for t in theories} + accuracy = { + t: round((full_hits[t] + semi_hits[t] * 0.5) / total_tested * 100, 2) if total_tested else 0 + for t in theories + } # Rolling accuracy over last 200 games window = 200 @@ -1212,6 +1222,8 @@ def _backtest_theories(winners): return { "total_tested": total_tested, "accuracy": accuracy, + "full_hits": {t: full_hits[t] for t in theories}, + "semi_hits": {t: semi_hits[t] for t in theories}, "rolling_accuracy": rolling_accuracy, "random_baseline": 33.33, } @@ -1230,12 +1242,16 @@ def _last_n_predictions(winners, n=20): m1, _ = _markov_matrix_1(history) m2, _ = _markov_matrix_2(history) pred, _ = _bayesian_prediction(history, m1, m2) - predicted = max(CHAIR_LABELS, key=lambda c: pred[c]) + ranked = sorted(CHAIR_LABELS, key=lambda c: pred[c], reverse=True) + predicted = ranked[0] + second_predicted = ranked[1] results.append({ "index": i, "predicted": predicted, + "second_predicted": second_predicted, "actual": actual, "correct": predicted == actual, + "semi_correct": predicted != actual and second_predicted == actual, "probs": {c: round(pred[c], 4) for c in CHAIR_LABELS}, }) return results diff --git a/static/predictions.html b/static/predictions.html index b0c276750..a0cdaccae 100644 --- a/static/predictions.html +++ b/static/predictions.html @@ -88,7 +88,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 18px; margin-bottom: 16px; } -.advisor-grid { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 16px; } +.advisor-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 16px; } .advisor-item { text-align: center; } .advisor-item .adv-label { font-size: 11px; color: var(--text3); font-weight: 600; text-transform: uppercase; margin-bottom: 4px; } .advisor-item .adv-value { font-size: 20px; font-weight: 800; } @@ -163,6 +163,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- .pred-history th, .pred-history td { padding: 7px 10px; text-align: center; border-bottom: 1px solid var(--border); } .pred-history th { color: var(--text2); font-weight: 600; text-transform: uppercase; font-size: 11px; background: var(--surface2); position: sticky; top: 0; } .pred-history .correct { color: var(--green); } +.pred-history .semi { color: #f59e0b; } .pred-history .wrong { color: var(--red); } .pred-history .winner-cell { font-weight: 800; } .prob-bar { display: inline-block; height: 6px; border-radius: 3px; vertical-align: middle; } @@ -173,6 +174,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans- } .result-flash.show { display: block; } .result-flash.win { background: rgba(16,185,129,0.15); border: 1px solid var(--green); color: var(--green); } +.result-flash.semi { background: rgba(245,158,11,0.15); border: 1px solid #f59e0b; color: #f59e0b; } .result-flash.loss { background: rgba(239,68,68,0.15); border: 1px solid var(--red); color: var(--red); } @keyframes flashIn { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; transform: translateY(0); } } @@ -260,7 +262,7 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
| Game | Predicted | P(A) | P(B) | P(C) | Actual | Result | |
|---|---|---|---|---|---|---|---|
| Game | Predicted | 2nd Pick | P(A) | P(B) | P(C) | Actual | Result |