diff --git a/investpy/__init__.py b/investpy/__init__.py index 541570c8..9a50134c 100644 --- a/investpy/__init__.py +++ b/investpy/__init__.py @@ -37,6 +37,6 @@ from .search import search_quotes -from .news import economic_calendar +from .news import economic_calendar, earnings_calendar from .technical import technical_indicators, moving_averages, pivot_points diff --git a/investpy/news.py b/investpy/news.py index 18d8de09..82d1a555 100644 --- a/investpy/news.py +++ b/investpy/news.py @@ -200,7 +200,7 @@ def economic_calendar(time_zone=None, time_filter='time_only', countries=None, i def_importances.append(key) break - if len(def_importances) > 0: + if len(def_importances) > 0: data.update({ 'importance[]': def_importances }) @@ -256,5 +256,246 @@ def economic_calendar(time_zone=None, time_filter='time_only', countries=None, i } results.append(result) - + + return pd.DataFrame(results) + + +def earnings_calendar(time_zone=None, time_filter='time_only', countries=None, importances=None, sectors=None, from_date=None, to_date=None): + """ + This function retrieves the earnings calendar, which covers financial earnings results and forecasts from all over the world + updated in real-time. By default, the earnings calendar of the currrent day from you local timezone will be retrieved, but + note that some parameters can be specified so that the economic calendar to retrieve can be filtered. + + Args: + time_zone (:obj:`str`): + time zone in GMT +/- hours:minutes format, which will be the reference time, if None, the local GMT time zone will be used. + time_filter (:obj:`str`): + it can be `time_only` or `time_remain`, so that the calendar will display the time when the event will occurr according to + the time zone or the remaining time until an event occurs. + countries (:obj:`list` of :obj:`str`): + list of countries from where the events of the economic calendar will be retrieved, all contries will be taken into consideration + if this parameter is None. + importances (:obj:`list` of :obj:`str`): + list of importances of the events to be taken into consideration, can contain: high, medium and low; if None all the importance + ratings will be taken into consideration including holidays. + sectors (:obj:`list` of :obj:`str`): + list of sectors to which the events will be related to, if None all the available categories will be taken into consideration. + from_date (:obj:`str`): + date from when the economic calendar will be retrieved in dd/mm/yyyy format, if None just current day's economic calendar will be retrieved. + to_date (:obj:`str`): + date until when the economic calendar will be retrieved in dd/mm/yyyy format, if None just current day's economic calendar will be retrieved. + + Returns: + :obj:`pandas.DataFrame` - earnings_calendar: + The resulting :obj:`pandas.DataFrame` will contain the retrieved information from the earnings calendar with the specified parameters + which will include information such as: date, company, symbol or eps_actual, market_cap, etc. Note that some of the retrieved fields + may be None since Investing.com does not provides that information. + + Raises: + ValueError: raised if any of the introduced parameters is not valid or errored. + + Examples: + >>> data = investpy.earnings_calendar() + >>> data.head() + + date company symbol eps_actual eps_forecast revenue_actual revenue_forecast market_cap earnings_time + 0 24/11/2020 Medtronic MDT 1.02 0.8024 7.65B 7.07B 153.93B None + 1 24/11/2020 Autodesk ADSK None 0.96 None 942.5M 56.49B None + 2 24/11/2020 Analog Devices ADI 1.44 1.33 1.53B 1.45B 50.01B None + 3 24/11/2020 Best Buy BBY 2.06 1.7 11.85B 10.97B 29.59B None + 4 24/11/2020 HP Inc HPQ None 0.5228 None 14.61B 29.54B None + + """ + + # TODO: would it be better to extract the shared logic with economic_calendar instead of the duplicated code? + if time_zone is not None and not isinstance(time_zone, str): + raise ValueError("ERR#0107: the introduced time_zone must be a string unless it is None.") + + if time_zone is None: + time_zone = 'GMT' + + diff = datetime.strptime(strftime('%d/%m/%Y %H:%M', localtime()), '%d/%m/%Y %H:%M') - \ + datetime.strptime(strftime('%d/%m/%Y %H:%M', gmtime()), '%d/%m/%Y %H:%M') + + hour_diff = int(diff.total_seconds() / 3600) + min_diff = int(diff.total_seconds() % 3600) * 60 + + if hour_diff != 0: + time_zone = "GMT " + ('+' if hour_diff > 0 else '') + str(hour_diff) + ":" + ('00' if min_diff < 30 else '30') + else: + if time_zone not in cst.TIMEZONES.keys(): + raise ValueError("ERR#0108: the introduced time_zone does not exist, please consider passing time_zone as None.") + + if not isinstance(time_filter, str): + raise ValueError("ERR#0109: the introduced time_filter is not valid since it must be a string.") + + if time_filter not in cst.TIME_FILTERS.keys(): + raise ValueError("ERR#0110: the introduced time_filter does not exist, available ones are: time_remaining and time_only.") + + if countries is not None and not isinstance(countries, list): + raise ValueError("ERR#0111: the introduced countries value is not valid since it must be a list of strings unless it is None.") + + if importances is not None and not isinstance(importances, list): + raise ValueError("ERR#0112: the introduced importances value is not valid since it must be a list of strings unless it is None.") + + if sectors is not None and not isinstance(sectors, list): + raise ValueError("ERR#0113: the introduced sectors value is not valid since it must be a list of strings unless it is None.") + + if from_date is not None and not isinstance(from_date, str): + raise ValueError("ERR#0114: the introduced date value must be a string unless it is None.") + + if to_date is not None and not isinstance(to_date, str): + raise ValueError("ERR#0114: the introduced date value must be a string unless it is None.") + + url = "https://www.investing.com/earnings-calendar/Service/getCalendarFilteredData" + + headers = { + "User-Agent": random_user_agent(), + "X-Requested-With": "XMLHttpRequest", + "Accept": "text/html", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + } + + dates = [from_date, to_date] + + if any(date is None for date in dates) is True: + data = { + 'timeZone': choice(cst.TIMEZONES[time_zone]), + 'timeFilter': cst.TIME_FILTERS[time_filter], + 'currentTab': 'today', + 'submitFilters': 1, + 'limit_from': 0 + } + else: + try: + datetime.strptime(from_date, '%d/%m/%Y') + except ValueError: + raise ValueError("ERR#0011: incorrect from_date date format, it should be 'dd/mm/yyyy'.") + + start_date = datetime.strptime(from_date, '%d/%m/%Y') + + try: + datetime.strptime(to_date, '%d/%m/%Y') + except ValueError: + raise ValueError("ERR#0012: incorrect to_date format, it should be 'dd/mm/yyyy'.") + + end_date = datetime.strptime(to_date, '%d/%m/%Y') + + if start_date >= end_date: + raise ValueError("ERR#0032: to_date should be greater than from_date, both formatted as 'dd/mm/yyyy'.") + + data = { + 'dateFrom': datetime.strptime(from_date, '%d/%m/%Y').strftime('%Y-%m-%d'), + 'dateTo': datetime.strptime(to_date, '%d/%m/%Y').strftime('%Y-%m-%d'), + 'timeZone': choice(cst.TIMEZONES[time_zone]), + 'timeFilter': cst.TIME_FILTERS[time_filter], + 'currentTab': 'custom', + 'submitFilters': 1, + 'limit_from': 0 + } + + if countries is not None: + def_countries = list() + + available_countries = list(cst.COUNTRY_ID_FILTERS.keys()) + + # TODO: improve loop using lambda + for country in countries: + country = unidecode(country.lower()) + country = country.strip() + + if country in available_countries: + def_countries.append(cst.COUNTRY_ID_FILTERS[country]) + + if len(def_countries) > 0: + data.update({ + 'country[]': def_countries + }) + + # TODO: sectors don't work yet, need to check investing.com API for the exact sectors definitions + if sectors is not None: + def_sectors = list() + + available_sectors = list(cst.SECTOR_FILTERS.keys()) + + # TODO: improve loop using lambda + for sector in sectors: + sector = unidecode(sector.lower()) + sector = sector.strip() + + if sector in available_sectors: + def_sectors.append(cst.SECTOR_FILTERS[sector]) + + if len(def_sectors) > 0: + data.update({ + 'sector[]': def_sectors + }) + + if importances is not None: + def_importances = list() + + # TODO: improve loop using lambda + for importance in importances: + importance = unidecode(importance.lower()) + importance = importance.strip() + + for key, value in cst.IMPORTANCE_RATINGS.items(): + if value == importance: + if key not in def_importances: + def_importances.append(key) + break + + if len(def_importances) > 0: + data.update({ + 'importance[]': def_importances + }) + + req = requests.post(url, headers=headers, data=data) + + root = fromstring(req.json()['data']) + table = root.xpath(".//tr") + + results = list() + + for row in table: + if row.get("tablesorterdivider") == '': + curr_date = datetime.strptime(row.xpath("td")[0].text_content(), '%A, %B %d, %Y').strftime("%d/%m/%Y") + else: + # TODO: missing the earnings_time parser (BMO, AMC), seems to be inconsistent in investing.com + company = symbol = eps_actual = eps_forecast = revenue_actual = revenue_forecast = market_cap = earnings_time = None + + for idx, val in enumerate(row.xpath("td")): + if not val.get("class"): + continue + + if val.get("class").__contains__('earnCalCompany'): + company_full = val.text_content().strip().split('(') + company = company_full[0][:-1] + symbol = company_full[1][:-1] + elif val.get("class").__contains__('eps_actual'): + eps_actual = val.text_content().strip() + eps_forecast_split = row.xpath("td")[idx + 1].text_content().split('/') + eps_forecast = eps_forecast_split[1].strip() if len(eps_forecast_split) > 0 else '' + elif val.get("class").__contains__('rev_actual'): + revenue_actual = val.text_content().strip() + revenue_forecast_split = row.xpath("td")[idx + 1].text_content().split('/') + revenue_forecast = revenue_forecast_split[1].strip() if len(revenue_forecast_split) > 0 else '' + elif val.get("class") == 'right': + market_cap = val.text_content().strip() + + result = { + 'date': curr_date, + 'company': None if company == '' else company, + 'symbol': None if symbol == '' else symbol, + 'eps_actual': None if eps_actual == '--' else eps_actual, + 'eps_forecast': None if eps_forecast == '--' else eps_forecast, + 'revenue_actual': None if revenue_actual == '--' else revenue_actual, + 'revenue_forecast': None if revenue_forecast == '--' else revenue_forecast, + 'market_cap': None if market_cap == '--' else market_cap, + 'earnings_time': None if earnings_time == '' else earnings_time + } + + results.append(result) + return pd.DataFrame(results) diff --git a/investpy/utils/constant.py b/investpy/utils/constant.py index 2631f3c1..293f1d06 100644 --- a/investpy/utils/constant.py +++ b/investpy/utils/constant.py @@ -106,6 +106,20 @@ 'bonds': '_Bonds' } +SECTOR_FILTERS = { + 'financial_services': 'financial_services', + 'consumer_cyclical': 'consumer_cyclical', + 'technology': 'technology', + 'capital_goods': 'capital_goods', + 'healthcare': 'healthcare', + 'basic_materials': 'basic_materials', + 'consumer_non_cyclical': 'consumer_non_cyclical', + 'energy': 'energy', + 'transportation': 'transportation', + 'utilities': 'utilities', + 'conglomerates': 'conglomerates' +} + IMPORTANCE_RATINGS = { 1: 'low', 2: 'medium', diff --git a/tests/test_investpy.py b/tests/test_investpy.py index df7f12db..8944a05d 100644 --- a/tests/test_investpy.py +++ b/tests/test_investpy.py @@ -201,7 +201,7 @@ def test_investpy_stocks(): for param in params: investpy.get_stock_financial_summary(stock=param['stock'], - country=param['country'], + country=param['country'], summary_type=param['summary_type'], period=param['period']) @@ -566,15 +566,15 @@ def test_investpy_indices(): for param in params: investpy.get_index_information(index=param['index'], country=param['country'], as_json=param['as_json']) - + params = [ { - 'country': 'united states', + 'country': 'united states', 'as_json': False, 'n_results': 10 }, { - 'country': 'united kingdom', + 'country': 'united kingdom', 'as_json': True, 'n_results': 10 } @@ -722,7 +722,7 @@ def test_investpy_currency_crosses(): for param in params: investpy.get_currency_cross_information(currency_cross=param['currency_cross'], as_json=param['as_json']) - + params = [ { 'currency': 'try', @@ -735,7 +735,7 @@ def test_investpy_currency_crosses(): 'n_results': 100 } ] - + for param in params: investpy.get_currency_crosses_overview(currency=param['currency'], as_json=param['as_json'], n_results=param['n_results']) @@ -844,7 +844,7 @@ def test_investpy_bonds(): for param in params: investpy.get_bond_information(bond=param['bond'], as_json=param['as_json']) - + params = [ { 'country': 'united states', @@ -968,7 +968,7 @@ def test_investpy_commodities(): for param in params: investpy.get_commodity_information(commodity=param['commodity'], country=param['country'], as_json=param['as_json']) - + params = [ { 'group': 'metals', @@ -992,7 +992,7 @@ def test_investpy_cryptos(): """ This function checks that crypto currencies data retrieval functions listed in investpy work properly. """ - + investpy.get_cryptos() investpy.get_cryptos_list() @@ -1008,7 +1008,7 @@ def test_investpy_cryptos(): { 'columns': None, 'as_json': True - }, + }, ] for param in params: @@ -1060,7 +1060,7 @@ def test_investpy_cryptos(): for param in params: investpy.get_crypto_information(crypto=param['crypto'], as_json=param['as_json']) - + params = [ { 'as_json': False, @@ -1198,7 +1198,7 @@ def test_investpy_certificates(): investpy.get_certificate_information(certificate=param['certificate'], country=param['country'], as_json=param['as_json']) - + params = [ { 'country': 'france', @@ -1311,12 +1311,42 @@ def test_investpy_news(): for param in params: investpy.economic_calendar(time_zone=param['time_zone'], - time_filter=param['time_filter'], - countries=param['countries'], - importances=param['importances'], - categories=param['categories'], - from_date=param['from_date'], - to_date=param['to_date']) + time_filter=param['time_filter'], + countries=param['countries'], + importances=param['importances'], + categories=param['categories'], + from_date=param['from_date'], + to_date=param['to_date']) + + params = [ + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': ['spain', 'france'], + 'importances': ['high', 'low'], + 'sectors': None, + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': 'GMT -3:00', + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': '01/01/2020', + 'to_date': '01/02/2020' + } + ] + + for param in params: + investpy.earnings_calendar(time_zone=param['time_zone'], + time_filter=param['time_filter'], + countries=param['countries'], + importances=param['importances'], + sectors=param['sectors'], + from_date=param['from_date'], + to_date=param['to_date']) def test_investpy_technical(): diff --git a/tests/test_investpy_errors.py b/tests/test_investpy_errors.py index 88c89c19..4ec58f49 100644 --- a/tests/test_investpy_errors.py +++ b/tests/test_investpy_errors.py @@ -606,7 +606,7 @@ def test_stocks_errors(): for param in params: try: investpy.get_stock_financial_summary(stock=param['stock'], - country=param['country'], + country=param['country'], summary_type=param['summary_type'], period=param['period']) except: @@ -1922,32 +1922,32 @@ def test_indices_errors(): params = [ { - 'country': None, + 'country': None, 'as_json': False, 'n_results': 10 }, { - 'country': ['error'], + 'country': ['error'], 'as_json': False, 'n_results': 10 }, { - 'country': 'spain', + 'country': 'spain', 'as_json': None, 'n_results': 10 }, { - 'country': 'spain', + 'country': 'spain', 'as_json': False, 'n_results': 'error' }, { - 'country': 'spain', + 'country': 'spain', 'as_json': False, 'n_results': 0 }, { - 'country': 'error', + 'country': 'error', 'as_json': False, 'n_results': 10 }, @@ -2312,7 +2312,7 @@ def test_currency_crosses_errors(): 'n_results': 10 } ] - + for param in params: try: investpy.get_currency_crosses_overview(currency=param['currency'], as_json=param['as_json'], n_results=param['n_results']) @@ -2644,7 +2644,7 @@ def test_bonds_errors(): investpy.get_bond_information(bond=param['bond'], as_json=param['as_json']) except: pass - + params = [ { 'country': None, @@ -3045,7 +3045,7 @@ def test_commodities_errors(): investpy.get_commodity_information(commodity=param['commodity'], country=param['country'], as_json=param['as_json']) except: pass - + params = [ { 'group': None, @@ -3350,7 +3350,7 @@ def test_crypto_errors(): investpy.get_crypto_information(crypto=param['crypto'], as_json=param['as_json']) except: pass - + params = [ { 'as_json': None, @@ -3566,7 +3566,7 @@ def test_certificate_errors(): interval=param['interval']) except: pass - + params = [ { 'certificate': None, @@ -3762,7 +3762,7 @@ def test_certificate_errors(): as_json=param['as_json']) except: pass - + params = [ { 'country': None, @@ -4061,12 +4061,144 @@ def test_news_errors(): for param in params: try: investpy.economic_calendar(time_zone=param['time_zone'], - time_filter=param['time_filter'], - countries=param['countries'], - importances=param['importances'], - categories=param['categories'], - from_date=param['from_date'], - to_date=param['to_date']) + time_filter=param['time_filter'], + countries=param['countries'], + importances=param['importances'], + categories=param['categories'], + from_date=param['from_date'], + to_date=param['to_date']) + except: + pass + + params = [ + { + 'time_zone': ['error'], + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': 'error', + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': None, + 'time_filter': None, + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': None, + 'time_filter': ['error'], + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': 'error', + 'importances': None, + 'sectors': None, + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': 'error', + 'sectors': None, + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': 'error', + 'from_date': None, + 'to_date': None + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': ['error'], + 'to_date': None + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': None, + 'to_date': ['error'] + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': '01/01/2020', + 'to_date': '01/02/2020' + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': 'error', + 'to_date': '01/02/2020' + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': '01/01/2020', + 'to_date': 'error' + }, + { + 'time_zone': None, + 'time_filter': 'time_only', + 'countries': None, + 'importances': None, + 'sectors': None, + 'from_date': '01/01/2020', + 'to_date': '01/01/2019' + } + ] + + for param in params: + try: + investpy.earnings_calendar(time_zone=param['time_zone'], + time_filter=param['time_filter'], + countries=param['countries'], + importances=param['importances'], + sectors=param['sectors'], + from_date=param['from_date'], + to_date=param['to_date']) except: pass