diff --git a/README.md b/README.md index 5d4cc56..63b91a8 100755 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ You should refer to Yahoo!'s terms of use (here, here, and here) for details on ## 2. Usage ### 2.1 If used as a CLI: -TODO; +TODO; also mention the strange but working GBp thing ### 2.2 If used as a Module in Python (via `import`): TODO; diff --git a/SimpleStockData.py b/SimpleStockData.py index b014334..43ed61e 100755 --- a/SimpleStockData.py +++ b/SimpleStockData.py @@ -15,15 +15,17 @@ class SimpleStockData: :param to_currency: currency to convert rates to (e.g. EUR) """ + self.currency_exceptions = {"GBp": 0.001} # dict where currencies with lowercase letters and how they convert to + # their uppercase equivalent (e.g. GBp is 0.001/1% of one GBP) self.ticker_list = ticker_list - self.to_currency = to_currency.upper() # make it uppercase + 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 + 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"): """ @@ -49,39 +51,82 @@ class SimpleStockData: """ # check if a to_currency is even given - if self.to_currency == "": + if self._to_currency == "": return False # create the list of currencies based on all the stocks of the class - _from_currency_list = [] + _shares_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 + 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 add_currency == f"{self.to_currency}{self.to_currency}=X": + if new_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) + 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 - tickers = yf.Tickers(" ".join(_from_currency_list)) # create a new Ticker instance with all wanted currencies + + # 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 - # 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)) + # 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 (_from_currency_list variable). The index is now taken - # to set the right names for every row in the dataframe + # 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 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 + 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: @@ -112,29 +157,9 @@ class SimpleStockData: 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"] + 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