Skip to content

Commit

Permalink
Kellycriterion (kyleskom#230)
Browse files Browse the repository at this point in the history
* refactored XGBoost color conditions, added kelly criterion file

* reverted some color logic, correct KC formula

* tweaked ReadMe, tested KC, updated NN_runner to have KC

* removed debug print statement, updated readme

* Added feature flag for kelly criterion - optionally available with -kc cli argument
  • Loading branch information
timseymour42 authored May 5, 2023
1 parent a297aa7 commit 05f71ed
Show file tree
Hide file tree
Showing 7 changed files with 81 additions and 29 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# NBA Sports Betting Using Machine Learning 🏀
<img src="https://github.com/kyleskom/NBA-Machine-Learning-Sports-Betting/blob/master/Screenshots/output.png" width="1010" height="292" />

A machine learning AI used to predict the winners and under/overs of NBA games. Takes all team data from the 2007-08 season to current season, matched with odds of those games, using a neural network to predict winning bets for today's games. Achieves ~75% accuracy on money lines and ~58% on under/overs. Outputs expected value for teams money lines to provide better insight.
A machine learning AI used to predict the winners and under/overs of NBA games. Takes all team data from the 2007-08 season to current season, matched with odds of those games, using a neural network to predict winning bets for today's games. Achieves ~75% accuracy on money lines and ~58% on under/overs. Outputs expected value for teams money lines to provide better insight. The fraction of your bankroll to bet based on the Kelly Criterion is also outputted. Note that a popular, less risky approach is to bet 50% of the stake recommended by the Kelly Criterion.
## Packages Used

Use Python 3.8. In particular the packages/libraries used are...
Expand Down Expand Up @@ -32,6 +32,8 @@ Odds data will be automatically fetched from sbrodds if the -odds option is prov

If `-odds` is not given, enter the under/over and odds for today's games manually after starting the script.

Optionally, you can add '-kc' as a command line argument to see the recommended fraction of your bankroll to wager based on the model's edge

## Flask Web App
<img src="https://github.com/kyleskom/NBA-Machine-Learning-Sports-Betting/blob/master/Screenshots/Flask-App.png" width="922" height="580" />

Expand Down
25 changes: 25 additions & 0 deletions Tests/Kelly_Criterion_Test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import unittest
from src.Utils import Kelly_Criterion as kc


class TestKellyCriterion(unittest.TestCase):

def test_calculate_kelly_criterion_1(self):
result = kc.calculate_kelly_criterion(-110, .6)
self.assertEqual(result, 16.04)

def test_calculate_kelly_criterion_2(self):
result = kc.calculate_kelly_criterion(-110, .4)
self.assertEqual(result, 0)

def test_calculate_kelly_criterion_3(self):
result = kc.calculate_kelly_criterion(400, .35)
self.assertEqual(result, 18.75)

def test_calculate_kelly_criterion_4(self):
result = kc.calculate_kelly_criterion(-500, .85)
self.assertEqual(result, 10)

def test_calculate_kelly_criterion_5(self):
result = kc.calculate_kelly_criterion(100, .99)
self.assertEqual(result, 98)
9 changes: 5 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,19 +105,19 @@ def main():
if args.nn:
print("------------Neural Network Model Predictions-----------")
data = tf.keras.utils.normalize(data, axis=1)
NN_Runner.nn_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds)
NN_Runner.nn_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds, args.kc)
print("-------------------------------------------------------")
if args.xgb:
print("---------------XGBoost Model Predictions---------------")
XGBoost_Runner.xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds)
XGBoost_Runner.xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds, args.kc)
print("-------------------------------------------------------")
if args.A:
print("---------------XGBoost Model Predictions---------------")
XGBoost_Runner.xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds)
XGBoost_Runner.xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds, args.kc)
print("-------------------------------------------------------")
data = tf.keras.utils.normalize(data, axis=1)
print("------------Neural Network Model Predictions-----------")
NN_Runner.nn_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds)
NN_Runner.nn_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds, args.kc)
print("-------------------------------------------------------")


Expand All @@ -127,5 +127,6 @@ def main():
parser.add_argument('-nn', action='store_true', help='Run with Neural Network Model')
parser.add_argument('-A', action='store_true', help='Run all Models')
parser.add_argument('-odds', help='Sportsbook to fetch from. (fanduel, draftkings, betmgm, pointsbet, caesars, wynn, bet_rivers_ny')
parser.add_argument('-kc', action='store_true', help='Calculates percentage of bankroll to bet based on model edge')
args = parser.parse_args()
main()
1 change: 0 additions & 1 deletion src/DataProviders/SbrOddsProvider.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ class SbrOddsProvider:
"""

def __init__(self, sportsbook="fanduel"):

sb = Scoreboard(sport="NBA")
self.games = sb.games if hasattr(sb, 'games') else []
self.sportsbook = sportsbook
Expand Down
30 changes: 17 additions & 13 deletions src/Predict/NN_Runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from colorama import Fore, Style, init, deinit
from tensorflow.keras.models import load_model
from src.Utils import Expected_Value
from src.Utils import Kelly_Criterion as kc

init()
model = load_model('Models/NN_Models/Trained-Model-ML-1680133120.689445')
ou_model = load_model("Models/NN_Models/Trained-Model-OU-1680133008.6887271")


def nn_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds):
def nn_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds, kelly_criterion):
ml_predictions_array = []

for row in data:
Expand Down Expand Up @@ -57,23 +58,26 @@ def nn_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_
print(Fore.RED + home_team + Style.RESET_ALL + ' vs ' + Fore.GREEN + away_team + Style.RESET_ALL + Fore.CYAN + f" ({winner_confidence}%)" + Style.RESET_ALL + ': ' +
Fore.BLUE + 'OVER ' + Style.RESET_ALL + str(todays_games_uo[count]) + Style.RESET_ALL + Fore.CYAN + f" ({un_confidence}%)" + Style.RESET_ALL)
count += 1

print("--------------------Expected Value---------------------")
if kelly_criterion:
print("------------Expected Value & Kelly Criterion-----------")
else:
print("---------------------Expected Value--------------------")
count = 0
for game in games:
home_team = game[0]
away_team = game[1]
ev_home = float(Expected_Value.expected_value(ml_predictions_array[count][0][1], int(home_team_odds[count])))
ev_away = float(Expected_Value.expected_value(ml_predictions_array[count][0][0], int(away_team_odds[count])))
if ev_home > 0:
print(home_team + ' EV: ' + Fore.GREEN + str(ev_home) + Style.RESET_ALL)
else:
print(home_team + ' EV: ' + Fore.RED + str(ev_home) + Style.RESET_ALL)
ev_home = ev_away = 0
if home_team_odds[count] and away_team_odds[count]:
ev_home = float(Expected_Value.expected_value(ml_predictions_array[count][0][1], int(home_team_odds[count])))
ev_away = float(Expected_Value.expected_value(ml_predictions_array[count][0][0], int(away_team_odds[count])))
expected_value_colors = {'home_color': Fore.GREEN if ev_home > 0 else Fore.RED,
'away_color': Fore.GREEN if ev_away > 0 else Fore.RED}
bankroll_descriptor = ' Fraction of Bankroll: '
bankroll_fraction_home = bankroll_descriptor + str(kc.calculate_kelly_criterion(home_team_odds[count], ml_predictions_array[count][0][1])) + '%'
bankroll_fraction_away = bankroll_descriptor + str(kc.calculate_kelly_criterion(away_team_odds[count], ml_predictions_array[count][0][0])) + '%'

if ev_away > 0:
print(away_team + ' EV: ' + Fore.GREEN + str(ev_away) + Style.RESET_ALL)
else:
print(away_team + ' EV: ' + Fore.RED + str(ev_away) + Style.RESET_ALL)
print(home_team + ' EV: ' + expected_value_colors['home_color'] + str(ev_home) + Style.RESET_ALL + (bankroll_fraction_home if kelly_criterion else ''))
print(away_team + ' EV: ' + expected_value_colors['away_color'] + str(ev_away) + Style.RESET_ALL + (bankroll_fraction_away if kelly_criterion else ''))
count += 1

deinit()
24 changes: 14 additions & 10 deletions src/Predict/XGBoost_Runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import xgboost as xgb
from colorama import Fore, Style, init, deinit
from src.Utils import Expected_Value
from src.Utils import Kelly_Criterion as kc


# from src.Utils.Dictionaries import team_index_current
Expand All @@ -16,7 +17,7 @@
xgb_uo.load_model('Models/XGBoost_Models/XGBoost_54.8%_UO-8.json')


def xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds):
def xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team_odds, kelly_criterion):
ml_predictions_array = []

for row in data:
Expand Down Expand Up @@ -69,7 +70,11 @@ def xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team
Fore.BLUE + 'OVER ' + Style.RESET_ALL + str(
todays_games_uo[count]) + Style.RESET_ALL + Fore.CYAN + f" ({un_confidence}%)" + Style.RESET_ALL)
count += 1
print("--------------------Expected Value---------------------")

if kelly_criterion:
print("------------Expected Value & Kelly Criterion-----------")
else:
print("---------------------Expected Value--------------------")
count = 0
for game in games:
home_team = game[0]
Expand All @@ -78,15 +83,14 @@ def xgb_runner(data, todays_games_uo, frame_ml, games, home_team_odds, away_team
if home_team_odds[count] and away_team_odds[count]:
ev_home = float(Expected_Value.expected_value(ml_predictions_array[count][0][1], int(home_team_odds[count])))
ev_away = float(Expected_Value.expected_value(ml_predictions_array[count][0][0], int(away_team_odds[count])))
if ev_home > 0:
print(home_team + ' EV: ' + Fore.GREEN + str(ev_home) + Style.RESET_ALL)
else:
print(home_team + ' EV: ' + Fore.RED + str(ev_home) + Style.RESET_ALL)
expected_value_colors = {'home_color': Fore.GREEN if ev_home > 0 else Fore.RED,
'away_color': Fore.GREEN if ev_away > 0 else Fore.RED}
bankroll_descriptor = ' Fraction of Bankroll: '
bankroll_fraction_home = bankroll_descriptor + str(kc.calculate_kelly_criterion(home_team_odds[count], ml_predictions_array[count][0][1])) + '%'
bankroll_fraction_away = bankroll_descriptor + str(kc.calculate_kelly_criterion(away_team_odds[count], ml_predictions_array[count][0][0])) + '%'

if ev_away > 0:
print(away_team + ' EV: ' + Fore.GREEN + str(ev_away) + Style.RESET_ALL)
else:
print(away_team + ' EV: ' + Fore.RED + str(ev_away) + Style.RESET_ALL)
print(home_team + ' EV: ' + expected_value_colors['home_color'] + str(ev_home) + Style.RESET_ALL + (bankroll_fraction_home if kelly_criterion else ''))
print(away_team + ' EV: ' + expected_value_colors['away_color'] + str(ev_away) + Style.RESET_ALL + (bankroll_fraction_away if kelly_criterion else ''))
count += 1

deinit()
17 changes: 17 additions & 0 deletions src/Utils/Kelly_Criterion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
def american_to_decimal(american_odds):
"""
Converts American odds to decimal odds (European odds).
"""
if american_odds >= 100:
decimal_odds = (american_odds / 100)
else:
decimal_odds = (100 / abs(american_odds))
return round(decimal_odds, 2)

def calculate_kelly_criterion(american_odds, model_prob):
"""
Calculates the fraction of the bankroll to be wagered on each bet
"""
decimal_odds = american_to_decimal(american_odds)
bankroll_fraction = round((100 * (decimal_odds * model_prob - (1 - model_prob))) / decimal_odds, 2)
return bankroll_fraction if bankroll_fraction > 0 else 0

0 comments on commit 05f71ed

Please sign in to comment.