├── README.md └── real_kelly-independent_concurrent_outcomes-.py /README.md: -------------------------------------------------------------------------------- 1 | # real_kelly-concurrent- 2 | The generalised Kelly Criterion (a.k.a. The Real Kelly) for concurrent events. 3 | 4 | An implementation of the generalised Kelly Criterion (a.k.a. The Real Kelly) for independent, concurrent events as discussed in this article https://www.pinnacle.com/en/betting-articles/Betting-Strategy/the-real-kelly-criterion/HZKJTFCB3KNYN9CJ 5 | 6 | The algorithm will find optimal bet sizes for a set of concurrent singles and/or 'round robin' combinations of parlays or teasers. 7 | -------------------------------------------------------------------------------- /real_kelly-independent_concurrent_outcomes-.py: -------------------------------------------------------------------------------- 1 | import math 2 | import time 3 | import itertools 4 | import numpy as np 5 | import scipy.optimize 6 | from datetime import datetime 7 | from collections import defaultdict 8 | from scipy.optimize import NonlinearConstraint 9 | 10 | 11 | def optimize(selections: list, bankroll: float, max_multiple: int): 12 | 13 | start_time = time.time() 14 | 15 | # MAXIMUM TEAMS IN A MULTIPLE MUST NOT EXCEED LEN(SELECTIONS) 16 | if max_multiple > len(selections): 17 | print(f'Error: Maximum multiple must not exceed {len(selections)}') 18 | return None 19 | 20 | # CREATE A MATRIX OF POSSIBLE COMBINATIONS AND A PROBABILITY VECTOR OF SIZE LEN(COMBINATIONS) 21 | combinations, probs = list(), list() 22 | for c in range(0, len(selections) + 1): 23 | for subset in itertools.combinations(selections, c): 24 | combination, prob = list(), 1.00 25 | for selection in selections: 26 | if selection in subset: 27 | combination.append(1) 28 | prob *= 1 / selection['odds_fair'] 29 | else: 30 | combination.append(0) 31 | prob *= (1 - 1 / selection['odds_fair']) 32 | combinations.append(combination) 33 | probs.append(prob) 34 | 35 | # CREATE A MATRIX OF POSSIBLE SINGLES & MULTIPLES 36 | bets, book_odds = list(), list() 37 | for multiple in range(1, max_multiple + 1): 38 | for subset in itertools.combinations(selections, multiple): 39 | bet, prod = list(), 1.00 40 | for selection in selections: 41 | if selection in subset: 42 | bet.append(1) 43 | prod *= selection['odds_book'] 44 | else: 45 | bet.append(0) 46 | bets.append(bet) 47 | book_odds.append(prod) 48 | 49 | # CACHE WINNING BETS 50 | winning_bets = defaultdict(list) 51 | for index_combination, combination in enumerate(combinations): 52 | for index_bet, bet in enumerate(bets): 53 | if sum([c * b for c, b in zip(combination, bet)]) == sum(bet): 54 | winning_bets[index_bet].append(index_combination) 55 | 56 | def f(stakes): 57 | """ This function will be called by scipy.optimize.minimize repeatedly to find the global maximum """ 58 | 59 | # INITIALIZE END_BANKROLLS AND OBJECTIVE BEFORE EACH OPTIMIZATION STEP 60 | objective, end_bankrolls = 0.00, len(combinations) * [bankroll - np.sum(stakes)] 61 | 62 | for index_bet, index_combinations in winning_bets.items(): 63 | for index_combination in index_combinations: 64 | end_bankrolls[index_combination] += stakes[index_bet] * book_odds[index_bet] 65 | 66 | # RETURN THE OBJECTIVE AS A SUMPRODUCT OF PROBABILITIES AND END_BANKROLLS - THIS IS THE FUNCTION TO BE MAXIMIZED 67 | return -sum([p * e for p, e in zip(probs, np.log(end_bankrolls))]) 68 | 69 | def constraint(stakes): 70 | """ Sum of all stakes must not exceed bankroll """ 71 | return sum(stakes) 72 | 73 | # FIND THE GLOBAL MAXIMUM USING SCIPY'S CONSTRAINED MINIMIZATION 74 | bounds = list(zip(len(bets) * [0], len(bets) * [bankroll])) 75 | nlc = NonlinearConstraint(constraint, -np.inf, bankroll) 76 | res = scipy.optimize.differential_evolution(func=f, bounds=bounds, constraints=(nlc)) 77 | 78 | runtime = time.time() - start_time 79 | print(f"\n{datetime.now().replace(microsecond=0)} - Optimization finished. Runtime --- {round(runtime, 3)} seconds ---\n") 80 | print(f"Objective: {round(res.fun, 5)}") 81 | print(f"Certainty Equivalent: {round(math.exp(-res.fun), 3)}\n") 82 | 83 | # CONSOLE OUTPUT 84 | for index_bet, bet in enumerate(bets): 85 | bet_strings = list() 86 | for index_sel, sel in enumerate(bet): 87 | if sel == 1: 88 | bet_strings.append(selections[index_sel]['name']) 89 | 90 | stake = res.x[index_bet] 91 | if stake >= 0.50: 92 | print(f"{(' / ').join(bet_strings)} @{round(book_odds[index_bet], 3)} - € {int(round(stake, 0))}") 93 | 94 | selections = list() 95 | selections.append({'name': 'BET 1', 'odds_book': 2.05, 'odds_fair': 1.735}) 96 | selections.append({'name': 'BET 2', 'odds_book': 1.95, 'odds_fair': 1.656}) 97 | selections.append({'name': 'BET 3', 'odds_book': 1.75, 'odds_fair': 1.725}) 98 | selections.append({'name': 'BET 4', 'odds_book': 1.88, 'odds_fair': 1.757}) 99 | selections.append({'name': 'BET 5', 'odds_book': 1.99, 'odds_fair': 1.787}) 100 | selections.append({'name': 'BET 6', 'odds_book': 2.11, 'odds_fair': 1.794}) 101 | 102 | optimize(selections=selections, bankroll=2500, max_multiple=2) 103 | 104 | 105 | --------------------------------------------------------------------------------