2
\$\begingroup\$

This is a Yahoo Finance wrapper to get income/balance statement for a stock ticker. What I have tried to do is make an abstract implementation so more features can be added easily in the future. The base-class YahooFin contains methods to get and beat the data into shape so it easily can be put into a pandas DataFrame. The child-classes IncomeStatementQ and BalanceSheetQ only has one method each which is to make a call to the Yahoo API and set query parameters to get the data from the response. I would highly appreciate any input on how abstraction can be increased so that new features can be added with even less code. Any other input is also highly appreciated no matter how small or big. This is my first time trying the use of decorators and properties and feedback on my utilization of them is welcomed. Thank you!

My ideal use of the program is to run it as below:

   data = IncomeStatementQ('INVE-B.ST')
   data.to_df()
   #Do whatever with the data
   #...

from datetime import datetime

import matplotlib.pyplot as plt
import pandas as pd
import requests

from matplotlib.backends.backend_pdf import PdfPages

from pprint import pprint


class YahooFin():
    BASE_URL = 'https://query1.finance.yahoo.com/v10/finance/quoteSummary/'

    def __init__(self, ticker):
      """Initiates the ticker
          Args:
            ticker (str): Stock-ticker Ex. 'AAPL'
      """
        self.ticker = ticker

    def make_request(self, url):
        """Makes a GET request"""
        return requests.get(url)

    def get_data(self):
      """Returns a json object from a GET request"""
        return self.make_request(self.url).json()

    def data(self):
      """Returns query result from json object"""
        data_temp = self.get_data()
        try:
            return data_temp.get('quoteSummary').get("result")
        except KeyError as e:
            print("Something went wrong")

    def convert_timestamp(self, raw):
      """Converts UNIX-timestamp to YYYY-MM-DD"""
        return datetime.utcfromtimestamp(raw).strftime('%Y-%m-%d')

    def extract_raw(func):
      """Decorator to remove keys from from json data 
      that is retreived from the yahoo-module
      """
      def wrapper_extract_raw(self, *args, **kwargs):
        sheet = func(self)
        for items in sheet:
            for key, value in items.items():
                if type(value) == dict and 'fmt' in value:
                    del value['fmt']
                if type(value) == dict and 'longFmt' in value:
                    del value['longFmt']
        return sheet
      return wrapper_extract_raw

    def create_dict(self):
      """Creates a dict from extracted data"""
        balance_sheet = []
        temp_data = self._dict
        for d in temp_data:
            temp_dict = {}
            for key, value in d.items():
                if type(value) == dict and 'raw' in value:
                    v = value['raw']
                    temp_dict[key] = v
            balance_sheet.append(temp_dict)
        return balance_sheet


    def to_df(self):
      """Creates a pandas Dataframe from dict"""
        self._df = pd.DataFrame.from_dict(self.create_dict())
        for index, row in self._df.iterrows():
            self._df.loc[index, 'endDate'] = self.convert_timestamp(self._df.at[index, 'endDate'])
        self._df = self._df.iloc[::-1]
        return self._df

class BalanceSheetQ(YahooFin):

  def __init__(self, ticker):
    super().__init__(ticker)
    self._module = 'balanceSheetHistoryQuarterly'
    self._url = (f'https://query1.finance.yahoo.com/v10/finance/quoteSummary/'
                    f'{self.ticker}?'
                    f'modules={self.module}')
    self._dict = self._balance_sheet()
    self._df = None

  @property
  def module(self):
      return self._module

  @property
  def url(self):
      return self._url

  @property
  def df(self):
      return self._df

  @YahooFin.extract_raw
  def _balance_sheet(self):
    """Returns a balance sheet statement"""
      data = self.data()
      query =  data[0]
      balance_sheet_qty = query['balanceSheetHistoryQuarterly']
      balance_sheet_statements = balance_sheet_qty['balanceSheetStatements']
      return balance_sheet_statements



class IncomeStatementQ(YahooFin):

  def __init__(self, ticker):
        super().__init__(ticker)
        self._module = 'incomeStatementHistoryQuarterly'
        self._url = (f'https://query1.finance.yahoo.com/v10/finance/quoteSummary/'
                    f'{self.ticker}?'
                    f'modules={self.module}')
        self._dict = self._income_statement()
        self._df = None

  @property
  def module(self):
      return self._module

  @property
  def url(self):
      return self._url

  @property
  def df(self):
      return self._df

  @YahooFin.extract_raw
  def _income_statement(self):
    """Returns a income statement"""
      data = self.data()
      query =  data[0]
      income_statement_qty = query['incomeStatementHistoryQuarterly']
      income_statement_statements = income_statement_qty['incomeStatementHistory']
      return income_statement_statements


if __name__ == '__main__':
    data = IncomeStatementQ('INVE-B.ST')
    print(data.to_df())
\$\endgroup\$
1
  • \$\begingroup\$ This is (mercifully) an API client, not a web scraper. \$\endgroup\$
    – Reinderien
    May 26, 2020 at 19:48

1 Answer 1

2
\$\begingroup\$

Type hints

You're most of the way to having a well-explained constructor:

ticker (str): Stock-ticker Ex. 'AAPL'

It's better to move that (str) to an actual type hint, i.e.

def __init__(self, ticker: str):

Superfluous methods

As it stands, make_request doesn't need to exist. It would make sense to keep it if you add a Session instance to the class, which (for an API client) you should do anyway. But if you don't have a session, delete this method and just use requests directly. The same goes for get_data.

Exception handling

This:

    except KeyError as e:
        print("Something went wrong")

should probably not be catching at all, or at least not catching here. If you are iterating over several web calls and you don't want them to cancel the loop due to an exception, catch in the loop. If you want to reformat an exception for printing to the console, do that somewhere up in the stack; but don't do it here. And if you do want to print an exception, print its str() representation; don't give a vague Something went wrong.

Statics

convert_timestamp doesn't reference self, so should be a static method at least. More likely a global, standalone function, since it has nothing to do with your client.

Type comparison

Don't do this:

type(value) == dict

Instead, use isinstance.

Parameters to web requests

self._url = (f'https://query1.finance.yahoo.com/v10/finance/quoteSummary/'
                f'{self.ticker}?'
                f'modules={self.module}')

should avoid baking in modules=. This is best done by passing params= to requests.get().

"Private" variables

This:

@property
def module(self):
    return self._module

is a Java-ism. In the strict sense, Python has no private variables, and the underscore is basically a "soft request" for exterior users to leave a variable alone. It's more common to simply expose self.module as a variable without the property.

\$\endgroup\$
1
  • \$\begingroup\$ Thank you for the constructive feedback! I will definitely read more about client APIs and optimal implementations! Also, thank you for pointing out that you don’t always want to catch an error, and how to properly display error messages if any are to be shown! \$\endgroup\$ May 27, 2020 at 6:05

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.