SimpleStockData/SimpleStockData.py
BlueFox 4a1d24c562
Fixed strange bug when trying to get the history of AZN.L as it was in GBp, not GBP. Also, some refactoring.
The issue was that with some shares, there is a currency with lowercase letters with means they are just a fraction of the all-uppercase equivalent (e.g. with AZN.L: GBp is 1/1000 of GBP). Now this is fixed, and newly discovered lowercase exceptions can be added in the init function by adding it to the currency_exceptions dictionary.
2024-01-19 18:39:01 +01:00

166 lines
7.9 KiB
Python
Executable File

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.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._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