add semi-win scoring (0.5 pts for 2nd pick) and whale/public bet recommendations

This commit is contained in:
2026-02-26 00:03:59 +05:00
parent 1eed8786db
commit 949d0c2a57
2 changed files with 111 additions and 30 deletions

View File

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

View File

@@ -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-
<div class="section-title">Last 20 Predictions vs Actual</div>
<div class="panel" style="max-height:500px;overflow-y:auto">
<table class="pred-history" id="pred-history">
<thead><tr><th>Game</th><th>Predicted</th><th>P(A)</th><th>P(B)</th><th>P(C)</th><th>Actual</th><th>Result</th></tr></thead>
<thead><tr><th>Game</th><th>Predicted</th><th>2nd Pick</th><th>P(A)</th><th>P(B)</th><th>P(C)</th><th>Actual</th><th>Result</th></tr></thead>
<tbody></tbody>
</table>
</div>
@@ -376,6 +378,7 @@ let predictionData = null;
let liveBets = {A: 0, B: 0, C: 0};
let liveGameNo = null;
let currentPrediction = null;
let currentSecondPrediction = null;
let roundBettors = {};
// Track whale-pick wins and public-pick wins from round results we witness
let crowdTracker = {whaleCorrect: 0, publicCorrect: 0, roundsSeen: 0};
@@ -399,6 +402,7 @@ function renderPrediction(data) {
const ranked = CHAIRS.slice().sort((a, b) => data.prediction[b] - data.prediction[a]);
const best = ranked[0], second = ranked[1];
currentPrediction = best;
currentSecondPrediction = second;
$('pred-game-label').textContent = data.total_games ? `(based on ${data.total_games} games)` : '';
const pot = liveBets.A + liveBets.B + liveBets.C;
@@ -442,10 +446,33 @@ function renderPrediction(data) {
renderBetAdvisor(data, pot);
}
function computeBetRecommendations() {
const bettors = Object.values(roundBettors);
if (bettors.length < 2) return null;
const sorted = bettors.slice().sort((a, b) => b.total - a.total);
const whales = sorted.slice(0, 5);
const pub = sorted.slice(5);
const whaleAvg = {A: 0, B: 0, C: 0};
const pubAvg = {A: 0, B: 0, C: 0};
if (whales.length > 0) {
for (const w of whales) for (const c of CHAIRS) whaleAvg[c] += w.chairs[c] || 0;
for (const c of CHAIRS) whaleAvg[c] = Math.round(whaleAvg[c] / whales.length);
}
if (pub.length > 0) {
for (const p of pub) for (const c of CHAIRS) pubAvg[c] += p.chairs[c] || 0;
for (const c of CHAIRS) pubAvg[c] = Math.round(pubAvg[c] / pub.length);
}
return { whaleAvg, pubAvg, whaleCount: whales.length, pubCount: pub.length };
}
function renderBetAdvisor(data, pot) {
const el = $('advisor-content');
const ranked = CHAIRS.slice().sort((a, b) => data.prediction[b] - data.prediction[a]);
const best = ranked[0];
const second = ranked[1];
const pBest = data.prediction[best];
const ev = pBest * PAYOUT - 1;
@@ -462,6 +489,30 @@ function renderBetAdvisor(data, pot) {
const highWinPct = brTotal > 0 ? ((br.high || 0) / brTotal * 100).toFixed(0) : '?';
const evClass = ev >= 0 ? 'ev-positive' : 'ev-negative';
// Whale & public bet recommendations
const recs = computeBetRecommendations();
let whaleHtml = '', pubHtml = '';
if (recs) {
const wPrimary = recs.whaleAvg[best] || 0;
const wSecondary = Math.round(wPrimary / 2);
whaleHtml = `
<div class="advisor-item">
<div class="adv-label">Whale Avg Bet</div>
<div class="adv-value" style="color:${CHAIR_COLORS[best]}">${fmt(wPrimary)}</div>
<div class="adv-note">on ${best} (primary) &middot; ${fmt(wSecondary)} on ${second} (2nd)</div>
</div>`;
const pPrimary = recs.pubAvg[best] || 0;
const pSecondary = Math.round(pPrimary / 2);
pubHtml = `
<div class="advisor-item">
<div class="adv-label">Public Avg Bet</div>
<div class="adv-value" style="color:${CHAIR_COLORS[best]}">${fmt(pPrimary)}</div>
<div class="adv-note">on ${best} (primary) &middot; ${fmt(pSecondary)} on ${second} (2nd)</div>
</div>`;
}
el.innerHTML = `
<div class="advisor-item">
<div class="adv-label">Best Chair</div>
@@ -478,6 +529,8 @@ function renderBetAdvisor(data, pot) {
<div class="adv-value">${lowWinPct}% low</div>
<div class="adv-note">Lowest-bet chair wins ${lowWinPct}% &middot; Highest wins ${highWinPct}%</div>
</div>
${whaleHtml}
${pubHtml}
`;
}
@@ -582,27 +635,32 @@ function renderCrowdStats(data) {
function renderLast20(predictions) {
const tbody = $('pred-history').querySelector('tbody');
if (!predictions || predictions.length === 0) {
tbody.innerHTML = '<tr><td colspan="7" style="color:var(--text3)">Not enough data</td></tr>';
tbody.innerHTML = '<tr><td colspan="8" style="color:var(--text3)">Not enough data</td></tr>';
return;
}
const correctCount = predictions.filter(p => p.correct).length;
const fullHits = predictions.filter(p => p.correct).length;
const semiHits = predictions.filter(p => p.semi_correct).length;
const score = fullHits + semiHits * 0.5;
tbody.innerHTML = predictions.slice().reverse().map(p => {
const maxProb = Math.max(p.probs.A, p.probs.B, p.probs.C);
const resultClass = p.correct ? 'correct' : (p.semi_correct ? 'semi' : 'wrong');
const resultLabel = p.correct ? 'HIT' : (p.semi_correct ? 'SEMI' : 'MISS');
return `<tr>
<td style="font-weight:700">#${p.game_no}</td>
<td class="winner-cell" style="color:${CHAIR_COLORS[p.predicted]}">${p.predicted}</td>
<td style="color:${CHAIR_COLORS[p.second_predicted]}">${p.second_predicted}</td>
${CHAIRS.map(c => {
const w = p.probs[c] / maxProb * 40;
return `<td><span class="prob-bar" style="width:${w}px;background:${CHAIR_COLORS[c]}"></span> ${pct(p.probs[c])}</td>`;
}).join('')}
<td class="winner-cell" style="color:${CHAIR_COLORS[p.actual]}">${p.actual}</td>
<td class="${p.correct ? 'correct' : 'wrong'}" style="font-weight:700">${p.correct ? 'HIT' : 'MISS'}</td>
<td class="${resultClass}" style="font-weight:700">${resultLabel}</td>
</tr>`;
}).join('') +
`<tr style="border-top:2px solid var(--border);font-weight:700">
<td colspan="5" style="text-align:right;color:var(--text2)">Accuracy (last ${predictions.length})</td>
<td colspan="2" style="color:${correctCount/predictions.length > 1/3 ? 'var(--green)' : 'var(--red)'}">
${(correctCount/predictions.length*100).toFixed(1)}% (${correctCount}/${predictions.length})
<td colspan="6" style="text-align:right;color:var(--text2)">Accuracy (last ${predictions.length})</td>
<td colspan="2" style="color:${score/predictions.length > 1/3 ? 'var(--green)' : 'var(--red)'}">
${(score/predictions.length*100).toFixed(1)}% (${score}/${predictions.length})
</td>
</tr>`;
}
@@ -686,11 +744,14 @@ function renderRunsTest(runs) {
function renderBacktest(bt) {
if (bt.error) { $('backtest-cards').innerHTML = `<div style="color:var(--text2)">${bt.error}</div>`; return; }
const names = {base_rate:'Base Rate',markov_1:'Markov-1',markov_2:'Markov-2',recent_20:'Recent 20',streak:'Streak',combined:'Combined'};
$('backtest-cards').innerHTML = Object.entries(bt.accuracy).map(([key, acc]) =>
`<div class="bt-card"><div class="bt-name">${names[key]||key}</div>
$('backtest-cards').innerHTML = Object.entries(bt.accuracy).map(([key, acc]) => {
const fh = bt.full_hits?.[key] ?? '?';
const sh = bt.semi_hits?.[key] ?? '?';
return `<div class="bt-card"><div class="bt-name">${names[key]||key}</div>
<div class="bt-acc ${acc > bt.random_baseline ? 'above' : 'below'}">${acc}%</div>
<div style="font-size:10px;color:var(--text3);margin-top:4px">vs ${bt.random_baseline}% random</div></div>`
).join('');
<div style="font-size:10px;color:var(--text3);margin-top:4px">${fh} hits + ${sh} semi</div>
<div style="font-size:10px;color:var(--text3)">vs ${bt.random_baseline}% random</div></div>`;
}).join('');
if (bt.rolling_accuracy) {
const ctx = $('backtest-chart').getContext('2d');
const colors = {base_rate:'#8b8fa3',markov_1:'#3b82f6',markov_2:'#ec4899',recent_20:'#f59e0b',streak:'#10b981',combined:'#6c5ce7'};
@@ -845,6 +906,7 @@ function trackBet(bet) {
rb.chairs[bet.chair_name] = (rb.chairs[bet.chair_name] || 0) + bet.bet_amount;
rb.total += bet.bet_amount;
renderWhaleTrend();
if (predictionData) renderBetAdvisor(predictionData, liveBets.A + liveBets.B + liveBets.C);
}
function updatePredCardBets() {
@@ -885,8 +947,11 @@ function onRoundResult(data) {
if (currentPrediction) {
const hit = currentPrediction === winner;
flash.className = `result-flash show ${hit ? 'win' : 'loss'}`;
flash.innerHTML = `Round #${data.game_no}: Predicted <b>${currentPrediction}</b> \u2014 <b>${winner}</b> won. ${hit ? 'HIT!' : 'MISS'}` +
const semiHit = !hit && currentSecondPrediction === winner;
const cls = hit ? 'win' : (semiHit ? 'semi' : 'loss');
const label = hit ? 'HIT!' : (semiHit ? 'SEMI-WIN (2nd pick)' : 'MISS');
flash.className = `result-flash show ${cls}`;
flash.innerHTML = `Round #${data.game_no}: Predicted <b>${currentPrediction}</b> \u2014 <b>${winner}</b> won. ${label}` +
(extra.length ? ` &nbsp;\u00b7&nbsp; ${extra.join(' &nbsp;\u00b7&nbsp; ')}` : '');
}