diff --git a/BharatFinTrack/__init__.py b/BharatFinTrack/__init__.py index bb7554b..e10098e 100644 --- a/BharatFinTrack/__init__.py +++ b/BharatFinTrack/__init__.py @@ -12,4 +12,4 @@ ] -__version__ = '0.1.8' +__version__ = '0.1.9' diff --git a/BharatFinTrack/data/equity_indices.xlsx b/BharatFinTrack/data/equity_indices.xlsx index b7c7143..b2994d9 100644 Binary files a/BharatFinTrack/data/equity_indices.xlsx and b/BharatFinTrack/data/equity_indices.xlsx differ diff --git a/BharatFinTrack/nse_tri.py b/BharatFinTrack/nse_tri.py index a65c314..bfabc2a 100644 --- a/BharatFinTrack/nse_tri.py +++ b/BharatFinTrack/nse_tri.py @@ -97,6 +97,9 @@ def download_historical_daily_data( index : str Name of the index. + excel_file : str, optional + Path to an Excel file to save the DataFrame. + start_date : str, optional Start date in the format 'DD-MMM-YYYY'. Defaults to the index's base date if None is provided. @@ -109,9 +112,6 @@ def download_historical_daily_data( HTTP headers for the web request. If not provided, defaults to :attr:`BharatFinTrack.core.Core.default_http_headers`. - excel_file : str, optional - Path to an Excel file to save the DataFrame. - Returns ------- DataFrame @@ -238,7 +238,7 @@ def download_daily_summary_equity_closing( Parameters ---------- - excel_file : str, optional + excel_file : str Path to an Excel file to save the DataFrame. http_headers : dict, optional @@ -884,3 +884,142 @@ def sip_summary_from_given_date( summary['XIRR (%)'] = f'{xirr_p:.1f}' return summary + + def sip_growth_comparison_across_indices( + self, + indices: list[str], + folder_path: str, + excel_file: str, + ) -> pandas.DataFrame: + + ''' + Generates a DataFrame that compares SIP investment growth on the + first date of each month across multiple indices over the years. + The output DataFrame is saved to an Excel file, where the cells with + the highest growth among indices for each year are highlighted in green-yellow, + and those with the lowest growth are highlighted in sandy brown. + + Parameters + ---------- + indices : list + A list of index names to compare in the SIP growth. + + folder_path : str + Path to the directory containing Excel files with historical data for each index. Each Excel file must be + named as '{index}.xlsx' corresponding to the index names provided in the `indices` list. These files should + be obtained from :meth:`BharatFinTrack.NSETRI.download_historical_daily_data` or + :meth:`BharatFinTrack.NSETRI.update_historical_daily_data`. + + excel_file : str + Path to an Excel file to save the output DataFrame. + + Returns + ------- + DataFrame + A DataFrame comparing SIP investment growth on the + first date of each month across multiple indices over the years. + ''' + + # check the Excel file extension first + excel_ext = Core()._excel_file_extension(excel_file) + if excel_ext == '.xlsx': + pass + else: + raise Exception(f'Input file extension "{excel_ext}" does not match the required ".xlsx".') + + # monthly investment amount + monthly_invest = 1000 + + # SIP dataframe of index + dataframes = [] + with tempfile.TemporaryDirectory() as tmp_dir: + for index in indices: + index_excel = os.path.join(folder_path, f'{index}.xlsx') + df = NSETRI().yearwise_sip_analysis( + input_excel=index_excel, + monthly_invest=monthly_invest, + output_excel=os.path.join(tmp_dir, 'output.xlsx') + ) + dataframes.append(df) + + # check equal close date for all DataFrames + close_date = dataframes[0]['Close Date'].iloc[0] + equal_closedate = all(map(lambda df: df['Close Date'].iloc[0] == close_date, dataframes)) + if equal_closedate is True: + pass + else: + raise Exception('Last date must be equal across all indices in the Excel files.') + + # filtered dataframes + common_year = min( + map(lambda df: int(df['Year'].max()), dataframes) + ) + dataframes = [ + df[df['Year'] <= common_year] for df in dataframes + ] + dataframes = [ + df.drop(columns=['Invest', 'Value', 'XIRR (%)']) for df in dataframes + ] + dataframes = [ + df.rename(columns={'Multiple (X)': f'{index} (X)'}) for df, index in zip(dataframes, indices) + ] + + # mergeing the DataFrames + merged_df = dataframes[0] + common_cols = list(merged_df.columns)[:-1] + for df in dataframes[1:]: + merged_df = pandas.merge(merged_df, df, on=common_cols, how='inner') + for col in merged_df.columns: + if col.endswith('(X)'): + merged_df[col] = merged_df[col].round(5) + else: + pass + + # saving DataFrame + with pandas.ExcelWriter(excel_file, engine='xlsxwriter') as excel_writer: + merged_df.to_excel(excel_writer, index=False) + workbook = excel_writer.book + worksheet = excel_writer.sheets['Sheet1'] + worksheet.set_column( + 0, len(common_cols) - 1, 15 + ) + worksheet.set_column( + len(common_cols), merged_df.shape[1] - 1, 20, + workbook.add_format({'num_format': '#,##0.0'}) + ) + # header formatting + header_format = workbook.add_format( + { + 'bold': True, + 'text_wrap': True, + 'align': 'center', + 'valign': 'vcenter' + } + ) + for col_num, col_df in enumerate(merged_df.columns): + worksheet.write(0, col_num, col_df, header_format) + # formatting for maximum and minimum value in each row + skip_cols = len(common_cols) + for row in range(merged_df.shape[0]): + # minimum value + worksheet.conditional_format( + row + 1, skip_cols, row + 1, merged_df.shape[1] - 1, + { + 'type': 'cell', + 'criteria': 'equal to', + 'value': merged_df.iloc[row, skip_cols:].min(), + 'format': workbook.add_format({'bg_color': '#F4A460'}) + } + ) + # maximim value + worksheet.conditional_format( + row + 1, skip_cols, row + 1, merged_df.shape[1] - 1, + { + 'type': 'cell', + 'criteria': 'equal to', + 'value': merged_df.iloc[row, skip_cols:].max(), + 'format': workbook.add_format({'bg_color': '#ADFF2F'}) + } + ) + + return merged_df diff --git a/BharatFinTrack/visual.py b/BharatFinTrack/visual.py index 6dfc2e5..18c8d65 100644 --- a/BharatFinTrack/visual.py +++ b/BharatFinTrack/visual.py @@ -471,7 +471,7 @@ def plot_top_cagr_indices( Path of the input Excel file containing the data. close_type : str - Type of closing value for indices, either PRICE or TRI. + Type of closing value for indices, either 'PRICE' or 'TRI'. figure_file : str File Path to save the output figue. @@ -614,12 +614,12 @@ def plot_yearwise_sip_returns( fontsize=12 ) subplot.set_xlabel( - xlabel='Start Date', + xlabel='SIP Start Date', fontsize=15 ) # y-axis customization - yaxis_max = (int((df['Value'].max() / monthly_invest + 50) / ytick_gap) + 1) * ytick_gap + yaxis_max = (round((df['Value'].max() / monthly_invest + 50) / ytick_gap) + 1) * ytick_gap subplot.set_ylim(0, yaxis_max) yticks = range(0, yaxis_max + 1, ytick_gap) subplot.set_yticks( @@ -656,7 +656,7 @@ def plot_yearwise_sip_returns( # figure customization figure_title = ( - f'{index.upper()}: Invest and Return with Multiples (X) and XIRR (%) of Monthly SIP {monthly_invest} Rupees Over Years' + f'{index.upper()}\n\nReturn with Multiples (X) and XIRR (%) for SIP 1000 Rupees on First Date of Each Month Over Years' ) figure.suptitle( t=figure_title, @@ -678,7 +678,7 @@ def plot_sip_index_vs_gsec( index: str, excel_file: str, figure_file: str, - bank_return: float = 7.5, + gsec_return: float = 8, ytick_gap: int = 500 ) -> matplotlib.figure.Figure: @@ -698,8 +698,8 @@ def plot_sip_index_vs_gsec( figure_file : str File Path to save the output figue. - bank_return : float, optional - Expected annual return rate of bank fixed deposit in percentage. Default is 7.5. + gsec_return : float, optional + Expected annual return rate of government bond in percentage. Default is 8. ytick_gap : int, optional Gap between two y-axis ticks. Default is 500. @@ -738,12 +738,12 @@ def plot_sip_index_vs_gsec( bank_df = Core().sip_growth( invest=monthly_invest, frequency='monthly', - annual_return=bank_return, + annual_return=gsec_return, years=sip_years ) # figure - fig_width = len(df) if len(df) > 10 else 10 + fig_width = len(df) * 1.2 if len(df) >= 9 else 10 figure = matplotlib.pyplot.figure( figsize=(fig_width, 10) ) @@ -765,7 +765,7 @@ def plot_sip_index_vs_gsec( x=xticks, height=bank_df['Value'] / monthly_invest, width=bar_width, - label='Bank', + label=f'Government ({gsec_return:.1f}%)', color='cyan' ) subplot.bar( @@ -775,6 +775,15 @@ def plot_sip_index_vs_gsec( label='Index', color='lightgreen' ) + for xt in xticks: + multiple = df['Multiple (X)'][xt] + xirr = df['XIRR (%)'][xt] + subplot.annotate( + f'{multiple:.1f}X,{xirr:.0f}%', + xy=(xticks[xt] + bar_width, df['Value'][xt] / monthly_invest), + ha='center', va='bottom', + fontsize=12 + ) # x-axis customization subplot.set_xticks( @@ -796,7 +805,7 @@ def plot_sip_index_vs_gsec( ) # y-axis customization - yaxis_max = (int((df['Value'].max() / monthly_invest + 50) / ytick_gap) + 1) * ytick_gap + yaxis_max = (round((df['Value'].max() / monthly_invest + 50) / ytick_gap) + 1) * ytick_gap subplot.set_ylim(0, yaxis_max) yticks = range(0, yaxis_max + 1, ytick_gap) subplot.set_yticks( @@ -833,7 +842,7 @@ def plot_sip_index_vs_gsec( # figure customization figure_title = ( - f'{index.upper()}: Comparison Return Between Index and Government Bond of Monthly SIP {monthly_invest} Rupees Over Years' + f'{index.upper()} Return with Multiples (X) and XIRR (%)\n\nComparison Return Between Index and Government Bond for SIP 1000 Rupees on First Date of Each Month Over Years' ) figure.suptitle( t=figure_title, @@ -863,7 +872,7 @@ def plot_sip_growth_comparison_across_indices( Parameters ---------- - indices : str + indices : list A list of index names to compare in the SIP growth plot. folder_path : str @@ -957,7 +966,7 @@ def plot_sip_growth_comparison_across_indices( fontsize=12 ) subplot.set_xlabel( - xlabel='Start Date', + xlabel='SIP Start Date', fontsize=15 ) @@ -1000,7 +1009,7 @@ def plot_sip_growth_comparison_across_indices( # figure customization figure_title = ( - 'Growth of Monthly SIP Investment Over Years' + 'Growth of Monthly SIP Investment on First Date of each Month Over Years' ) figure.suptitle( t=figure_title, diff --git a/README.md b/README.md index 18a1f9b..d1583ff 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ * Computes the year-wise SIP return for a fixed monthly contribution to a specified NSE equity `TRI` index. * Calculates the closing summary of an SIP with a fixed monthly contribution to a specified NSE equity `TRI` index, starting from a given date. +* Compares the growth of a monthly SIP investment across multiple NSE equity `TRI` indices over the years. * Estimates annual SIP performance, including investment amount, closing balance, and cumulative growth over a specified number of years and expected annual return, with options for yearly, quarterly, monthly, or weekly contributions. @@ -30,9 +31,9 @@ * Displays bar graphs of NSE equity indices’ closing values with descending CAGR (%) since inception, both overall and by index category. * Shows bar graphs of top-performing NSE equity indices by CAGR (%) since launch, with options to view a specified number of top indices, either overall or within each category. -* Compares the growth of a monthly SIP investment across multiple NSE equity `TRI` indices over the years. * Depicts a bar graph of year-wise investments and returns for a monthly SIP of 1,000 Rupees in a specified NSE equity `TRI` index since its inception. * Provides a return comparison between a specified index and government bonds for a monthly SIP of 1,000 Rupees over the years. +* Illustrates a line plot comparing the growth of a monthly SIP investment across multiple NSE equity `TRI` indices over the years. # Example Insights @@ -45,7 +46,8 @@ In this graph, the `NIFTY MIDCAP150 MOMENTUM 50` stands out as one of the best-p ![Year-wise SIP analysis of NIFTY_MIDCAP150_MOMENTUM_50](https://github.com/debpal/BharatFinTrack/raw/master/docs/_static/sip_yearwise_NIFTY_MIDCAP150_MOMENTUM_50.png) -Additionally, the following plot compares the growth of a monthly SIP investment across `TRI` indices, including `NIFTY 50` and several other top-performing NSE equity indices over the years. +Additionally, the following plot compares the growth multiples (X) of a monthly SIP investment across `TRI` indices, including popular indices such as `NIFTY 50` and `NIFTY 500`, as well as several other top-performing NSE equity indices over the years. + ![Year-wise SIP growth comparison across multiple indices](https://github.com/debpal/BharatFinTrack/raw/master/docs/_static/sip_invest_growth_across_indices.png) diff --git a/docs/_static/sip_gsec_vs_NIFTY_MIDCAP150_MOMENTUM_50.png b/docs/_static/sip_gsec_vs_NIFTY_MIDCAP150_MOMENTUM_50.png new file mode 100644 index 0000000..993545c Binary files /dev/null and b/docs/_static/sip_gsec_vs_NIFTY_MIDCAP150_MOMENTUM_50.png differ diff --git a/docs/_static/sip_invest_growth_across_indices.png b/docs/_static/sip_invest_growth_across_indices.png index 5cd1e56..5c09a71 100644 Binary files a/docs/_static/sip_invest_growth_across_indices.png and b/docs/_static/sip_invest_growth_across_indices.png differ diff --git a/docs/_static/sip_yearwise_NIFTY_MIDCAP150_MOMENTUM_50.png b/docs/_static/sip_yearwise_NIFTY_MIDCAP150_MOMENTUM_50.png index 0722d12..493c868 100644 Binary files a/docs/_static/sip_yearwise_NIFTY_MIDCAP150_MOMENTUM_50.png and b/docs/_static/sip_yearwise_NIFTY_MIDCAP150_MOMENTUM_50.png differ diff --git a/docs/_static/tri_top_cagr.png b/docs/_static/tri_top_cagr.png index 0c47bfa..138a7f8 100644 Binary files a/docs/_static/tri_top_cagr.png and b/docs/_static/tri_top_cagr.png differ diff --git a/docs/_static/tri_top_cagr_by_category.png b/docs/_static/tri_top_cagr_by_category.png index 2602dd4..0737cf4 100644 Binary files a/docs/_static/tri_top_cagr_by_category.png and b/docs/_static/tri_top_cagr_by_category.png differ diff --git a/docs/changelog.rst b/docs/changelog.rst index 9f39b69..411d806 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,6 +2,21 @@ Release Notes =============== +Version 0.1.9 +--------------- + +* **Release date:** 15-Nov-2024 + +* **Feature Additions:** + + * Added support for newly launched NSE equity indices. + * Enhanced functionality for SIP comparison across indices in the :class:`BharatFinTrack.NSETRI` class. + + +* **Development Status:** Upgraded from Beta to Production/Stable. + + + Version 0.1.8 --------------- diff --git a/docs/functionality.rst b/docs/functionality.rst index 1b7d5ed..34e9618 100644 --- a/docs/functionality.rst +++ b/docs/functionality.rst @@ -104,6 +104,32 @@ Computes the year-wise SIP return for a fixed monthly contribution to a specifie monthly_invest=1000, output_excel=r"C:\Users\Username\Folder\SIP_Yearwise_NIFTY_50.xlsx" ) + + +SIP Comparison Across Indices +------------------------------- + +Comparing the growth multiples (X) of a monthly SIP investment across `TRI` indices, including popular indices such as NIFTY 50 and NIFTY 500, as well as several other top-performing NSE equity indices over the years. The data required for SIP calculations must be sourced from the Excel files generated in the :ref:`Total Return Index (TRI) ` section. Ensure that all Excel files are stored in the designated input folder, with each file named as `{index}.xlsx` to correspond to the index names provided in the list of indices. The output DataFrame is saved to an Excel file, where the cells with the highest growth among indices for each year are highlighted in green-yellow, and those with the lowest growth are highlighted in sandy brown. + + +.. code-block:: python + + index_list = [ + 'NIFTY 50', + 'NIFTY 500', + 'NIFTY ALPHA 50', + 'NIFTY MIDCAP150 MOMENTUM 50', + 'NIFTY500 MOMENTUM 50', + 'NIFTY MIDSMALLCAP400 MOMENTUM QUALITY 100', + 'NIFTY SMALLCAP250 MOMENTUM QUALITY 100' + ] + + nse_tri.sip_growth_comparison_across_indices( + indices=index_list + folder_path=r"C:\Users\Username\Folder", + excel_file=r"C:\Users\Username\Folder\sip_invest_growth_across_indices.xlsx" + ) + SIP Calculator diff --git a/docs/introduction.rst b/docs/introduction.rst index 690b17b..1603587 100644 --- a/docs/introduction.rst +++ b/docs/introduction.rst @@ -28,6 +28,7 @@ Systematic Investment Plan (SIP) * Computes the year-wise SIP return for a fixed monthly contribution to a specified NSE equity `TRI` index. * Calculates the closing summary of an SIP with a fixed monthly contribution to a specified NSE equity `TRI` index, starting from a given date. +* Compares the growth of a monthly SIP investment across multiple NSE equity `TRI` indices over the years. * Estimates annual SIP performance, including investment amount, closing balance, and cumulative growth over a specified number of years and expected annual return, with options for yearly, quarterly, monthly, or weekly contributions. @@ -36,7 +37,7 @@ Visualization * Displays bar graphs of NSE equity indices’ closing values with descending CAGR (%) since inception, both overall and by index category. * Shows bar graphs of top-performing NSE equity indices by CAGR (%) since launch, with options to view a specified number of top indices, either overall or within each category. -* Compares the growth of a monthly SIP investment across multiple NSE equity `TRI` indices over the years. * Depicts a bar graph of year-wise investments and returns for a monthly SIP of 1,000 Rupees in a specified NSE equity `TRI` index since its inception. * Provides a return comparison between a specified index and government bonds for a monthly SIP of 1,000 Rupees over the years. +* Illustrates a line plot comparing the growth of a monthly SIP investment across multiple NSE equity `TRI` indices over the years. \ No newline at end of file diff --git a/docs/visualization.rst b/docs/visualization.rst index bdc0613..19056db 100644 --- a/docs/visualization.rst +++ b/docs/visualization.rst @@ -77,8 +77,8 @@ A bar plot displays investments and returns over the years for `TRI` data of the visual.plot_yearwise_sip_returns( index='NIFTY MIDCAP150 MOMENTUM 50' - excel_file=r"C:\Users\Username\Folder\NIFTY MIDCAP150 MOMENTUM 50.xlsx", - figure_file=r"C:\Users\Username\Folder\SIP_Yearwise_NIFTY_50.png" + excel_file=r"C:\Users\Username\Folder\index_data.xlsx", + figure_file=r"C:\Users\Username\Folder\SIP_Yearwise.png" ) @@ -88,20 +88,44 @@ The resulting plot will look similar to the example below. :align: left +SIP Comparison with Government Securities +------------------------------------------- +A bar plot displays the comparison of investments and returns over the years for the `TRI` data of the `NIFTY MIDCAP150 MOMENTUM 50` index and government secutity bond with an assumed coupon rate. + + +.. code-block:: python + + visual.plot_sip_index_vs_gsec( + index='NIFTY MIDCAP150 MOMENTUM 50' + excel_file=r"C:\Users\Username\Folder\index_data.xlsx", + figure_file=r"C:\Users\Username\Folder\SIP_gsec_vs_index.png", + gsec_return=8 + ) + + +The resulting plot will look similar to the example below. + +.. image:: _static/sip_gsec_vs_NIFTY_MIDCAP150_MOMENTUM_50.png + :align: left + + SIP Comparison Across Indices ------------------------------- -A plot comparing the growth of a monthly SIP investment across `TRI` indices, including NIFTY 50 and several other top-performing NSE equity indices over the years. The data required for SIP calculations must be sourced from the Excel files generated in the :ref:`Total Return Index (TRI) ` section. Ensure that all Excel files are stored in the designated input folder, with each file named as `{index}.xlsx` to correspond to the index names provided in the list of indices. +A plot comparing the growth multiples (X) of a monthly SIP investment across `TRI` indices, including popular indices such as NIFTY 50 and NIFTY 500, as well as several other top-performing NSE equity indices over the years. The data required for SIP calculations must be sourced from the Excel files generated in the :ref:`Total Return Index (TRI) ` section. Ensure that all Excel files are stored in the designated input folder, with each file named as `{index}.xlsx` to correspond to the index names provided in the list of indices. + .. code-block:: python index_list = [ 'NIFTY 50', + 'NIFTY 500', + 'NIFTY ALPHA 50', 'NIFTY MIDCAP150 MOMENTUM 50', 'NIFTY500 MOMENTUM 50', 'NIFTY MIDSMALLCAP400 MOMENTUM QUALITY 100', - 'NIFTY SMALLCAP250 MOMENTUM QUALITY 100', + 'NIFTY SMALLCAP250 MOMENTUM QUALITY 100' ] visual.plot_sip_growth_comparison_across_indices( diff --git a/tests/test_bharatfintrack.py b/tests/test_bharatfintrack.py index 3ebe267..60dad35 100644 --- a/tests/test_bharatfintrack.py +++ b/tests/test_bharatfintrack.py @@ -207,8 +207,8 @@ def test_download_historical_daily_data( @pytest.mark.parametrize( 'index, expected_value', [ - ('NIFTY TOP 20 EQUAL WEIGHT', 12041.27), ('NIFTY INDIA NEW AGE CONSUMPTION', 14709.71), + ('NIFTY INDIA SELECT 5 CORPORATE GROUPS (MAATR)', 47085.20), ] ) def test_index_download_historical_daily_data( @@ -485,7 +485,7 @@ def test_sip( index=index, excel_file=input_excel, figure_file=figure_file, - bank_return=7.5, + gsec_return=7.5, ytick_gap=25 ) assert os.path.exists(figure_file) is True @@ -499,6 +499,20 @@ def test_sip( ) assert os.path.exists(figure_file) is True assert sum([file.endswith('.png') for file in os.listdir(tmp_dir)]) == 3 + # pass test for comparison of SIP for multiple indices + index_1 = 'NIFTY MIDCAP150 MOMENTUM 50' + nse_tri.download_historical_daily_data( + index=index_1, + excel_file=os.path.join(tmp_dir, f'{index_1}.xlsx'), + start_date='15-Oct-2022', + end_date='15-Oct-2024' + ) + merged_df = nse_tri.sip_growth_comparison_across_indices( + indices=[index, index_1], + folder_path=tmp_dir, + excel_file=os.path.join(tmp_dir, 'sip_invest_growth_across_indices.xlsx') + ) + assert len(merged_df.columns) == 5 # error test for unequal end date of two indices nse_tri.download_historical_daily_data( index='NIFTY ALPHA 50', @@ -513,6 +527,13 @@ def test_sip( figure_file=figure_file ) assert exc_info.value.args[0] == 'Last date must be equal across all indices in the Excel files.' + with pytest.raises(Exception) as exc_info: + nse_tri.sip_growth_comparison_across_indices( + indices=['NIFTY 50', 'NIFTY ALPHA 50'], + folder_path=tmp_dir, + excel_file=os.path.join(tmp_dir, 'sip_invest_growth_across_indices.xlsx') + ) + assert exc_info.value.args[0] == 'Last date must be equal across all indices in the Excel files.' # error test for invalid input of year and month with pytest.raises(Exception) as exc_info: nse_tri.sip_summary_from_given_date( @@ -608,6 +629,14 @@ def test_error_excel( ) assert exc_info.value.args[0] == message['error_excel'] + with pytest.raises(Exception) as exc_info: + nse_tri.sip_growth_comparison_across_indices( + indices=['NIFTY 50'], + folder_path=r"C:\Users\Username\Folder", + excel_file='output.xl' + ) + assert exc_info.value.args[0] == message['error_excel'] + def test_error_figure( visual, @@ -648,7 +677,7 @@ def test_error_figure( index='NIFTY 50', excel_file='NIFTY 50.xlsx', figure_file='figure_file.pn', - bank_return=7.5, + gsec_return=7.5, ytick_gap=500 ) assert exc_info.value.args[0] == message['error_figure']