fix streak signal, reweight predictions, and reorder UI
- Fix streak signal: was giving 100% to streak chair after normalization (non-streak chairs were 0), now properly distributes probability with streak chair getting less as streak grows (actual mean reversion) - Change recent window from 20 to 50 games - Reweight signals based on backtest: base_rate 0.15→0.20 (best performer), recent 0.10→0.15, streak 0.10→0.05, balance 0.15→0.10 - Move Live Market Sentiment above Signal Breakdown
This commit is contained in:
53
app/db.py
53
app/db.py
@@ -1031,12 +1031,12 @@ def _bayesian_prediction(winners, markov1, markov2):
|
|||||||
key2 = f"{winners[-2]}{winners[-1]}"
|
key2 = f"{winners[-2]}{winners[-1]}"
|
||||||
m2 = markov2.get(key2, {c: 1 / 3 for c in CHAIR_LABELS})
|
m2 = markov2.get(key2, {c: 1 / 3 for c in CHAIR_LABELS})
|
||||||
|
|
||||||
# Signal 4: Recent 20-game frequency — 15%
|
# Signal 4: Recent 50-game frequency — 15%
|
||||||
recent = winners[-20:] if len(winners) >= 20 else winners
|
recent = winners[-50:] if len(winners) >= 50 else winners
|
||||||
recent_total = len(recent)
|
recent_total = len(recent)
|
||||||
rec = {c: recent.count(c) / recent_total for c in CHAIR_LABELS}
|
rec = {c: recent.count(c) / recent_total for c in CHAIR_LABELS}
|
||||||
|
|
||||||
# Signal 5: Streak momentum/regression — 10%
|
# Signal 5: Streak regression — 5%
|
||||||
streak_chair = winners[-1]
|
streak_chair = winners[-1]
|
||||||
streak_len = 0
|
streak_len = 0
|
||||||
for w in reversed(winners):
|
for w in reversed(winners):
|
||||||
@@ -1045,20 +1045,11 @@ def _bayesian_prediction(winners, markov1, markov2):
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
# Regression to mean: longer streaks → lower probability of continuation
|
# Regression to mean: longer streaks → lower probability of continuation
|
||||||
streak = {}
|
cont_prob = max(0.1, 1 / 3 - streak_len * 0.05)
|
||||||
for c in CHAIR_LABELS:
|
other_prob = (1 - cont_prob) / 2
|
||||||
if c == streak_chair:
|
streak = {c: (cont_prob if c == streak_chair else other_prob) for c in CHAIR_LABELS}
|
||||||
streak[c] = max(0.1, 1 / 3 - streak_len * 0.05)
|
|
||||||
else:
|
|
||||||
streak[c] = 0
|
|
||||||
# Normalize streak signal
|
|
||||||
s_total = sum(streak.values())
|
|
||||||
if s_total > 0:
|
|
||||||
streak = {c: streak[c] / s_total for c in CHAIR_LABELS}
|
|
||||||
else:
|
|
||||||
streak = {c: 1 / 3 for c in CHAIR_LABELS}
|
|
||||||
|
|
||||||
# Signal 6: Balance / Mean Reversion — 15%
|
# Signal 6: Balance / Mean Reversion — 10%
|
||||||
# Look at last 50 games, invert frequencies to favor under-represented chairs
|
# Look at last 50 games, invert frequencies to favor under-represented chairs
|
||||||
window = min(50, len(winners))
|
window = min(50, len(winners))
|
||||||
recent_50 = winners[-window:]
|
recent_50 = winners[-window:]
|
||||||
@@ -1067,8 +1058,8 @@ def _bayesian_prediction(winners, markov1, markov2):
|
|||||||
bal_total = sum(balance.values())
|
bal_total = sum(balance.values())
|
||||||
balance = {c: balance[c] / bal_total for c in CHAIR_LABELS}
|
balance = {c: balance[c] / bal_total for c in CHAIR_LABELS}
|
||||||
|
|
||||||
weights = {"base_rate": 0.15, "markov_1": 0.25, "markov_2": 0.25, "recent_20": 0.10, "streak": 0.10, "balance": 0.15}
|
weights = {"base_rate": 0.20, "markov_1": 0.25, "markov_2": 0.25, "recent_50": 0.15, "streak": 0.05, "balance": 0.10}
|
||||||
signals = {"base_rate": base, "markov_1": m1, "markov_2": m2, "recent_20": rec, "streak": streak, "balance": balance}
|
signals = {"base_rate": base, "markov_1": m1, "markov_2": m2, "recent_50": rec, "streak": streak, "balance": balance}
|
||||||
|
|
||||||
combined = {c: 0 for c in CHAIR_LABELS}
|
combined = {c: 0 for c in CHAIR_LABELS}
|
||||||
for sig_name, weight in weights.items():
|
for sig_name, weight in weights.items():
|
||||||
@@ -1189,7 +1180,7 @@ def _backtest_theories(winners):
|
|||||||
if len(winners) <= warmup:
|
if len(winners) <= warmup:
|
||||||
return {"error": "Not enough data for backtesting"}
|
return {"error": "Not enough data for backtesting"}
|
||||||
|
|
||||||
theories = ["base_rate", "markov_1", "markov_2", "recent_20", "streak", "balance", "combined"]
|
theories = ["base_rate", "markov_1", "markov_2", "recent_50", "streak", "balance", "combined"]
|
||||||
full_hits = {t: 0 for t in theories}
|
full_hits = {t: 0 for t in theories}
|
||||||
semi_hits = {t: 0 for t in theories}
|
semi_hits = {t: 0 for t in theories}
|
||||||
total_tested = 0
|
total_tested = 0
|
||||||
@@ -1217,8 +1208,8 @@ def _backtest_theories(winners):
|
|||||||
m2_probs = m2.get(key2, {c: 1 / 3 for c in CHAIR_LABELS})
|
m2_probs = m2.get(key2, {c: 1 / 3 for c in CHAIR_LABELS})
|
||||||
m2_ranked = sorted(CHAIR_LABELS, key=lambda c: m2_probs.get(c, 0), reverse=True)
|
m2_ranked = sorted(CHAIR_LABELS, key=lambda c: m2_probs.get(c, 0), reverse=True)
|
||||||
|
|
||||||
# Recent-20
|
# Recent-50
|
||||||
recent = history[-20:] if len(history) >= 20 else history
|
recent = history[-50:] if len(history) >= 50 else history
|
||||||
rec = {c: recent.count(c) / len(recent) for c in CHAIR_LABELS}
|
rec = {c: recent.count(c) / len(recent) for c in CHAIR_LABELS}
|
||||||
rec_ranked = sorted(CHAIR_LABELS, key=lambda c: rec[c], reverse=True)
|
rec_ranked = sorted(CHAIR_LABELS, key=lambda c: rec[c], reverse=True)
|
||||||
|
|
||||||
@@ -1230,17 +1221,9 @@ def _backtest_theories(winners):
|
|||||||
streak_len += 1
|
streak_len += 1
|
||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
streak_probs = {}
|
cont_prob = max(0.1, 1 / 3 - streak_len * 0.05)
|
||||||
for c in CHAIR_LABELS:
|
other_prob = (1 - cont_prob) / 2
|
||||||
if c == streak_chair:
|
streak_probs = {c: (cont_prob if c == streak_chair else other_prob) for c in CHAIR_LABELS}
|
||||||
streak_probs[c] = max(0.1, 1 / 3 - streak_len * 0.05)
|
|
||||||
else:
|
|
||||||
streak_probs[c] = 0
|
|
||||||
s_total = sum(streak_probs.values())
|
|
||||||
if s_total > 0:
|
|
||||||
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_ranked = sorted(CHAIR_LABELS, key=lambda c: streak_probs[c], reverse=True)
|
streak_ranked = sorted(CHAIR_LABELS, key=lambda c: streak_probs[c], reverse=True)
|
||||||
|
|
||||||
# Balance / Mean Reversion
|
# Balance / Mean Reversion
|
||||||
@@ -1254,8 +1237,8 @@ def _backtest_theories(winners):
|
|||||||
|
|
||||||
# Combined Bayesian
|
# Combined Bayesian
|
||||||
combined = {c: 0 for c in CHAIR_LABELS}
|
combined = {c: 0 for c in CHAIR_LABELS}
|
||||||
weights = {"base_rate": 0.15, "markov_1": 0.25, "markov_2": 0.25, "recent_20": 0.10, "streak": 0.10, "balance": 0.15}
|
weights = {"base_rate": 0.20, "markov_1": 0.25, "markov_2": 0.25, "recent_50": 0.15, "streak": 0.05, "balance": 0.10}
|
||||||
signals = {"base_rate": base, "markov_1": m1_probs, "markov_2": m2_probs, "recent_20": rec, "streak": streak_probs, "balance": bal_probs}
|
signals = {"base_rate": base, "markov_1": m1_probs, "markov_2": m2_probs, "recent_50": rec, "streak": streak_probs, "balance": bal_probs}
|
||||||
for sig_name, weight in weights.items():
|
for sig_name, weight in weights.items():
|
||||||
for c in CHAIR_LABELS:
|
for c in CHAIR_LABELS:
|
||||||
combined[c] += weight * signals[sig_name].get(c, 1 / 3)
|
combined[c] += weight * signals[sig_name].get(c, 1 / 3)
|
||||||
@@ -1263,7 +1246,7 @@ def _backtest_theories(winners):
|
|||||||
|
|
||||||
ranked = {
|
ranked = {
|
||||||
"base_rate": base_ranked, "markov_1": m1_ranked, "markov_2": m2_ranked,
|
"base_rate": base_ranked, "markov_1": m1_ranked, "markov_2": m2_ranked,
|
||||||
"recent_20": rec_ranked, "streak": streak_ranked, "balance": bal_ranked,
|
"recent_50": rec_ranked, "streak": streak_ranked, "balance": bal_ranked,
|
||||||
"combined": combined_ranked,
|
"combined": combined_ranked,
|
||||||
}
|
}
|
||||||
for t in theories:
|
for t in theories:
|
||||||
|
|||||||
@@ -253,19 +253,8 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
|||||||
<div id="impact-content"></div>
|
<div id="impact-content"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel" style="margin-top:8px">
|
|
||||||
<div class="panel-title">Signal Breakdown</div>
|
|
||||||
<table class="signal-table" id="signal-table">
|
|
||||||
<thead><tr><th>Signal</th><th>Weight</th><th>A</th><th>B</th><th>C</th></tr></thead>
|
|
||||||
<tbody></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Live Trends: Whale + Public -->
|
<!-- Live Trends: Whale + Public -->
|
||||||
<div class="section">
|
<div class="trends-grid" style="margin-top:16px">
|
||||||
<div class="section-title">Live Market Sentiment</div>
|
|
||||||
<div class="trends-grid">
|
|
||||||
<div class="trend-panel">
|
<div class="trend-panel">
|
||||||
<div class="panel-title">Whale Trend (Top 5 Bettors)</div>
|
<div class="panel-title">Whale Trend (Top 5 Bettors)</div>
|
||||||
<div id="whale-trend"><div class="trend-empty">Waiting for bets...</div></div>
|
<div id="whale-trend"><div class="trend-empty">Waiting for bets...</div></div>
|
||||||
@@ -278,7 +267,15 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- Historical: did crowd pick the winner? -->
|
<!-- Historical: did crowd pick the winner? -->
|
||||||
<div id="crowd-stats" class="crowd-stats"></div>
|
<div id="crowd-stats" class="crowd-stats" style="margin-top:12px"></div>
|
||||||
|
|
||||||
|
<div class="panel" style="margin-top:12px">
|
||||||
|
<div class="panel-title">Signal Breakdown</div>
|
||||||
|
<table class="signal-table" id="signal-table">
|
||||||
|
<thead><tr><th>Signal</th><th>Weight</th><th>A</th><th>B</th><th>C</th></tr></thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Last 50 Predictions vs Actual -->
|
<!-- Last 50 Predictions vs Actual -->
|
||||||
@@ -480,7 +477,7 @@ function renderPrediction(data) {
|
|||||||
|
|
||||||
// Signal table
|
// Signal table
|
||||||
const tbody = $('signal-table').querySelector('tbody');
|
const tbody = $('signal-table').querySelector('tbody');
|
||||||
const sigNames = {'base_rate':'Base Rate','markov_1':'Markov-1','markov_2':'Markov-2','recent_20':'Recent 20','streak':'Streak','balance':'Balance'};
|
const sigNames = {'base_rate':'Base Rate','markov_1':'Markov-1','markov_2':'Markov-2','recent_50':'Recent 50','streak':'Streak','balance':'Balance'};
|
||||||
tbody.innerHTML = Object.entries(data.signals).map(([key, sig]) =>
|
tbody.innerHTML = Object.entries(data.signals).map(([key, sig]) =>
|
||||||
`<tr><td style="text-align:left">${sigNames[key]||key}</td><td>${(sig.weight*100).toFixed(0)}%</td>` +
|
`<tr><td style="text-align:left">${sigNames[key]||key}</td><td>${(sig.weight*100).toFixed(0)}%</td>` +
|
||||||
CHAIRS.map(c => `<td style="color:${CHAIR_COLORS[c]}">${pct(sig.probs[c])}</td>`).join('') + '</tr>'
|
CHAIRS.map(c => `<td style="color:${CHAIR_COLORS[c]}">${pct(sig.probs[c])}</td>`).join('') + '</tr>'
|
||||||
@@ -1051,7 +1048,7 @@ function renderRunsTest(runs) {
|
|||||||
|
|
||||||
function renderBacktest(bt) {
|
function renderBacktest(bt) {
|
||||||
if (bt.error) { $('backtest-cards').innerHTML = `<div style="color:var(--text2)">${bt.error}</div>`; return; }
|
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',balance:'Balance',combined:'Combined'};
|
const names = {base_rate:'Base Rate',markov_1:'Markov-1',markov_2:'Markov-2',recent_50:'Recent 50',streak:'Streak',balance:'Balance',combined:'Combined'};
|
||||||
$('backtest-cards').innerHTML = Object.entries(bt.accuracy).map(([key, acc]) => {
|
$('backtest-cards').innerHTML = Object.entries(bt.accuracy).map(([key, acc]) => {
|
||||||
const fh = bt.full_hits?.[key] ?? '?';
|
const fh = bt.full_hits?.[key] ?? '?';
|
||||||
const sh = bt.semi_hits?.[key] ?? '?';
|
const sh = bt.semi_hits?.[key] ?? '?';
|
||||||
@@ -1062,7 +1059,7 @@ function renderBacktest(bt) {
|
|||||||
}).join('');
|
}).join('');
|
||||||
if (bt.rolling_accuracy) {
|
if (bt.rolling_accuracy) {
|
||||||
const ctx = $('backtest-chart').getContext('2d');
|
const ctx = $('backtest-chart').getContext('2d');
|
||||||
const colors = {base_rate:'#8b8fa3',markov_1:'#3b82f6',markov_2:'#ec4899',recent_20:'#f59e0b',streak:'#10b981',balance:'#f472b6',combined:'#6c5ce7'};
|
const colors = {base_rate:'#8b8fa3',markov_1:'#3b82f6',markov_2:'#ec4899',recent_50:'#f59e0b',streak:'#10b981',balance:'#f472b6',combined:'#6c5ce7'};
|
||||||
const datasets = Object.entries(bt.rolling_accuracy).map(([key, data]) => ({
|
const datasets = Object.entries(bt.rolling_accuracy).map(([key, data]) => ({
|
||||||
label: names[key]||key, data, borderColor: colors[key]||'#fff', backgroundColor: 'transparent',
|
label: names[key]||key, data, borderColor: colors[key]||'#fff', backgroundColor: 'transparent',
|
||||||
borderWidth: key === 'combined' ? 3 : 1.5, pointRadius: 0, tension: 0.3,
|
borderWidth: key === 'combined' ? 3 : 1.5, pointRadius: 0, tension: 0.3,
|
||||||
|
|||||||
Reference in New Issue
Block a user