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
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
from itertools import chain
import argparse
from dotenv import load_dotenv
from rich import print
@@ -41,20 +40,12 @@ def main():
)
args = parser.parse_args()
key = args.key
region = args.region
print_unformatted = args.unformatted
cutoff = args.cutoff/100
# logic
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)
arbitrage_opportunities = get_arbitrage_opportunities(key=args.key, region=args.region, cutoff=cutoff)
if print_unformatted:
print(arbitrage_opportunities)
if args.unformatted:
print(list(arbitrage_opportunities))
else:
arbitrage_opportunities = list(arbitrage_opportunities)
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
rich~=12.6.0
rich~=12.6.0

View File

@@ -1,38 +1,54 @@
from typing import Iterable, Generator
import time
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
class RateLimitException(RuntimeError):
class RateLimitException(APIException):
pass
def handle_faulty_response(response: requests.Response):
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:
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]:
url = f"{BASE_URL}/sports/"
escaped_url = PROTOCOL + requests.utils.quote(url)
querystring = {"apiKey": key}
response = requests.get(url, params=querystring)
response = requests.get(escaped_url, params=querystring)
if not 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"):
url = f"{BASE_URL}/sports/{sport}/odds/"
escaped_url = PROTOCOL + requests.utils.quote(url)
querystring = {
"apiKey": key,
"regions": region,
@@ -40,7 +56,7 @@ def get_data(key: str, sport: str, region: str = "eu"):
"dateFormat": "unix"
}
response = requests.get(url, params=querystring)
response = requests.get(escaped_url, params=querystring)
if not 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]:
"""Extracts all matches that are available and calculates some things about them, such as the time to start and
the best available implied odds."""
matches = tqdm(matches, desc="Checking all matches", leave=False, unit=" matches")
for match in matches:
start_time = int(match["commence_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,
"best_outcome_odds": best_odd_per_outcome,
"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