To all those interested, I'm looking for comments on the beta version of my Simultaneous Event Kelly Calculator.
Unfortunately, no documentation is yet available.
Go to SBR Betting Tools and click "Kelly Calculator (Simultaneous Events)".
|
Full Poll Results
|
SBR Book & Casino
|
|||||
| #1 5Dimes (SBR rating A+) | 294 Pts | #1 Tatddy | 14,300 Pts | SBR Sportsbook | ||
| #2 Pinnacle (SBR rating A+) | 264 Pts | #2 Studmlb55 | 10,522 Pts | SBR Sportsbook | ||
| #3 Bookmaker / BetDSI (A+) | 236 Pts | #3 Gators0708 | 5,100 Pts | SBR Sportsbook | ||
| #4 Heritage (SBR rating A) | 203 Pts | #4 Riskyprops | 4,600 Pts | SBR Sportsbook | ||
| #5 Bet365 (SBR rating A) | 78 Pts | #5 The Kraken | 4,300 Pts | SBR Casino | ||
To all those interested, I'm looking for comments on the beta version of my Simultaneous Event Kelly Calculator.
Unfortunately, no documentation is yet available.
Go to SBR Betting Tools and click "Kelly Calculator (Simultaneous Events)".
SBR Founder Join Date: 8/28/2005
It looks sharp but what the fuk is it? How does it work?
Thanks Ganch
SBR Founder Join Date: 7/20/2005
The Kelly formula as traditionally stated (K = [% Edge]/[Decimal Odds-1]) is only correct in the case of single, non-simultaneous bets. This Simultaneous Event Kelly Calculator, as the name implies, calculates exact Kelly-style stakes for up to 15 simultaneous events (bets). It does so by implementing the recursive methodology I outlined in this post.
Inputs:
- # of Simultaneous Events on which you're considering placing bets.
- Kelly Multiplier: A Kelly multiplier of ½ would imply half-Kelly, of 1 would imply full Kelly, and 2 would imply double Kelly. Note that results will true Kelly fractions, a concept I explain to discuss in Part II of my Kelly article series, and as half-Kelly won't always be exactly one-half of full Kelly.
- Consecutive Series: This represents the number of times you anticipate placing bets similar to these consecutively. See example below.
- Bankroll: The size of your betting bankroll. Setting to 1 or 100% will give reasults as percentage of bankroll. Setting to a dollar value with no decimal point will give integer results. With 2 decimal points, results would be given to the nearest penny.
- Calculate Kelly button calculates true Kelly stakes.
- The textarea shows the Kelly weights as a set of multiple parlays. The given weights may be edited compare the Kelly expectations with those of another staking plan.
- Expected Profit shows expected dollar or percentage profit from making the bet one time.
- Expected Growth shows the theoretical expected dollar or percentage bankroll growth from making the bet one time.
- Expected Bankroll shows the expected bankroll value after placing the bets the number of times show in the "Consecutive Series" box above. Most of time you'll do worse than this, but a small percentage of the time you'll do much better.
- Median Bankroll shows the bankroll you'd be most likely to see after placing the bets the number of times show in the "Consecutive Series" box above (assuming it's sufficiently large). It also represents the outcome such that you're just as likely to underperfom as you'd be to outperform.
Example:
Playing full Kelly, Chuck has a bankroll of $10,000.00. During NFL season, he typically makes 5 bets each of the 17 regular season NFL weeks at -107, on which you identify an edge of 5% on each, you'd set Simult. Events to 5, Consecutive Series to 17, enter -107 in the five US text boxes, and 5% in the five Edge/Probability text boxes.
Click the "Calculate Kelly" button.
Optimal Kelly bets are $432.47 on each of the 5 singles, $24.69 on each of the 10 2-team parlays that can be made on from the 5 singles, $1.41 on each of the 10 3-team parlays, $0.08 on the 5 4-team parlays, and nothing on the one 5-team parlay.
Chuck's expected profit the first week is $135.73 and his expected bankroll growth is $67.85.
After 17 weeks, Chuck's expectation bankroll is $12,575.82. His most likely bankroll is $11,218.23.
However, because Chuck only has limited time, he's not going to bother with the 16 larger parlays (3-teams or more), but is worried how much it's going to hurt him. So clicking in each of 3-team, 4-team and 5-team parlays he changes the weights to $0 for each one. He click "Calculate Expectations" and sees by limiting himself only to single bets and 2-team parlays, he reduces his expected end-of-season bankroll by $34.58 to $12,541.24, and his most-likely end-of-season bankroll by $0.25 to $11,217.98.
Chuck decides he can live with it and sticks with the singles and 2-team parlays only.
SBR Founder Join Date: 8/28/2005
SBR Founder Join Date: 8/28/2005
They're dimensioned by 2n because the same structure used for mutually exclusive outcome bets is also used for independent outcome bets. For the latter, there are 2n - 1 possible bets when we include parlays. This is obviously irrelevant for mutually exclusive outcome bets, but it still needs to fit in to the existing programmatic structure.
In other words, it's a pure programming issued that can be completely ignored when considering the problem algorithmically.
Anyway, here's a plain-English description of the algorithm to calculate Kelly stakes on mutually exclusive events.
- Sort all bets by edge, from highest to lowest.
- Calculate the fair implied probability for each bet. This is just the reciprocal of the decimal odds.
- Starting with the highest edge bet, calculate a running total of the the implied probability and the actual probability. The running total for each bet includes the sum of the implied and actual probabilities for that bet and every bet with a higher edge.
- If the sum of all the implied probabilities is less than 1 (i.e., a true arb exists), then for each bet the stake will be the actual probability. If this is the case, we can stop here.
- If the sum of all the implied probabilities is greater than 1, then for each bet calculate the quotient (1 – the sum of actual probabilities) / (1- sum of implied probabilities).
- Find the smallest value of this quotient that’s greater than zero. If no quotient is greater than zero then no bets will be made.
- Then for each bet the stake will be the actual probability minus the minimum quotient from 6) above multiplied by the fair implied probability.
SBR Founder Join Date: 8/28/2005
The javascript source for the Kelly Calculator is located here:
http://www.sbrforum.com/forum/js/kelly.js
SBR
Bash 2012
Attendee
8/17/2012
The calcMutExKelly() function posted by durito is only for mutually exclusive events (i.e., multi-way contests).
The calcKelly() function in the .js to which MonkeyF0cker linked handles (sloppily) simultaneous independent events.
A much better algorithm (described here) for independent event Kelly staking is implemented in the following C snippet:
Code:// Author: ganchrow@heritagesports.com #define ODDS_ARE_DECIMAL 0 #define ODDS_ARE_US 1 #define ODDS_ARE_MIXED 2 double KellyVector( double *dProbs, // array of independent event probabilities (input not changed) double *dOdds, // array of independent event odds (input not changed) double *dKellyOutVector,// array of Kelly stakes (output) long lEvents, // number of events (input not changed) double dKellyMult, // Kelly multiplier > 0 (input not changed) long lOddsType /* odds type (input not changed) 0 ==> decimal odds 1 ==> US odds 2 ==> US odds if < 0 or ≥100 */ ){ long i, j; long lParlaySize, lParlayNum, lThisAND, lOutcomes, lBetIdx, lResIdx; lOutcomes = 1<<lEvents; long *lCombins = (long*)malloc( (lEvents+1) * sizeof(long)); long *lBetsPerSize = (long*)malloc( (lEvents+1) * sizeof(long)); long *lMapV = (long*)malloc( lOutcomes * sizeof(long)); double *dSingKellyV = (double*)malloc(lEvents * sizeof(double)); if(lCombins==NULL || lBetsPerSize==NULL || lMapV==NULL || dSingKellyV==NULL) // memory allocation error goto END; for(i=0; i<lEvents; i++) { if(i) { lBetsPerSize[i] = (lEvents-i+1)/i * (lBetsPerSize[i-1] || 1); lCombins[i] = lCombins[i-1] + lBetsPerSize[i]; } else { lCombins[0] = lBetsPerSize[0] = 0; } lMapV[i] = 1<<(lEvents - i - 1); if (dOdds[i] < 0) { dOdds[i] = g_US2DEC(dOdds[i]); } else { switch(lOddsType) { case ODDS_ARE_DECIMAL : break; case ODDS_ARE_US : dOdds[i] = g_US2DEC(dOdds[i]); break; case ODDS_ARE_MIXED : default : if(dOdds[i] >= 100) dOdds[i] = g_US2DEC(dOdds[i]); } } if(dOdds[i] == NULL) goto END; dSingKellyV[i] = SBKelly(dProbs[i], dOdds[i], dKellyMult); } lMapV[lOutcomes-1] = 0; lBetsPerSize[lEvents] = 1; lCombins[lEvents] = lOutcomes-1; for(i=1; i<lOutcomes; i++) { lParlaySize = lParlayNum = 0; for(j=0; j<lEvents; j++) { lThisAND = i & lMapV[j]; lParlayNum += lThisAND; lParlaySize += lThisAND ? 1 : 0; } lResIdx = lCombins[lParlaySize-1] + lBetsPerSize[lParlaySize] - 1; lMapV[lResIdx] = lParlayNum; lBetsPerSize[lParlaySize]--; dKellyOutVector[lResIdx] = 1; } for(lResIdx=0;lResIdx<lOutcomes-1;lResIdx++) { for(i=0; i<lEvents; i++) { lBetIdx = 1<<(lEvents - i - 1); if(lMapV[lResIdx] & lBetIdx) dKellyOutVector[lResIdx] *= dSingKellyV[i]; else dKellyOutVector[lResIdx] *= (1 - dSingKellyV[i]); } } END: // clean up free(lCombins); free(lBetsPerSize); free(lMapV); free(dSingKellyV); return dKellyOutVector; } inline double SBKelly(double dWinProb, double dDecOdds, double dKellyMult){ double dFracOdds, dFairOdds, dOddsRatio; if(dDecOdds <= 1 || dWinProb > 1 || dWinProb < 0 || dKellyMult <= 0) return NULL; else if(dDecOdds * dWinProb <= 1) // non-positive edge ==> no bet return 0; dFracOdds = dDecOdds - 1; dFairOdds = 1/dWinProb - 1; dOddsRatio = pow(dFairOdds/dFracOdds, dKellyMult); return (1 - dOddsRatio) / (1 + dFracOdds * dOddsRatio); } inline double g_US2DEC(double dUSOdds){ if(dUSOdds<0) return 1-100/dUSOdds; else if(dUSOdds>0) return 1+dUSOdds/100; else return 1; }
Last edited by Ganchrow; 05-11-12 at 07:43 PM. Reason: removed call to missing combinatorial function
SBR Founder Join Date: 8/28/2005
Hey Ganch,
You mind if I post a C# port of your function?
SBR
Bash 2012
Attendee
8/17/2012
SBR Founder Join Date: 8/28/2005
Cool. Good to see you around, man.
Hope all is well.
SBR
Bash 2012
Attendee
8/17/2012
Here is the code (a C# port of Ganch's function above). Doesn't format very well on here but oh well...
Code:public const ushort ODDS_ARE_DECIMAL = 0; public const ushort ODDS_ARE_US = 1; public const ushort ODDS_ARE_MIXED = 2; private List<double> KellyVector(List<double> dProbs, List<double> dOdds, int lEvents, double dKellyMult, ushort lOddsType) { int lParlaySize, lParlayNum, lThisAND, lOutcomes, lBetIdx, lResIdx; List<double> dKellyOutVector = new List<double>(); lOutcomes = 1 << lEvents; int[] lCombins = new int[lEvents + 1]; int[] lBetsPerSize = new int[lEvents + 1]; int[] lMapV = new int[lOutcomes]; double[] dSingKellyV = new double[lEvents]; for(int i = 0; i < lEvents; i++) { if (i > 0) { lBetsPerSize[i] = combin(lEvents, i); lCombins[i] = lCombins[i-1] + lBetsPerSize[i]; } else { lCombins[0] = 0; lBetsPerSize[0] = 0; } lMapV[i] = 1 << (lEvents - i - 1); if (dOdds[i] < 0) { dOdds[i] = g_US2DEC(dOdds[i]); } else { switch(lOddsType) { case ODDS_ARE_DECIMAL : break; case ODDS_ARE_US : dOdds[i] = g_US2DEC(dOdds[i]); break; default : if(dOdds[i] >= 100) dOdds[i] = g_US2DEC(dOdds[i]); break; } } dSingKellyV[i] = SBKelly(dProbs[i], dOdds[i], dKellyMult); } lMapV[lOutcomes-1] = 0; lBetsPerSize[lEvents] = 1; lCombins[lEvents] = lOutcomes-1; for(int i = 1; i < lOutcomes; i++) { lParlaySize = lParlayNum = 0; for(int j = 0; j < lEvents; j++) { lThisAND = i & lMapV[j]; lParlayNum += lThisAND; lParlaySize += lThisAND > 0 ? 1 : 0; } lResIdx = lCombins[lParlaySize-1] + lBetsPerSize[lParlaySize] - 1; lMapV[lResIdx] = lParlayNum; lBetsPerSize[lParlaySize]--; dKellyOutVector.Add(1); } for(lResIdx = 0; lResIdx < lOutcomes - 1; lResIdx++) { for(int i = 0; i < lEvents; i++) { lBetIdx = 1 << (lEvents - i - 1); long lMapAnd = lMapV[lResIdx] & lBetIdx; if( lMapAnd > 0) { dKellyOutVector[lResIdx] *= dSingKellyV[i]; } else { dKellyOutVector[lResIdx] *= (1 - dSingKellyV[i]); } } } return dKellyOutVector; } private double SBKelly(double dWinProb, double dDecOdds, double dKellyMult) { double dFracOdds, dFairOdds, dOddsRatio; if(dDecOdds <= 1 || dWinProb > 1 || dWinProb < 0 || dKellyMult <= 0) return 0; else if(dDecOdds * dWinProb <= 1) // non-positive edge ==> no bet return 0; dFracOdds = dDecOdds - 1; dFairOdds = 1/dWinProb - 1; dOddsRatio = Math.Pow(dFairOdds/dFracOdds, dKellyMult); return (1 - dOddsRatio) / (1 + dFracOdds * dOddsRatio); } private double g_US2DEC(double dUSOdds) { if(dUSOdds<0) return 1 - 100 / dUSOdds; else if(dUSOdds>0) return 1 + dUSOdds / 100; else return 1; } private int combin(int n, int k) { if (k > n) { return 0; } long a, b; a = 1; b = 1; int v = n - k; for (int i = v + 1; i < n + 1; i++) { a *= i; } b = factorial(k); a = a / b; return (int)a; } private long factorial(int n) { int i; long a = 1; if (n > 1) { for (i = 2; i < n + 1; i++) a *= i; } return a; }
Last edited by MonkeyF0cker; 05-11-12 at 02:19 AM.
SBR
Bash 2012
Attendee
8/17/2012
Well ported.
Looking over your code I realized my snippet omitted the definition of the combinatorial function (which you helpfully included).
It dawned on me, however, that the function calls are actually unnecessary (and wasteful) as the combinatorial progression can be evaluated recursively by:
combin(n,r) = 1 for r = 0
combin(n,r) = combin(n,r-1) * (n-r+1)/r for r > 0
Modification to C code above shown in red.
|
|
SBR Founder Join Date: 8/28/2005
Great to see you back and am a huge fan of your work. however, you just eliminated 99%+ of the board with that one. additionally, and i fully accept that i am a loser and keep bookies and books in business, but i humble suggest that Kelly is flawed as it is built on the assumption that one can accurately gauge his edge.
keep up the solid work Ganch, asset to the board. I still refer back to your thread defending Parlays, loved it.
SBR POKER TOURNEY7th Place 5/13/2013
Good call. Here's the newly ported C# code...
Code:public const ushort ODDS_ARE_DECIMAL = 0; public const ushort ODDS_ARE_US = 1; public const ushort ODDS_ARE_MIXED = 2; private List<double> KellyVector(List<double> dProbs, List<double> dOdds, int lEvents, double dKellyMult, ushort lOddsType) { int lParlaySize, lParlayNum, lThisAND, lOutcomes, lBetIdx, lResIdx; List<double> dKellyOutVector = new List<double>(); lOutcomes = 1 << lEvents; int[] lCombins = new int[lEvents + 1]; int[] lBetsPerSize = new int[lEvents + 1]; int[] lMapV = new int[lOutcomes]; double[] dSingKellyV = new double[lEvents]; for(int i = 0; i < lEvents; i++) { if (i == 0) { lCombins[0] = 0; lBetsPerSize[0] = 0; } else if (i == 1) { lBetsPerSize[i] = (lEvents - i + 1) / i; lCombins[i] = lCombins[i - 1] + lBetsPerSize[i]; } else { lBetsPerSize[i] = (lEvents - i + 1) / i * lBetsPerSize[i - 1]; lCombins[i] = lCombins[i - 1] + lBetsPerSize[i]; } lMapV[i] = 1 << (lEvents - i - 1); if (dOdds[i] < 0) { dOdds[i] = g_US2DEC(dOdds[i]); } else { switch(lOddsType) { case ODDS_ARE_DECIMAL : break; case ODDS_ARE_US : dOdds[i] = g_US2DEC(dOdds[i]); break; default : if(dOdds[i] >= 100) dOdds[i] = g_US2DEC(dOdds[i]); break; } } dSingKellyV[i] = SBKelly(dProbs[i], dOdds[i], dKellyMult); } lMapV[lOutcomes-1] = 0; lBetsPerSize[lEvents] = 1; lCombins[lEvents] = lOutcomes-1; for(int i = 1; i < lOutcomes; i++) { lParlaySize = lParlayNum = 0; for(int j = 0; j < lEvents; j++) { lThisAND = i & lMapV[j]; lParlayNum += lThisAND; lParlaySize += lThisAND > 0 ? 1 : 0; } lResIdx = lCombins[lParlaySize-1] + lBetsPerSize[lParlaySize] - 1; lMapV[lResIdx] = lParlayNum; lBetsPerSize[lParlaySize]--; dKellyOutVector.Add(1); } for(lResIdx = 0; lResIdx < lOutcomes - 1; lResIdx++) { for(int i = 0; i < lEvents; i++) { lBetIdx = 1 << (lEvents - i - 1); long lMapAnd = lMapV[lResIdx] & lBetIdx; if( lMapAnd > 0) { dKellyOutVector[lResIdx] *= dSingKellyV[i]; } else { dKellyOutVector[lResIdx] *= (1 - dSingKellyV[i]); } } } return dKellyOutVector; } private double SBKelly(double dWinProb, double dDecOdds, double dKellyMult) { double dFracOdds, dFairOdds, dOddsRatio; if(dDecOdds <= 1 || dWinProb > 1 || dWinProb < 0 || dKellyMult <= 0) return 0; else if(dDecOdds * dWinProb <= 1) // non-positive edge ==> no bet return 0; dFracOdds = dDecOdds - 1; dFairOdds = 1/dWinProb - 1; dOddsRatio = Math.Pow(dFairOdds/dFracOdds, dKellyMult); return (1 - dOddsRatio) / (1 + dFracOdds * dOddsRatio); } private double g_US2DEC(double dUSOdds) { if(dUSOdds<0) return 1 - 100 / dUSOdds; else if(dUSOdds>0) return 1 + dUSOdds / 100; else return 1; }
|
|
SBR
Bash 2012
Attendee
8/17/2012
Here's a C# port of the mutually exclusive Kelly staking function. You should be able to port it over to VB pretty easily. There are a couple of classes at the end - the SortableEdge class just makes it easier to sort the edges and the MutualExKellyStakes class combines the string and double (Bet Number and Stakes) variables so both can be returned by the function.
I removed the 2^n parlay ordering so you don't have to sift through null events.
Code:private MutualExKellyStakes calcMutExKelly(List<double> a_dEdge, List<double> a_dOdds, double dKellyMult) { if (a_dEdge.Count != a_dOdds.Count) { MessageBox.Show("Edges and Odds mismatch: The arrays must be the same length."); return null; } if (dKellyMult <= 0) { dKellyMult = 1; } int lSingles = a_dOdds.Count; string[] a_sParlayNames = new string[lSingles]; double[] a_dRealKellyStakes = new double[lSingles]; List<SortableEdge> oSortedByEdge = new List<SortableEdge>(); MutualExKellyStakes mutStakes = new MutualExKellyStakes(); double dTotProb = 0; for (int i = 0; i < lSingles; i++) { double mydProb = edge2prob(a_dEdge[i], a_dOdds[i]); dTotProb += mydProb ; SortableEdge sEdge = new SortableEdge(); sEdge.Id = i; sEdge.Edge = a_dEdge[i]; sEdge.Odds = a_dOdds[i]; sEdge.Probability = mydProb; oSortedByEdge.Add(sEdge); } if (dTotProb > 1 + 1e-6) { MessageBox.Show("Sum of probabilities of mutually exclusive outcomes (" + dTotProb + ") may not be > 1"); return null; } oSortedByEdge.Sort(); double dMinResult = 1, dOverround = 0, dSumProb = 0; for (int i = 0; i < lSingles; i++) { dSumProb += oSortedByEdge[i].Probability; if ( dSumProb > 1 ) { dSumProb = 1; } dOverround += 1 / oSortedByEdge[i].Odds; var dProposedMinResult = (1-dSumProb) / (1-dOverround ); if (dProposedMinResult > 0 && dProposedMinResult < dMinResult) { dMinResult = dProposedMinResult; } } for (int i = 0; i < lSingles; i++) { if (dOverround < 1 && dSumProb >= 1 - 1e-7 ) { a_dRealKellyStakes[i] = oSortedByEdge[i].Probability; } else { a_dRealKellyStakes[i] = Math.Max(0, oSortedByEdge[i].Probability - dMinResult / oSortedByEdge[i].Odds); } a_sParlayNames[i] = "" + (1 + oSortedByEdge[i].Id); } mutStakes.Names = a_sParlayNames; mutStakes.KellyStakes = a_dRealKellyStakes; return mutStakes; } private double edge2prob(double edge, double odds) { return (1 + edge) / odds; } public class MutualExKellyStakes { public string[] Names; public double[] KellyStakes; } public class SortableEdge : IComparable { public int Id; public double Edge, Odds, Probability; public SortableEdge() { } public int CompareTo(object obj) { if (obj == null) return 1; SortableEdge a = obj as SortableEdge; if (a != null) return -(this.Edge.CompareTo(a.Edge)); else throw new ArgumentException("Object is not a SortableEdge class member."); } }
Last edited by MonkeyF0cker; 05-18-12 at 01:34 AM.
|
|
SBR
Bash 2012
Attendee
8/17/2012
That was written in .NET4. So, if you're porting to an earlier version of .NET, you may have to change the IComparable routine.
SBR
Bash 2012
Attendee
8/17/2012
SBR Founder Join Date: 8/28/2005