Compare commits
10 Commits
d03391c271
...
1e2ce481bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e2ce481bb | ||
|
|
7343ec4196 | ||
|
|
daf4e6f632 | ||
|
|
166332a669 | ||
|
|
f9e125fc65 | ||
|
|
ed08cf73a2 | ||
|
|
e19e2896a2 | ||
|
|
c91b0f32b0 | ||
|
|
6cf1eae5c9 | ||
|
|
684b345e19 |
17
main.py
17
main.py
@@ -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:'}")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user