├── .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 |
10 |
11 |
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 |
5 |
6 |
7 | ApexVCS
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.idea/modules.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/.idea/other.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 | 
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 |
--------------------------------------------------------------------------------