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.
- cagrx-0.1.0/.agent/workflows/publish_to_pypi.md +27 -0
- cagrx-0.1.0/.gitignore +36 -0
- cagrx-0.1.0/.python-version +1 -0
- cagrx-0.1.0/LICENSE +21 -0
- cagrx-0.1.0/PKG-INFO +300 -0
- cagrx-0.1.0/README.md +282 -0
- cagrx-0.1.0/pyproject.toml +31 -0
- cagrx-0.1.0/src/cagrx/__init__.py +20 -0
- cagrx-0.1.0/src/cagrx/amfi.py +157 -0
- cagrx-0.1.0/src/cagrx/return_metrics.py +238 -0
- cagrx-0.1.0/src/cagrx/utils.py +22 -0
- cagrx-0.1.0/tests/test_cagrx.py +95 -0
|
@@ -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
|
+
 
|
|
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
|
+
 
|
|
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()
|