How it works: the tool turns xG into Poisson goal probabilities, optionally applies Dixon–Coles for low-score correlation, sums the score matrix to get P(home/draw/away) and AH cover probabilities, compares your model probability to bookmaker implied probability, computes EV and Kelly stake.j) p_home+=prob; else if(i==j) p_draw+=prob; else p_away+=prob;
const m = i-j; marginCounts[m] = (marginCounts[m]||0) + prob;
}
}
// renormalize if sum !=1
let total=0; for(const k in marginCounts) total+=marginCounts[k];
if(Math.abs(total-1)>1e-6){ for(const k in marginCounts) marginCounts[k]=marginCounts[k]/total; p_home/=total; p_draw/=total; p_away/=total; }
return {p_home,p_draw,p_away,marginCounts};
}
function calc(){
const t1 = document.getElementById('team1').value||'Home';
const t2 = document.getElementById('team2').value||'Away';
const xg1 = parseFloat(document.getElementById('xg1').value);
const xg2 = parseFloat(document.getElementById('xg2').value);
const market = document.getElementById('market').value;
const odds = parseFloat(document.getElementById('odds').value);
const bankroll = parseFloat(document.getElementById('bankroll').value);
const kellyFrac = parseFloat(document.getElementById('kellyFrac').value);
const maxG = parseInt(document.getElementById('maxGoals').value,10);
const useDixon = document.getElementById('dixon').value==='yes';
if(isNaN(xg1)||isNaN(xg2)||isNaN(odds)||isNaN(bankroll)){ alert('Please fill numeric values'); return; }
const dist = computeDistributions(xg1,xg2,maxG,useDixon);
const p_home = dist.p_home, p_draw = dist.p_draw, p_away = dist.p_away, marginCounts = dist.marginCounts;
// market probability
let p_market=0, pushProb=0;
if(market==='home') p_market = p_home;
else if(market==='away') p_market = p_away;
else if(market==='draw') p_market = p_draw;
else if(market==='ah-0.5'){ let s=0; for(const k in marginCounts) if(parseInt(k)>=1) s+=marginCounts[k]; p_market=s; }
else if(market==='ah-0.75'){ // -0.75 = half (-0.5 + -1.0): half stake is AH-0.5, half is AH-1.0
// payout logic: we calculate expected profit per unit stake later. For probability, compute full-win and half-win
// full win if margin>=2, half win if margin==1, loss otherwise
let full=0, half=0; for(const k in marginCounts){ const m=parseInt(k); if(m>=2) full+=marginCounts[k]; else if(m===1) half+=marginCounts[k]; }
p_market = full + 0.5*half; // approximate "expected win probability equivalent"
pushProb = 0; // handled in EV calc
}
else if(market==='ah-1.0'){ let win=0, push=0; for(const k in marginCounts){ const m=parseInt(k); if(m>=2) win+=marginCounts[k]; else if(m===1) push+=marginCounts[k]; } p_market = win; pushProb = push; }
else if(market==='ah-1.25'){ // split -1.0 & -1.5
// fractionally: half stake on -1.0, half on -1.5
let win125=0, halfwin125=0; // approximate expected
// for -1.5: full win if m>=2
let win15=0; for(const k in marginCounts){ const m=parseInt(k); if(m>=2) win15+=marginCounts[k]; }
// for -1.0: full win if m>=2, push if m==1
let win10=0, push10=0; for(const k in marginCounts){ const m=parseInt(k); if(m>=2) win10+=marginCounts[k]; else if(m===1) push10+=marginCounts[k]; }
// expected proportion (average of the two legs)
p_market = 0.5*win10 + 0.5*win15 + 0.5*0.5*push10; // crude approx accounting push as half
pushProb = 0.5*push10;
}
else if(market==='ah-1.5'){ let s=0; for(const k in marginCounts) if(parseInt(k)>=2) s+=marginCounts[k]; p_market=s; }
else if(market==='ah+0.5'){ let s=0; for(const k in marginCounts) if(parseInt(k)>=0) s+=marginCounts[k]; p_market=s; }
else if(market==='ah+1.0'){ // +1.0: win if m>=-1 ? actually home+1 means home gets +1 goal; covers if home loses by 0-1? We'll compute as: successful if margin+1 >0 => m>=0 => home not losing by >1
let s=0; for(const k in marginCounts) if(parseInt(k)>=-1) s+=marginCounts[k]; p_market=s; }
else if(market==='btts'){ // both teams score yes: probability that both score at least 1
let prob=0; for(const k in marginCounts){ // need joint distribution - recompute
// easier: compute joint and sum where i>=1 && j>=1
}
// compute directly
let jointTotal=0; let both=0;
for(let i=0;i<=maxG;i++){ for(let j=0;j<=maxG;j++){ let prob = poissonP(xg1,i)*poissonP(xg2,j); if(useDixon) prob*= ( (i<=2 && j<=2) ? dixonColesAdj(i,j,xg1,xg2,-0.05) : 1 ); if(i>=1 && j>=1) both+=prob; jointTotal+=prob; }}
both = both / jointTotal; p_market = both;
}
else if(market==='over2.5'){ // prob sum goals >=3
let s=0; for(const k in marginCounts){ /* compute via joint */ }
let jointTotal=0; let over=0; for(let i=0;i<=maxG;i++){ for(let j=0;j<=maxG;j++){ let prob = poissonP(xg1,i)*poissonP(xg2,j); if(useDixon) prob*= ( (i<=2 && j<=2) ? dixonColesAdj(i,j,xg1,xg2,-0.05) : 1 ); if(i+j>=3) over+=prob; jointTotal+=prob; }} over=over/jointTotal; p_market=over;
}
// implied probability
const implied = 1/odds;
// EV per unit stake calculation (handling pushes for AH -1.0/-1.25/-0.75)
let evPerUnit = 0; const b = odds - 1;
if(market==='ah-1.0'){
const loseProb = 1 - (p_market + (pushProb||0));
evPerUnit = p_market*(odds-1) + (pushProb*0) - loseProb*1;
} else if(market==='ah-0.75'){
// half stake on -0.5 (requires win by >=1) and half on -1.0 (requires win by 2, push on 1)
// Compute probabilities
let full2=0, oneGoal=0, other=0; for(const k in marginCounts){ const m=parseInt(k); if(m>=2) full2+=marginCounts[k]; else if(m===1) oneGoal+=marginCounts[k]; else other+=marginCounts[k]; }
// half stake leg1 (AH -0.5): wins on m>=1 => prob_leg1 = full2 + oneGoal
const prob_leg1 = full2 + oneGoal;
// half stake leg2 (AH -1.0): wins on full2, push on oneGoal
const prob_leg2_win = full2; const prob_leg2_push = oneGoal; const prob_leg2_lose = 1 - (prob_leg2_win+prob_leg2_push);
// expected profit per unit stake = 0.5*(leg1Profit) + 0.5*(leg2Profit)
const leg1Profit = prob_leg1*(odds-1) - (1-prob_leg1)*1;
const leg2Profit = prob_leg2_win*(odds-1) + prob_leg2_push*0 - prob_leg2_lose*1;
evPerUnit = 0.5*leg1Profit + 0.5*leg2Profit;
} else if(market==='ah-1.25'){
// half on -1.0, half on -1.5
// compute probabilities
let full2=0, oneGoal=0, other=0; for(const k in marginCounts){ const m=parseInt(k); if(m>=2) full2+=marginCounts[k]; else if(m===1) oneGoal+=marginCounts[k]; else other+=marginCounts[k]; }
const leg1 = full2; const push1 = oneGoal; const lose1 = 1-(leg1+push1);
const leg2 = full2; const lose2 = 1-leg2;
const profit1 = leg1*(odds-1) + push1*0 - lose1*1;
const profit2 = leg2*(odds-1) - lose2*1;
evPerUnit = 0.5*profit1 + 0.5*profit2;
} else if(market==='btts' || market==='over2.5'){
evPerUnit = p_market*(odds-1) - (1-p_market);
} else {
evPerUnit = p_market*(odds-1) - (1-p_market);
}
// Kelly
const q = 1 - p_market; let kelly = 0; if(b>0) kelly = Math.max(0, (b*p_market - q)/b);
const fractionalKelly = kelly * kellyFrac; const recommendedStake = fractionalKelly * bankroll;
// Output
let out = `
Match: ${t1} vs ${t2}
Market: ${market}
Model probability: ${(p_market*100).toFixed(2)}%`;
if(pushProb) out += `
Push prob: ${(pushProb*100).toFixed(2)}%`;
out += `
Book odds: ${odds} (implied ${(implied*100).toFixed(2)}%)
Edge: ${((p_market - implied)*100).toFixed(2)}%`;
out += `
EV per unit stake: ${evPerUnit.toFixed(4)}
Full Kelly: ${kelly.toFixed(4)}
Used Kelly: ${fractionalKelly.toFixed(4)}
Recommended stake: $${recommendedStake.toFixed(2)} (bankroll $${bankroll.toFixed(2)})
`;
out += `
Quick rules:
Only bet when Edge >= 3% (conservative) or >=5% (strict).
Use fractional Kelly 0.25-0.5. Cap single bet to 3-4% of bankroll.
Prefer top leagues and shop odds across books.
`;
document.getElementById('results').innerHTML = out;
document.getElementById('explain').innerHTML = `
Model summary
Home win: ${(p_home*100).toFixed(2)}% Draw: ${(p_draw*100).toFixed(2)}% Away: ${(p_away*100).toFixed(2)}%
Dixon–Coles applied: ${useDixon ? 'Yes' : 'No'}
`;
}
document.getElementById('calcBtn').addEventListener('click',calc);
// initial
calc();