AH Value Toolkit

AH Value Toolkit

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();