diff --git a/BharatFinTrack/__init__.py b/BharatFinTrack/__init__.py index a8916df..1f1bbd3 100644 --- a/BharatFinTrack/__init__.py +++ b/BharatFinTrack/__init__.py @@ -1,11 +1,13 @@ from .nse_product import NSEProduct +from .nse_index import NSEIndex from .nse_tri import NSETRI __all__ = [ 'NSEProduct', + 'NSEIndex', 'NSETRI' ] -__version__ = '0.1.0' +__version__ = '0.1.1' diff --git a/BharatFinTrack/data/equity_indices.xlsx b/BharatFinTrack/data/equity_indices.xlsx index 27746f4..99ebf97 100644 Binary files a/BharatFinTrack/data/equity_indices.xlsx and b/BharatFinTrack/data/equity_indices.xlsx differ diff --git a/BharatFinTrack/nse_index.py b/BharatFinTrack/nse_index.py new file mode 100644 index 0000000..724c692 --- /dev/null +++ b/BharatFinTrack/nse_index.py @@ -0,0 +1,194 @@ +import os +import tempfile +import typing +import datetime +import dateutil.relativedelta +import pandas +import requests +import bs4 +import matplotlib +from .nse_product import NSEProduct +from .core import Core + + +class NSEIndex: + + ''' + Download and analyze NSE index price data. + ''' + + def all_equity_index_cagr_from_inception( + self, + excel_file: str, + http_headers: typing.Optional[dict[str, str]] = None + ) -> pandas.DataFrame: + + ''' + Returns a DataFrame with the CAGR(%) of all NSE equity indices + (excluding dividend reinvestment) from inception. + + Parameters + ---------- + excel_file : str + Path to an Excel file to save the DataFrame. + + http_headers : dict, optional + HTTP headers for the web request. Defaults to + :attr:`BharatFinTrack.core.Core.default_http_headers` if not provided. + + Returns + ------- + DataFrame + A multi-index DataFrame with the CAGR(%) for all NSE equity indices from inception, + sorted in descending order by CAGR(%) within each index category. + ''' + + # web request headers + headers = Core().default_http_headers if http_headers is None else http_headers + + # download data + main_url = 'https://www.niftyindices.com' + csv_url = main_url + '/reports/daily-reports' + response = requests.get(csv_url, headers=headers) + soup = bs4.BeautifulSoup(response.content, 'html.parser') + for anchor in soup.find_all('a'): + if anchor['href'].endswith('.csv') and anchor['id'] == 'dailysnapOneDaybefore': + csv_link = main_url + anchor['href'] + else: + pass + response = requests.get(csv_link, headers=headers) + with tempfile.TemporaryDirectory() as tmp_dir: + daily_file = os.path.join(tmp_dir, 'daily.csv') + with open(daily_file, 'wb') as daily_data: + daily_data.write(response.content) + daily_df = pandas.read_csv(daily_file) + + # processing downloaded data + date_string = datetime.datetime.strptime( + daily_df.loc[0, 'Index Date'], '%d-%m-%Y' + ) + daily_date = date_string.date() + daily_df = daily_df[['Index Name', 'Index Date', 'Closing Index Value']] + daily_df.columns = ['Index Name', 'Date', 'Close'] + daily_df['Index Name'] = daily_df['Index Name'].apply(lambda x: x.upper()) + exclude_word = [ + 'G-SEC', + '1D RATE', + 'INDIA VIX', + 'DIVIDEND POINTS' + ] + exclude_index = daily_df['Index Name'].apply( + lambda x: any(word in x for word in exclude_word) + ) + daily_df = daily_df[~exclude_index].reset_index(drop=True) + daily_df['Date'] = daily_date + + # processing base DataFrame + base_df = NSEProduct()._dataframe_equity_index + base_df = base_df.reset_index() + category = list(base_df['Category'].unique()) + base_df = base_df.drop(columns=['ID', 'API TRI']) + base_df['Base Date'] = base_df['Base Date'].apply(lambda x: x.date()) + + # checking absent indices in both files + base_unmatch = { + 'NIFTY 50 FUTURES INDEX': 'NIFTY 50 FUTURES PR', + 'NIFTY 50 FUTURES TR INDEX': 'NIFTY 50 FUTURES TR', + 'NIFTY HEALTHCARE INDEX': 'NIFTY HEALTHCARE' + } + daily_df['Index Name'] = daily_df['Index Name'].apply( + lambda x: base_unmatch.get(x, x) + ) + # base_index = base_df['Index Name'] + # daily_index = daily_df['Index Name'] + # unavailable_base = list(base_index[~base_index.isin(daily_index)]) + # print(f'Base indices {unavailable_base} are not available in daily indices.') + # unavailable_daily = list(daily_index[~daily_index.isin(base_index)]) + # print(f'Daily indices {unavailable_daily} are not available in base indices.') + + # merging data + cagr_df = base_df.merge(daily_df) + cagr_df['Return(1 INR)'] = (cagr_df['Close'] / cagr_df['Base Value']).round(2) + cagr_df['Years'] = list( + map( + lambda x: dateutil.relativedelta.relativedelta(daily_date, x).years, cagr_df['Base Date'] + ) + ) + cagr_df['Days'] = list( + map( + lambda x, y: (daily_date - x.replace(year=x.year + y)).days, cagr_df['Base Date'], cagr_df['Years'] + ) + ) + total_years = cagr_df['Years'] + (cagr_df['Days'] / 365) + cagr_df['CAGR(%)'] = 100 * (pow(cagr_df['Close'] / cagr_df['Base Value'], 1 / total_years) - 1) + + # Convert 'Category' column to categorical data types with a defined order + cagr_df['Category'] = pandas.Categorical( + cagr_df['Category'], + categories=category, + ordered=True + ) + + # Sort the dataframe + cagr_df = cagr_df.sort_values( + by=['Category', 'CAGR(%)', 'Years', 'Days'], + ascending=[True, False, False, False] + ) + + # output + dfs_category = map(lambda x: cagr_df[cagr_df['Category'] == x], category) + dataframes = list( + map( + lambda x: x.drop(columns=['Category']).reset_index(drop=True), dfs_category + ) + ) + output = pandas.concat( + dataframes, + keys=[word.upper() for word in category], + names=['Category', 'ID'] + ) + + # saving the DataFrame + excel_ext = Core()._excel_file_extension(excel_file) + if excel_ext != '.xlsx': + raise Exception( + f'Input file extension "{excel_ext}" does not match the required ".xlsx".' + ) + else: + with pandas.ExcelWriter(excel_file, engine='xlsxwriter') as excel_writer: + output.to_excel(excel_writer, index=True) + workbook = excel_writer.book + worksheet = excel_writer.sheets['Sheet1'] + # number of columns for DataFrame indices + index_cols = len(output.index.names) + # format columns + worksheet.set_column(0, index_cols - 1, 15) + worksheet.set_column(index_cols, index_cols, 60) + worksheet.set_column(index_cols + 1, index_cols + output.shape[1] - 2, 15) + worksheet.set_column( + index_cols + output.shape[1] - 1, + index_cols + output.shape[1] - 1, 15, + workbook.add_format({'num_format': '#,##0.0'}) + ) + # Dataframe colors + get_colormap = matplotlib.colormaps.get_cmap('Pastel2') + colors = [ + get_colormap(count / len(category)) for count in range(len(category)) + ] + hex_colors = [ + '{:02X}{:02X}{:02X}'.format(*[int(num * 255) for num in color]) for color in colors + ] + # coloring of DataFrames + start_col = index_cols - 1 + end_col = index_cols + len(output.columns) - 1 + start_row = 1 + for df, color in zip(dataframes, hex_colors): + color_format = workbook.add_format({'bg_color': color}) + end_row = start_row + len(df) - 1 + worksheet.conditional_format( + start_row, start_col, end_row, end_col, + {'type': 'no_blanks', 'format': color_format} + ) + start_row = end_row + 1 + + return output diff --git a/BharatFinTrack/nse_tri.py b/BharatFinTrack/nse_tri.py index a1a046c..1eff518 100644 --- a/BharatFinTrack/nse_tri.py +++ b/BharatFinTrack/nse_tri.py @@ -8,7 +8,8 @@ class NSETRI: ''' - Download and analyze NSE TRI (Total Return Index) data. + Download and analyze NSE TRI (Total Return Index) data, + including both price index and dividend reinvestment. ''' @property diff --git a/README.md b/README.md index dee3d94..6fb320f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ It is designed to simplify the process of downloading and analyzing financial da * [Nifty Indices](https://www.niftyindices.com/) - Provides access to the characteristics of NSE equity indices. - - Facilitates downloading TRI (Total Return Index) data for all NSE equity indices. + - Calculates the CAGR(%) of all NSE equity indices (excluding dividend reinvestment) from inception. + - Facilitates downloading Total Return Index, including both price and dividend reinvestment, data for all NSE equity indices. ## Roadmap diff --git a/docs/changelog.rst b/docs/changelog.rst index 50d0a14..f27ccc4 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -2,14 +2,27 @@ Release Notes =============== + +Version 0.1.1 +-------------- + +* **Release date:** 02-Oct-2024 + +* **Feature Additions:** Introduced the :class:`BharatFinTrack.NSEIndex` class, which currently calculates the CAGR(%) of all NSE equity indices + (excluding dividend reinvestment) from inception. Additional features are planned for future releases. + +* **Documentation:** Updated to reflect the newly introduced features. + +* **Development Status:** Upgraded from Pre-Alpha to Alpha. + + Version 0.1.0 --------------- * **Release date:** 30-Sep-2024. * **Feature Additions:** Introduced :class:`BharatFinTrack.NSETRI` class, which facilitates downloading Total Return Index (TRI) data for all NSE equity indices. - - + * **Changes:** * Renamed class :class:`BharatFinTrack.NSETrack` to :class:`BharatFinTrack.NSEProduct` for improved clarity. @@ -17,7 +30,7 @@ Version 0.1.0 * **Documentation:** Added a tutorial on how to use the newly introduced features. -* **Development status:** Upgraded to Pre-Alpha from Planning. +* **Development status:** Upgraded from Planning to Pre-Alpha. Version 0.0.3 diff --git a/docs/data_download.rst b/docs/data_download.rst deleted file mode 100644 index c95949e..0000000 --- a/docs/data_download.rst +++ /dev/null @@ -1,44 +0,0 @@ -================== -Downloading Data -================== - -A brief overview of the features for downloading data. - - -Total Return Index Data -------------------------- -Download historical daily data for the NIFTY 50 index: - -.. code-block:: python - - import BharatFinTrack - nse_tri = BharatFinTrack.NSETRI() - nse_tri.download_historical_daily_data( - index='NIFTY 50', - start_date='23-SEP-2024', - end_date='27-SEP-2024' - ) - - -Expected output: - -.. code-block:: text - - Date Close - 0 2024-09-23 38505.51 - 1 2024-09-24 38507.55 - 2 2024-09-25 38602.21 - 3 2024-09-26 38916.76 - 4 2024-09-27 38861.64 - - - - - - - - - - - - diff --git a/docs/functionality.rst b/docs/functionality.rst new file mode 100644 index 0000000..a0a5e67 --- /dev/null +++ b/docs/functionality.rst @@ -0,0 +1,29 @@ +=============== +Functionality +=============== + + +Index CAGR(%) +--------------- +Save the CAGR(%) of all NSE equity indices (excluding dividend reinvestment) from inception to an Excel file. + +.. code-block:: python + + import BharatFinTrack + nse_index = BharatFinTrack.NSEIndex() + nse_index.all_index_cagr_from_inception( + excel_file=r"C:\Users\Username\Folder\file.xlsx" + ) + + + + + + + + + + + + + diff --git a/docs/index.rst b/docs/index.rst index df13ae4..74d3942 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,7 +13,7 @@ Welcome to BharatFinTrack's documentation! introduction installation quickstart - data_download + functionality modules changelog diff --git a/docs/quickstart.rst b/docs/quickstart.rst index 9f7051b..29ce379 100644 --- a/docs/quickstart.rst +++ b/docs/quickstart.rst @@ -90,4 +90,35 @@ Expected output: .. code-block:: text '03-Nov-1995' - 1000.0 \ No newline at end of file + 1000.0 + + + +Download Data +--------------- + +Total Return Index (TRI) +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Download historical daily TRI data, including both price and dividend reinvestment, for the NIFTY 50 index: + +.. code-block:: python + + import BharatFinTrack + nse_tri = BharatFinTrack.NSETRI() + nse_tri.download_historical_daily_data( + index='NIFTY 50', + start_date='23-SEP-2024', + end_date='27-SEP-2024' + ) + + +Expected output: + +.. code-block:: text + + Date Close + 0 2024-09-23 38505.51 + 1 2024-09-24 38507.55 + 2 2024-09-25 38602.21 + 3 2024-09-26 38916.76 + 4 2024-09-27 38861.64 \ No newline at end of file diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index f9d885e..1aae231 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -3,5 +3,7 @@ pandas>=2.2.2 requests>=2.32.3 openpyxl>=3.1.5 xlsxwriter>=3.2.0 +beautifulsoup4>=4.12.3 +matplotlib>=3.9.2 diff --git a/pyproject.toml b/pyproject.toml index 761248c..e9e7da5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,12 +13,14 @@ dependencies = [ "pandas>=2.2.2", "requests>=2.32.3", "openpyxl>=3.1.5", - "xlsxwriter>=3.2.0" + "xlsxwriter>=3.2.0", + "beautifulsoup4>=4.12.3", + "matplotlib>=3.9.2" ] readme = "README.md" requires-python = ">=3.10" classifiers = [ - "Development Status :: 2 - Pre-Alpha", + "Development Status :: 3 - Alpha", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", diff --git a/requirements-gh-action.txt b/requirements-gh-action.txt index eab35f6..0c2aa22 100644 --- a/requirements-gh-action.txt +++ b/requirements-gh-action.txt @@ -4,3 +4,5 @@ pandas>=2.2.2 requests>=2.32.3 openpyxl>=3.1.5 xlsxwriter>=3.2.0 +beautifulsoup4>=4.12.3 +matplotlib>=3.9.2 diff --git a/requirements-mypy.txt b/requirements-mypy.txt index e5738a6..e77c8cb 100644 --- a/requirements-mypy.txt +++ b/requirements-mypy.txt @@ -1,2 +1,3 @@ types-requests +types-python-dateutil mypy diff --git a/tests/test_bharatfintrack.py b/tests/test_bharatfintrack.py index e0992a6..856b5b9 100644 --- a/tests/test_bharatfintrack.py +++ b/tests/test_bharatfintrack.py @@ -12,6 +12,12 @@ def nse_product(): yield BharatFinTrack.NSEProduct() +@pytest.fixture(scope='class') +def nse_index(): + + yield BharatFinTrack.NSEIndex() + + @pytest.fixture(scope='class') def nse_tri(): @@ -55,7 +61,7 @@ def test_save_dataframes_equity_indices( # pass test with tempfile.TemporaryDirectory() as tmp_dir: - excel_file = os.path.join(tmp_dir, 'equity_index.xlsx') + excel_file = os.path.join(tmp_dir, 'equity.xlsx') df = nse_product.save_dataframe_equity_index_parameters( excel_file=excel_file ) @@ -186,7 +192,7 @@ def test_download_historical_daily_data( # pass test for saving the output DataFrame to an Excel file with tempfile.TemporaryDirectory() as tmp_dir: - excel_file = os.path.join(tmp_dir, 'equity_index.xlsx') + excel_file = os.path.join(tmp_dir, 'equity.xlsx') nse_tri.download_historical_daily_data( index='NIFTY 50', start_date='23-Sep-2024', @@ -241,3 +247,26 @@ def test_index_download_historical_daily_data( end_date='27-Sep-2024' ) assert float(df.iloc[-1, -1]) == expected_value + + +def test_all_index_cagr_from_inception( + nse_index, + message +): + + # pass test + with tempfile.TemporaryDirectory() as tmp_dir: + excel_file = os.path.join(tmp_dir, 'equity.xlsx') + nse_index.all_equity_index_cagr_from_inception( + excel_file=excel_file + ) + df = pandas.read_excel(excel_file, index_col=[0, 1]) + assert df.shape[1] == 9 + assert len(df.index.get_level_values('Category').unique()) == 5 + + # error test for invalid Excel file input + with pytest.raises(Exception) as exc_info: + nse_index.all_equity_index_cagr_from_inception( + excel_file='equily.xl' + ) + assert exc_info.value.args[0] == message['error_excel']