SimpleStockData/SimpleStockData.py

141 lines
6.5 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.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