diff --git a/README.md b/README.md index f3ec7c153..a65c7ae21 100644 --- a/README.md +++ b/README.md @@ -68,24 +68,30 @@ msft.dividends # show splits msft.splits -# show financials -msft.financials -msft.quarterly_financials +# show share count +msft.shares -# show major holders -msft.major_holders - -# show institutional holders -msft.institutional_holders +# show income statement +msft.income_stmt +msft.quarterly_income_stmt # show balance sheet msft.balance_sheet msft.quarterly_balance_sheet -# show cashflow +# show cash flow statement msft.cashflow msft.quarterly_cashflow +# show major holders +msft.major_holders + +# show institutional holders +msft.institutional_holders + +# show mutualfund holders +msft.mutualfund_holders + # show earnings msft.earnings msft.quarterly_earnings @@ -95,6 +101,12 @@ msft.sustainability # show analysts recommendations msft.recommendations +msft.recommendations_summary +# show analysts other work +msft.analyst_price_target +mfst.revenue_forecasts +mfst.earnings_forecasts +mfst.earnings_trend # show next event (earnings, etc) msft.calendar diff --git a/test_yfinance.py b/test_yfinance.py index adc14a188..098eaa45a 100644 --- a/test_yfinance.py +++ b/test_yfinance.py @@ -37,23 +37,28 @@ def test_attributes(self): ticker.dividends ticker.splits ticker.actions + ticker.shares ticker.info ticker.calendar ticker.recommendations ticker.earnings ticker.quarterly_earnings - ticker.financials - ticker.quarterly_financials + ticker.income_stmt + ticker.quarterly_income_stmt ticker.balance_sheet ticker.quarterly_balance_sheet ticker.cashflow ticker.quarterly_cashflow + ticker.recommendations_summary + ticker.analyst_price_target + ticker.revenue_forecasts ticker.sustainability ticker.options ticker.news - ticker.shares + ticker.earnings_trend ticker.earnings_history ticker.earnings_dates + ticker.earnings_forecasts def test_holders(self): for ticker in tickers: diff --git a/yfinance/base.py b/yfinance/base.py index f474bb76d..ff4527575 100644 --- a/yfinance/base.py +++ b/yfinance/base.py @@ -60,9 +60,14 @@ def __init__(self, ticker, session=None): self._fundamentals = False self._info = None - self._analysis = None + self._earnings_trend = None self._sustainability = None self._recommendations = None + self._analyst_trend_details = None + self._analyst_price_target = None + self._rev_est = None + self._eps_est = None + self._major_holders = None self._institutional_holders = None self._mutualfund_holders = None @@ -77,8 +82,6 @@ def __init__(self, ticker, session=None): self._earnings = None self._financials = None - self._balancesheet = None - self._cashflow = None # accept isin as ticker if utils.is_isin(self.ticker): @@ -97,7 +100,7 @@ def stats(self, proxy=None): ticker_url = "{}/{}".format(self._scrape_url, self.ticker) # get info and sustainability - data = utils.get_json(ticker_url, proxy, self.session) + data = utils.get_json_data_stores(ticker_url, proxy, self.session)["QuoteSummaryStore"] return data def history(self, period="1mo", interval="1d", @@ -613,7 +616,7 @@ def _get_info(self, proxy=None): ticker_url = "{}/{}".format(self._scrape_url, self.ticker) # get info and sustainability - data = utils.get_json(ticker_url, proxy, self.session) + data = utils.get_json_data_stores(ticker_url, proxy, self.session)['QuoteSummaryStore'] # sustainability d = {} @@ -711,6 +714,9 @@ def _get_info(self, proxy=None): def _get_fundamentals(self, proxy=None): def cleanup(data): + ''' + The cleanup function is used for parsing yahoo finance json financial statement data into a pandas dataframe format. + ''' df = _pd.DataFrame(data).drop(columns=['maxAge']) for col in df.columns: df[col] = _np.where( @@ -759,9 +765,6 @@ def cleanup(data): elif len(holders) >= 1: self._major_holders = holders[0] - # self._major_holders = holders[0] - # self._institutional_holders = holders[1] - if self._institutional_holders is not None: if 'Date Reported' in self._institutional_holders: self._institutional_holders['Date Reported'] = _pd.to_datetime( @@ -781,37 +784,26 @@ def cleanup(data): self._get_info(proxy) # get fundamentals - data = utils.get_json(ticker_url + '/financials', proxy, self.session) + fin_data = utils.get_json_data_stores(ticker_url + '/financials', proxy, self.session) + fin_data_quote = fin_data['QuoteSummaryStore'] # generic patterns - self._earnings = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()} - self._cashflow = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()} - self._balancesheet = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()} - self._financials = {"yearly": utils.empty_df(), "quarterly": utils.empty_df()} - for key in ( - (self._cashflow, 'cashflowStatement', 'cashflowStatements'), - (self._balancesheet, 'balanceSheet', 'balanceSheetStatements'), - (self._financials, 'incomeStatement', 'incomeStatementHistory') - ): - item = key[1] + 'History' - if isinstance(data.get(item), dict): - try: - key[0]['yearly'] = cleanup(data[item][key[2]]) - except Exception: - pass - - item = key[1] + 'HistoryQuarterly' - if isinstance(data.get(item), dict): - try: - key[0]['quarterly'] = cleanup(data[item][key[2]]) - except Exception: - pass + self._earnings = {"yearly": utils._pd.DataFrame(), "quarterly": utils._pd.DataFrame()} + self._financials = {} + for name in ["income", "balance-sheet", "cash-flow"]: + self._financials[name] = {"yearly":utils._pd.DataFrame(), "quarterly":utils._pd.DataFrame()} + for name in ["income", "balance-sheet", "cash-flow"]: + annual, qtr = self._create_financials_table(name, proxy) + if annual is not None: + self._financials[name]["yearly"] = annual + if qtr is not None: + self._financials[name]["quarterly"] = qtr # earnings - if isinstance(data.get('earnings'), dict): + if isinstance(fin_data_quote.get('earnings'), dict): try: - earnings = data['earnings']['financialsChart'] - earnings['financialCurrency'] = 'USD' if 'financialCurrency' not in data['earnings'] else data['earnings']['financialCurrency'] + earnings = fin_data_quote['earnings']['financialsChart'] + earnings['financialCurrency'] = 'USD' if 'financialCurrency' not in fin_data_quote['earnings'] else fin_data_quote['earnings']['financialCurrency'] self._earnings['financialCurrency'] = earnings['financialCurrency'] df = _pd.DataFrame(earnings['yearly']).set_index('date') df.columns = utils.camel2title(df.columns) @@ -828,7 +820,7 @@ def cleanup(data): # shares outstanding try: # keep only years with non None data - available_shares = [shares_data for shares_data in data['annualBasicAverageShares'] if shares_data] + available_shares = [shares_data for shares_data in fin_data['QuoteTimeSeriesStore']['timeSeries']['annualBasicAverageShares'] if shares_data] shares = _pd.DataFrame(available_shares) shares['Year'] = shares['asOfDate'].agg(lambda x: int(x[:4])) shares.set_index('Year', inplace=True) @@ -841,7 +833,7 @@ def cleanup(data): pass # Analysis - data = utils.get_json(ticker_url + '/analysis', proxy, self.session) + data = utils.get_json_data_stores(ticker_url + '/analysis', proxy, self.session)["QuoteSummaryStore"] if isinstance(data.get('earningsTrend'), dict): try: @@ -863,7 +855,7 @@ def cleanup(data): utils.camel2title([k])[0] analysis.loc[idx, new_colname] = v - self._analysis = analysis[[ + self._earnings_trend = analysis[[ c for c in analysis.columns if c not in dict_cols]] except Exception: pass @@ -910,8 +902,84 @@ def cleanup(data): if 'trailingPegRatio' in res: self._info['trailingPegRatio'] = res['trailingPegRatio'] + # Analysis Data/Analyst Forecasts + try: + analysis_data = utils.get_json_data_stores(ticker_url+'/analysis',proxy,self.session) + analysis_data = analysis_data['QuoteSummaryStore'] + except Exception as e: + analysis_data = {} + try: + self._analyst_trend_details = _pd.DataFrame(analysis_data['recommendationTrend']['trend']) + except Exception as e: + self._analyst_trend_details = None + try: + self._analyst_price_target = _pd.DataFrame(analysis_data['financialData'], index=[0])[['targetLowPrice','currentPrice','targetMeanPrice','targetHighPrice','numberOfAnalystOpinions']].T + except Exception as e: + self._analyst_price_target = None + earnings_estimate = [] + revenue_estimate = [] + if len(self._analyst_trend_details) != 0: + for key in analysis_data['earningsTrend']['trend']: + try: + earnings_dict = key['earningsEstimate'] + earnings_dict['period'] = key['period'] + earnings_dict['endDate'] = key['endDate'] + earnings_estimate.append(earnings_dict) + + revenue_dict = key['revenueEstimate'] + revenue_dict['period'] = key['period'] + revenue_dict['endDate'] = key['endDate'] + revenue_estimate.append(revenue_dict) + except Exception as e: + pass + self._rev_est = _pd.DataFrame(revenue_estimate) + self._eps_est = _pd.DataFrame(earnings_estimate) + else: + self._rev_est = _pd.DataFrame() + self._eps_est = _pd.DataFrame() + self._fundamentals = True + def _create_financials_table(self, name, proxy): + acceptable_names = ["income", "balance-sheet", "cash-flow"] + if not name in acceptable_names: + raise Exception("name '{}' must be one of: {}".format(name, acceptable_names)) + + if name == "income": + # Yahoo stores the 'income' table internally under 'financials' key + name = "financials" + + ticker_url = "{}/{}".format(self._scrape_url, self.ticker) + data_store = utils.get_json_data_stores(ticker_url+'/'+name, proxy, self.session) + + _stmt_annual = None + _stmt_qtr = None + try: + # Developers note: TTM and template stuff allows for reproducing the nested structure + # visible on Yahoo website. But more work needed to make it user-friendly! Ideally + # return a tree data structure instead of Pandas MultiIndex + # So until this is implemented, just return simple tables + _stmt_annual = utils.get_financials_time_series(self.ticker, name, "annual", ticker_url, proxy, self.session) + _stmt_qtr = utils.get_financials_time_series(self.ticker, name, "quarterly", ticker_url, proxy, self.session) + + # template_ttm_order, template_annual_order, template_order, level_detail = utils.build_template(data_store["FinancialTemplateStore"]) + # TTM_dicts, Annual_dicts = utils.retreive_financial_details(data_store['QuoteTimeSeriesStore']) + # if name == "balance-sheet": + # # Note: balance sheet is the only financial statement with no ttm detail + # _stmt_annual = utils.format_annual_financial_statement(level_detail, Annual_dicts, template_annual_order) + # else: + # _stmt_annual = utils.format_annual_financial_statement(level_detail, Annual_dicts, template_annual_order, TTM_dicts, template_ttm_order) + + # Data store doesn't contain quarterly data, so retrieve using different url: + # _qtr_data = utils.get_financials_time_series(self.ticker, name, "quarterly", ticker_url, proxy, self.session) + # _stmt_qtr = utils.format_quarterly_financial_statement(_qtr_data, level_detail, template_order) + + except: + pass + + return _stmt_annual, _stmt_qtr + + def get_recommendations(self, proxy=None, as_dict=False, *args, **kwargs): self._get_info(proxy) data = self._recommendations @@ -963,42 +1031,67 @@ def get_sustainability(self, proxy=None, as_dict=False, *args, **kwargs): return data.to_dict() return data - def get_earnings(self, proxy=None, as_dict=False, freq="yearly"): + def get_recommendations_summary(self, proxy=None, as_dict=False, *args, **kwargs): self._get_fundamentals(proxy=proxy) - data = self._earnings[freq] + data = self._analyst_trend_details if as_dict: - dict_data = data.to_dict() - dict_data['financialCurrency'] = 'USD' if 'financialCurrency' not in self._earnings else self._earnings['financialCurrency'] - return dict_data + return data.to_dict() return data - def get_analysis(self, proxy=None, as_dict=False, *args, **kwargs): + def get_analyst_price_target(self, proxy=None, as_dict=False, *args, **kwargs): self._get_fundamentals(proxy=proxy) - data = self._analysis + data = self._analyst_price_target if as_dict: return data.to_dict() return data - def get_financials(self, proxy=None, as_dict=False, freq="yearly"): + def get_rev_forecast(self, proxy=None, as_dict=False, *args, **kwargs): self._get_fundamentals(proxy=proxy) - data = self._financials[freq] + data = self._rev_est if as_dict: return data.to_dict() return data - def get_balancesheet(self, proxy=None, as_dict=False, freq="yearly"): + def get_earnings_forecast(self, proxy=None, as_dict=False, *args, **kwargs): self._get_fundamentals(proxy=proxy) - data = self._balancesheet[freq] + data = self._eps_est if as_dict: return data.to_dict() return data - def get_balance_sheet(self, proxy=None, as_dict=False, freq="yearly"): - return self.get_balancesheet(proxy, as_dict, freq) + def get_earnings_trend(self, proxy=None, as_dict=False, *args, **kwargs): + self._get_fundamentals(proxy=proxy) + data = self._earnings_trend + if as_dict: + return data.to_dict() + return data + def get_earnings(self, proxy=None, as_dict=False, freq="yearly"): + self._get_fundamentals(proxy=proxy) + data = self._earnings[freq] + if as_dict: + dict_data = data.to_dict() + dict_data['financialCurrency'] = 'USD' if 'financialCurrency' not in self._earnings else self._earnings['financialCurrency'] + return dict_data + return data + + def get_income_stmt(self, proxy=None, as_dict=False, freq="yearly"): + self._get_fundamentals(proxy=proxy) + data = self._financials["income"][freq] + if as_dict: + return data.to_dict() + return data + + def get_balance_sheet(self, proxy=None, as_dict=False, freq="yearly"): + self._get_fundamentals(proxy=proxy) + data = self._financials["balance-sheet"][freq] + if as_dict: + return data.to_dict() + return data + def get_cashflow(self, proxy=None, as_dict=False, freq="yearly"): self._get_fundamentals(proxy=proxy) - data = self._cashflow[freq] + data = self._financials["cash-flow"][freq] if as_dict: return data.to_dict() return data @@ -1222,6 +1315,6 @@ def get_earnings_history(self, proxy=None): self._earnings_history = data # if no tables are found a ValueError is thrown except ValueError: - print("Could not find data for {}.".format(self.ticker)) + print("Could not find earnings history data for {}.".format(self.ticker)) return return data diff --git a/yfinance/ticker.py b/yfinance/ticker.py index cdd3d1471..d14af9599 100644 --- a/yfinance/ticker.py +++ b/yfinance/ticker.py @@ -163,37 +163,49 @@ def quarterly_earnings(self): return self.get_earnings(freq='quarterly') @property - def financials(self): - return self.get_financials() + def income_stmt(self): + return self.get_income_stmt() @property - def quarterly_financials(self): - return self.get_financials(freq='quarterly') + def quarterly_income_stmt(self): + return self.get_income_stmt(freq='quarterly') @property def balance_sheet(self): - return self.get_balancesheet() + return self.get_balance_sheet() @property def quarterly_balance_sheet(self): - return self.get_balancesheet(freq='quarterly') + return self.get_balance_sheet(freq='quarterly') @property def balancesheet(self): - return self.get_balancesheet() + return self.balance_sheet @property def quarterly_balancesheet(self): - return self.get_balancesheet(freq='quarterly') + return self.quarterly_balance_sheet @property def cashflow(self): - return self.get_cashflow() + return self.get_cashflow(freq="yearly") @property def quarterly_cashflow(self): return self.get_cashflow(freq='quarterly') + @property + def recommendations_summary(self): + return self.get_recommendations_summary() + + @property + def analyst_price_target(self): + return self.get_analyst_price_target() + + @property + def revenue_forecasts(self): + return self.get_rev_forecast() + @property def sustainability(self): return self.get_sustainability() @@ -209,8 +221,8 @@ def news(self): return self.get_news() @property - def analysis(self): - return self.get_analysis() + def earnings_trend(self): + return self.get_earnings_trend() @property def earnings_history(self): @@ -219,3 +231,7 @@ def earnings_history(self): @property def earnings_dates(self): return self.get_earnings_dates() + + @property + def earnings_forecasts(self): + return self.get_earnings_forecast() diff --git a/yfinance/utils.py b/yfinance/utils.py index 826ae6540..2e19c19c1 100644 --- a/yfinance/utils.py +++ b/yfinance/utils.py @@ -116,26 +116,16 @@ def get_html(url, proxy=None, session=None): return html -def get_json(url, proxy=None, session=None): +def get_json_data_stores(url, proxy=None, session=None): + ''' + get_json_data_stores returns a python dictionary of the data stores in yahoo finance web page. + ''' session = session or _requests html = session.get(url=url, proxies=proxy, headers=user_agent_headers).text - if "QuoteSummaryStore" not in html: - html = session.get(url=url, proxies=proxy).text - if "QuoteSummaryStore" not in html: - return {} - json_str = html.split('root.App.main =')[1].split( '(this)')[0].split(';\n}')[0].strip() - data = _json.loads(json_str)[ - 'context']['dispatcher']['stores']['QuoteSummaryStore'] - # add data about Shares Outstanding for companies' tickers if they are available - try: - data['annualBasicAverageShares'] = _json.loads( - json_str)['context']['dispatcher']['stores'][ - 'QuoteTimeSeriesStore']['timeSeries']['annualBasicAverageShares'] - except Exception: - pass + data = _json.loads(json_str)['context']['dispatcher']['stores'] # return data new_data = _json.dumps(data).replace('{}', 'null') @@ -145,6 +135,206 @@ def get_json(url, proxy=None, session=None): return _json.loads(new_data) +def build_template(data): + ''' + build_template returns the details required to rebuild any of the yahoo finance financial statements in the same order as the yahoo finance webpage. The function is built to be used on the "FinancialTemplateStore" json which appears in any one of the three yahoo finance webpages: "/financials", "/cash-flow" and "/balance-sheet". + + Returns: + - template_annual_order: The order that annual figures should be listed in. + - template_ttm_order: The order that TTM (Trailing Twelve Month) figures should be listed in. + - template_order: The order that quarterlies should be in (note that quarterlies have no pre-fix - hence why this is required). + - level_detail: The level of each individual line item. E.g. for the "/financials" webpage, "Total Revenue" is a level 0 item and is the summation of "Operating Revenue" and "Excise Taxes" which are level 1 items. + + ''' + template_ttm_order = [] # Save the TTM (Trailing Twelve Months) ordering to an object. + template_annual_order = [] # Save the annual ordering to an object. + template_order = [] # Save the ordering to an object (this can be utilized for quarterlies) + level_detail = [] #Record the level of each line item of the income statement ("Operating Revenue" and "Excise Taxes" sum to return "Total Revenue" we need to keep track of this) + for key in data['template']: # Loop through the json to retreive the exact financial order whilst appending to the objects + template_ttm_order.append('trailing{}'.format(key['key'])) + template_annual_order.append('annual{}'.format(key['key'])) + template_order.append('{}'.format(key['key'])) + level_detail.append(0) + if 'children' in key: + for child1 in key['children']: # Level 1 + template_ttm_order.append('trailing{}'.format(child1['key'])) + template_annual_order.append('annual{}'.format(child1['key'])) + template_order.append('{}'.format(child1['key'])) + level_detail.append(1) + if 'children' in child1: + for child2 in child1['children']: # Level 2 + template_ttm_order.append('trailing{}'.format(child2['key'])) + template_annual_order.append('annual{}'.format(child2['key'])) + template_order.append('{}'.format(child2['key'])) + level_detail.append(2) + if 'children' in child2: + for child3 in child2['children']: # Level 3 + template_ttm_order.append('trailing{}'.format(child3['key'])) + template_annual_order.append('annual{}'.format(child3['key'])) + template_order.append('{}'.format(child3['key'])) + level_detail.append(3) + if 'children' in child3: + for child4 in child3['children']: # Level 4 + template_ttm_order.append('trailing{}'.format(child4['key'])) + template_annual_order.append('annual{}'.format(child4['key'])) + template_order.append('{}'.format(child4['key'])) + level_detail.append(4) + if 'children' in child4: + for child5 in child4['children']: # Level 5 + template_ttm_order.append('trailing{}'.format(child5['key'])) + template_annual_order.append('annual{}'.format(child5['key'])) + template_order.append('{}'.format(child5['key'])) + level_detail.append(5) + return template_ttm_order, template_annual_order, template_order, level_detail + + +def retreive_financial_details(data): + ''' + retreive_financial_details returns all of the available financial details under the "QuoteTimeSeriesStore" for any of the following three yahoo finance webpages: "/financials", "/cash-flow" and "/balance-sheet". + + Returns: + - TTM_dicts: A dictionary full of all of the available Trailing Twelve Month figures, this can easily be converted to a pandas dataframe. + - Annual_dicts: A dictionary full of all of the available Annual figures, this can easily be converted to a pandas dataframe. + ''' + TTM_dicts = [] # Save a dictionary object to store the TTM financials. + Annual_dicts = [] # Save a dictionary object to store the Annual financials. + for key in data['timeSeries']: # Loop through the time series data to grab the key financial figures. + try: + if len(data['timeSeries'][key]) > 0: + time_series_dict = {} + time_series_dict['index'] = key + for each in data['timeSeries'][key]: # Loop through the years + if each == None: + continue + else: + time_series_dict[each['asOfDate']] = each['reportedValue'] + # time_series_dict["{}".format(each['asOfDate'])] = data['timeSeries'][key][each]['reportedValue'] + if 'trailing' in key: + TTM_dicts.append(time_series_dict) + elif 'annual' in key: + Annual_dicts.append(time_series_dict) + except Exception as e: + pass + return TTM_dicts, Annual_dicts + + +def format_annual_financial_statement(level_detail, annual_dicts, annual_order, ttm_dicts=None, ttm_order=None): + ''' + format_annual_financial_statement formats any annual financial statement + + Returns: + - _statement: A fully formatted annual financial statement in pandas dataframe. + ''' + Annual = _pd.DataFrame.from_dict(annual_dicts).set_index("index") + Annual = Annual.reindex(annual_order) + Annual.index = Annual.index.str.replace(r'annual','') + + # Note: balance sheet is the only financial statement with no ttm detail + if (ttm_dicts not in [[], None]) and (ttm_order not in [[], None]): + TTM = _pd.DataFrame.from_dict(ttm_dicts).set_index("index") + TTM = TTM.reindex(ttm_order) + TTM.columns = ['TTM ' + str(col) for col in TTM.columns] # Add 'TTM' prefix to all column names, so if combined we can tell the difference between actuals and TTM (similar to yahoo finance). + TTM.index = TTM.index.str.replace(r'trailing', '') + _statement = Annual.merge(TTM, left_index=True, right_index=True) + else: + _statement = Annual + + _statement.index = camel2title(_statement.T) + _statement['level_detail'] = level_detail + _statement = _statement.set_index([_statement.index,'level_detail']) + _statement = _statement[sorted(_statement.columns, reverse=True)] + _statement = _statement.dropna(how='all') + return _statement + + +def format_quarterly_financial_statement(_statement, level_detail, order): + ''' + format_quarterly_financial_statements formats any quarterly financial statement + + Returns: + - _statement: A fully formatted quarterly financial statement in pandas dataframe. + ''' + _statement = _statement.reindex(order) + _statement.index = camel2title(_statement.T) + _statement['level_detail'] = level_detail + _statement = _statement.set_index([_statement.index,'level_detail']) + _statement = _statement[sorted(_statement.columns, reverse=True)] + _statement = _statement.dropna(how='all') + _statement.columns = _pd.to_datetime(_statement.columns).date + return _statement + + +def get_financials_time_series(ticker, name, timescale, ticker_url, proxy=None, session=None): + acceptable_names = ["financials", "balance-sheet", "cash-flow"] + if not name in acceptable_names: + raise Exception("name '{}' must be one of: {}".format(name, acceptable_names)) + acceptable_timestamps = ["annual", "quarterly"] + if not timescale in acceptable_timestamps: + raise Exception("timescale '{}' must be one of: {}".format(timescale, acceptable_timestamps)) + + session = session or _requests + + financials_data = get_json_data_stores(ticker_url+'/'+name, proxy, session) + + # Step 1: get the keys: + def _finditem1(key, obj): + values = [] + if isinstance(obj,dict): + if key in obj.keys(): + values.append(obj[key]) + for k,v in obj.items(): + values += _finditem1(key,v) + elif isinstance(obj,list): + for v in obj: + values += _finditem1(key,v) + return values + keys = _finditem1("key",financials_data['FinancialTemplateStore']) + + # Step 2: construct url: + ts_url_base = "https://query2.finance.yahoo.com/ws/fundamentals-timeseries/v1/finance/timeseries/{0}?symbol={0}".format(ticker) + if len(keys) == 0: + raise Exception("Fetching keys failed") + url = ts_url_base + "&type=" + ",".join([timescale+k for k in keys]) + # Yahoo returns maximum 4 years or 5 quarters, regardless of start_dt: + start_dt = _datetime.datetime(2016, 12, 31) + end = (_datetime.datetime.now() + _datetime.timedelta(days=366)) + url += "&period1={}&period2={}".format(int(start_dt.timestamp()), int(end.timestamp())) + + # Step 3: fetch and reshape data + json_str = session.get(url=url, proxies=proxy, headers=user_agent_headers).text + json_data = _json.loads(json_str) + data_raw = json_data["timeseries"]["result"] + # data_raw = [v for v in data_raw if len(v) > 1] # Discard keys with no data + for d in data_raw: + del d["meta"] + + # Now reshape data into a table: + # Step 1: get columns and index: + timestamps = set() + data_unpacked = {} + for x in data_raw: + for k in x.keys(): + if k=="timestamp": + timestamps.update(x[k]) + else: + data_unpacked[k] = x[k] + timestamps = sorted(list(timestamps)) + dates = _pd.to_datetime(timestamps, unit="s") + df = _pd.DataFrame(columns=dates, index=data_unpacked.keys()) + for k,v in data_unpacked.items(): + if df is None: + df = _pd.DataFrame(columns=dates, index=[k]) + df.loc[k] = {_pd.Timestamp(x["asOfDate"]):x["reportedValue"]["raw"] for x in v} + + df.index = df.index.str.replace("^"+timescale, "", regex=True) + + # Reorder table to match order on Yahoo website + df = df.reindex([k for k in keys if k in df.index]) + df = df[sorted(df.columns, reverse=True)] + + return df + + def camel2title(o): return [_re.sub("([a-z])([A-Z])", r"\g<1> \g<2>", i).title() for i in o]