""" SimpleStockData is a simple library providing an API to access basic stock prices based on Yahoo Finances. Copyright (C) 2024 Benjamin Burkhardt This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ import yfinance as yf import pandas as pd class SSD: def __init__(self, ticker_list: list, period_start: str, period_end: str, to_currency: str, ohcl: str = "Close"): """ :param period_start: start date (format YYYY-MM-DD) :param period_end: end date (format YYYY-MM-DD) :param ticker_list: list containing all stocks/exchange rates (yfinance considers both as "Tickers") Example: [] :param to_currency: currency to convert rates to (e.g. EUR) """ self.currency_exceptions = {"GBp": 0.01} # dict where currencies with lowercase letters and how they convert to # their uppercase equivalent (e.g. GBp (pence) is 0.01/1% of one GBP (pound) self.ticker_list = ticker_list self._to_currency = to_currency.upper() # make it uppercase self._period_start = period_start self._period_end = period_end self._ohcl = ohcl.capitalize() self._able_to_convert = self._create_exchange_dataframe() # initialize self.exchange_df attribute and define if # convertible now def _get_history(self, idx, interval="1d"): """ Function for internal use; Just a wrapper around the .history method of the yfinance Ticker class :param idx: the index of the share :param interval: granularity of data - valid values are 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo :return: pandas.DataFrame """ return yf.Ticker(self.ticker_list[idx]).history(interval=interval, start=self._period_start, end=self._period_end) def _create_exchange_dataframe(self): """ The class has two separate attributes, one to store the plain convert list (_from_currency_list), and one containing the real mapping needed to convert. The mapping is recreated by this function following the information in the _from_currency_list. return: boolean - success or not """ # check if a to_currency is even given if self._to_currency == "": return False # create the list of currencies based on all the stocks of the class _shares_currency_list = [] for i in range(len(self.ticker_list)): # to get all indexes; this adds an entry for each currency new_currency = f"{self.get_info(i, 'currency')}{self._to_currency}=X" # Format: "fffttt=X" f=from, t=to new_currency = new_currency.upper() # make everything uppercase # for the case that FROM and TO are equal, just don't download the data (as conversion factor's 1) if new_currency == f"{self._to_currency}{self._to_currency}=X": pass elif new_currency not in _shares_currency_list: # add a new item if not already there _shares_currency_list.append(new_currency) # now the real process begins # create a new Tickers instance with all wanted currencies tickers = yf.Tickers(" ".join(_shares_currency_list)) exchange_rates = [] # temporary variable where all exchange rates are stored in (as objects of pd.Series) for er_name in tickers.tickers: # get all the history of each currency conversion factors # using the conversion factor at wanted time (given as ohcl at object init; example: "Close", "High", etc.) exchange_rates.append( (tickers.tickers[er_name].history(start=self._period_start, end=self._period_end)[self._ohcl], er_name)) # now exchange_rates contains tuples of the form (ticker, er_name) where er_name is the name of the # currency ticker, used only internal in this method. The index is now taken to set the right names for # every row in the dataframe # now, the rates are taken from the exchange_rates list and are all wrapped up in a beautiful pandas DataFrame self._exchange_df = pd.DataFrame() for exchange_rate, er_name in exchange_rates: self._exchange_df[er_name] = exchange_rate self._exchange_df[ f"{self._to_currency}{self._to_currency}=X"] = 1.0 # for FROM and TO being equal: set factor to 1 # now the currency exceptions (as GBp isn't the same as GBP, see comment at definition of self.currency_exceptions for currency_exception in self.currency_exceptions: self._exchange_df[f"{currency_exception}{currency_exception.upper()}"] = self.currency_exceptions[currency_exception] return True def _get_history_convert_result(self, result, ticker_currency): """ Helper method for the get_history method. Converts the share prices in a given result DataFrame from the currency specified by ticker_currency to the class-wide self.to_currency and stores it in new columns in the df :param result: the result containing the unconverted share prices :param ticker_currency: the currency from which convert to the self._to_currency :return: pandas.DataFrame (with extra columns for the converted value) """ exceptive_conversion = ticker_currency in self.currency_exceptions if exceptive_conversion: exceptive_conversion_factor = self.currency_exceptions[ticker_currency] # the additional conversion factor (usually 0.01) else: exceptive_conversion_factor = 1 # 1:1, don't change values ex_rate_name = f"{ticker_currency.upper()}{self._to_currency}=X" result["ex_rate_name"] = ex_rate_name result["exceptive_conversion"] = exceptive_conversion if exceptive_conversion: result["exceptive_conversion_factor"] = exceptive_conversion_factor ex_rate_series = self._exchange_df[ex_rate_name] # now there's a result dataframe with ticker, currency, rate name etc. as column names # to add only matching ex rates per day (sometimes there are more days with exchange rates recorded than # share prices), the result df has to be transposed so that the following function df.append can select # by columns. # TODO: implement the bug fix when not the exact same timestamps and amount of data are given in both the series and the df result["ex_rate"] = ex_rate_series.to_list() # TODO: won't work with mismatching data (e.g. different exchange gaps over christmas) result[f"{self._ohcl} in {self._to_currency}"] = (result[self._ohcl] * exceptive_conversion_factor) * result["ex_rate"] return result def get_info(self, idx, key=""): """ :param idx: the index of the share :param key: OPTIONAL. gives which specific datum is wanted :return: """ info = yf.Ticker(self.ticker_list[idx]).info if key != "": # if just one specific information is wanted return info[key.lower()] return info def get_history(self, idx, interval="1d", convert=True): """ Just a wrapper around the .history method of the yfinance Ticker class. Adds a new column containing the internal index of the ticker. :param idx: the index of the share :param interval: granularity of data - valid values are 1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo :param convert: decides if the resulting values should be converted to the specified to_convert currency (given at object creation) :return: pandas.DataFrame (with extra columns for the converted value if wanted) """ result = self._get_history(idx, interval)[self._ohcl].to_frame() result["Ticker Index"] = idx ticker_currency = self.get_info(idx, "currency") if convert and self._able_to_convert: result = self._get_history_convert_result(result, ticker_currency) return result