├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .idea ├── WagerBrain.iml ├── dataSources.xml ├── encodings.xml ├── misc.xml ├── modules.xml ├── other.xml └── vcs.xml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md └── WagerBrain ├── __init__.py ├── bankroll.py ├── data ├── MLB Odds Complete.csv ├── NBA Odds Complete.csv ├── NFL Odds Complete.csv ├── mlb_team_data_1970_2014.db ├── nba_team_data_1980_2014.db ├── nfl_team_data_2000_2015.db ├── nhl_team_data_2006_2015.db └── scraper.py ├── odds.py ├── payouts.py ├── probs.py ├── strats ├── arb.py └── value.py ├── tests ├── __init__.py ├── test_odds.py ├── test_payouts.py └── test_probs.py └── utils.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.idea/WagerBrain.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | -------------------------------------------------------------------------------- /.idea/dataSources.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | sqlite.xerial 6 | true 7 | org.sqlite.JDBC 8 | jdbc:sqlite:$PROJECT_DIR$/WagerBrain/data/mlb_team_data_1970_2014.db 9 | 10 | 11 | 12 | 13 | 14 | file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.25.1/sqlite-jdbc-3.25.1.jar 15 | 16 | 17 | file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.25.1/license.txt 18 | 19 | 20 | 21 | 22 | sqlite.xerial 23 | true 24 | org.sqlite.JDBC 25 | jdbc:sqlite:$PROJECT_DIR$/WagerBrain/data/nfl_team_data_2000_2015.db 26 | 27 | 28 | 29 | 30 | 31 | file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.25.1/sqlite-jdbc-3.25.1.jar 32 | 33 | 34 | file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.25.1/license.txt 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | ApexVCS 8 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/other.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at sedemmler@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This should be simple. Anything proposed on the readme list is fair game, as are improvements to what exists. 2 | Anything that improves what I have done is wonderful. 3 | Everything should try to be written as brief and efficient as possible. 4 | Everything shoud be written as discrete as possible with flexibility of use in mind. 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Steven Demmler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WagerBrain 2 | A package containing the essential math and tools required for sports betting and gambling. Once you've scraped odds from Covers.com, Pinnacle, Betfair, or wherever, import WagerBrain and start hunting for value bets. 3 | 4 | ![Image of The Big Board](https://miro.medium.com/max/1312/1*bGOGcEPpsa0tetM5u-J9NA.jpeg) 5 | 6 | **Phase 1 (_complete_):** 7 | - Convert Odds between American, Decimal, Fractional 8 | - Convert Odds to Implied Win Probabilities and back to Odds 9 | - Calculate Profit and Total Payouts 10 | - Calculate Expected Value 11 | - Calculate Kelly Criterion 12 | - Calculate Parlay Odds, Total Payout, Profit 13 | 14 | 15 | **Phase 2 (_complete_):** 16 | - Evaluate Wager-Arbitrage Opportunities 17 | - Calculate bookmaker spread/cost 18 | - Calculate the Bookmaker's Vig 19 | - Calculate Win Probability from a team's ELO (538-style) 20 | 21 | 22 | **Phase 3 (_in progress_):** 23 | - Scrapers to gather data (Basketball Reference, KenPom etc.) [_Partially implemented_] 24 | - Value Bets (take in sets of odds, probabilities and output the most effective betting implementation) 25 | - Scan for Arbitrage (search scrape bookmakers to feed into Phase 2's Arbitrage evaluator) 26 | 27 | 28 | # Examples 29 | 30 | Parlay 3 wagers from different sites offering different odds-styles: 31 | ``` 32 | odds = [1.91, -110, '9/10'] 33 | parlay_odds(odds) 34 | >>>> 6.92 35 | ``` 36 | No clue how to read decimal odds because you're American? (wager * decimals odds, though...super simple), then convert them back to American-style odds: 37 | ``` 38 | american_odds(6.92) 39 | >>>> +592 40 | ``` 41 | What's the Vig on the Yankees vs Dodgers? 42 | ``` 43 | Yankees -115 44 | Dodgers +105 45 | Betting 115 to win 100 on Yankees 46 | Betting 100 to win 205 on Dodgers 47 | 48 | vig(115,215,100,205) 49 | >>>> 2.26% 50 | ``` 51 | Arbitrage Example 52 | ``` 53 | 5Dimes Pinnacle 54 | Djokovic *1.360* 1.189 55 | Nadal 3.170 *5.500* 56 | 57 | odds = [1.36, 5.5] 58 | stake = 1000 59 | basic_arbitrage(odds, stake) 60 | 61 | >>>> Bet $801.53 on Djokovic 62 | >>>> Bet $198.47 on Nadal 63 | >>>> Win $90.51 regardless of the outcome 64 | ``` 65 | KenPom NCAAB Scraper 66 | ``` 67 | ken_pom_scrape() 68 | >>>> 69 | Rk Team Conf ... OppO OppD NCOS AdjEM 70 | 0 1.0 Kansas B12 ... 107.4 94.7 9.58 71 | 1 2.0 Gonzaga WCC ... 103.5 101.0 -2.09 72 | 2 3.0 Baylor B12 ... 106.4 96.2 1.38 73 | 3 4.0 Dayton A10 ... 104.1 101.3 -0.74 74 | 4 5.0 Duke ACC ... 106.0 98.7 2.60 75 | .. ... ... ... ... ... ... ... 76 | 364 349.0 Maryland Eastern Shore MEAC ... 97.6 104.1 7.78 77 | 365 350.0 Howard MEAC ... 96.7 105.0 0.96 78 | 366 351.0 Mississippi Valley St. SWAC ... 97.8 103.9 5.14 79 | 367 352.0 Kennesaw St. ASun ... 102.0 103.7 4.10 80 | 368 353.0 Chicago St. WAC ... 100.6 104.3 -0.75 81 | ``` 82 | -------------------------------------------------------------------------------- /WagerBrain/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sedemmler/WagerBrain/b1cc33f5eb7a6130106bf8251b554718e2d22172/WagerBrain/__init__.py -------------------------------------------------------------------------------- /WagerBrain/bankroll.py: -------------------------------------------------------------------------------- 1 | from WagerBrain.odds import decimal_odds 2 | """ 3 | 4 | Bankroll management functions 5 | 6 | """ 7 | 8 | 9 | def basic_kelly_criterion(prob, odds, kelly_size=1): 10 | """ 11 | :param prob: Float. Estimated probability of winning the wager 12 | :param odds: Integer (American), Float(Decimal), String or Fraction Class (Fractional). Stated odds from bookmaker 13 | :param kelly_size: Integer. Risk management. (e.g., 1 is Kelly Criterion, .5 is Half Kelly, 2+ is Levered Kelly) 14 | :return: Float. % of bankroll one should commit to wager 15 | """ 16 | b = decimal_odds(odds) - 1 17 | q = 1 - prob 18 | return ((b * q - prob) / b) * kelly_size 19 | 20 | 21 | def fibonacci_bankroll(odds, bet_num=1, unit_size=.01): 22 | """ 23 | :param odds: Integer (American), Float(Decimal), String or Fraction Class (Fractional). Stated odds from bookmaker 24 | :param bet_num: Integer. How many bets you've made so far 25 | :param unit_size: Float. % of bankroll wagered per bet 26 | :return: Float. Fibonacci multiplied bet size 27 | """ 28 | fib = [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 100] 29 | if bet_num > len(fib): 30 | bet_num = fib[-1] 31 | 32 | if decimal_odds(odds) > 2.618: 33 | return unit_size * fib[bet_num] 34 | else: 35 | return None 36 | 37 | 38 | def five_bet_labouchere_bankroll(target): 39 | """ 40 | Pick a target $-amount you want to win. and then make a sequence of bets that follows the ratio output below. 41 | 42 | Each wager must be at odds such that the profit (payout) is the sum of the first and last number in your list. 43 | If bet 1 wins 20 (.1 + .1) then cross off the first and last numbers. 44 | 45 | If you lose add your stake to the list below. 46 | 47 | :param target: Integer. How much do you want to win? 48 | :return: List. Sequence of $ amount bets to make. 49 | """ 50 | labouchere_div = [.1, .2, .4, .2, .1] 51 | return [target * x for x in labouchere_div] 52 | 53 | -------------------------------------------------------------------------------- /WagerBrain/data/mlb_team_data_1970_2014.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sedemmler/WagerBrain/b1cc33f5eb7a6130106bf8251b554718e2d22172/WagerBrain/data/mlb_team_data_1970_2014.db -------------------------------------------------------------------------------- /WagerBrain/data/nba_team_data_1980_2014.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sedemmler/WagerBrain/b1cc33f5eb7a6130106bf8251b554718e2d22172/WagerBrain/data/nba_team_data_1980_2014.db -------------------------------------------------------------------------------- /WagerBrain/data/nfl_team_data_2000_2015.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sedemmler/WagerBrain/b1cc33f5eb7a6130106bf8251b554718e2d22172/WagerBrain/data/nfl_team_data_2000_2015.db -------------------------------------------------------------------------------- /WagerBrain/data/nhl_team_data_2006_2015.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sedemmler/WagerBrain/b1cc33f5eb7a6130106bf8251b554718e2d22172/WagerBrain/data/nhl_team_data_2006_2015.db -------------------------------------------------------------------------------- /WagerBrain/data/scraper.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from bs4 import BeautifulSoup 3 | import pandas as pd 4 | 5 | 6 | def ken_pom_scrape(): 7 | """ 8 | :return: DataFrame of KenPom's complete College Rankings / Advanced Stats 9 | """ 10 | url = 'https://kenpom.com/' 11 | r = requests.get(url) 12 | soup = BeautifulSoup(r.content, 'lxml') 13 | 14 | table = soup.table 15 | table_rows = table.find_all('tr') 16 | 17 | team_rows = list() 18 | for tr in table_rows: 19 | td = tr.find_all('td') 20 | row = [i.text for i in td] 21 | team_rows.append(row) 22 | 23 | cols = ["Rk", "Team", "Conf", "Record", "AdjEM", "AdjO", "a", "AdjD", "b", "AdjT", "c", "Luck", "d", 24 | "SoS AdjEM", "e", "OppO", "f", "OppD", "g", "NCOS AdjEM", "h"] 25 | 26 | ken_pom = pd.DataFrame(team_rows[2:], columns=cols) 27 | 28 | ken_pom.drop(['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'], axis=1, inplace=True) 29 | 30 | ken_pom['Rk'] = ken_pom['Rk'].astype('float') 31 | ken_pom['AdjEM'] = ken_pom['AdjEM'].astype('float') 32 | ken_pom['AdjO'] = ken_pom['AdjO'].astype('float') 33 | ken_pom['AdjD'] = ken_pom['AdjD'].astype('float') 34 | ken_pom['AdjT'] = ken_pom['AdjT'].astype('float') 35 | ken_pom['Luck'] = ken_pom['Luck'].astype('float') 36 | ken_pom['SoS AdjEM'] = ken_pom['SoS AdjEM'].astype('float') 37 | ken_pom['OppO'] = ken_pom['OppO'].astype('float') 38 | ken_pom['OppD'] = ken_pom['OppD'].astype('float') 39 | ken_pom['NCOS AdjEM'] = ken_pom['NCOS AdjEM'].astype('float') 40 | 41 | return ken_pom 42 | 43 | 44 | def bball_ref_per100(): 45 | url = 'https://www.basketball-reference.com/leagues/NBA_2020_per_poss.html' 46 | r = requests.get(url) 47 | soup = BeautifulSoup(r.content, 'lxml') 48 | 49 | table = soup.table 50 | table_rows = table.find_all('tr') 51 | 52 | team_rows = list() 53 | for tr in table_rows: 54 | td = tr.find_all('td') 55 | row = [i.text for i in td] 56 | team_rows.append(row) 57 | 58 | bball_ref_cols = ['Player', 'Pos', 'Age', 'Tm', 'G', 'GS', 'MP', 'FG', 'FGA', 'FG_perc', '3P', '3PA', 59 | '3_perc', '2P', '2PA', '2_perc', 'FT', 'FTA', 'FT_perc', 'ORB', 'DRB', 'TRB', 'AST', 60 | 'STL', 'BLK', 'TOV', 'PF', 'PTS', 'None', 'ORtg', 'DRtg'] 61 | 62 | per_100_poss = pd.DataFrame(team_rows, columns=bball_ref_cols) 63 | 64 | per_100_poss = per_100_poss.mask(per_100_poss.eq('None')).dropna() 65 | 66 | per_100_poss['Age'] = per_100_poss['Age'].astype('float') 67 | per_100_poss['G'] = per_100_poss['G'].astype('float') 68 | per_100_poss['GS'] = per_100_poss['GS'].astype('float') 69 | per_100_poss['MP'] = per_100_poss['MP'].astype('float') 70 | per_100_poss['FG'] = per_100_poss['FG'].astype('float') 71 | per_100_poss['FGA'] = per_100_poss['FGA'].astype('float') 72 | # per_100_poss['FG_perc'] = per_100_poss['FG_perc'].astype('float') 73 | per_100_poss['3P'] = per_100_poss['3P'].astype('float') 74 | per_100_poss['3PA'] = per_100_poss['3PA'].astype('float') 75 | # per_100_poss['3_perc'] = per_100_poss['3_perc'].astype('float') 76 | per_100_poss['2P'] = per_100_poss['2P'].astype('float') 77 | per_100_poss['2PA'] = per_100_poss['2PA'].astype('float') 78 | # per_100_poss['2_perc'] = per_100_poss['2_perc'].astype('float') 79 | per_100_poss['FT'] = per_100_poss['FT'].astype('float') 80 | per_100_poss['FTA'] = per_100_poss['FTA'].astype('float') 81 | # per_100_poss['FT_perc'] = per_100_poss['FT_perc'].astype('float') 82 | per_100_poss['ORB'] = per_100_poss['ORB'].astype('float') 83 | per_100_poss['DRB'] = per_100_poss['DRB'].astype('float') 84 | per_100_poss['TRB'] = per_100_poss['TRB'].astype('float') 85 | per_100_poss['AST'] = per_100_poss['AST'].astype('float') 86 | per_100_poss['STL'] = per_100_poss['STL'].astype('float') 87 | per_100_poss['BLK'] = per_100_poss['BLK'].astype('float') 88 | per_100_poss['TOV'] = per_100_poss['TOV'].astype('float') 89 | per_100_poss['PF'] = per_100_poss['PF'].astype('float') 90 | per_100_poss['PTS'] = per_100_poss['PTS'].astype('float') 91 | # per_100_poss['ORtg'] = per_100_poss['ORtg'].astype('float') 92 | per_100_poss['DRtg'] = per_100_poss['DRtg'].astype('float') 93 | 94 | return per_100_poss 95 | 96 | 97 | def bball_ref_adv(): 98 | url = 'https://www.basketball-reference.com/leagues/NBA_2020_advanced.html' 99 | r = requests.get(url) 100 | soup = BeautifulSoup(r.content, 'lxml') 101 | 102 | table = soup.table 103 | table_rows = table.find_all('tr') 104 | 105 | team_rows = list() 106 | for tr in table_rows: 107 | td = tr.find_all('td') 108 | row = [i.text for i in td] 109 | team_rows.append(row) 110 | 111 | cols = ['Player', 'Pos', 'Age', 'Tm', 'G', 'MP', 'PER', 'TS%', '3PAr', 'FTr', 'ORB%', 'DRB%', 'TRB%', 'AST%', 112 | 'STL%', 'BLK%', 'TOV%', 'USG%', "NONE", 'OWS', 'DWS', 'WS', 'WS_48', 'NONE', 'OBPM', 'DBPM', 'BPM', 'VORP'] 113 | 114 | adv = pd.DataFrame(team_rows, columns=cols) 115 | 116 | adv = adv.mask(adv.eq('NONE')).dropna() 117 | 118 | return adv 119 | -------------------------------------------------------------------------------- /WagerBrain/odds.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from math import gcd 3 | import numpy as np 4 | 5 | 6 | """ 7 | Convert the style of gambling odds to Function Name (Decimal, American, Fractional). 8 | 9 | TO DO: Fix edge case related to Fraction module that causes weird rounding / slightly off output 10 | """ 11 | 12 | 13 | def american_odds(odds): 14 | """ 15 | :param odds: Float (e.g., 2.25) or String (e.g., '3/1' or '5/4'). 16 | :return: Integer. Odds expressed in American terms. 17 | """ 18 | if isinstance(odds, int): 19 | return odds 20 | 21 | elif isinstance(odds, float): 22 | if odds > 2.0: 23 | return round((odds - 1) * 100, 0) 24 | else: 25 | return round(-100 / (odds - 1), 0) 26 | 27 | elif "/" in odds: 28 | odds = Fraction(odds) 29 | 30 | if odds.numerator > odds.denominator: 31 | return (odds.numerator / odds.denominator) * 100 32 | else: 33 | return -100 / (odds.numerator / odds.denominator) 34 | 35 | 36 | def decimal_odds(odds): 37 | """ 38 | :param odds: Integer (e.g., -350) or String (e.g., '3/1' or '5/4'). 39 | :return: Float. Odds expressed in Decimal terms. 40 | """ 41 | if isinstance(odds, float): 42 | return odds 43 | 44 | elif isinstance(odds, int): 45 | if odds >= 100: 46 | return abs(1 + (odds / 100)) 47 | elif odds <= -101 : 48 | return 100 / abs(odds) + 1 49 | else: 50 | return float(odds) 51 | 52 | elif "/" in odds: 53 | odds = Fraction(odds) 54 | return round((odds.numerator / odds.denominator) + 1, 2) 55 | 56 | 57 | def fractional_odds(odds): 58 | """ 59 | :param odds: Numeric. (e.g., 2.25 or -350). 60 | :return: Fraction Class. Odds expressed in Fractional terms. 61 | """ 62 | if isinstance(odds, str): 63 | return Fraction(odds) 64 | 65 | elif isinstance(odds, int): 66 | if odds > 0: 67 | denom = 100 68 | g_cd = gcd(odds, denom) 69 | num = int(odds / g_cd) 70 | denom = int(denom / g_cd) 71 | 72 | return Fraction(num, denom) 73 | 74 | else: 75 | num = 100 76 | g_cd = gcd(num, odds) 77 | num = int(num / g_cd) 78 | denom = int(odds / g_cd) 79 | 80 | return -Fraction(num, denom) 81 | 82 | elif isinstance(odds, float): 83 | new_odds = int((odds - 1) * 100) 84 | g_cd = gcd(new_odds, 100) 85 | return Fraction(int(new_odds/g_cd), int(100/g_cd)) 86 | 87 | 88 | def parlay_odds(odds): 89 | """ 90 | :param odds: List. A list of odds for wagers to be included in parlay 91 | :return: Parlay odds in Decimal terms 92 | """ 93 | return np.prod(np.array([decimal_odds(x) for x in odds])) 94 | 95 | 96 | def convert_odds(odds, odds_style='a'): 97 | """ 98 | :param odds: Stated odds from bookmaker (American, Decimal, or Fractional) 99 | :param odds_style: American ('a', 'amer', 'american'), Decimal ('d', dec','decimal) Fractional ('f','frac','fractional) 100 | :return: Numeric. Odds converted to selected style. 101 | """ 102 | try: 103 | if odds_style.lower() == "american" or odds_style.lower() == 'amer' or odds_style.lower() == 'a': 104 | return american_odds(odds) 105 | 106 | elif odds_style.lower() == "decimal" or odds_style.lower() == 'dec' or odds_style.lower() == 'd': 107 | return decimal_odds(odds) 108 | 109 | elif odds_style.lower() == "fractional" or odds_style.lower() == 'frac' or odds_style.lower() == 'f': 110 | return fractional_odds(odds) 111 | 112 | except (ValueError, KeyError, NameError): 113 | return None 114 | -------------------------------------------------------------------------------- /WagerBrain/payouts.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from WagerBrain.odds import parlay_odds 3 | """ 4 | 5 | Calculate payouts and profits. 6 | --- Payout = Stake + Profit 7 | --- Profit = Payout - Stake 8 | 9 | """ 10 | 11 | 12 | def american_payout(stake, odds): 13 | if odds > 0: 14 | return (stake * (odds / 100)) + stake 15 | else: 16 | return abs((stake / (odds / 100))) + stake 17 | 18 | 19 | def decimal_payout(stake, odds): 20 | return stake * odds 21 | 22 | 23 | def fractional_payout(stake, odds): 24 | odds = Fraction(odds) 25 | return (stake * (odds.numerator / odds.denominator)) + stake 26 | 27 | 28 | def american_profit(stake, odds): 29 | if odds > 0: 30 | return stake * (odds / 100) 31 | else: 32 | return abs(stake / (odds / 100)) 33 | 34 | 35 | def decimal_profit(stake, odds): 36 | return stake * (odds - 1) 37 | 38 | 39 | def fractional_profit(stake, odds): 40 | odds = Fraction(odds) 41 | return stake * (odds.numerator / odds.denominator) 42 | 43 | 44 | def get_payout(odds, stake, odds_style='a'): 45 | try: 46 | if odds_style.lower() == "american" or odds_style.lower() == 'amer' or odds_style.lower() == 'a': 47 | return american_payout(stake, odds) 48 | 49 | elif odds_style.lower() == "decimal" or odds_style.lower() == 'dec' or odds_style.lower() == 'd': 50 | return decimal_payout(stake, odds) 51 | 52 | elif odds_style.lower() == "fractional" or odds_style.lower() == 'frac' or odds_style.lower() == 'f': 53 | return fractional_payout(stake, odds) 54 | 55 | except (ValueError, KeyError, NameError): 56 | return None 57 | 58 | 59 | def get_profit(odds, stake, odds_style='a'): 60 | try: 61 | if odds_style.lower() == "american" or odds_style.lower() == 'amer' or odds_style.lower() == 'a': 62 | return american_profit(stake, odds) 63 | 64 | elif odds_style.lower() == "decimal" or odds_style.lower() == 'dec' or odds_style.lower() == 'd': 65 | return decimal_profit(stake, odds) 66 | 67 | elif odds_style.lower() == "fractional" or odds_style.lower() == 'frac' or odds_style.lower() == 'f': 68 | return fractional_profit(stake, odds) 69 | 70 | except (ValueError, KeyError, NameError): 71 | return None 72 | 73 | 74 | def parlay_profit(odds, stake): 75 | """ 76 | :param odds: List. A list of odds for wagers to be included in parlay 77 | :param stake: Float. How much cash you're throwing down on this ill-advised parlay 78 | :return: Float. Net profit = profit - stake 79 | """ 80 | odds = parlay_odds(odds) 81 | return (odds * stake) - stake 82 | 83 | 84 | def parlay_payout(odds, stake): 85 | """ 86 | :param odds: List. A list of odds for wagers to be included in parlay 87 | :param stake: Float. How much cash you're throwing down on this ill-advised parlay 88 | :return: Float. Your total payout (stake + profit) 89 | """ 90 | odds = parlay_odds(odds) 91 | return odds * stake 92 | -------------------------------------------------------------------------------- /WagerBrain/probs.py: -------------------------------------------------------------------------------- 1 | from fractions import Fraction 2 | from math import gcd 3 | from WagerBrain.payouts import decimal_profit, decimal_payout 4 | from WagerBrain.odds import fractional_odds, decimal_odds, american_odds 5 | from WagerBrain.utils import break_even_pct 6 | 7 | """ 8 | 9 | Calculate Implied Win %'s from American, Decimal, Fractional odds 10 | Calculate Expected Value of a wager 11 | Calculate Odds (Amer, Dec, Frac) from Implied Win %'s 12 | 13 | """ 14 | 15 | 16 | def decimal_implied_win_prob(odds): 17 | """ 18 | :param odds: Float. Odds expressed in Decimal terms. 19 | :return: Float. The implied win % of stated odds. 20 | """ 21 | return round(1 / decimal_odds(odds), 3) 22 | 23 | 24 | def american_implied_win_prob(odds): 25 | """ 26 | :param odds: Integer. Odds expressed in American terms. 27 | :return: Float. The implied win % of stated odds. 28 | """ 29 | if odds > 0: 30 | return round(100 / (american_odds(odds) + 100), 3) 31 | else: 32 | return round(abs(american_odds(odds)) / (abs(american_odds(odds)) + 100), 3) 33 | 34 | 35 | def fractional_implied_win_prob(odds): 36 | """ 37 | :param odds: String (e.g., '3/1') or Python Fraction Class. 38 | :return: Float. The implied win % of stated odds. 39 | """ 40 | odds = fractional_odds(odds) 41 | return round(1 / ((odds.numerator / odds.denominator) + 1), 3) 42 | 43 | 44 | def stated_odds_ev(stake_win, profit_win, stake_lose, profit_lose): 45 | """ 46 | This is the Expected Value (ev) derived from stated odds at a bookmaker. It uses implied win % break-evens. This adds to more than 47 | 100% because it incorporates the Vig. Use "true_odds_ev" to plug in user-calculated odds. 48 | Most stated odds will produce negative EV. The edge is in your own work and could be seen in true_odds_ev. 49 | :param stake_win: Float. Amount wagered on FAVORITE. 50 | :param profit_win: Float. Net amount won on FAVORITE. 51 | :param stake_lose: Float. Float. Amount wagered on UNDERDOG. 52 | :param profit_lose: Float. Net amount won on UNDERDOG. 53 | :return: Float. The expected value of wagering on winner. 54 | """ 55 | payout_win = stake_win + profit_win 56 | payout_lose = stake_lose + profit_lose 57 | 58 | win_prob = break_even_pct(stake_win, payout_win) 59 | lose_prob = break_even_pct(stake_lose, payout_lose) 60 | 61 | return (win_prob * profit_win) - (lose_prob * stake_win) 62 | 63 | 64 | def true_odds_ev(stake, profit, prob): 65 | """ 66 | This is the Expected Value (ev) derived from user-calculated odds. For EV on stated odds and implied win % from a 67 | bookmaker, use 'stated_odds_ev. 68 | :param stake: Float. Amount wagered. 69 | :param profit: Float. Net amount returned by wager. 70 | :param prob: Float. % chance of winning outcome. 71 | :return: Float. The expected value of wagering on winner. 72 | """ 73 | return (profit * prob) - (stake * (1 - prob)) 74 | 75 | 76 | def win_prob_to_odds(prob, odds_style="a"): 77 | """ 78 | :param prob: Float. Implied winning % of a given wager 79 | :param odds_style: Integer (American), Float(Decimal), String or Fraction Class (Fractional) 80 | :return: The stated odds of a bet in a given style 81 | """ 82 | try: 83 | if odds_style.lower() == "american" or odds_style.lower() == 'amer' or odds_style.lower() == 'a': 84 | if prob >= .50: 85 | return int(prob / (1 - prob) * -100) 86 | else: 87 | return int((1 - prob) / prob * 100) 88 | 89 | elif odds_style.lower() == "decimal" or odds_style.lower() == 'dec' or odds_style.lower() == 'd': 90 | return round((100 / prob) / 100, 2) 91 | 92 | elif odds_style.lower() == "fractional" or odds_style.lower() == 'frac' or odds_style.lower() == 'f': 93 | return Fraction((1 / prob) - 1).limit_denominator() 94 | 95 | except (ValueError, KeyError, NameError): 96 | return None 97 | 98 | 99 | def elo_prob(elo_diff): 100 | """ 101 | :param elo_diff: Team A’s ELO rating minus Team B’s ELO rating, plus or minus the difference in several adjustments 102 | :return: % win probability for Team A 103 | """ 104 | return 1 / (10**(-elo_diff/400) + 1) 105 | -------------------------------------------------------------------------------- /WagerBrain/strats/arb.py: -------------------------------------------------------------------------------- 1 | from WagerBrain.probs import decimal_implied_win_prob 2 | from WagerBrain.odds import decimal_odds 3 | 4 | """ 5 | "Arb_hunter_estimator" is currently unused but is here for future utilization. 6 | arb_hunter_estimator = {1.2: 6.0, 7 | 1.3: 4.33, 8 | 1.4: 3.5, 9 | 1.5: 3.0, 10 | 1.6: 2.66, 11 | 1.7: 2.42, 12 | 1.8: 2.25, 13 | 1.9: 2.11, 14 | 2.0: 2.0} 15 | """ 16 | 17 | 18 | def arb_percentage(odds): 19 | """ 20 | :param odds: List of Floats. Pair of odds for a single matchup - Player 1 and Player 2. 21 | :return: List of Floats. Sum of implied win probabilities and win_probs for Player 1 and Player 2 22 | """ 23 | try: 24 | if len(odds) == 2: 25 | pass 26 | except ValueError: 27 | print("Odds input needs to be a list of length 2 (odds bet 1 / odds bet 2") 28 | 29 | bet1 = decimal_implied_win_prob(decimal_odds(odds[0])) 30 | bet2 = decimal_implied_win_prob(decimal_odds(odds[1])) 31 | arb_percent = bet1 + bet2 32 | 33 | return [arb_percent, bet1, bet2] 34 | 35 | 36 | def arb_profit(arb_percent, stake): 37 | """ 38 | :param arb_percent: List. Helper function must be used with arb_percentage. This is sum of combined implied probabilities < 100% mean arb opportunity 39 | :param stake: Float. How much you intend to throw down on the wager. 40 | :return: Float. Riskless profit if executed at the terms of the parameters. 41 | """ 42 | return stake / arb_percent[0] - stake 43 | 44 | 45 | def basic_arbitrage(odds, stake): 46 | """ 47 | A basic arbitrage calculator. Riskless profits generally implemented at two separate sportsbooks. 48 | 49 | :param odds: Float. A pair of odds Player 1 and their opponent. Odds generally from different sites. 50 | :param stake: Float. How much you intend to throw down. 51 | :return: List of Floats. Profit Arb'd. Wager for Player 1; Wager for Player 2. 52 | """ 53 | try: 54 | if len(odds) != 2: 55 | return None 56 | 57 | arb_percent = arb_percentage(odds) 58 | 59 | if arb_percent[0] > 1.0: 60 | return None 61 | else: 62 | arb_prof = arb_profit(arb_percent, stake) 63 | bet1_size = (arb_percent[1] * stake) / arb_percent[0] 64 | bet2_size = (arb_percent[2] * stake) / arb_percent[0] 65 | return [round(arb_prof, 2), round(bet1_size, 2), round(bet2_size, 2)] 66 | except ValueError: 67 | print("You probably fed too many or too few values into the Odds parameter") 68 | -------------------------------------------------------------------------------- /WagerBrain/strats/value.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import pandas as pd 3 | from WagerBrain.probs import win_prob_to_odds 4 | 5 | 6 | def spread_home_dog_to_fav(df): 7 | """ 8 | American Odds of Home team winning after flipping from Dog to Fav 9 | If better than -235 (e.g., -200) that's a value bet. You're getting more favorable terms that historic results. 10 | :param df: dataframe of NBA odds provided in WagerBrain 11 | :return: The odds for the win % of function strategy 12 | """ 13 | 14 | # How Often Does the Home Team Win When They Move From Dog to Fav 15 | idx = np.where((df['Home Spread Close'] < 0) & (df['Home Score'] > df['Away Score'])) 16 | home_flip_spread_win = df.loc[idx] 17 | 18 | idc = np.where((df['Home Spread Close'] < 0)) 19 | home_flip_spread_total = df.loc[idc] 20 | 21 | perc = len(home_flip_spread_win) / len(home_flip_spread_total) 22 | 23 | return win_prob_to_odds(perc) 24 | 25 | 26 | def spread_home_fav_to_dog(df): 27 | # How Often Does the Home Team Win When They Move From Fav to Dog 28 | idx = np.where((df['Home Spread Close'] >= 0) & (df['Home Score'] > df['Away Score'])) 29 | home_flip_spread_win = df.loc[idx] 30 | 31 | idc = np.where((df['Home Spread Close'] >= 0)) 32 | home_flip_spread_total = df.loc[idc] 33 | 34 | perc = len(home_flip_spread_win) / len(home_flip_spread_total) 35 | 36 | return win_prob_to_odds(perc) 37 | -------------------------------------------------------------------------------- /WagerBrain/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sedemmler/WagerBrain/b1cc33f5eb7a6130106bf8251b554718e2d22172/WagerBrain/tests/__init__.py -------------------------------------------------------------------------------- /WagerBrain/tests/test_odds.py: -------------------------------------------------------------------------------- 1 | from WagerBrain.odds import * 2 | 3 | 4 | def test_american_odds(): 5 | decimal = american_odds(1.91) 6 | frac = american_odds("5/1") 7 | assert decimal == -110.0 8 | assert frac == 500.0 9 | 10 | 11 | def test_decimal_odds(): 12 | amer = round(decimal_odds(-110),2) 13 | frac = decimal_odds("5/1") 14 | assert amer == 1.91 15 | assert frac == 6 16 | 17 | def test_fractional_odds(): 18 | amer = fractional_odds(500) 19 | decimal = fractional_odds(6.0) 20 | assert amer == 5 21 | assert decimal == 5 22 | 23 | 24 | def test_parlary_odds(): 25 | par = parlay_odds([500, -110, 250]) 26 | assert round(par, 2) == 40.09 -------------------------------------------------------------------------------- /WagerBrain/tests/test_payouts.py: -------------------------------------------------------------------------------- 1 | from WagerBrain.payouts import * 2 | 3 | 4 | def test_american_payout(): 5 | neg = american_payout(100, -110) 6 | pos = american_payout(100, 200) 7 | assert pos == 300 8 | assert round(neg, 2) == 190.91 9 | 10 | 11 | def test_american_profit(): 12 | neg = american_profit(100, -110) 13 | pos = american_profit(100, 200) 14 | assert pos == 200 15 | assert round(neg, 2) == 90.91 16 | 17 | 18 | def test_decimal_payout(): 19 | assert decimal_payout(100, 2) == 200 20 | 21 | 22 | def test_decimal_profit(): 23 | assert decimal_profit(100, 2) == 100 24 | 25 | 26 | def test_fractional_payout(): 27 | assert fractional_payout(100, "5/1") == 600.0 28 | assert fractional_profit(100, "1/4") == 25.0 29 | 30 | 31 | def test_parlay_profits(): 32 | pass 33 | 34 | 35 | def test_parlay_payouts(): 36 | pass 37 | -------------------------------------------------------------------------------- /WagerBrain/tests/test_probs.py: -------------------------------------------------------------------------------- 1 | from WagerBrain.probs import * 2 | 3 | 4 | def test_decimal_implied_win_prob(): 5 | assert False 6 | -------------------------------------------------------------------------------- /WagerBrain/utils.py: -------------------------------------------------------------------------------- 1 | from WagerBrain.odds import decimal_odds 2 | 3 | 4 | def break_even_pct(stake, payout): 5 | """ 6 | :param stake: Float. Currency amount wagered. 7 | :param payout: Float. Currency amount paid out (stake + profit) 8 | :return: Float. % Amount of time you need to win more than to be profitable. % > 100 because of Vig 9 | """ 10 | return stake / payout 11 | 12 | 13 | def vig(f_stake, f_payout, u_stake, u_payout): 14 | """ 15 | :param f_stake: Float. Amount bet on Favorite. 16 | :param f_payout: Float. Total payout on Favorite. 17 | :param u_stake: Float. Amount bet on Underdog. 18 | :param u_payout: Float. Total payout on Underdog. 19 | :return: % vig paid to the bookmaker. 20 | """ 21 | return (break_even_pct(f_stake, f_payout) + break_even_pct(u_stake, u_payout)) - 1 22 | 23 | 24 | def bookmaker_margin(fav_odds, dog_odds, draw_odds=None): 25 | """ 26 | :param fav_odds: Integer. (American), Float(Decimal), String or Fraction Class (Fractional) The odds on offer for the favorite 27 | :param dog_odds: Integer. (American), Float(Decimal), String or Fraction Class (Fractional) The odds on offer for the underdog 28 | :param draw_odds: Integer. (American), Float(Decimal), String or Fraction Class (Fractional) The odds on offer for a tie 29 | :return: Float. Percentage of wager paying bookmaker margin (bookmaker's edge normalized as % in decimal terms 30 | """ 31 | if not draw_odds: 32 | # Formula requires decimal odds 33 | fav_odds = decimal_odds(fav_odds) 34 | dog_odds = decimal_odds(dog_odds) 35 | 36 | return ((1 / fav_odds) + (1 / dog_odds)) - 1 37 | 38 | else: 39 | fav_odds = decimal_odds(fav_odds) 40 | dog_odds = decimal_odds(dog_odds) 41 | draw_odds = decimal_odds(draw_odds) 42 | 43 | return ((1 / fav_odds) + (1 / dog_odds) + (1 / draw_odds)) - 1 44 | 45 | 46 | def bookmaker_commission(fav_odds, dog_odds, commish, draw_odds=None): 47 | """ 48 | :param fav_odds: Integer (American), Float(Decimal), String or Fraction Class (Fractional). Market odds on favorite 49 | :param dog_odds: Integer (American), Float(Decimal), String or Fraction Class (Fractional). Market odds on underdog 50 | :param commish: Float. Betting exchange's commission 51 | :param draw_odds: Integer (American), Float(Decimal), String or Fraction Class (Fractional). Market odds on a draw outcome if applicable 52 | :return: Float. % representing true cost / edge to bookmaker, exchange normalized as % in decimal terms 53 | """ 54 | if not draw_odds: 55 | fav_odds = decimal_odds(fav_odds) 56 | dog_odds = decimal_odds(dog_odds) 57 | 58 | fav_odds = 1 + ((1 - (commish / 100)) * (fav_odds - 1)) 59 | dog_odds = 1 + ((1 - (commish / 100)) * (dog_odds - 1)) 60 | 61 | return (((1 / fav_odds) * 100 + (1 / dog_odds) * 100) - 100) / 100 62 | 63 | else: 64 | fav_odds = decimal_odds(fav_odds) 65 | dog_odds = decimal_odds(dog_odds) 66 | draw_odds = decimal_odds(draw_odds) 67 | 68 | fav_odds = 1 + ((1 - (commish / 100)) * (fav_odds - 1)) 69 | dog_odds = 1 + ((1 - (commish / 100)) * (dog_odds - 1)) 70 | draw_odds = 1 + ((1 - (commish / 100)) * (draw_odds - 1)) 71 | 72 | return (((1 / fav_odds) * 100 + (1 / dog_odds) * 100 + (1 / draw_odds) * 100) - 100) / 100 73 | --------------------------------------------------------------------------------