Commit e29fa291 authored by Hermann Krumrey's avatar Hermann Krumrey

Merge branch 'develop' into 'master'

Develop

See merge request !4
parents 168d902e 91133e09
......@@ -6,7 +6,7 @@ stages:
- release
default:
image: namboy94/ci-docker-environment:0.9.0
image: namboy94/ci-docker-environment:0.10.0
before_script:
- echo "$SERVER_ACCESS_KEY" > ~/.ssh/id_rsa
- chmod 0600 ~/.ssh/id_rsa
......
V 0.4.0:
- Added betting bot that uses a neural network to predict matches
V 0.3.0:
- Added Random Betting
V 0.2.0:
......
......@@ -4,6 +4,7 @@ MAINTAINER Hermann Krumrey <hermann@krumreyh.com>
ENV DEBIAN_FRONTEND=noninteractive
RUN apt update && apt install -y python3 python3-pip
RUN pip3 install tensorflow keras
ADD . bot
RUN cd bot && python3 setup.py install
......
......@@ -100,6 +100,8 @@ class ApiConnection:
bet_dict = {}
for bet in bets:
bet_dict.update(bet.to_dict())
self.logger.info(f"Generated bet: {bet}")
data = self.execute_api_call("bet", "PUT", True, bet_dict)
if data["status"] != "ok":
self.logger.error("Failed to place bets")
......
......@@ -44,3 +44,9 @@ class Bet:
f"{self.match_id}-home": self.home_score,
f"{self.match_id}-away": self.away_score
}
def __str__(self) -> str:
"""
:return: A string representation of the bet
"""
return f"[{self.match_id}] {self.home_score}:{self.away_score}"
......@@ -30,7 +30,8 @@ class Match:
_id: int,
matchday: int,
home_team: str,
away_team: str
away_team: str,
finished: bool
):
"""
Initializes the Match
......@@ -38,11 +39,13 @@ class Match:
:param matchday: The matchday of the match
:param home_team: The name of the home team
:param away_team: The name of the away team
:param finished: Whether the match is already finished or not
"""
self.id = _id
self.matchday = matchday
self.home_team = home_team
self.away_team = away_team
self.finished = finished
@classmethod
def from_json(cls, json_data: Dict[str, Any]):
......@@ -55,5 +58,6 @@ class Match:
json_data["id"],
json_data["matchday"],
json_data["home_team"]["abbreviation"],
json_data["away_team"]["abbreviation"]
json_data["away_team"]["abbreviation"],
json_data["finished"]
)
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
import requests
from bs4 import BeautifulSoup
def load_odds(country: str, league: str, season: int):
url = f"https://www.oddsportal.com/soccer/" \
f"{country}/{league}-{season}-{season + 1}/results/"
print(url)
data = requests.get(url)
soup = BeautifulSoup(data.text, "html.parser")
for x in soup.select(".deactivate"):
print(x.text)
if __name__ == '__main__':
load_odds("germany", "bundesliga", 2019)
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Tuple
from betbot.neural.data.openligadb.Team import Team
from betbot.neural.data.openligadb.TableEntry import TableEntry
class HistoryRecord:
"""
Class that keeps track of a team's recent history
Only keeps track of stats within a league!
"""
def __init__(
self,
team: Team
):
"""
Initializes the HistoryRecord object
"""
self.team = team
self._history: Dict[int, Dict[int, Tuple[int, int, int]]] \
= {}
def add_table_entry(self, entry: TableEntry):
"""
Adds points and goals history data based on a table entry
:param entry: The table entry
:return: None
"""
if entry.season not in self._history:
self._history[entry.season] = {}
self._history[entry.season][entry.matchday] = (
entry.points,
entry.goals_for,
entry.goals_against
)
def get_stats(self, season: int, matchday: int, previous: bool) \
-> Tuple[int, int, int]:
"""
Retrieves stats for a season and matchday for this team's history
If no history is found, 0 values will be returned
:param season: The season
:param matchday: The matchday
:param previous: Whether or not to use the previous matchday
This is useful if a match is already finished
:return: The points, goals for and goals against
"""
if previous:
if matchday == 1:
if (season - 1) not in self._history:
return 0, 0, 0
else:
season -= 1
matchday = max(self._history[season].keys())
else:
matchday -= 1
stats = self._history.get(season, {}).get(matchday)
return stats if stats is not None else (0, 0, 0)
def get_stats_interval(
self,
season: int,
matchday: int,
interval: int,
finished: bool
) -> Tuple[int, int, int]:
"""
Retrieves stats for an interval
:param season: The season
:param matchday: The 'anchor' matchday
:param interval: The size of the interval
:param finished: If the match was finished or not
:return: The points and goals during the interval
"""
# To actually include the first entry in the interval
interval += 1
points, goals_for, goals_against = \
self.get_stats(season, matchday, finished)
if matchday - interval <= 0 and (season - 1) in self._history:
max_matchday = max(self._history[season - 1].keys())
start_matchday = max_matchday - (interval - matchday)
start_points, start_goals_for, start_goals_against = \
self.get_stats(season - 1, start_matchday, False)
end_points, end_goals_for, end_goals_against = \
self.get_stats(season - 1, max_matchday, False)
points += (end_points - start_points)
goals_for += (end_goals_for - start_goals_for)
goals_against += (end_goals_against - start_goals_against)
elif matchday == 1:
return 0, 0, 0
elif matchday - interval <= 0:
pass # In the first season, simply use current stats
else:
start_points, start_goals_for, start_goals_against = \
self.get_stats(season, matchday - interval, False)
points -= start_points
goals_for -= start_goals_for
goals_against -= start_goals_against
return points, goals_for, goals_against
def get_max_matchday(self, season: int):
return max(self._history[season].keys())
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import List, Union
from betbot.neural.data.openligadb.HistoryRecord import HistoryRecord
class InputVector:
"""
Class that models an input vector to be used in training for a neural
network that predicts scores.
"""
def __init__(
self,
season: int,
matchday: int,
finished: bool,
home_team_history: HistoryRecord,
away_team_history: HistoryRecord
):
"""
Initializes the InputVector
:param season: The season of the input
:param matchday: The match day
:param finished: Whether or not the match has finished
:param home_team_history: The table history of the home team
:param away_team_history: The table history of the away team
"""
self.season = season
self.matchday = matchday
self.home_team_history = home_team_history
self.away_team_history = away_team_history
self.current_stats = \
list(home_team_history.get_stats(season, matchday, finished)) + \
list(away_team_history.get_stats(season, matchday, finished))
self.history_stats = {}
for interval in [5]:
self.history_stats[interval] = \
list(home_team_history.get_stats_interval(
season, matchday, interval, finished
)) + \
list(away_team_history.get_stats_interval(
season, matchday, interval, finished
))
@property
def vector(self) -> List[int]:
"""
:return: The input vector as a list of integers
"""
vector = self.current_stats
for key in sorted(self.history_stats.keys()):
vector += self.history_stats[key]
return vector
def normalize(self, vector: List[Union[int, float]]) -> List[float]:
"""
Normalizes the input vector to values between 0 and 1
:param vector: The vector to normalize
:return: The normalized vector
"""
vector = list(vector) # copy
for overall_index in range(0, 6):
vector[overall_index] = \
min(1.0, vector[overall_index] / (self.matchday * 3))
for history_index in range(6, len(vector)):
vector[history_index] = \
min(1.0, vector[history_index] / (5 * 3))
return vector
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Any
from betbot.neural.data.openligadb.Team import Team
class Match:
"""
Class that models an OpenligaDB match
"""
def __init__(
self,
league: str,
season: int,
match_json: Dict[str, Any],
teams: Dict[int, Team]
):
"""
Initializes the match object
:param league: The league of the match
:param season: The season of the match
:param match_json: The OpenligaDB JSON data for the match
:param teams: The teams mapped to their IDs
"""
finished = match_json["MatchIsFinished"]
if finished:
results = match_json["MatchResults"]
if results[0]["ResultName"] == "Endergebnis":
result = results[0]
else:
result = results[1]
home_score = result["PointsTeam1"]
away_score = result["PointsTeam2"]
else:
home_score, away_score = None, None
self.league = league
self.season = season
self.matchday = match_json["Group"]["GroupOrderID"]
self.home_team = teams[match_json["Team1"]["TeamId"]]
self.away_team = teams[match_json["Team2"]["TeamId"]]
self.home_score = home_score
self.away_score = away_score
self.finished = finished
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import List
class OutputVector:
"""
Class that models the output vector for a neural network training dataset
"""
def __init__(self, home_score: int, away_score: int):
"""
Initializes the OutputVector object
:param home_score: The home score
:param away_score: The away score
"""
self.home_score = home_score
self.away_score = away_score
@property
def vector(self) -> List[int]:
return [self.home_score, self.away_score]
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from betbot.neural.data.openligadb.Team import Team
from betbot.neural.data.openligadb.Match import Match
class TableEntry:
"""
Class that models a league table entry for one club
"""
def __init__(
self,
league: str,
season: int,
matchday: int,
team: Team,
points: int,
goals_for: int,
goals_against: int
):
"""
Initializes the TableEntry object
:param league: The league of the entry
:param season: The season of the entry
:param matchday: The matchday of the entry
:param team: The team for which this is an entry
:param points: The points of the team
:param goals_for: The goals the team scored
:param goals_against: The goals the team conceded
"""
self.league = league
self.season = season
self.matchday = matchday
self.team = team
self.points = points
self.goals_for = goals_for
self.goals_against = goals_against
def merge(self, entry: "TableEntry"):
"""
Merges another table entry's points and goals data into this one
:param entry: The entry to merge into this one
:return: Nonew
"""
self.points += entry.points
self.goals_for += entry.goals_for
self.goals_against += entry.goals_against
@classmethod
def from_match(cls, match: Match, team: Team) -> "TableEntry":
"""
Generates a table entry based on a match
:param match: The match
:param team: The team for which to generate the entry
:return: The table entry
"""
is_home_team = team.id == match.home_team.id
if is_home_team:
score_for, score_against = match.home_score, match.away_score
else:
score_for, score_against = match.away_score, match.home_score
if not match.finished:
points = 0
score_for = 0
score_against = 0
elif score_for > score_against:
points = 3
elif score_for == score_against:
points = 1
else:
points = 0
return cls(
match.league,
match.season,
match.matchday,
team,
points,
score_for,
score_against
)
"""LICENSE
Copyright 2020 Hermann Krumrey <hermann@krumreyh.com>
This file is part of betbot.
betbot is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
betbot is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with betbot. If not, see <http://www.gnu.org/licenses/>.
LICENSE"""
from typing import Dict, Any
class Team:
"""
Class that models an OpenLigaDB Team
"""
def __init__(self, team_json: Dict[str, Any]):
"""
Initializes the team object
:param team_json: The OpenLigaDB JSON for the team
"""
self.id = team_json["TeamId"]
self.team_name = team_json["TeamName"]
self.abbreviation = {
"1. FC Nürnberg": "FCN",
"1. FSV Mainz 05": "M05",
"Bayer Leverkusen": "B04",
"Borussia Dortmund": "BVB",
"Borussia Mönchengladbach": "BMG",
"Eintracht Frankfurt": "SGE",
"FC Augsburg": "FCA",
"FC Bayern": "FCB",
"FC Schalke 04": "S04",
"Fortuna Düsseldorf": "F95",
"Hannover 96": "H96",