diff --git a/setup.py b/setup.py index 160927e..a932c20 100644 --- a/setup.py +++ b/setup.py @@ -1,26 +1,26 @@ -import setuptools - -with open("README.md", "r") as fh: - long_description = fh.read() - -setuptools.setup( - name="thepassiveinvestor", - packages=["thepassiveinvestor"], - version="1.0.9", - license="MIT", - description="Passive Investing for the Average Joe.", - author="JerBouma", - author_email="jer.bouma@gmail.com", - url="https://github.com/JerBouma/ThePassiveInvestor", - long_description=long_description, - long_description_content_type="text/markdown", - keywords=["passive", "investing", "finance", "etfs"], - classifiers=[ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Financial and Insurance Industry", - "Topic :: Office/Business :: Financial :: Investment", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - "Programming Language :: Python :: 3" - ], +import setuptools + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="thepassiveinvestor", + packages=["thepassiveinvestor"], + version="1.0.10", + license="MIT", + description="Passive Investing for the Average Joe.", + author="JerBouma", + author_email="jer.bouma@gmail.com", + url="https://github.com/JerBouma/ThePassiveInvestor", + long_description=long_description, + long_description_content_type="text/markdown", + keywords=["passive", "investing", "finance", "etfs"], + classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Financial and Insurance Industry", + "Topic :: Office/Business :: Financial :: Investment", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3" + ], ) \ No newline at end of file diff --git a/thepassiveinvestor/create_report.py b/thepassiveinvestor/create_report.py index 120f39b..3eb2f2c 100644 --- a/thepassiveinvestor/create_report.py +++ b/thepassiveinvestor/create_report.py @@ -1,137 +1,139 @@ -import pandas as pd -import yfinance as yf -import os -from openpyxl import Workbook -from openpyxl.styles import Font, Alignment -from openpyxl.utils.dataframe import dataframe_to_rows - -from .collect_data import collect_data -from .config import EMPTY_RISK_STATISTICS -from .utils import data_placer, image_placer, graph_placer - - -def create_ETF_report(tickers, filename, folder=None): - """ - Description - ---- - This function creates an Excel Report based on the tickers entered in the function. This function - only accepts ETFs and can not handle any other financial product. If other tickers are included, - by default no data is returned for that ticker. - - Input - ---- - tickers (list or string) - A list of tickers or a single ticker from an ETF (i.e. VOO) - filename (string) - The name and location of the file you wish to save the data to. - folder (string, default is None) - If prefered, you can seperate filename and folder. - - Output - ---- - Returns an Excel file with the given filename with data on each ticker. - """ - workbook = Workbook() - stock_data = yf.download(tickers, period='10y', progress=False)['Adj Close'] - - if isinstance(tickers, str): - tickers = [tickers] - - if isinstance(stock_data, pd.DataFrame): - stock_data = stock_data[tickers] - elif isinstance(stock_data, pd.Series): - stock_data = pd.DataFrame(stock_data) - - if filename[-4:] not in ['xlsx', 'xlsm', 'xlsb']: - filename = f"{filename}.xlsx" - if folder is not None: - filename = os.path.join(folder, filename) - - workbook.create_sheet(title='Stock Data') - stock_sheet = workbook['Stock Data'] - - for row in dataframe_to_rows(stock_data, index=True, header=True): - stock_sheet.append(row) - stock_sheet.column_dimensions['A'].width = len(str(stock_data.index[0])) - stock_sheet.sheet_view.showGridLines = False - - min_col, min_row, max_col = 1, 3, 1 - - for ticker in tickers: - workbook.create_sheet(title=ticker) - sheet = workbook[ticker] - sheet.sheet_view.showGridLines = False - - try: - ticker_data = collect_data(ticker) - except (KeyError, TypeError): - sheet['B2'].value = "No data available" - sheet['B2'].font = Font(italic=True) - continue - - sheet['B2'].value = ticker_data['long_name'] - sheet['B2'].font = Font(bold=True, size=15) - sheet['B2'].alignment = Alignment(horizontal='left') - sheet.merge_cells('B2:M2') - - sheet['B3'].value = ticker_data['summary'] - sheet['B3'].alignment = Alignment(wrap_text=True, vertical='center', horizontal='left') - sheet.merge_cells('B3:M3') - sheet.row_dimensions[3].height = 100 - - sheet['B4'].value = "Sector Holdings" - sheet['B4'].font = Font(bold=True) - - sheet['B17'].value = "Top Company Holdings" - sheet['B17'].font = Font(bold=True) - - sheet['E19'].value = "Risk Statistics" - sheet['E19'].font = Font(bold=True) - sheet['E19'].alignment = Alignment(horizontal='center') - sheet.merge_cells('E19:J19') - - sheet['L21'].value = "Last Five Annual Returns" - sheet['L21'].font = Font(bold=True) - - sheet['L4'].value = 'Key Characteristics' - sheet['L4'].font = Font(bold=True) - - data_placer(ticker_data['sector_holdings'], sheet, 5, 2, 'B', 'C') - data_placer(ticker_data['company_holdings'], sheet, 18, 2, 'B', 'C', - change_key_dimensions=False) - data_placer(ticker_data['annual_returns'], sheet, 22, 12, 'L', 'M', - change_key_dimensions=False, change_value_dimensions=False) - data_placer(ticker_data['key_characteristics'], sheet, 5, 12, 'L', 'M', - horizontal_alignment_value='left') - - try: - data_placer(ticker_data['risk_data']['3y'], sheet, 20, 5, 'E', 'F', False, 'right', True, False) - except KeyError: - risk_data = EMPTY_RISK_STATISTICS - risk_data['year'] = '3y' - data_placer(ticker_data['risk_data'], sheet, 20, 5, 'E', 'F', False, 'right', True, False) - try: - data_placer(ticker_data['risk_data']['5y'], sheet, 20, 7, 'G', 'H', False, 'right', True, False) - except KeyError: - risk_data = EMPTY_RISK_STATISTICS - risk_data['year'] = '5y' - data_placer(ticker_data['risk_data'], sheet, 20, 7, 'G', 'H', False, 'right', True, False) - try: - data_placer(ticker_data['risk_data']['10y'], sheet, 20, 9, 'I', 'J', False, 'right', True, False) - except KeyError: - risk_data = EMPTY_RISK_STATISTICS - risk_data['year'] = '10y' - data_placer(ticker_data['risk_data'], sheet, 20, 9, 'I', 'J', False, 'right', True, False) - - image_placer(ticker_data['image_URL'], sheet, 'L12') - graph_placer(stock_sheet, stock_data, sheet, min_col, min_row, max_col, "E4") - min_col += 1 - max_col += 1 - - try: - workbook.remove(workbook['Sheet']) - except KeyError: - pass - - stock_sheet.sheet_state = 'hidden' - workbook.save(filename) +import pandas as pd +import yfinance as yf +import os +from openpyxl import Workbook +from openpyxl.styles import Font, Alignment +from openpyxl.utils.dataframe import dataframe_to_rows + +from .collect_data import collect_data +from .config import EMPTY_RISK_STATISTICS +from .utils import data_placer, image_placer, graph_placer + + +def create_ETF_report(tickers, filename, folder=None): + """ + Description + ---- + This function creates an Excel Report based on the tickers entered in the function. This function + only accepts ETFs and can not handle any other financial product. If other tickers are included, + by default no data is returned for that ticker. + + Input + ---- + tickers (list or string) + A list of tickers or a single ticker from an ETF (i.e. VOO) + filename (string) + The name and location of the file you wish to save the data to. + folder (string, default is None) + If prefered, you can seperate filename and folder. + + Output + ---- + Returns an Excel file with the given filename with data on each ticker. + """ + workbook = Workbook() + stock_data = yf.download(tickers, period='10y', progress=False)['Adj Close'] + + if isinstance(tickers, str): + tickers = [tickers] + + if isinstance(stock_data, pd.DataFrame): + stock_data = stock_data[tickers] + elif isinstance(stock_data, pd.Series): + stock_data = pd.DataFrame(stock_data) + + if filename[-4:] not in ['xlsx', 'xlsm', 'xlsb']: + filename = f"{filename}.xlsx" + if folder is not None: + filename = os.path.join(folder, filename) + + workbook.create_sheet(title='Stock Data') + stock_sheet = workbook['Stock Data'] + + for row in dataframe_to_rows(stock_data, index=True, header=True): + stock_sheet.append(row) + stock_sheet.column_dimensions['A'].width = len(str(stock_data.index[0])) + stock_sheet.sheet_view.showGridLines = False + + min_col, min_row, max_col = 1, 3, 1 + + for ticker in tickers: + workbook.create_sheet(title=ticker) + sheet = workbook[ticker] + sheet.sheet_view.showGridLines = False + + try: + ticker_data = collect_data(ticker) + except (KeyError, TypeError): + sheet['B2'].value = "No data available" + sheet['B2'].font = Font(italic=True) + continue + + sheet['B2'].value = ticker_data['long_name'] + sheet['B2'].font = Font(bold=True, size=15) + sheet['B2'].alignment = Alignment(horizontal='left') + sheet.merge_cells('B2:M2') + + sheet['B3'].value = ticker_data['summary'] + sheet['B3'].alignment = Alignment(wrap_text=True, vertical='center', horizontal='left') + sheet.merge_cells('B3:M3') + sheet.row_dimensions[3].height = 100 + + sheet['B4'].value = "Sector Holdings" + sheet['B4'].font = Font(bold=True) + + sheet['B17'].value = "Top Company Holdings" + sheet['B17'].font = Font(bold=True) + + sheet['E19'].value = "Risk Statistics" + sheet['E19'].font = Font(bold=True) + sheet['E19'].alignment = Alignment(horizontal='center') + sheet.merge_cells('E19:J19') + + sheet['L21'].value = "Last Five Annual Returns" + sheet['L21'].font = Font(bold=True) + + sheet['L4'].value = 'Key Characteristics' + sheet['L4'].font = Font(bold=True) + + data_placer(ticker_data['sector_holdings'], sheet, 5, 2, 'B', 'C', + value_formatting_style='percentage') + data_placer(ticker_data['company_holdings'], sheet, 18, 2, 'B', 'C', + change_key_dimensions=False, value_formatting_style='percentage') + data_placer(ticker_data['annual_returns'], sheet, 22, 12, 'L', 'M', + change_key_dimensions=False, change_value_dimensions=False, + value_formatting_style='percentage') + data_placer(ticker_data['key_characteristics'], sheet, 5, 12, 'L', 'M', + horizontal_alignment_value='left') + + try: + data_placer(ticker_data['risk_data']['3y'], sheet, 20, 5, 'E', 'F', False, 'right', True, False) + except KeyError: + risk_data = EMPTY_RISK_STATISTICS + risk_data['year'] = '3y' + data_placer(ticker_data['risk_data'], sheet, 20, 5, 'E', 'F', False, 'right', True, False) + try: + data_placer(ticker_data['risk_data']['5y'], sheet, 20, 7, 'G', 'H', False, 'right', True, False) + except KeyError: + risk_data = EMPTY_RISK_STATISTICS + risk_data['year'] = '5y' + data_placer(ticker_data['risk_data'], sheet, 20, 7, 'G', 'H', False, 'right', True, False) + try: + data_placer(ticker_data['risk_data']['10y'], sheet, 20, 9, 'I', 'J', False, 'right', True, False) + except KeyError: + risk_data = EMPTY_RISK_STATISTICS + risk_data['year'] = '10y' + data_placer(ticker_data['risk_data'], sheet, 20, 9, 'I', 'J', False, 'right', True, False) + + image_placer(ticker_data['image_URL'], sheet, 'L12') + graph_placer(stock_sheet, stock_data, sheet, min_col, min_row, max_col, "E4") + min_col += 1 + max_col += 1 + + try: + workbook.remove(workbook['Sheet']) + except KeyError: + pass + + stock_sheet.sheet_state = 'hidden' + workbook.save(filename) diff --git a/thepassiveinvestor/utils.py b/thepassiveinvestor/utils.py index 4eae546..42279d5 100644 --- a/thepassiveinvestor/utils.py +++ b/thepassiveinvestor/utils.py @@ -1,145 +1,156 @@ -import io - -import urllib3 -from openpyxl.chart import Reference, LineChart -from openpyxl.chart.axis import DateAxis -from openpyxl.drawing.image import Image as ExcelImage -from openpyxl.styles import Alignment, Font - - -def data_placer(data, sheet, starting_row, column, column_key, column_value, horizontal_alignment_key=False, - horizontal_alignment_value=False, change_key_dimensions=True, change_value_dimensions=True): - """ - Description - ---- - This function places data in the given Excel sheet based on the positions given. - - Input - ---- - data (dictionary): - The data that is filled into the sheet. - sheet (object): - The sheet that is filled within the Workbook. - starting_row (integer or float): - The first row the data is placed. - column (integer or float): - The column that is filled in. - column_key (string): - Which column the key of the dictionary is placed. - column_value (string): - Which columns the value of the dictionary is placed. - horizontal_alignment_key (boolean, default is False): - Align the key of the dictionary horizontally. - horizontal_alignment_value (boolean, default is False): - Align the value of the dictionary horizontally. - change_key_dimensions (boolean, default is True): - Increase the width of the cell of the key of the dictionary. - change_value_dimensions (boolean, default is True): - Increase the width of the cell of the key of the dictionary. - - Output - ---- - Fills in the sheet with the data based on the parameters. - """ - max_length_key = 0 - max_length_value = 0 - - for key, value in data.items(): - key_position = sheet.cell(column=column, row=starting_row, value=key) - value_position = sheet.cell(column=column + 1, row=starting_row, value=value) - - starting_row += 1 - - if horizontal_alignment_key: - key_position.alignment = Alignment(horizontal=horizontal_alignment_key) - - if horizontal_alignment_value: - value_position.alignment = Alignment(horizontal=horizontal_alignment_value) - - length_key = len(str(key_position.value)) - length_value = len(str(value_position.value)) - - if length_key > max_length_key: - max_length_key = length_key - - if length_value > max_length_value: - max_length_value = length_value - - if change_key_dimensions: - sheet.column_dimensions[column_key].width = max_length_key * 1.2 - - if change_value_dimensions: - sheet.column_dimensions[column_value].width = max_length_value * 1.2 - - -def image_placer(image_url, sheet, location): - """ - Description - ---- - This function places an image in the given Excel sheet based on the location given. - - Input - ---- - image_url (string): - The data that is filled into the sheet. - sheet (object): - The sheet that is filled within the Workbook. - location (string): - The exact location the graph should be placed. - - Output - ---- - Fills in the sheet with the selected image at the specified location. - """ - try: - http = urllib3.PoolManager() - image_location = http.request('GET', image_url) - image_file = io.BytesIO(image_location.data) - image = ExcelImage(image_file) - sheet.add_image(image, location) - except Exception: - sheet[location] = "No image available" - sheet[location].font = Font(italic=True) - - -def graph_placer(stock_sheet, stock_data, sheet, min_col, min_row, max_col, location): - """ - Description - ---- - This function places a stock data graph at a specified location. - - Input - ---- - stock_sheet (object): - A sheet filled with stock data. - stock_data (DataFrame): - All stock data where the right position is determined automatically. - sheet (string): - A sheet that is filled in. - min_col (integer): - The minimum column of the stock sheet where the data starts. - min_row (integer): - The minimum row of the stock sheet where the data starts. - max_col (integer): - The maximum column available in the stock sheet. - - Output - ---- - Fills in the sheet with the selected graph at the specified location. - """ - data = Reference(stock_sheet, min_col=min_col + 1, min_row=min_row, max_col=max_col + 1, max_row=len(stock_data)) - cats = Reference(stock_sheet, min_col=1, min_row=3, max_col=1, max_row=len(stock_data)) - - chart = LineChart() - chart.title = None - chart.legend = None - chart.y_axis.title = "Stock Price" - chart.y_axis.crossAx = 500 - chart.x_axis = DateAxis() - chart.x_axis.number_format = 'yyyy' - chart.x_axis.title = "Date" - - chart.add_data(data) - chart.set_categories(cats) - - sheet.add_chart(chart, location) +import io + +import urllib3 +from openpyxl.chart import Reference, LineChart +from openpyxl.chart.axis import DateAxis +from openpyxl.drawing.image import Image as ExcelImage +from openpyxl.styles import Alignment, Font +from openpyxl.styles.numbers import FORMAT_PERCENTAGE_00 + + +def data_placer(data, sheet, starting_row, column, column_key, column_value, horizontal_alignment_key=False, + horizontal_alignment_value=False, change_key_dimensions=True, change_value_dimensions=True, + value_formatting_style=None): + """ + Description + ---- + This function places data in the given Excel sheet based on the positions given. + + Input + ---- + data (dictionary): + The data that is filled into the sheet. + sheet (object): + The sheet that is filled within the Workbook. + starting_row (integer or float): + The first row the data is placed. + column (integer or float): + The column that is filled in. + column_key (string): + Which column the key of the dictionary is placed. + column_value (string): + Which columns the value of the dictionary is placed. + horizontal_alignment_key (boolean, default is False): + Align the key of the dictionary horizontally. + horizontal_alignment_value (boolean, default is False): + Align the value of the dictionary horizontally. + change_key_dimensions (boolean, default is True): + Increase the width of the cell of the key of the dictionary. + change_value_dimensions (boolean, default is True): + Increase the width of the cell of the key of the dictionary. + value_formatting_style (string, default is None): + Option to change the formatting style of the value. Currently only works with 'percentage'. + + Output + ---- + Fills in the sheet with the data based on the parameters. + """ + max_length_key = 0 + max_length_value = 0 + + for key, value in data.items(): + if value_formatting_style == 'percentage': + try: + value = float(value[:-1]) / 100 + sheet[f"{column_value}{starting_row}"].number_format = FORMAT_PERCENTAGE_00 + except ValueError: + pass + + key_position = sheet.cell(column=column, row=starting_row, value=key) + value_position = sheet.cell(column=column + 1, row=starting_row, value=value) + + starting_row += 1 + + if horizontal_alignment_key: + key_position.alignment = Alignment(horizontal=horizontal_alignment_key) + + if horizontal_alignment_value: + value_position.alignment = Alignment(horizontal=horizontal_alignment_value) + + length_key = len(str(key_position.value)) + length_value = len(str(value_position.value)) + + if length_key > max_length_key: + max_length_key = length_key + + if length_value > max_length_value: + max_length_value = length_value + + if change_key_dimensions: + sheet.column_dimensions[column_key].width = max_length_key * 1.2 + + if change_value_dimensions: + sheet.column_dimensions[column_value].width = max_length_value * 1.2 + + +def image_placer(image_url, sheet, location): + """ + Description + ---- + This function places an image in the given Excel sheet based on the location given. + + Input + ---- + image_url (string): + The data that is filled into the sheet. + sheet (object): + The sheet that is filled within the Workbook. + location (string): + The exact location the graph should be placed. + + Output + ---- + Fills in the sheet with the selected image at the specified location. + """ + try: + http = urllib3.PoolManager() + image_location = http.request('GET', image_url) + image_file = io.BytesIO(image_location.data) + image = ExcelImage(image_file) + sheet.add_image(image, location) + except Exception: + sheet[location] = "No image available" + sheet[location].font = Font(italic=True) + + +def graph_placer(stock_sheet, stock_data, sheet, min_col, min_row, max_col, location): + """ + Description + ---- + This function places a stock data graph at a specified location. + + Input + ---- + stock_sheet (object): + A sheet filled with stock data. + stock_data (DataFrame): + All stock data where the right position is determined automatically. + sheet (string): + A sheet that is filled in. + min_col (integer): + The minimum column of the stock sheet where the data starts. + min_row (integer): + The minimum row of the stock sheet where the data starts. + max_col (integer): + The maximum column available in the stock sheet. + + Output + ---- + Fills in the sheet with the selected graph at the specified location. + """ + data = Reference(stock_sheet, min_col=min_col + 1, min_row=min_row, max_col=max_col + 1, max_row=len(stock_data)) + cats = Reference(stock_sheet, min_col=1, min_row=3, max_col=1, max_row=len(stock_data)) + + chart = LineChart() + chart.title = None + chart.legend = None + chart.y_axis.title = "Stock Price" + chart.y_axis.crossAx = 500 + chart.x_axis = DateAxis() + chart.x_axis.number_format = 'yyyy' + chart.x_axis.title = "Date" + + chart.add_data(data) + chart.set_categories(cats) + + sheet.add_chart(chart, location)