Compare commits

...

10 Commits

Author SHA1 Message Date
Daan Koning
1e2ce481bb Bump requests version
needed for security reasons
2024-01-14 20:08:33 +01:00
Daan Koning
7343ec4196 Clean up 2023-05-24 16:56:57 +02:00
Daan Koning
daf4e6f632 Pull out logic.py into new directory 2023-01-23 14:02:15 +01:00
Daan Koning
166332a669 Improve error handling 2023-01-23 13:55:12 +01:00
Daan Koning
f9e125fc65 Extend on refactor 2023-01-23 13:12:41 +01:00
Daan Koning
ed08cf73a2 Refactor: main.py is more legible 2023-01-23 13:09:12 +01:00
Daan Koning
e19e2896a2 Basic tqdm functionality 2023-01-23 12:57:23 +01:00
Daan Koning
c91b0f32b0 Fix the unformatted printing 2023-01-22 22:31:20 +01:00
Daan Koning
6cf1eae5c9 Fix various bugs 2023-01-22 22:27:56 +01:00
Daan Koning
684b345e19 Improve error handling: introduce generic 2023-01-22 22:18:10 +01:00
3 changed files with 42 additions and 24 deletions

17
main.py
View File

@@ -2,9 +2,8 @@
The tool fetches the odds from The Odds API (https://the-odds-api.com/) and compares the odds at different The tool fetches the odds from The Odds API (https://the-odds-api.com/) and compares the odds at different
bookmakers to each other in order to determine whether there are profitable and risk-free bets available.""" bookmakers to each other in order to determine whether there are profitable and risk-free bets available."""
from logic import * from src.logic import get_arbitrage_opportunities
import os import os
from itertools import chain
import argparse import argparse
from dotenv import load_dotenv from dotenv import load_dotenv
from rich import print from rich import print
@@ -41,20 +40,12 @@ def main():
) )
args = parser.parse_args() args = parser.parse_args()
key = args.key
region = args.region
print_unformatted = args.unformatted
cutoff = args.cutoff/100 cutoff = args.cutoff/100
# logic arbitrage_opportunities = get_arbitrage_opportunities(key=args.key, region=args.region, cutoff=cutoff)
sports = get_sports(key)
data = chain.from_iterable(get_data(key, sport, region=region) for sport in sports)
data = filter(lambda x: x != "message", data)
results = process_data(data)
arbitrage_opportunities = filter(lambda x: x["total_implied_odds"] < 1-cutoff, results)
if print_unformatted: if args.unformatted:
print(arbitrage_opportunities) print(list(arbitrage_opportunities))
else: else:
arbitrage_opportunities = list(arbitrage_opportunities) arbitrage_opportunities = list(arbitrage_opportunities)
print(f"{len(arbitrage_opportunities)} arbitrage opportunities found {':money-mouth_face:' if len(arbitrage_opportunities) > 0 else ':man_shrugging:'}") print(f"{len(arbitrage_opportunities)} arbitrage opportunities found {':money-mouth_face:' if len(arbitrage_opportunities) > 0 else ':man_shrugging:'}")

View File

@@ -1,3 +1,3 @@
requests~=2.28.1 requests>=2.31.0
python-dotenv~=0.21.0 python-dotenv~=0.21.0
rich~=12.6.0 rich~=12.6.0

View File

@@ -1,38 +1,54 @@
from typing import Iterable, Generator from typing import Iterable, Generator
import time import time
import requests import requests
from itertools import chain
BASE_URL = "https://api.the-odds-api.com/v4" try:
from tqdm import tqdm
except ImportError:
tqdm = lambda *args, **kwargs: args[0]
BASE_URL = "api.the-odds-api.com/v4"
PROTOCOL = "https://"
class AuthenticationException(RuntimeError): class APIException(RuntimeError):
def __str__(self):
return f"('{self.args[0]}', '{self.args[1].json()['message']}')"
class AuthenticationException(APIException):
pass pass
class RateLimitException(RuntimeError): class RateLimitException(APIException):
pass pass
def handle_faulty_response(response: requests.Response): def handle_faulty_response(response: requests.Response):
if response.status_code == 401: if response.status_code == 401:
raise AuthenticationException("Failed to authenticate with the API. is the API key valid?") raise AuthenticationException("Failed to authenticate with the API. is the API key valid?", response)
elif response.status_code == 429: elif response.status_code == 429:
raise RateLimitException("Encountered API rate limit.") raise RateLimitException("Encountered API rate limit.", response)
else:
raise APIException("Unknown issue arose while trying to access the API.", response)
def get_sports(key: str) -> set[str]: def get_sports(key: str) -> set[str]:
url = f"{BASE_URL}/sports/" url = f"{BASE_URL}/sports/"
escaped_url = PROTOCOL + requests.utils.quote(url)
querystring = {"apiKey": key} querystring = {"apiKey": key}
response = requests.get(url, params=querystring) response = requests.get(escaped_url, params=querystring)
if not response: if not response:
handle_faulty_response(response) handle_faulty_response(response)
return {item["group"] for item in response.json()} return {item["key"] for item in response.json()}
def get_data(key: str, sport: str, region: str = "eu"): def get_data(key: str, sport: str, region: str = "eu"):
url = f"{BASE_URL}/sports/{sport}/odds/" url = f"{BASE_URL}/sports/{sport}/odds/"
escaped_url = PROTOCOL + requests.utils.quote(url)
querystring = { querystring = {
"apiKey": key, "apiKey": key,
"regions": region, "regions": region,
@@ -40,7 +56,7 @@ def get_data(key: str, sport: str, region: str = "eu"):
"dateFormat": "unix" "dateFormat": "unix"
} }
response = requests.get(url, params=querystring) response = requests.get(escaped_url, params=querystring)
if not response: if not response:
handle_faulty_response(response) handle_faulty_response(response)
@@ -50,6 +66,7 @@ def get_data(key: str, sport: str, region: str = "eu"):
def process_data(matches: Iterable, include_started_matches: bool = True) -> Generator[dict, None, None]: def process_data(matches: Iterable, include_started_matches: bool = True) -> Generator[dict, None, None]:
"""Extracts all matches that are available and calculates some things about them, such as the time to start and """Extracts all matches that are available and calculates some things about them, such as the time to start and
the best available implied odds.""" the best available implied odds."""
matches = tqdm(matches, desc="Checking all matches", leave=False, unit=" matches")
for match in matches: for match in matches:
start_time = int(match["commence_time"]) start_time = int(match["commence_time"])
if not include_started_matches and start_time < time.time(): if not include_started_matches and start_time < time.time():
@@ -76,4 +93,14 @@ def process_data(matches: Iterable, include_started_matches: bool = True) -> Gen
"league": league, "league": league,
"best_outcome_odds": best_odd_per_outcome, "best_outcome_odds": best_odd_per_outcome,
"total_implied_odds": total_implied_odds, "total_implied_odds": total_implied_odds,
} }
def get_arbitrage_opportunities(key: str, region: str, cutoff: float):
sports = get_sports(key)
data = chain.from_iterable(get_data(key, sport, region=region) for sport in sports)
data = filter(lambda x: x != "message", data)
results = process_data(data)
arbitrage_opportunities = filter(lambda x: 0 < x["total_implied_odds"] < 1-cutoff, results)
return arbitrage_opportunities