├── requirements.txt ├── .gitignore └── betbot.py /requirements.txt: -------------------------------------------------------------------------------- 1 | lxml==3.4.1 2 | requests==2.5.0 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /betbot.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import logging 3 | import argparse 4 | from io import BytesIO, StringIO 5 | from lxml import etree 6 | 7 | 8 | logger = logging.getLogger('betbot') 9 | logging.basicConfig(level=logging.ERROR) 10 | 11 | 12 | class Event(object): 13 | """ 14 | An event is a day of a league of a sport. 15 | An event can have multiple matches. 16 | """ 17 | 18 | def __init__(self, sport, league): 19 | self.sport = sport 20 | self.league = league 21 | self.quotes = {} 22 | 23 | def __str__(self): 24 | out = StringIO() 25 | print('{:=^44}'.format((self.sport + ' - ' + self.league).upper()), 26 | file=out) 27 | for match, sites_quotes in self.quotes.items(): 28 | print('-' * 44, file=out) 29 | print('| {:^40} |'.format(match), file=out) 30 | print('-' * 44, file=out) 31 | for site, quotes in sites_quotes.items(): 32 | print('| {0:<16} | {1[0]:>5.2f} | {1[1]:>5.2f} | {1[2]:>5.2f} |' 33 | .format(site, quotes), file=out) 34 | print('', file=out) 35 | return out.getvalue() 36 | 37 | def make_id(self, teams): 38 | return ' VS '.join(t.lower() for t in teams) 39 | 40 | def get_or_create_match(self, teams, quotes): 41 | """ 42 | Associate this match with one of the saved one from this event, 43 | by looking at similarities of teams, quotes, and possibly other factors 44 | in the future (like start time). 45 | If this is a new match (i.e. we can't find a similar one), create it. 46 | """ 47 | 48 | def quote_distance(quote_a, quote_b): 49 | return sum(abs(1/q_a - 1/q_b) for q_a, q_b in zip(quote_a, quote_b)) 50 | 51 | for match, site_quotes in self.quotes.items(): 52 | for site, s_quotes in site_quotes.items(): 53 | if quote_distance(s_quotes, quotes) < 0.05: 54 | return match 55 | match_id = self.make_id(teams) 56 | self.quotes[match_id] = {} 57 | return match_id 58 | 59 | def add_site_quotes(self, site_name, quotes): 60 | for match_teams, match_quotes in quotes.items(): 61 | match_id = self.get_or_create_match(match_teams, match_quotes) 62 | self.quotes[match_id][site_name] = match_quotes 63 | 64 | 65 | class SitesManager(object): 66 | 67 | def __init__(self, sites=[]): 68 | self.leagues = {} 69 | self.sites = [] 70 | for site in sites: 71 | self.add_site(site) 72 | logger.info('SitesManager initialized') 73 | logger.info('Sites registered: {}'.format( 74 | ', '.join([s.__class__.__name__ for s in self.sites]))) 75 | logger.info('Leagues registered: {}'.format(self.leagues)) 76 | 77 | def add_site(self, site): 78 | # update the global leagues set to include this site leagues 79 | for sport in site.url_params: 80 | if not sport in self.leagues: 81 | self.leagues[sport] = set() 82 | self.leagues[sport].update( 83 | set(site.url_params[sport]['leagues'].keys())) 84 | # add the site 85 | self.sites.append(site) 86 | 87 | def check_for_sure_bets(self, sports=None): 88 | if sports is None: 89 | sports = self.leagues.keys() 90 | for sport in sports: 91 | for league in self.leagues[sport]: 92 | event = Event(sport, league) 93 | for site in self.sites: 94 | site_league_quotes = site.get_league_quotes(sport, league) 95 | if site_league_quotes: 96 | event.add_site_quotes(site.__class__.__name__, 97 | site_league_quotes) 98 | print(event) 99 | 100 | 101 | class Site(object): 102 | url = '' 103 | url_params = {} 104 | 105 | def parse_response(self, reponse): 106 | logger.debug('Parsing reponse for generic site') 107 | pass 108 | 109 | def get_league_quotes(self, sport, league): 110 | logger.info('Checking quotes for {}-{} on {}'.format( 111 | sport, league, self.__class__.__name__)) 112 | try: 113 | sport_id = self.url_params[sport]['id'] 114 | league_id = self.url_params[sport]['leagues'][league] 115 | except KeyError: 116 | # This site does not have this league, so no quotes 117 | return [] 118 | headers = { 119 | 'User-Agent': ('Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:34.0)' 120 | 'Gecko/20100101 Firefox/34.0') 121 | } 122 | response = requests.get(self.url.format(sport=sport_id, 123 | league=league_id), 124 | headers=headers) 125 | logger.debug(self.url.format(sport=sport_id, league=league_id)) 126 | return self.parse_response(response) 127 | 128 | 129 | class BWin(Site): 130 | url = ('https://it-it.mobileapi.bwinlabs.com/api/iphone/v2/events/search?' 131 | 'sportid={sport}&leagueid={league}&fields=details.(' 132 | 'short_name%2Cstarts_at%2Cleague%2Cparticipants)%2Cscore_board' 133 | '%2Ctype%2Ceventids%2Cgames%5B0%5D.(id%2Cname%2C' 134 | 'results.(name%2Codds))&sort=live%20desc%2Cstartsat%20asc%2Csportid' 135 | '%20asc%2Ceventname%20asc&content=list&page_number=1&page_size=40' 136 | '&partnerid=mobit2013') 137 | url_params = { 138 | 'soccer': { 139 | 'id': 4, 140 | 'leagues': { 141 | 'poland-ekstraklasa': 21543, 142 | 'cipro-1-division': 39123, 143 | 'israel-premier-league': 24835, 144 | }, 145 | } 146 | } 147 | 148 | def parse_response(self, response): 149 | logger.debug('Parsing quotes as JSON for BWin') 150 | data = response.json() 151 | matches_quotes = {} 152 | for event in data['response']['items']['events']: 153 | game = [g for g in event['non_live']['games'] if g['name'] == '1X2'] 154 | try: 155 | game = game[0] 156 | except IndexError: 157 | continue 158 | teams = tuple(event['details']['short_name'].split(' - ')) 159 | matches_quotes[teams] = tuple( 160 | float(result['odds']) for result in game['results']) 161 | return matches_quotes 162 | 163 | 164 | class Sisal(Site): 165 | url = 'http://mobile.sisal.it/events_wap.t?league={sport}_{league}' 166 | url_params = { 167 | 'soccer': { 168 | 'id': 1, 169 | 'leagues': { 170 | 'poland-ekstraklasa': 183, 171 | 'cipro-1-division': 290, 172 | 'israel-premier-league': 215, 173 | }, 174 | } 175 | } 176 | 177 | def parse_response(self, response): 178 | logger.debug('Parsing response as HTML tree for Sisal') 179 | data = BytesIO(response.text.encode('utf-8')) 180 | parser = etree.HTMLParser(remove_blank_text=True, remove_comments=True, 181 | recover=True) 182 | html = etree.parse(data, parser) 183 | match_nodes = html.getroot().xpath( 184 | '//table[@class="event-header"]//span[@class="fs15px b"]') 185 | quote_nodes = html.getroot().xpath( 186 | '//span[@class="odds-convert"]') 187 | matches_quotes = {} 188 | for match, quotes in zip( 189 | match_nodes, 190 | (quote_nodes[i:i+3] for i in range(0, len(quote_nodes), 3))): 191 | matches_quotes[tuple(match.text.split(' - '))] = tuple( 192 | float(quote.text.replace(',', '.')) for quote in quotes) 193 | return matches_quotes 194 | 195 | 196 | class Bet365(Site): 197 | url = ('https://mobile.bet365.it/sport/coupon/?ptid={sport}&key=' 198 | '1-1-13-{league}-2-17-0-0-1-0-0-4100-0-0-1-0-0-0-0-0-0') 199 | url_params = { 200 | 'soccer': { 201 | 'id': 4100, 202 | 'leagues': { 203 | 'poland-ekstraklasa': '26304997', 204 | 'cipro-1-division': '26404811', 205 | 'israel-premier-league': '26405613', 206 | }, 207 | }, 208 | } 209 | 210 | def parse_response(self, response): 211 | logger.debug('Parsing response as HTML tree for Bet365') 212 | data = BytesIO(response.text.encode('utf-8')) 213 | parser = etree.HTMLParser(remove_blank_text=True, remove_comments=True, 214 | recover=True) 215 | html = etree.parse(data, parser) 216 | match_nodes = html.getroot().xpath( 217 | '//th[@class="DarkMidGrey"][@colspan="2"]') 218 | quote_nodes = html.getroot().xpath( 219 | '//td[contains(@class, "nFTRr2")]') 220 | matches_quotes = {} 221 | for match, quotes in zip( 222 | match_nodes, 223 | (quote_nodes[i:i+3] for i in range(0, len(quote_nodes), 3))): 224 | matches_quotes[tuple(match.text.split(' v '))] = tuple( 225 | float(quote.text) for quote in quotes) 226 | return matches_quotes 227 | 228 | if __name__ == '__main__': 229 | # App config 230 | log_levels = ['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'] 231 | parser = argparse.ArgumentParser(description='Find sure bets.') 232 | parser.add_argument('-l', '--log-level', default='WARNING', 233 | choices=log_levels, dest='log_level', 234 | type=lambda x: x.upper()) 235 | args = parser.parse_args() 236 | logger.setLevel(getattr(logging, args.log_level)) 237 | 238 | sites_manager = SitesManager([Sisal(), BWin(), Bet365()]) 239 | sites_manager.check_for_sure_bets() 240 | --------------------------------------------------------------------------------