import yfinance as yf import pandas as pd class SimpleStockData: 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.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._exchange_df = None # Mapping: time mapped to conversion factor, to get the right converted value per date self._create_exchange_dataframe() # initialize self.exchange_df attribute 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 _from_currency_list = [] for i in range(len(self.ticker_list)): # to get all indexes; this adds an entry for each currency add_currency = f"{self.get_info(i, 'currency')}{self.to_currency}=X" # Format: "fffttt=X" f=from, t=to add_currency = add_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 add_currency == f"{self.to_currency}{self.to_currency}=X": pass elif add_currency not in _from_currency_list: # add a new item if not already there _from_currency_list.append(add_currency) # now the real process begins tickers = yf.Tickers(" ".join(_from_currency_list)) # create a new Ticker instance with all wanted currencies 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 # for simplicity: using the conversion factor of closing (.Close at the end) 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 (_from_currency_list variable). 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 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 return True 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").upper() # upper it as sometimes it doesn't fit if convert: exrate_name = f"{ticker_currency}{self.to_currency}=X" result["ex_rate_name"] = exrate_name ex_rate_series = self._exchange_df[exrate_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 = result.T ex_rate_df = ex_rate_df.to_list() print(ex_rate_df) result.append(ex_rate_df[result.columns], ignore_index=True) # magic - see above :) result = result.T # transpose back result[f"{self._ohcl} in {self.to_currency}"] = result[self._ohcl] / result["ex_rate"] """ result["ex_rate"] = ex_rate_series.to_list() result[f"{self._ohcl} in {self.to_currency}"] = result[self._ohcl] / result["ex_rate"] return result