Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#155 Implement get_stock_earnings function. #249

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion investpy/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from .stocks import get_stocks, get_stocks_list, get_stocks_dict, get_stock_countries, get_stock_recent_data, \
get_stock_historical_data, get_stock_company_profile, get_stock_dividends, get_stock_information, get_stocks_overview, \
get_stock_financial_summary, search_stocks
get_stock_financial_summary, search_stocks, get_stock_earnings

from .funds import get_funds, get_funds_list, get_funds_dict, get_fund_countries, get_fund_recent_data, \
get_fund_historical_data, get_fund_information, get_funds_overview, search_funds
Expand Down
233 changes: 203 additions & 30 deletions investpy/stocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ def get_stocks_list(country=None):
ValueError: raised whenever any of the introduced arguments is not valid.
FileNotFoundError: raised if `stocks.csv` file was not found.
IOError: raised when `stocks.csv` file is missing or empty.

"""

return stocks_as_list(country)
Expand Down Expand Up @@ -138,7 +138,7 @@ def get_stock_countries():
Returns:
:obj:`list` - countries:
The resulting :obj:`list` contains all the available countries with stocks as indexed in Investing.com

"""

return list(cst.STOCK_COUNTRIES.keys())
Expand All @@ -150,7 +150,7 @@ def get_stock_recent_data(stock, country, as_json=False, order='ascending', inte
of the introduced stock from the specified country will be retrieved and returned as a :obj:`pandas.DataFrame` if
the parameters are valid and the request to Investing.com succeeds. Note that additionally some optional parameters
can be specified: as_json and order, which let the user decide if the data is going to be returned as a
:obj:`json` or not, and if the historical data is going to be ordered ascending or descending (where the index is the
:obj:`json` or not, and if the historical data is going to be ordered ascending or descending (where the index is the
date), respectively.

Args:
Expand All @@ -171,9 +171,9 @@ def get_stock_recent_data(stock, country, as_json=False, order='ascending', inte

The resulting recent data, in case that the default parameters were applied, will look like::

Date || Open | High | Low | Close | Volume | Currency
Date || Open | High | Low | Close | Volume | Currency
-----||------|------|-----|-------|--------|----------
xxxx || xxxx | xxxx | xxx | xxxxx | xxxxxx | xxxxxxxx
xxxx || xxxx | xxxx | xxx | xxxxx | xxxxxx | xxxxxxxx

but in case that as_json parameter was defined as True, then the output will be::

Expand Down Expand Up @@ -299,7 +299,7 @@ def get_stock_recent_data(stock, country, as_json=False, order='ascending', inte

root_ = fromstring(req.text)
path_ = root_.xpath(".//table[@id='curr_table']/tbody/tr")

result = list()

if path_:
Expand Down Expand Up @@ -376,9 +376,9 @@ def get_stock_historical_data(stock, country, from_date, to_date, as_json=False,

The returned data is case we use default arguments will look like::

Date || Open | High | Low | Close | Volume | Currency
Date || Open | High | Low | Close | Volume | Currency
-----||------|------|-----|-------|--------|----------
xxxx || xxxx | xxxx | xxx | xxxxx | xxxxxx | xxxxxxxx
xxxx || xxxx | xxxx | xxx | xxxxx | xxxxxx | xxxxxxxx

but if we define `as_json=True`, then the output will be::

Expand Down Expand Up @@ -575,9 +575,9 @@ def get_stock_historical_data(stock, country, from_date, to_date, as_json=False,
raise IndexError("ERR#0007: stock information unavailable or not found.")
else:
data_flag = True

info = []

for nested_ in elements_.xpath(".//td"):
info.append(nested_.get('data-real-value'))

Expand Down Expand Up @@ -769,7 +769,7 @@ def get_stock_company_profile(stock, country='spain', language='english'):
company_profile['desc'] = ' '.join(text.replace('\n', ' ').replace('\xa0', ' ').split())

return company_profile

elif selected_source == 'Investing':
tag = stocks.loc[(stocks['symbol'].apply(unidecode).str.lower() == stock).idxmax(), 'tag']

Expand Down Expand Up @@ -911,7 +911,7 @@ def get_stock_dividends(stock, country):
try:
value = int(element_.getnext().get('data-value'))
dividend_payment_date = datetime.strptime(str(datetime.fromtimestamp(value).date()), '%Y-%m-%d')
except:
except BaseException:
dividend_payment_date = None
next_element_ = element_.getnext()
dividend_yield = next_element_.getnext().text_content()
Expand Down Expand Up @@ -975,7 +975,7 @@ def get_stock_dividends(stock, country):
try:
value = int(element_.getnext().get('data-value'))
dividend_payment_date = datetime.strptime(str(datetime.fromtimestamp(value).date()), '%Y-%m-%d')
except:
except BaseException:
dividend_payment_date = None
next_element_ = element_.getnext()
dividend_yield = next_element_.getnext().text_content()
Expand All @@ -997,8 +997,8 @@ def get_stock_dividends(stock, country):

def get_stock_information(stock, country, as_json=False):
"""
This function retrieves fundamental financial information from the specified stock. The retrieved
information from the stock can be valuable as it is additional information that can be used combined
This function retrieves fundamental financial information from the specified stock. The retrieved
information from the stock can be valuable as it is additional information that can be used combined
with OHLC values, so to determine financial insights from the company which holds the specified stock.

Args:
Expand Down Expand Up @@ -1150,8 +1150,8 @@ def get_stocks_overview(country, as_json=False, n_results=100):
"""
This function retrieves an overview containing all the real time data available for the main stocks from a country,
such as the names, symbols, current value, etc. as indexed in Investing.com. So on, the main usage of this
function is to get an overview on the main stocks from a country, so to get a general view. Note that since
this function is retrieving a lot of information at once, by default just the overview of the Top 100 stocks
function is to get an overview on the main stocks from a country, so to get a general view. Note that since
this function is retrieving a lot of information at once, by default just the overview of the Top 100 stocks
is being retrieved, but an additional parameter called n_results can be specified so to retrieve N results.

Args:
Expand All @@ -1170,16 +1170,16 @@ def get_stocks_overview(country, as_json=False, n_results=100):
country | name | symbol | last | high | low | change | change_percentage | turnover | currency
--------|------|--------|------|------|-----|--------|-------------------|----------|----------
xxxxxxx | xxxx | xxxxxx | xxxx | xxxx | xxx | xxxxxx | xxxxxxxxxxxxxxxxx | xxxxxxxx | xxxxxxxx

Raises:
ValueError: raised if any of the introduced arguments errored.
FileNotFoundError: raised when `stocks.csv` file is missing.
IOError: raised if data could not be retrieved due to file error.
RuntimeError:
raised either if the introduced country does not match any of the listed ones or if no overview results could be
RuntimeError:
raised either if the introduced country does not match any of the listed ones or if no overview results could be
retrieved from Investing.com.
ConnectionError: raised if GET requests does not return 200 status code.

"""

if country is None:
Expand Down Expand Up @@ -1312,22 +1312,22 @@ def get_stock_financial_summary(stock, country, summary_type='income_statement',
stock (:obj:`str`): symbol of the stock to retrieve its financial summary.
country (:obj:`str`): name of the country from where the introduced stock symbol is.
summary_type (:obj:`str`, optional):
type of the financial summary table to retrieve, default value is `income_statement`, but all the
type of the financial summary table to retrieve, default value is `income_statement`, but all the
available types are: `income_statement`, `cash_flow_statement` and `balance_sheet`.
period (:obj:`str`, optional):
period range of the financial summary table to rertieve, detault value is `annual`, but all the
period range of the financial summary table to rertieve, detault value is `annual`, but all the
available periods are: `annual` and `quarterly`.

Returns:
:obj:`pandas.DataFrame` - financial_summary:
The resulting :obj:`pandas.DataFrame` contains the table of the requested financial summary from the
The resulting :obj:`pandas.DataFrame` contains the table of the requested financial summary from the
introduced stock, so the fields/column names may vary, since it depends on the summary_type introduced.
So on, the returned table will have the following format/structure::

Date || Field 1 | Field 2 | ... | Field N
Date || Field 1 | Field 2 | ... | Field N
-----||---------|---------|-----|---------
xxxx || xxxxxxx | xxxxxxx | xxx | xxxxxxx
xxxx || xxxxxxx | xxxxxxx | xxx | xxxxxxx

Raises:
ValueError: raised if any of the introduced parameters is not valid or errored.
FileNotFoundError: raised if the stocks.csv file was not found.
Expand All @@ -1339,7 +1339,7 @@ def get_stock_financial_summary(stock, country, summary_type='income_statement',
>>> data = investpy.get_stock_financial_summary(stock='AAPL', country='United States', summary_type='income_statement', period='annual')
>>> data.head()
Total Revenue Gross Profit Operating Income Net Income
Date
Date
2019-09-28 260174 98392 63930 55256
2018-09-29 265595 101839 70898 59531
2017-09-30 229234 88186 61344 48351
Expand Down Expand Up @@ -1422,7 +1422,7 @@ def get_stock_financial_summary(stock, country, summary_type='income_statement',
}

url = 'https://www.investing.com/instruments/Financials/changesummaryreporttypeajax'

req = requests.get(url, params=params, headers=headers)

if req.status_code != 200:
Expand Down Expand Up @@ -1514,7 +1514,7 @@ def search_stocks(by, value):

stocks['matches'] = stocks[by].str.contains(value, case=False)

search_result = stocks.loc[stocks['matches'] == True].copy()
search_result = stocks.loc[stocks['matches']].copy()

if len(search_result) == 0:
raise RuntimeError('ERR#0043: no results were found for the introduced ' + str(by) + '.')
Expand All @@ -1523,3 +1523,176 @@ def search_stocks(by, value):
search_result.reset_index(drop=True, inplace=True)

return search_result


def get_stock_earnings(stock, country):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As there are some changes in the stocks.py file in the master branch, could you pull the current file from the master and add your function at the end so that the PR has no merge conflicts?

"""
This function retrieves the earnings data from the specified stock. Earnings data include date of the
release date of the earnings report, period end of the earnings report, EPS, forecast EPS, revenue,
and forecast revenue.

Args:
stock (:obj:`str`): symbol of the stock to retrieve its earnings from.
country (:obj:`country`): name of the country from where the stock is from.

Returns:
:obj:`pandas.DataFrame` - stock_dividends:
Returns a :obj:`pandas.DataFrame` containing the retrieved information of stock earnings for every stock
symbol introduced as parameter.

So on, the resulting :obj:`pandas.DataFrame` will look like::

Release Date Period End EPS EPS Forcast Revenue Revenue Forcast
0 02/02/2021 12/2020 NaN 1.3900 None 101.65B
1 29/10/2020 09/2020 0.7300 0.7099 64.7B 63.8B
2 30/07/2020 06/2020 0.6450 0.5100 59.69B 52.29B
3 30/04/2020 03/2020 0.6375 2.2500 58.31B 54.9B
...

"""

if not stock:
raise ValueError("ERR#0013: stock parameter is mandatory and must be a valid stock symbol.")

if not isinstance(stock, str):
raise ValueError("ERR#0027: stock argument needs to be a str.")

if country is None:
raise ValueError("ERR#0039: country can not be None, it should be a str.")

if country is not None and not isinstance(country, str):
raise ValueError("ERR#0025: specified country value not valid.")

resource_package = 'investpy'
resource_path = '/'.join((('resources', 'stocks.csv')))
if pkg_resources.resource_exists(resource_package, resource_path):
stocks = pd.read_csv(pkg_resources.resource_filename(resource_package, resource_path), keep_default_na=False)
else:
raise FileNotFoundError("ERR#0056: stocks file not found or errored.")

if stocks is None:
raise IOError("ERR#0001: stocks object not found or unable to retrieve.")

if unidecode(country.lower()) not in get_stock_countries():
raise RuntimeError("ERR#0034: country " + country.lower() + " not found, check if it is correct.")

stocks = stocks[stocks['country'].str.lower() == unidecode(country.lower())]

stock = unidecode(stock.strip().lower())

if stock not in [unidecode(value.strip().lower()) for value in stocks['symbol'].tolist()]:
raise RuntimeError("ERR#0018: stock " + stock + " not found, check if it is correct.")

tag_ = stocks.loc[(stocks['symbol'].str.lower() == stock).idxmax(), 'tag']

headers = {
"User-Agent": random_user_agent(),
"X-Requested-With": "XMLHttpRequest",
"Accept": "text/html",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
}

url = 'https://www.investing.com/equities/' + str(tag_) + '-earnings'

req = requests.get(url=url, headers=headers)

if req.status_code != 200:
raise ConnectionError("ERR#0015: error " + str(req.status_code) + ", try again later.")

root_ = fromstring(req.text)
path_ = root_.xpath(".//table[contains(@id, 'earningsHistory')]")

if path_:
more_results_id = path_[0].get('id').replace('earningsHistory', '')

path_ = root_.xpath(".//table[@id='earningsHistory" + str(more_results_id) + "']/tbody/tr")

objs = list()

if path_:
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing else statement, should raise an Exception just like all the other stock functions, please check https://github.com/alvarobartt/investpy/blob/master/investpy/stocks.py to see the latest changes and apply them here!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the else statement and Exception are already implemented as same as the following function.

def get_stock_dividends(stock, country):

last_timestamp = path_[-1].get('event_timestamp')
for elements_ in path_:
info = []
release_date = period_end = eps = eps_forcast = revenue = revenue_forcast = None
for e_ in elements_.xpath(".//td"):
info.append(unidecode(e_.text_content()).strip())
if len(info) != 6: # earning info size should be 6.
continue
release_date = datetime.strptime(info[0], '%b %d, %Y').strftime("%d/%m/%Y")
period_end = info[1]
eps = None if '--' in info[2] else float(info[2])
eps_forcast = None if '--' in info[3] else float(info[3].split(" ")[-1]) # remove "/ " from forcast info
revenue = None if '--' in info[4]else info[4]
revenue_forcast = None if '--' in info[5] else info[5].split(" ")[-1] # remove "/ " from forcast info
obj = {
'Release Date': release_date,
'Period End': period_end,
'EPS': eps,
'EPS Forcast': eps_forcast,
'Revenue': revenue,
'Revenue Forcast': revenue_forcast,
}
objs.append(obj)

flag = True

while flag:
headers = {
"User-Agent": random_user_agent(),
"X-Requested-With": "XMLHttpRequest",
"Accept": "text/html",
"Accept-Encoding": "gzip, deflate",
"Connection": "keep-alive",
}

params = {
'pairID': int(more_results_id),
'last_timestamp': last_timestamp
}

url = 'https://www.investing.com/equities/morehistory'

req = requests.post(url=url, headers=headers, params=params)

if req.status_code != 200:
raise ConnectionError("ERR#0015: error " + str(req.status_code) + ", try again later.")

res = req.json()
if res['hasMoreHistory'] is False:
flag = False

if res['hasMoreHistory'] is None or not res['historyRows']:
break

root_ = fromstring(res['historyRows'])
path_ = root_.xpath(".//tr")
if path_:
last_timestamp = path_[-1].get('event_timestamp')
for elements_ in path_:
release_date = period_end = eps = eps_forcast = revenue = revenue_forcast = None
info = []
for e_ in elements_.xpath(".//td"):
info.append(unidecode(e_.text_content()).strip())
if len(info) != 6: # earning info size should be 6.
continue
release_date = datetime.strptime(info[0], '%b %d, %Y').strftime("%d/%m/%Y")
period_end = info[1]
eps = None if '--' in info[2] else float(info[2])
eps_forcast = None if '--' in info[3] else float(info[3].split(" ")[-1]) # remove "/ " from forcast info
revenue = None if '--' in info[4]else info[4]
revenue_forcast = None if '--' in info[5] else info[5].split(" ")[-1] # remove "/ " from forcast info
obj = {
'Release Date': release_date,
'Period End': period_end,
'EPS': eps,
'EPS Forcast': eps_forcast,
'Revenue': revenue,
'Revenue Forcast': revenue_forcast,
}
objs.append(obj)

df = pd.DataFrame(objs)
return df
else:
raise RuntimeError("ERR#0061: introduced stock has no earnings history's data to display.")
Loading