cagrx 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,27 @@
1
+ ---
2
+ description: how to publish the package to PyPI using uv
3
+ ---
4
+ 1. **Build the Package**: Generate the distribution files (sdist and wheel) using `uv`.
5
+ ```bash
6
+ uv build
7
+ ```
8
+ This will create a `dist/` directory containing the build artifacts.
9
+
10
+ 2. **Upload to PyPI**: Upload the package using `uv`.
11
+ ```bash
12
+ # For TestPyPI (Recommended for first time)
13
+ # You need to set the token variable first or pass it directly
14
+ # $Env:UV_PUBLISH_TOKEN = "pypi-..."
15
+ uv publish --publish-url https://test.pypi.org/legacy/
16
+
17
+ # For Real PyPI
18
+ # $Env:UV_PUBLISH_TOKEN = "pypi-..."
19
+ uv publish
20
+ ```
21
+ `uv` will look for the `UV_PUBLISH_TOKEN` environment variable or can be configured via `uv.toml` or global configuration.
22
+
23
+ 3. **Clean Up**: Remove the build artifacts if needed (optional, `uv` handles rebuilds well).
24
+ ```bash
25
+ # Windows (PowerShell)
26
+ Remove-Item -Recurse -Force dist
27
+ ```
cagrx-0.1.0/.gitignore ADDED
@@ -0,0 +1,36 @@
1
+ # Python-generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # Virtual environments
10
+ .venv
11
+ venv/
12
+ env/
13
+
14
+ # IDE settings
15
+ .vscode/
16
+ .idea/
17
+
18
+ # Testing and Type Checking
19
+ .coverage
20
+ htmlcov/
21
+ .mypy_cache/
22
+ .pytest_cache/
23
+
24
+ # Environment variables
25
+ .env
26
+
27
+ # System files
28
+ .DS_Store
29
+ Thumbs.db
30
+
31
+ # Project specific
32
+ *.csv
33
+ !122639.csv
34
+ .!amfi_navall.csv
35
+
36
+
@@ -0,0 +1 @@
1
+ 3.9
cagrx-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 rakesh
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
cagrx-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,300 @@
1
+ Metadata-Version: 2.4
2
+ Name: cagrx
3
+ Version: 0.1.0
4
+ Summary: A Python library to calculate return metrics like CAGR, XIRR, and SIP returns using AMFI data
5
+ Project-URL: Homepage, https://github.com/yourusername/cagrx
6
+ Project-URL: Repository, https://github.com/yourusername/cagrx
7
+ Project-URL: Issues, https://github.com/yourusername/cagrx/issues
8
+ Author-email: Your Name <your.email@example.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Requires-Python: >=3.9
15
+ Requires-Dist: pandas
16
+ Requires-Dist: requests
17
+ Description-Content-Type: text/markdown
18
+
19
+ # 📈 cagrx
20
+
21
+ ![Status](https://img.shields.io/badge/status-work--in--progress-orange) ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
22
+
23
+ > **No Cap, only CAGR – Calculate Mutual fund Returns Like a Pro**
24
+
25
+ *Download, analyze, and unlock insights from Indian mutual funds with ease.*
26
+
27
+ ---
28
+
29
+ > [!WARNING]
30
+ > **Work in Progress**: This library is under active development. APIs may change, and some features are still being refined. Contributions and feedback are welcome!
31
+
32
+ **CagrX** is a powerful Python library to analyze mutual funds using AMFI (Association of Mutual Funds in India) data. Whether you're building a fintech app, conducting investment research, or analyzing portfolio performance, cagrx provides the tools you need to work with mutual fund data effortlessly.
33
+
34
+ ## ✨ Features
35
+
36
+ - **Download Mutual Fund Data**: Fetch NAV (Net Asset Value) data for any Indian mutual fund scheme
37
+ - **Historical Data**: Access historical NAV data with automatic chunking for large date ranges
38
+ - **Fund Discovery**: Browse and search through all available mutual fund schemes
39
+ - **Performance Analysis**: Built-in metrics for calculating returns:
40
+ - CAGR (Compound Annual Growth Rate)
41
+ - Trailing returns (1Y, 3Y, 5Y, etc.)
42
+ - Rolling returns with max/min/average statistics
43
+ - SIP (Systematic Investment Plan) return calculations
44
+ - **Data Persistence**: Automatic caching of schemes list for faster subsequent access
45
+
46
+ ## 🚀 Installation
47
+
48
+ > [!NOTE]
49
+ > This library is not yet published to PyPI. Install from source for now.
50
+
51
+ **Install from source:**
52
+
53
+ ```bash
54
+ git clone https://github.com/yourusername/cagrx.git
55
+ cd cagrx
56
+ pip install -e .
57
+ ```
58
+
59
+ **Or install dependencies manually:**
60
+
61
+ ```bash
62
+ pip install pandas requests
63
+ ```
64
+
65
+ ## 📦 Requirements
66
+
67
+ - Python >= 3.9
68
+ - pandas
69
+ - requests
70
+
71
+ ## 🔧 Usage
72
+
73
+ ### Getting Started with AMFI Data
74
+
75
+ ```python
76
+ from cagrx.amfi import Amfi
77
+
78
+ # Initialize AMFI client
79
+ amfi = Amfi()
80
+ ```
81
+
82
+ ### Browse Available Mutual Funds
83
+
84
+ ```python
85
+ # Get all fund houses
86
+ fund_houses = amfi.get_fund_houses()
87
+ print(fund_houses)
88
+
89
+ # Get all schemes from a specific fund house
90
+ schemes = amfi.get_schemes_by_fund_house("HDFC Mutual Fund")
91
+ print(schemes)
92
+
93
+ # Get complete schemes list (cached locally as amfi_navall.csv)
94
+ all_schemes = amfi.list_all_schemes()
95
+
96
+ # Force refresh the schemes list from AMFI and update cache
97
+ # Useful when you want to ensure you have the new funds included
98
+ refreshed_schemes = amfi.refresh_schemes()
99
+ ```
100
+
101
+ ### Download Historical NAV Data
102
+
103
+ ```python
104
+ # Download NAV data for a specific scheme
105
+ # Scheme code can be found from list_all_schemes()
106
+ nav_data = amfi.get_nav_history(
107
+ scheme_id="122639", # Example: HDFC Flexi Cap Fund
108
+ start_date="2020-01-01",
109
+ end_date="2023-12-31"
110
+ )
111
+
112
+ # Returns a pandas DataFrame with date as index and NAV values
113
+ print(nav_data.head())
114
+ ```
115
+
116
+ ### Calculate Performance Metrics
117
+
118
+ #### CAGR (Compound Annual Growth Rate)
119
+
120
+ > [!NOTE]
121
+ > You can use `-1` as a period to calculate the CAGR for the entire available historical data (Max CAGR).
122
+
123
+ ```python
124
+ from cagrx.return_metrics import cagr, calculate_trailing_cagr
125
+
126
+ # Calculate overall CAGR
127
+ overall_cagr = cagr(nav_data)
128
+ print(f"Overall CAGR: {overall_cagr * 100:.2f}%")
129
+
130
+ # Calculate trailing CAGR for multiple periods
131
+ trailing_returns = calculate_trailing_cagr(
132
+ nav_data,
133
+ periods=[-1, 1, 3, 5] # -1 for Max CAGR, 1Y, 3Y, 5Y
134
+ )
135
+ print(trailing_returns)
136
+ # Output: {'Max_CAGR': 0.165, '1Y_CAGR': 0.123, '3Y_CAGR': 0.156, '5Y_CAGR': 0.142}
137
+ ```
138
+
139
+ #### Rolling Returns
140
+
141
+ ```python
142
+ from cagrx.return_metrics import calculate_rolling_returns
143
+ import pandas as pd
144
+
145
+ # Calculate 1-year rolling returns
146
+ rolling_metrics = calculate_rolling_returns(
147
+ nav_data,
148
+ period=pd.DateOffset(years=1)
149
+ )
150
+
151
+ print(f"Max rolling return: {rolling_metrics['max_returns'] * 100:.2f}%")
152
+ print(f"Period: {rolling_metrics['max_return_period']}")
153
+ print(f"Min rolling return: {rolling_metrics['min_returns'] * 100:.2f}%")
154
+ print(f"Average rolling return: {rolling_metrics['avg_return'] * 100:.2f}%")
155
+ ```
156
+
157
+ #### SIP Returns (What-If Analysis)
158
+
159
+ Analyze how your periodic investments would have performed:
160
+
161
+ ```python
162
+ from cagrx.amfi import Amfi
163
+ from cagrx.return_metrics import calculate_sip_returns
164
+ import pandas as pd
165
+
166
+ # First, get the NAV data for the fund
167
+ amfi = Amfi()
168
+ nav_data = amfi.get_nav_history(
169
+ scheme_id="122639",
170
+ start_date="2020-01-01",
171
+ end_date="2023-12-31"
172
+ )
173
+
174
+ # Example 1: Regular monthly SIP of ₹5,000 for 3 years
175
+ sip_dates = pd.date_range(start='2020-01-01', periods=36, freq='MS')
176
+ sip_cashflows = pd.DataFrame({'amount': 5000}, index=sip_dates)
177
+
178
+ returns = calculate_sip_returns(sip_cashflows, nav_data)
179
+ print(f"Invested: ₹{returns['total_invested']:,.0f} → Current: ₹{returns['current_value']:,.0f}")
180
+ print(f"Returns: {returns['return_percentage']:.2f}% (Annualized: {returns['annualized_return']:.2f}%)")
181
+
182
+ # Example 2: Irregular investments (lump sum + step-up SIP)
183
+ irregular_investments = pd.DataFrame({
184
+ 'amount': [50000, 5000, 7500, 10000, 15000]
185
+ }, index=pd.to_datetime([
186
+ '2020-01-15', # Initial lump sum
187
+ '2020-06-01', # ₹5k after 6 months
188
+ '2021-01-01', # Stepped up to ₹7.5k
189
+ '2021-06-01', # Stepped up to ₹10k
190
+ '2022-01-01' # Stepped up to ₹15k
191
+ ]))
192
+
193
+ irregular_returns = calculate_sip_returns(irregular_investments, nav_data)
194
+ print(f"Irregular Returns: {irregular_returns['return_percentage']:.2f}%")
195
+ ```
196
+
197
+ ### Complete Example
198
+
199
+ ```python
200
+ from cagrx.amfi import Amfi
201
+ from cagrx.return_metrics import cagr, calculate_trailing_cagr, calculate_rolling_returns
202
+ import pandas as pd
203
+
204
+ # Initialize
205
+ amfi = Amfi()
206
+
207
+ # Browse HDFC schemes
208
+ hdfc_schemes = amfi.get_schemes_by_fund_house("HDFC Mutual Fund")
209
+ print("Available HDFC Schemes:")
210
+ print(hdfc_schemes.head())
211
+
212
+ # Download NAV data for HDFC Flexi Cap Fund
213
+ nav_data = amfi.get_nav_history(
214
+ scheme_id="122639",
215
+ start_date="2018-01-01",
216
+ end_date="2024-12-31"
217
+ )
218
+
219
+ # Save to CSV for future use
220
+ nav_data.to_csv("hdfc_flexi_cap_nav.csv")
221
+
222
+ # Calculate performance metrics
223
+ print("\n=== Performance Metrics ===")
224
+
225
+ # Overall CAGR
226
+ overall_return = cagr(nav_data)
227
+ print(f"\nOverall CAGR: {overall_return * 100:.2f}%")
228
+
229
+ # Trailing returns
230
+ trailing = calculate_trailing_cagr(nav_data, periods=[1, 3, 5])
231
+ print("\nTrailing Returns:")
232
+ for period, value in trailing.items():
233
+ if value:
234
+ print(f" {period}: {value * 100:.2f}%")
235
+ else:
236
+ print(f" {period}: Insufficient data")
237
+
238
+ # Rolling returns
239
+ rolling = calculate_rolling_returns(nav_data, period=pd.DateOffset(years=1))
240
+ print(f"\n1-Year Rolling Returns:")
241
+ print(f" Max: {rolling['max_returns'] * 100:.2f}%")
242
+ print(f" Min: {rolling['min_returns'] * 100:.2f}%")
243
+ print(f" Avg: {rolling['avg_return'] * 100:.2f}%")
244
+ ```
245
+
246
+ ## 📊 Data Sources
247
+
248
+ All data is sourced from official AMFI (Association of Mutual Funds in India) APIs:
249
+ - **Schemes List**: https://www.amfiindia.com/spages/NAVAll.txt
250
+ - **Historical NAV**: https://www.amfiindia.com/api/nav-history
251
+
252
+ ## 🏗️ Project Structure
253
+
254
+ ```
255
+ cagrx/
256
+ ├── src/
257
+ │ └── cagrx/
258
+ │ ├── __init__.py # Main package entry point
259
+ │ ├── amfi.py # AMFI data fetching and management
260
+ │ ├── return_metrics.py # Performance calculation utilities
261
+ │ └── utils.py # Helper functions
262
+ ├── pyproject.toml # Project configuration
263
+ └── README.md # This file
264
+ ```
265
+
266
+ ## 🧪 Development
267
+
268
+ ### Running Tests
269
+
270
+ ```bash
271
+ # Run the test file
272
+ python src/cagrx/amfi_tests.py
273
+ ```
274
+
275
+ ## 🤝 Contributing
276
+
277
+ Contributions are welcome! Please feel free to submit a Pull Request.
278
+
279
+ 1. Fork the project
280
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
281
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
282
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
283
+ 5. Open a Pull Request
284
+
285
+ ## 📝 License
286
+
287
+ This project is licensed under the MIT License - see the LICENSE file for details.
288
+
289
+ ## 🙏 Acknowledgments
290
+
291
+ - Data provided by AMFI (Association of Mutual Funds in India)
292
+ - Built with Python, pandas, and requests
293
+
294
+ ## 📮 Contact
295
+
296
+ For questions or feedback, please open an issue on GitHub.
297
+
298
+ ---
299
+
300
+ **Note**: This library is for educational and research purposes. Please verify all calculations independently before making investment decisions.
cagrx-0.1.0/README.md ADDED
@@ -0,0 +1,282 @@
1
+ # 📈 cagrx
2
+
3
+ ![Status](https://img.shields.io/badge/status-work--in--progress-orange) ![Python](https://img.shields.io/badge/python-3.9%2B-blue)
4
+
5
+ > **No Cap, only CAGR – Calculate Mutual fund Returns Like a Pro**
6
+
7
+ *Download, analyze, and unlock insights from Indian mutual funds with ease.*
8
+
9
+ ---
10
+
11
+ > [!WARNING]
12
+ > **Work in Progress**: This library is under active development. APIs may change, and some features are still being refined. Contributions and feedback are welcome!
13
+
14
+ **CagrX** is a powerful Python library to analyze mutual funds using AMFI (Association of Mutual Funds in India) data. Whether you're building a fintech app, conducting investment research, or analyzing portfolio performance, cagrx provides the tools you need to work with mutual fund data effortlessly.
15
+
16
+ ## ✨ Features
17
+
18
+ - **Download Mutual Fund Data**: Fetch NAV (Net Asset Value) data for any Indian mutual fund scheme
19
+ - **Historical Data**: Access historical NAV data with automatic chunking for large date ranges
20
+ - **Fund Discovery**: Browse and search through all available mutual fund schemes
21
+ - **Performance Analysis**: Built-in metrics for calculating returns:
22
+ - CAGR (Compound Annual Growth Rate)
23
+ - Trailing returns (1Y, 3Y, 5Y, etc.)
24
+ - Rolling returns with max/min/average statistics
25
+ - SIP (Systematic Investment Plan) return calculations
26
+ - **Data Persistence**: Automatic caching of schemes list for faster subsequent access
27
+
28
+ ## 🚀 Installation
29
+
30
+ > [!NOTE]
31
+ > This library is not yet published to PyPI. Install from source for now.
32
+
33
+ **Install from source:**
34
+
35
+ ```bash
36
+ git clone https://github.com/yourusername/cagrx.git
37
+ cd cagrx
38
+ pip install -e .
39
+ ```
40
+
41
+ **Or install dependencies manually:**
42
+
43
+ ```bash
44
+ pip install pandas requests
45
+ ```
46
+
47
+ ## 📦 Requirements
48
+
49
+ - Python >= 3.9
50
+ - pandas
51
+ - requests
52
+
53
+ ## 🔧 Usage
54
+
55
+ ### Getting Started with AMFI Data
56
+
57
+ ```python
58
+ from cagrx.amfi import Amfi
59
+
60
+ # Initialize AMFI client
61
+ amfi = Amfi()
62
+ ```
63
+
64
+ ### Browse Available Mutual Funds
65
+
66
+ ```python
67
+ # Get all fund houses
68
+ fund_houses = amfi.get_fund_houses()
69
+ print(fund_houses)
70
+
71
+ # Get all schemes from a specific fund house
72
+ schemes = amfi.get_schemes_by_fund_house("HDFC Mutual Fund")
73
+ print(schemes)
74
+
75
+ # Get complete schemes list (cached locally as amfi_navall.csv)
76
+ all_schemes = amfi.list_all_schemes()
77
+
78
+ # Force refresh the schemes list from AMFI and update cache
79
+ # Useful when you want to ensure you have the new funds included
80
+ refreshed_schemes = amfi.refresh_schemes()
81
+ ```
82
+
83
+ ### Download Historical NAV Data
84
+
85
+ ```python
86
+ # Download NAV data for a specific scheme
87
+ # Scheme code can be found from list_all_schemes()
88
+ nav_data = amfi.get_nav_history(
89
+ scheme_id="122639", # Example: HDFC Flexi Cap Fund
90
+ start_date="2020-01-01",
91
+ end_date="2023-12-31"
92
+ )
93
+
94
+ # Returns a pandas DataFrame with date as index and NAV values
95
+ print(nav_data.head())
96
+ ```
97
+
98
+ ### Calculate Performance Metrics
99
+
100
+ #### CAGR (Compound Annual Growth Rate)
101
+
102
+ > [!NOTE]
103
+ > You can use `-1` as a period to calculate the CAGR for the entire available historical data (Max CAGR).
104
+
105
+ ```python
106
+ from cagrx.return_metrics import cagr, calculate_trailing_cagr
107
+
108
+ # Calculate overall CAGR
109
+ overall_cagr = cagr(nav_data)
110
+ print(f"Overall CAGR: {overall_cagr * 100:.2f}%")
111
+
112
+ # Calculate trailing CAGR for multiple periods
113
+ trailing_returns = calculate_trailing_cagr(
114
+ nav_data,
115
+ periods=[-1, 1, 3, 5] # -1 for Max CAGR, 1Y, 3Y, 5Y
116
+ )
117
+ print(trailing_returns)
118
+ # Output: {'Max_CAGR': 0.165, '1Y_CAGR': 0.123, '3Y_CAGR': 0.156, '5Y_CAGR': 0.142}
119
+ ```
120
+
121
+ #### Rolling Returns
122
+
123
+ ```python
124
+ from cagrx.return_metrics import calculate_rolling_returns
125
+ import pandas as pd
126
+
127
+ # Calculate 1-year rolling returns
128
+ rolling_metrics = calculate_rolling_returns(
129
+ nav_data,
130
+ period=pd.DateOffset(years=1)
131
+ )
132
+
133
+ print(f"Max rolling return: {rolling_metrics['max_returns'] * 100:.2f}%")
134
+ print(f"Period: {rolling_metrics['max_return_period']}")
135
+ print(f"Min rolling return: {rolling_metrics['min_returns'] * 100:.2f}%")
136
+ print(f"Average rolling return: {rolling_metrics['avg_return'] * 100:.2f}%")
137
+ ```
138
+
139
+ #### SIP Returns (What-If Analysis)
140
+
141
+ Analyze how your periodic investments would have performed:
142
+
143
+ ```python
144
+ from cagrx.amfi import Amfi
145
+ from cagrx.return_metrics import calculate_sip_returns
146
+ import pandas as pd
147
+
148
+ # First, get the NAV data for the fund
149
+ amfi = Amfi()
150
+ nav_data = amfi.get_nav_history(
151
+ scheme_id="122639",
152
+ start_date="2020-01-01",
153
+ end_date="2023-12-31"
154
+ )
155
+
156
+ # Example 1: Regular monthly SIP of ₹5,000 for 3 years
157
+ sip_dates = pd.date_range(start='2020-01-01', periods=36, freq='MS')
158
+ sip_cashflows = pd.DataFrame({'amount': 5000}, index=sip_dates)
159
+
160
+ returns = calculate_sip_returns(sip_cashflows, nav_data)
161
+ print(f"Invested: ₹{returns['total_invested']:,.0f} → Current: ₹{returns['current_value']:,.0f}")
162
+ print(f"Returns: {returns['return_percentage']:.2f}% (Annualized: {returns['annualized_return']:.2f}%)")
163
+
164
+ # Example 2: Irregular investments (lump sum + step-up SIP)
165
+ irregular_investments = pd.DataFrame({
166
+ 'amount': [50000, 5000, 7500, 10000, 15000]
167
+ }, index=pd.to_datetime([
168
+ '2020-01-15', # Initial lump sum
169
+ '2020-06-01', # ₹5k after 6 months
170
+ '2021-01-01', # Stepped up to ₹7.5k
171
+ '2021-06-01', # Stepped up to ₹10k
172
+ '2022-01-01' # Stepped up to ₹15k
173
+ ]))
174
+
175
+ irregular_returns = calculate_sip_returns(irregular_investments, nav_data)
176
+ print(f"Irregular Returns: {irregular_returns['return_percentage']:.2f}%")
177
+ ```
178
+
179
+ ### Complete Example
180
+
181
+ ```python
182
+ from cagrx.amfi import Amfi
183
+ from cagrx.return_metrics import cagr, calculate_trailing_cagr, calculate_rolling_returns
184
+ import pandas as pd
185
+
186
+ # Initialize
187
+ amfi = Amfi()
188
+
189
+ # Browse HDFC schemes
190
+ hdfc_schemes = amfi.get_schemes_by_fund_house("HDFC Mutual Fund")
191
+ print("Available HDFC Schemes:")
192
+ print(hdfc_schemes.head())
193
+
194
+ # Download NAV data for HDFC Flexi Cap Fund
195
+ nav_data = amfi.get_nav_history(
196
+ scheme_id="122639",
197
+ start_date="2018-01-01",
198
+ end_date="2024-12-31"
199
+ )
200
+
201
+ # Save to CSV for future use
202
+ nav_data.to_csv("hdfc_flexi_cap_nav.csv")
203
+
204
+ # Calculate performance metrics
205
+ print("\n=== Performance Metrics ===")
206
+
207
+ # Overall CAGR
208
+ overall_return = cagr(nav_data)
209
+ print(f"\nOverall CAGR: {overall_return * 100:.2f}%")
210
+
211
+ # Trailing returns
212
+ trailing = calculate_trailing_cagr(nav_data, periods=[1, 3, 5])
213
+ print("\nTrailing Returns:")
214
+ for period, value in trailing.items():
215
+ if value:
216
+ print(f" {period}: {value * 100:.2f}%")
217
+ else:
218
+ print(f" {period}: Insufficient data")
219
+
220
+ # Rolling returns
221
+ rolling = calculate_rolling_returns(nav_data, period=pd.DateOffset(years=1))
222
+ print(f"\n1-Year Rolling Returns:")
223
+ print(f" Max: {rolling['max_returns'] * 100:.2f}%")
224
+ print(f" Min: {rolling['min_returns'] * 100:.2f}%")
225
+ print(f" Avg: {rolling['avg_return'] * 100:.2f}%")
226
+ ```
227
+
228
+ ## 📊 Data Sources
229
+
230
+ All data is sourced from official AMFI (Association of Mutual Funds in India) APIs:
231
+ - **Schemes List**: https://www.amfiindia.com/spages/NAVAll.txt
232
+ - **Historical NAV**: https://www.amfiindia.com/api/nav-history
233
+
234
+ ## 🏗️ Project Structure
235
+
236
+ ```
237
+ cagrx/
238
+ ├── src/
239
+ │ └── cagrx/
240
+ │ ├── __init__.py # Main package entry point
241
+ │ ├── amfi.py # AMFI data fetching and management
242
+ │ ├── return_metrics.py # Performance calculation utilities
243
+ │ └── utils.py # Helper functions
244
+ ├── pyproject.toml # Project configuration
245
+ └── README.md # This file
246
+ ```
247
+
248
+ ## 🧪 Development
249
+
250
+ ### Running Tests
251
+
252
+ ```bash
253
+ # Run the test file
254
+ python src/cagrx/amfi_tests.py
255
+ ```
256
+
257
+ ## 🤝 Contributing
258
+
259
+ Contributions are welcome! Please feel free to submit a Pull Request.
260
+
261
+ 1. Fork the project
262
+ 2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
263
+ 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
264
+ 4. Push to the branch (`git push origin feature/AmazingFeature`)
265
+ 5. Open a Pull Request
266
+
267
+ ## 📝 License
268
+
269
+ This project is licensed under the MIT License - see the LICENSE file for details.
270
+
271
+ ## 🙏 Acknowledgments
272
+
273
+ - Data provided by AMFI (Association of Mutual Funds in India)
274
+ - Built with Python, pandas, and requests
275
+
276
+ ## 📮 Contact
277
+
278
+ For questions or feedback, please open an issue on GitHub.
279
+
280
+ ---
281
+
282
+ **Note**: This library is for educational and research purposes. Please verify all calculations independently before making investment decisions.
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "cagrx"
3
+ version = "0.1.0"
4
+ description = "A Python library to calculate return metrics like CAGR, XIRR, and SIP returns using AMFI data"
5
+ readme = "README.md"
6
+ requires-python = ">=3.9"
7
+ license = {text = "MIT"}
8
+ authors = [
9
+ {name = "Your Name", email = "your.email@example.com"},
10
+ ]
11
+ classifiers = [
12
+ "Programming Language :: Python :: 3",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Operating System :: OS Independent",
15
+ ]
16
+ dependencies = [
17
+ "pandas",
18
+ "requests",
19
+ ]
20
+
21
+ [project.urls]
22
+ Homepage = "https://github.com/yourusername/cagrx"
23
+ Repository = "https://github.com/yourusername/cagrx"
24
+ Issues = "https://github.com/yourusername/cagrx/issues"
25
+
26
+ [project.scripts]
27
+ cagrx = "cagrx:main"
28
+
29
+ [build-system]
30
+ requires = ["hatchling"]
31
+ build-backend = "hatchling.build"
@@ -0,0 +1,20 @@
1
+ from cagrx.amfi import Amfi
2
+ from cagrx.return_metrics import (
3
+ cagr,
4
+ calculate_trailing_cagr,
5
+ calculate_rolling_returns,
6
+ calculate_sip_returns,
7
+ xirr
8
+ )
9
+
10
+ __all__ = [
11
+ "Amfi",
12
+ "cagr",
13
+ "calculate_trailing_cagr",
14
+ "calculate_rolling_returns",
15
+ "calculate_sip_returns",
16
+ "xirr",
17
+ ]
18
+
19
+ def main() -> None:
20
+ print("Hello from cagrx!")
@@ -0,0 +1,157 @@
1
+ import requests
2
+ import csv, os
3
+ import pandas as pd
4
+ from functools import cache
5
+
6
+ from cagrx.utils import split_into_date_pairs
7
+
8
+ SCHEMES_URL = "https://www.amfiindia.com/spages/NAVAll.txt"
9
+ NAV_HISTORY_URL = "https://www.amfiindia.com/api/nav-history"
10
+
11
+ class Amfi:
12
+
13
+ def __init__(self):
14
+ self.cache_file = "amfi_navall.csv"
15
+ self.schemes_list = self._load_schemes()
16
+
17
+
18
+ def list_all_schemes(self):
19
+ """
20
+ Get all schemes from the AMFI list
21
+
22
+ :returns: pandas dataframe containing all schemes
23
+ """
24
+ return self.schemes_list
25
+
26
+ def get_fund_houses(self):
27
+ """
28
+ Get all the available fund houses from the AMFI list
29
+
30
+ :returns: set of fund house names
31
+ """
32
+ return set(self.schemes_list["fund_house"].dropna().unique())
33
+
34
+ def refresh_schemes(self) -> pd.DataFrame:
35
+ """
36
+ Force refresh schemes list from AMFI and update cache.
37
+ """
38
+ self.schemes_list = self._get_schemes_from_amfi()
39
+ self.schemes_list.to_csv(self.cache_file, index=False)
40
+ return self.schemes_list
41
+
42
+ def get_schemes_by_fund_house(self, fund_house):
43
+ """
44
+ Get available schemes for a given fund house
45
+
46
+ :param fund_house: name of the fund house
47
+ :returns: pandas dataframe containing scheme codes and names
48
+ """
49
+ return self.schemes_list[self.schemes_list["fund_house"] == fund_house][['scheme_code', 'scheme_name']]
50
+
51
+ def get_nav_history(self, scheme_id, start_date, end_date):
52
+ """
53
+ Download NAV data for a given mutual fund scheme within the date range
54
+
55
+ This method fetches the data in chunks of 5 years
56
+
57
+ :param start_date: start_date of the requested data period
58
+ :param end_date: end_date of the requested data period
59
+ :param scheme_id: scheme_id of the mutual fund for which the NAV should be fetched
60
+ :param freq: frequency of the data
61
+
62
+ :returns: pandas dataframe containing nav data
63
+ """
64
+
65
+ # AMFI allows maximum of five_years to be downloaded at a time
66
+ date_ranges = split_into_date_pairs(start_date, end_date, n_days=365 * 5)
67
+ nav_records = []
68
+
69
+ for from_date, to_date in date_ranges:
70
+ records = self._fetch_historical_nav(scheme_id, from_date, to_date)
71
+ nav_records.extend(records)
72
+
73
+ return self._create_dataframe(nav_records)
74
+
75
+ def _get_schemes_from_amfi(self):
76
+ """
77
+ Get all schemes from the AMFI list
78
+
79
+ :returns: pandas dataframe containing all schemes
80
+ """
81
+ # Fetch data
82
+ raw_lines = self._fetch_raw_nav_lines()
83
+
84
+ current_fund_house = None
85
+ lines = []
86
+
87
+ for line in raw_lines[1:]: #skip first line containing headers
88
+ line = line.strip()
89
+
90
+ if line.endswith("Mutual Fund"):
91
+ current_fund_house = line #if the current line is fund house name, store it and skip the iteration
92
+ continue
93
+
94
+ row = line.split(";")
95
+ if len(row) < 5:
96
+ continue #skip invalid or header lines
97
+
98
+ row.append(current_fund_house)
99
+ lines.append(row)
100
+
101
+ columns = ["scheme_code", "isin_growth", "isin_reinv", "scheme_name", "nav", "date", "fund_house"]
102
+
103
+ return pd.DataFrame(lines, columns=columns)
104
+
105
+ def _fetch_historical_nav(self, scheme_id, from_date, to_date):
106
+ """
107
+ Actual method implementing the network/API call to the AMFI URL
108
+
109
+ :param scheme_id: scheme_id of the mutual fund for which the NAV should be fetched
110
+ :param from_date: start_date of the requested data period
111
+ :param to_date: end_date of the requested data period
112
+ """
113
+
114
+ query_params = {
115
+ "query_type": "historical_period",
116
+ "sd_id": scheme_id,
117
+ "from_date": from_date,
118
+ "to_date": to_date,
119
+ }
120
+ resp = requests.get(
121
+ NAV_HISTORY_URL, params=query_params
122
+ )
123
+
124
+ if resp.status_code == 200:
125
+ raw_json = resp.json()
126
+ if "data" in raw_json:
127
+ return raw_json["data"]["nav_groups"][0]["historical_records"]
128
+ return [] #if no data found for the given date range
129
+ else:
130
+ raise ValueError(resp.text)
131
+
132
+ def _load_schemes(self):
133
+ """
134
+ Load schemes list from cache if available, otherwise sync from AMFI
135
+ """
136
+ if os.path.exists(self.cache_file):
137
+ return pd.read_csv(self.cache_file)
138
+
139
+ return self.refresh_schemes()
140
+
141
+ def _fetch_raw_nav_lines(self):
142
+ response = requests.get(SCHEMES_URL)
143
+ response.raise_for_status()
144
+
145
+ return response.text.strip().splitlines()
146
+
147
+ def _create_dataframe(self, records):
148
+ """creates pandas dataframe from the raw records"""
149
+
150
+ df = pd.DataFrame(records)
151
+ df['nav'] = pd.to_numeric(df['nav'])
152
+ df["date"] = pd.to_datetime(df["date"])
153
+ return df.set_index("date")
154
+
155
+
156
+
157
+
@@ -0,0 +1,238 @@
1
+ import pandas as pd
2
+
3
+
4
+ def cagr(df, column="nav"):
5
+ """
6
+ Calculate the Compound Annual Growth Rate (CAGR) for the given column over the DataFrame's period.
7
+
8
+ :param df: DataFrame with datetime index
9
+ :param column: Column name to calculate CAGR on (default: 'nav')
10
+ :returns CAGR as a float (e.g., 0.12 for 12% annualized growth)
11
+
12
+ note: This method doesn't sort the index of the DataFrame. It is expected that the index is sorted in ascending order.
13
+ """
14
+ start_value = df[column].iloc[0]
15
+ end_value = df[column].iloc[-1]
16
+
17
+ num_years = (df.index[-1] - df.index[0]).days / 365
18
+
19
+ if start_value <= 0 or num_years <= 0:
20
+ raise ValueError("Invalid data for CAGR calculation.")
21
+
22
+ cagr = (end_value / start_value) ** (1 / num_years) - 1
23
+ return round(cagr, 3)
24
+
25
+
26
+ def calculate_trailing_cagr(df, column="nav", periods=None):
27
+ """
28
+ Calculate the Compound Annual Growth Rate (CAGR) of a fund over the given periods.
29
+
30
+ :param df: DataFrame with datetime index
31
+ :param column: Column name to calculate CAGR on (default: 'nav')
32
+ :returns CAGR as a float (e.g., 0.12 for 12% annualized growth)
33
+
34
+ note: -1 period is used to calculate CAGR of MAX period (considering all the data available)
35
+ """
36
+ if column not in df.columns:
37
+ raise ValueError(f"Column '{column}' not found in DataFrame")
38
+
39
+ df = df.sort_index()
40
+ cagr_metrics = {}
41
+
42
+ for period in periods:
43
+ if period == -1:
44
+ cagr_metrics[f'Max_CAGR'] = cagr(df, column=column)
45
+ continue
46
+
47
+ start_date = df.index[-1] - pd.DateOffset(years=period)
48
+
49
+ # Not enough historical data for this period
50
+ if start_date < df.index[0]:
51
+ cagr_metrics[f'{period}Y_CAGR'] = None
52
+ continue
53
+
54
+ #calculate cagr for the `n` period
55
+ cagr_metrics[f'{period}Y_CAGR'] = cagr(df.loc[start_date:], column=column)
56
+
57
+ return cagr_metrics
58
+
59
+ def calculate_rolling_returns(df, column="nav", period=pd.DateOffset(years=1)):
60
+ """
61
+ Calculate the rolling returns for the given column over the DataFrame's period.
62
+
63
+ :param df: DataFrame with datetime index
64
+ :param column: Column name to calculate rolling returns on (default: 'nav')
65
+ :param period: Period to calculate rolling returns for (default: 1 year)
66
+ :returns Rolling returns as a float (e.g., 0.12 for 12% annualized growth)
67
+ """
68
+ df = df.copy()[[column]]
69
+ dates_in_df = df.index.date
70
+ df['past_date'] = dates_in_df - period
71
+
72
+ rolling_df = pd.merge_asof(df, df[['nav']], left_on='past_date', right_on='date', suffixes=('_current', '_past'))
73
+ rolling_df['past_date'] = rolling_df['past_date'].dt.date
74
+ rolling_df.index = dates_in_df
75
+
76
+ #drop rows where historical data for past dates are not available
77
+ rolling_df = rolling_df.dropna(subset=['nav_past'])
78
+ rolling_df['returns'] = ((rolling_df['nav_current'] - rolling_df['nav_past']) / rolling_df['nav_past']).round(3)
79
+
80
+ max_row = rolling_df.loc[rolling_df['returns'].idxmax()]
81
+ min_row = rolling_df.loc[rolling_df['returns'].idxmin()]
82
+ metrics = {
83
+ 'max_returns': float(max_row['returns']),
84
+ 'max_return_period': (str(max_row['past_date']), str(max_row.name)),
85
+ 'min_returns': float(min_row['returns']),
86
+ 'min_return_period': (str(min_row['past_date']), str(min_row.name)),
87
+ 'avg_return': float(rolling_df['returns'].mean().round(3))
88
+ }
89
+
90
+ return metrics
91
+
92
+ def xirr(cashflows, dates, guess=0.1, max_iterations=100, tolerance=1e-6):
93
+ """
94
+ Calculate XIRR (Extended Internal Rate of Return) for irregular cash flows.
95
+
96
+ Uses the Newton-Raphson method to find the rate that makes NPV = 0.
97
+
98
+ :param cashflows: List or array of cash flows (negative for investments, positive for returns)
99
+ :param dates: List or array of dates corresponding to each cash flow (datetime objects)
100
+ :param guess: Initial guess for the rate (default: 0.1 or 10%)
101
+ :param max_iterations: Maximum number of iterations (default: 100)
102
+ :param tolerance: Convergence tolerance (default: 1e-6)
103
+ :returns: XIRR as a decimal (e.g., 0.15 for 15% annual return)
104
+
105
+ Example:
106
+ cashflows = [-5000, -5000, -5000, 17500]
107
+ dates = pd.to_datetime(['2020-01-01', '2020-07-01', '2021-01-01', '2021-12-31'])
108
+ rate = xirr(cashflows, dates)
109
+ print(f"XIRR: {rate * 100:.2f}%")
110
+ """
111
+ if len(cashflows) != len(dates):
112
+ raise ValueError("cashflows and dates must have the same length")
113
+
114
+ if len(cashflows) < 2:
115
+ raise ValueError("At least 2 cash flows are required")
116
+
117
+ # Convert dates to pandas datetime if not already
118
+ dates = pd.to_datetime(dates)
119
+
120
+ # Sort by dates
121
+ sorted_indices = dates.argsort()
122
+ cashflows = [cashflows[i] for i in sorted_indices]
123
+ dates = dates[sorted_indices]
124
+
125
+ # Calculate days from first date
126
+ start_date = dates[0]
127
+ days = [(date - start_date).days for date in dates]
128
+
129
+ # Newton-Raphson method
130
+ rate = guess
131
+
132
+ for iteration in range(max_iterations):
133
+ # Calculate NPV (Net Present Value)
134
+ npv = sum(cf / ((1 + rate) ** (day / 365.0)) for cf, day in zip(cashflows, days))
135
+
136
+ # Calculate derivative of NPV
137
+ dnpv = sum(-cf * day / 365.0 / ((1 + rate) ** (day / 365.0 + 1)) for cf, day in zip(cashflows, days))
138
+
139
+ # Check for convergence
140
+ if abs(npv) < tolerance:
141
+ return round(rate, 6)
142
+
143
+ # Avoid division by zero
144
+ if abs(dnpv) < 1e-10:
145
+ raise ValueError("XIRR calculation failed: derivative too small")
146
+
147
+ # Update rate using Newton-Raphson formula
148
+ rate = rate - npv / dnpv
149
+
150
+ # Check if rate is becoming unreasonable
151
+ if rate < -0.99 or rate > 10: # -99% to 1000% range
152
+ raise ValueError("XIRR calculation failed: rate out of reasonable bounds")
153
+
154
+ raise ValueError(f"XIRR did not converge after {max_iterations} iterations")
155
+
156
+ def calculate_sip_returns(sip_cashflows, nav_df, column="nav"):
157
+ """
158
+ Calculate total returns using SIP (Systematic Investment Plan) cashflow.
159
+
160
+ :param sip_cashflows: DataFrame with datetime index and 'amount' column representing investment amounts
161
+ :param nav_df: DataFrame with datetime index and NAV column
162
+ :param column: Column name for NAV values (default: 'nav')
163
+ :returns: Dictionary with total invested, current value, absolute returns, and return percentage
164
+
165
+ Example:
166
+ sip_cashflows = pd.DataFrame({
167
+ 'amount': [5000, 5000, 5000]
168
+ }, index=pd.to_datetime(['2024-01-01', '2024-02-01', '2024-03-01']))
169
+
170
+ nav_df = pd.DataFrame({
171
+ 'nav': [100, 105, 110, 115]
172
+ }, index=pd.to_datetime(['2024-01-01', '2024-02-01', '2024-03-01', '2024-04-01']))
173
+
174
+ returns = calculate_sip_returns(sip_cashflows, nav_df)
175
+ """
176
+ if 'amount' not in sip_cashflows.columns:
177
+ raise ValueError("sip_cashflows must have 'amount' column")
178
+ if column not in nav_df.columns:
179
+ raise ValueError(f"Column '{column}' not found in nav_df")
180
+
181
+ # Sort both dataframes by index
182
+ sip_cashflows = sip_cashflows.sort_index()
183
+ nav_df = nav_df.sort_index()
184
+
185
+ # Merge SIP cashflows with NAV data to get NAV at each investment date
186
+ # direction is set to `forward` because we want the NAV that's available on the
187
+ # same day or after the investment date
188
+ # eg: If investment is made on a holiday, we want the NAV that's available on the next trading day (not the previous trading day)
189
+ merged = pd.merge_asof(
190
+ sip_cashflows,
191
+ nav_df[[column]],
192
+ left_index=True,
193
+ right_index=True,
194
+ direction='forward'
195
+ )
196
+
197
+ print(merged)
198
+ # Calculate units purchased at each SIP date
199
+ merged['units'] = merged['amount'] / merged[column]
200
+
201
+ # Calculate total invested and total units
202
+ total_invested = merged['amount'].sum()
203
+ total_units = merged['units'].sum()
204
+
205
+ # Get current NAV (last available NAV)
206
+ current_nav = nav_df[column].iloc[-1]
207
+
208
+ # Calculate current value
209
+ current_value = total_units * current_nav
210
+
211
+ # Calculate absolute returns
212
+ absolute_returns = current_value - total_invested
213
+ return_percentage = (absolute_returns / total_invested) * 100 if total_invested > 0 else 0
214
+
215
+ # Calculate annualized returns (XIRR approximation using simple CAGR)
216
+ if len(sip_cashflows) > 1:
217
+ # Time period from first investment to last NAV date
218
+ days = (nav_df.index[-1] - sip_cashflows.index[0]).days
219
+ years = days / 365.25
220
+
221
+ if years > 0 and total_invested > 0:
222
+ # Simple annualized return approximation
223
+ annualized_return = ((current_value / total_invested) ** (1 / years) - 1) * 100
224
+ else:
225
+ annualized_return = 0
226
+ else:
227
+ annualized_return = 0
228
+
229
+ return {
230
+ 'total_invested': round(total_invested, 2),
231
+ 'current_value': round(current_value, 2),
232
+ 'absolute_returns': round(absolute_returns, 2),
233
+ 'return_percentage': round(return_percentage, 2),
234
+ 'annualized_return': round(annualized_return, 2),
235
+ 'total_units': round(total_units, 4),
236
+ 'current_nav': round(current_nav, 2),
237
+ 'investment_period_days': (nav_df.index[-1] - sip_cashflows.index[0]).days if len(sip_cashflows) > 0 else 0
238
+ }
@@ -0,0 +1,22 @@
1
+ import time
2
+ from datetime import datetime, timedelta
3
+
4
+ def split_into_date_pairs(start_date_str, end_date_str, n_days=100):
5
+ """
6
+ Splits the range between start_date and end_date into `n_days=n` intervals
7
+ Returns a list of (start, end) date pairs.
8
+ """
9
+ start_date = datetime.strptime(start_date_str, "%Y-%m-%d")
10
+ end_date = datetime.strptime(end_date_str, "%Y-%m-%d")
11
+
12
+ pairs = []
13
+ current_start = start_date
14
+
15
+ while current_start < end_date:
16
+ current_end = min(current_start + timedelta(days=n_days - 1), end_date)
17
+ pairs.append(
18
+ (current_start.strftime("%Y-%m-%d"), current_end.strftime("%Y-%m-%d"))
19
+ )
20
+ current_start = current_end + timedelta(days=1)
21
+
22
+ return pairs
@@ -0,0 +1,95 @@
1
+
2
+ import unittest
3
+ import pandas as pd
4
+ from datetime import datetime
5
+ from cagrx.return_metrics import cagr, calculate_trailing_cagr, calculate_rolling_returns, xirr, calculate_sip_returns
6
+ from cagrx.amfi import Amfi
7
+ import os
8
+
9
+ class TestReturnMetrics(unittest.TestCase):
10
+
11
+ def setUp(self):
12
+ # Setup common data
13
+ self.dates = pd.date_range(start='2020-01-01', end='2023-01-01', freq='D')
14
+ self.nav_values = [100 * (1.00032)**i for i in range(len(self.dates))] # Approx 12% annual growth
15
+ self.df = pd.DataFrame({'nav': self.nav_values}, index=self.dates)
16
+
17
+ def test_cagr_calculation(self):
18
+ # Test basic CAGR
19
+ result = cagr(self.df)
20
+ self.assertIsInstance(result, float)
21
+ self.assertAlmostEqual(result, 0.123, places=2) # Expect approx 12-13%
22
+
23
+ def test_trailing_cagr(self):
24
+ # Test trailing CAGR including Max (-1)
25
+ periods = [1, -1]
26
+ result = calculate_trailing_cagr(self.df, periods=periods)
27
+
28
+ self.assertIn('1Y_CAGR', result)
29
+ self.assertIn('Max_CAGR', result)
30
+ self.assertIsNotNone(result['1Y_CAGR'])
31
+ self.assertIsNotNone(result['Max_CAGR'])
32
+
33
+ # Max CAGR should equal total period cagr
34
+ self.assertEqual(result['Max_CAGR'], cagr(self.df))
35
+
36
+ def test_xirr(self):
37
+ # Test cases from original test_xirr.py
38
+
39
+ # Test 1: Regular investments
40
+ cashflows = [-5000, -5000, -5000, 17500]
41
+ dates = pd.to_datetime(['2020-01-01', '2020-07-01', '2021-01-01', '2021-12-31'])
42
+ rate = xirr(cashflows, dates)
43
+ self.assertAlmostEqual(rate, 0.11, places=1) # Approx 11%
44
+
45
+ # Test 2: Irregular investments
46
+ cashflows2 = [-10000, -5000, -7500, 25000]
47
+ dates2 = pd.to_datetime(['2020-01-15', '2020-06-01', '2021-01-01', '2022-06-30'])
48
+ rate2 = xirr(cashflows2, dates2)
49
+ self.assertIsInstance(rate2, float)
50
+
51
+ # Test 3: Loss scenario
52
+ cashflows3 = [-10000, -10000, 18000]
53
+ dates3 = pd.to_datetime(['2020-01-01', '2020-06-01', '2021-12-31'])
54
+ rate3 = xirr(cashflows3, dates3)
55
+ self.assertLess(rate3, 0) # Should be negative
56
+
57
+ def test_sip_returns(self):
58
+ # Simple SIP test
59
+ sip_dates = pd.date_range(start='2020-01-01', periods=12, freq='MS')
60
+ sip_cashflows = pd.DataFrame({'amount': 1000}, index=sip_dates)
61
+
62
+ result = calculate_sip_returns(sip_cashflows, self.df)
63
+
64
+ self.assertIn('total_invested', result)
65
+ self.assertIn('current_value', result)
66
+ self.assertIn('return_percentage', result)
67
+ self.assertEqual(result['total_invested'], 12000)
68
+
69
+ class TestAmfiIntegration(unittest.TestCase):
70
+
71
+ def setUp(self):
72
+ self.amfi = Amfi()
73
+ self.scheme_id = "122639" # Common fund for testing
74
+
75
+ def test_get_historical_nav(self):
76
+ # This test hits the network
77
+ try:
78
+ nav = self.amfi.get_historical_nav(self.scheme_id, "2023-01-01", "2023-01-10")
79
+ self.assertIsInstance(nav, pd.DataFrame)
80
+ self.assertFalse(nav.empty)
81
+ self.assertIn('nav', nav.columns)
82
+ self.assertEqual(len(nav), len(nav.dropna()))
83
+ except Exception as e:
84
+ self.skipTest(f"Network or API issue: {e}")
85
+
86
+ def test_refresh_schemes(self):
87
+ try:
88
+ schemes = self.amfi.refresh_schemes()
89
+ self.assertIsInstance(schemes, pd.DataFrame)
90
+ self.assertFalse(schemes.empty)
91
+ except Exception as e:
92
+ self.skipTest(f"Network or API issue: {e}")
93
+
94
+ if __name__ == '__main__':
95
+ unittest.main()