mftool 3.4__py3-none-any.whl
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.
- mftool/__init__.py +26 -0
- mftool/cache.py +233 -0
- mftool/const.json +20065 -0
- mftool/mftool.py +935 -0
- mftool/utils.py +56 -0
- mftool-3.4.dist-info/METADATA +69 -0
- mftool-3.4.dist-info/RECORD +10 -0
- mftool-3.4.dist-info/WHEEL +5 -0
- mftool-3.4.dist-info/licenses/LICENSE +22 -0
- mftool-3.4.dist-info/top_level.txt +1 -0
mftool/mftool.py
ADDED
|
@@ -0,0 +1,935 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MIT License (MIT)
|
|
3
|
+
Copyright (c) 2026 Sujit Nayakwadi
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
14
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
15
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
16
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
17
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
18
|
+
SOFTWARE.
|
|
19
|
+
"""
|
|
20
|
+
# -*- coding: UTF-8 -*-
|
|
21
|
+
import requests
|
|
22
|
+
import httpx
|
|
23
|
+
from bs4 import BeautifulSoup
|
|
24
|
+
import yfinance as yf
|
|
25
|
+
import datetime
|
|
26
|
+
from deprecated import deprecated
|
|
27
|
+
from matplotlib import pyplot as plt
|
|
28
|
+
from .utils import Utilities, is_holiday, get_today, get_friday, render_response, get_52_week_high_low
|
|
29
|
+
from .cache import CacheManager
|
|
30
|
+
import pandas as pd
|
|
31
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
32
|
+
from typing import List, Dict, Union
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class Mftool:
|
|
36
|
+
"""
|
|
37
|
+
class which implements all the functionality for
|
|
38
|
+
Mutual Funds in India
|
|
39
|
+
"""
|
|
40
|
+
def __init__(self):
|
|
41
|
+
self._session = requests.session()
|
|
42
|
+
self._const = Utilities().values
|
|
43
|
+
# URL list
|
|
44
|
+
self._get_quote_url = self._const['get_quote_url']
|
|
45
|
+
self._get_scheme_url = self._const['get_scheme_url']
|
|
46
|
+
self._get_amc_details_url = self._const['get_amc_details_url']
|
|
47
|
+
self._get_open_ended_equity_scheme_url = self._const['get_open_ended_equity_scheme_url']
|
|
48
|
+
self._get_avg_aum = self._const['get_avg_aum_url']
|
|
49
|
+
self._open_ended_equity_category = self._const['open_ended_equity_category']
|
|
50
|
+
self._open_ended_debt_category = self._const['open_ended_debt_category']
|
|
51
|
+
self._open_ended_hybrid_category = self._const['open_ended_hybrid_category']
|
|
52
|
+
self._open_ended_solution_category = self._const['open_ended_solution_category']
|
|
53
|
+
self._open_ended_other_category = self._const['open_ended_other_category']
|
|
54
|
+
self._amc=self._const['amc']
|
|
55
|
+
self._user_agent = self._const['user_agent']
|
|
56
|
+
self._codes = self._const['codes']
|
|
57
|
+
|
|
58
|
+
# Initialize caching layer
|
|
59
|
+
# NAV data: 24 hours TTL (86400 seconds) - updates once daily
|
|
60
|
+
self._cache = CacheManager(default_ttl=86400)
|
|
61
|
+
# Scheme codes: 7 days TTL - rarely changes
|
|
62
|
+
self._scheme_codes_cache = CacheManager(default_ttl=604800)
|
|
63
|
+
|
|
64
|
+
self._scheme_codes = self.get_scheme_codes().keys()
|
|
65
|
+
|
|
66
|
+
def set_proxy(self, proxy):
|
|
67
|
+
"""
|
|
68
|
+
This is optional method to work with proxy server before getting any data.
|
|
69
|
+
:param proxy: provide dictionary for proxies setup as
|
|
70
|
+
proxy = { 'http': 'http://user:pass@10.10.1.0:1080',
|
|
71
|
+
'https': 'http://user:pass@10.10.1.0:1090'}
|
|
72
|
+
:return: None
|
|
73
|
+
"""
|
|
74
|
+
self._session.proxies = proxy
|
|
75
|
+
|
|
76
|
+
def get_scheme_codes(self, as_json=False):
|
|
77
|
+
"""
|
|
78
|
+
returns a dictionary with key as scheme code and value as scheme name.
|
|
79
|
+
cache handled internally
|
|
80
|
+
:return: dict / json
|
|
81
|
+
"""
|
|
82
|
+
# Try to get from cache first
|
|
83
|
+
cache_key = f"scheme_codes:{as_json}"
|
|
84
|
+
cached_result = self._scheme_codes_cache.get(cache_key)
|
|
85
|
+
if cached_result is not None:
|
|
86
|
+
return cached_result
|
|
87
|
+
|
|
88
|
+
scheme_info = {}
|
|
89
|
+
url = self._get_quote_url
|
|
90
|
+
response = self._session.get(url)
|
|
91
|
+
data = response.text.split("\n")
|
|
92
|
+
for scheme_data in data:
|
|
93
|
+
if ";" in scheme_data:
|
|
94
|
+
scheme = scheme_data.split(";")
|
|
95
|
+
scheme_info[scheme[0]] = f"{scheme[3]} - {scheme[5]}" if len(scheme) > 5 else scheme[3]
|
|
96
|
+
|
|
97
|
+
result = render_response(scheme_info, as_json)
|
|
98
|
+
# Cache the result
|
|
99
|
+
self._scheme_codes_cache.set(cache_key, result)
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
def get_available_schemes(self, amc_name):
|
|
103
|
+
"""
|
|
104
|
+
returns a dictionary with key as scheme code and value as scheme name for given amc.
|
|
105
|
+
:param amc_name: a string name of amc eg- Axis, ICICI, Reliance
|
|
106
|
+
:return: dict / json
|
|
107
|
+
"""
|
|
108
|
+
all_schemes = self.get_scheme_codes(as_json=False)
|
|
109
|
+
return {k: v for (k, v) in all_schemes.items() if amc_name.lower() in v.lower()}
|
|
110
|
+
|
|
111
|
+
def is_valid_code(self, code):
|
|
112
|
+
"""
|
|
113
|
+
check whether a given scheme code is correct or NOT
|
|
114
|
+
:param code: a string scheme code
|
|
115
|
+
:return: Boolean
|
|
116
|
+
"""
|
|
117
|
+
if code:
|
|
118
|
+
# Performance improvement
|
|
119
|
+
return True if code in self._scheme_codes else False
|
|
120
|
+
else:
|
|
121
|
+
return False
|
|
122
|
+
|
|
123
|
+
def is_code(self, code):
|
|
124
|
+
"""
|
|
125
|
+
check whether a New scheme code is correct or NOT, only used with mf.history()
|
|
126
|
+
:param code: a string scheme code
|
|
127
|
+
:return: Boolean
|
|
128
|
+
"""
|
|
129
|
+
if code:
|
|
130
|
+
return any(code in cd for cd in self._codes)
|
|
131
|
+
else:
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
def get_scheme_quote(self, code, as_json=False):
|
|
135
|
+
"""
|
|
136
|
+
gets the quote for a given scheme code
|
|
137
|
+
:param code: scheme code
|
|
138
|
+
:param as_json: default false
|
|
139
|
+
:return: dict or None
|
|
140
|
+
:raises: HTTPError, URLError
|
|
141
|
+
"""
|
|
142
|
+
code = str(code)
|
|
143
|
+
if self.is_valid_code(code):
|
|
144
|
+
# Try to get from cache first
|
|
145
|
+
cache_key = f"quote:{code}:{as_json}"
|
|
146
|
+
cached_result = self._cache.get(cache_key)
|
|
147
|
+
if cached_result is not None:
|
|
148
|
+
return cached_result
|
|
149
|
+
|
|
150
|
+
scheme_info = {}
|
|
151
|
+
url = self._get_quote_url
|
|
152
|
+
response = self._session.get(url)
|
|
153
|
+
data = response.text.split("\n")
|
|
154
|
+
for scheme_data in data:
|
|
155
|
+
if code in scheme_data:
|
|
156
|
+
scheme = scheme_data.split(";")
|
|
157
|
+
scheme_info['scheme_code'] = scheme[0]
|
|
158
|
+
scheme_info['scheme_name'] = scheme[3]
|
|
159
|
+
scheme_info['plan'] = scheme[4]
|
|
160
|
+
scheme_info['option'] = scheme[5]
|
|
161
|
+
scheme_info['last_updated'] = scheme[7].replace("\r", "")
|
|
162
|
+
scheme_info['nav'] = scheme[6]
|
|
163
|
+
break
|
|
164
|
+
|
|
165
|
+
result = render_response(scheme_info, as_json)
|
|
166
|
+
# Cache the result
|
|
167
|
+
self._cache.set(cache_key, result)
|
|
168
|
+
return result
|
|
169
|
+
else:
|
|
170
|
+
return None
|
|
171
|
+
|
|
172
|
+
def get_bulk_quotes(self, scheme_codes: List[Union[str, int]], as_json=False,
|
|
173
|
+
max_workers=10, show_progress=False) -> Dict[str, Union[dict, None]]:
|
|
174
|
+
"""
|
|
175
|
+
Fetch quotes for multiple schemes concurrently for better performance.
|
|
176
|
+
Perfect for portfolio-level operations.
|
|
177
|
+
|
|
178
|
+
:param scheme_codes: List of scheme codes to fetch
|
|
179
|
+
:param as_json: Return data in JSON format (default: False)
|
|
180
|
+
:param max_workers: Maximum number of concurrent threads (default: 10)
|
|
181
|
+
:param show_progress: Print progress during fetching (default: False)
|
|
182
|
+
:return: Dictionary with scheme codes as keys and quote data as values
|
|
183
|
+
:raises: HTTPError, URLError
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> mf = Mftool()
|
|
187
|
+
>>> codes = ['119597', '119062', '119061']
|
|
188
|
+
>>> quotes = mf.get_bulk_quotes(codes)
|
|
189
|
+
>>> print(quotes['119597']['nav'])
|
|
190
|
+
"""
|
|
191
|
+
# Validate all codes first
|
|
192
|
+
valid_codes = []
|
|
193
|
+
invalid_codes = []
|
|
194
|
+
|
|
195
|
+
for code in scheme_codes:
|
|
196
|
+
code = str(code)
|
|
197
|
+
if self.is_valid_code(code):
|
|
198
|
+
valid_codes.append(code)
|
|
199
|
+
else:
|
|
200
|
+
invalid_codes.append(code)
|
|
201
|
+
|
|
202
|
+
if invalid_codes and show_progress:
|
|
203
|
+
print(f"Warning: {len(invalid_codes)} invalid scheme codes found: {invalid_codes[:5]}")
|
|
204
|
+
|
|
205
|
+
results = {}
|
|
206
|
+
|
|
207
|
+
# Check cache first for all codes
|
|
208
|
+
uncached_codes = []
|
|
209
|
+
for code in valid_codes:
|
|
210
|
+
cache_key = f"quote:{code}:{as_json}"
|
|
211
|
+
cached_result = self._cache.get(cache_key)
|
|
212
|
+
if cached_result is not None:
|
|
213
|
+
results[code] = cached_result
|
|
214
|
+
else:
|
|
215
|
+
uncached_codes.append(code)
|
|
216
|
+
|
|
217
|
+
if show_progress:
|
|
218
|
+
print(f"Fetching {len(uncached_codes)} quotes ({len(results)} from cache)...")
|
|
219
|
+
|
|
220
|
+
# Fetch uncached quotes concurrently
|
|
221
|
+
if uncached_codes:
|
|
222
|
+
def fetch_single_quote(code):
|
|
223
|
+
try:
|
|
224
|
+
quote = self.get_scheme_quote(code, as_json=as_json)
|
|
225
|
+
return code, quote
|
|
226
|
+
except Exception as e:
|
|
227
|
+
if show_progress:
|
|
228
|
+
print(f"Error fetching {code}: {str(e)[:50]}")
|
|
229
|
+
return code, None
|
|
230
|
+
|
|
231
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
232
|
+
futures = {executor.submit(fetch_single_quote, code): code for code in uncached_codes}
|
|
233
|
+
|
|
234
|
+
completed = 0
|
|
235
|
+
for future in as_completed(futures):
|
|
236
|
+
code, quote = future.result()
|
|
237
|
+
results[code] = quote
|
|
238
|
+
completed += 1
|
|
239
|
+
|
|
240
|
+
if show_progress and completed % 10 == 0:
|
|
241
|
+
print(f"Progress: {completed}/{len(uncached_codes)} completed")
|
|
242
|
+
|
|
243
|
+
if show_progress:
|
|
244
|
+
print(f"✓ Fetched {len(results)} quotes successfully")
|
|
245
|
+
|
|
246
|
+
return results
|
|
247
|
+
|
|
248
|
+
def search_schemes(self, search_term: str, limit: int = 10, as_json=False) -> Union[List[Dict[str, str]], str]:
|
|
249
|
+
"""
|
|
250
|
+
Search for mutual fund schemes by name using fuzzy matching.
|
|
251
|
+
Makes it easy to find schemes without knowing exact codes.
|
|
252
|
+
|
|
253
|
+
:param search_term: Name or partial name to search for (case-insensitive)
|
|
254
|
+
:param limit: Maximum number of results to return (default: 10, use 0 for all)
|
|
255
|
+
:param as_json: Return data in JSON format (default: False)
|
|
256
|
+
:return: List of matching schemes with code and name
|
|
257
|
+
|
|
258
|
+
Example:
|
|
259
|
+
>>> mf = Mftool()
|
|
260
|
+
>>> results = mf.search_schemes("HDFC midcap")
|
|
261
|
+
>>> for scheme in results:
|
|
262
|
+
... print(f"{scheme['code']}: {scheme['name']}")
|
|
263
|
+
|
|
264
|
+
>>> # Get scheme code for first match
|
|
265
|
+
>>> matches = mf.search_schemes("Axis bluechip", limit=1)
|
|
266
|
+
>>> code = matches[0]['code'] if matches else None
|
|
267
|
+
"""
|
|
268
|
+
search_term = search_term.lower().strip()
|
|
269
|
+
|
|
270
|
+
if not search_term:
|
|
271
|
+
return render_response([], as_json)
|
|
272
|
+
|
|
273
|
+
# Get all scheme codes and names
|
|
274
|
+
all_schemes = self.get_scheme_codes(as_json=False)
|
|
275
|
+
|
|
276
|
+
# Search for matches
|
|
277
|
+
matches = []
|
|
278
|
+
for code, name in all_schemes.items():
|
|
279
|
+
name_lower = name.lower()
|
|
280
|
+
|
|
281
|
+
# Check if search term is in the scheme name
|
|
282
|
+
if search_term in name_lower:
|
|
283
|
+
# Calculate relevance score (lower is better)
|
|
284
|
+
# Exact matches get highest priority
|
|
285
|
+
if name_lower == search_term:
|
|
286
|
+
score = 0
|
|
287
|
+
# Matches at the start of the name get high priority
|
|
288
|
+
elif name_lower.startswith(search_term):
|
|
289
|
+
score = 1
|
|
290
|
+
# Matches of whole words get medium priority
|
|
291
|
+
elif f" {search_term} " in f" {name_lower} ":
|
|
292
|
+
score = 2
|
|
293
|
+
# Partial matches get lower priority
|
|
294
|
+
else:
|
|
295
|
+
score = 3
|
|
296
|
+
|
|
297
|
+
matches.append({
|
|
298
|
+
'code': code,
|
|
299
|
+
'name': name,
|
|
300
|
+
'score': score
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
# Sort by relevance (score) and then alphabetically by name
|
|
304
|
+
matches.sort(key=lambda x: (x['score'], x['name']))
|
|
305
|
+
|
|
306
|
+
# Remove score from results
|
|
307
|
+
results = [{'code': m['code'], 'name': m['name']} for m in matches]
|
|
308
|
+
|
|
309
|
+
# Apply limit if specified
|
|
310
|
+
if limit > 0:
|
|
311
|
+
results = results[:limit]
|
|
312
|
+
|
|
313
|
+
return render_response(results, as_json)
|
|
314
|
+
|
|
315
|
+
def search_schemes_by_amc(self, amc_name: str, search_term: str = "",
|
|
316
|
+
limit: int = 10, as_json=False) -> Union[List[Dict[str, str]], str]:
|
|
317
|
+
"""
|
|
318
|
+
Search for schemes within a specific AMC (fund house).
|
|
319
|
+
|
|
320
|
+
:param amc_name: Name of AMC (e.g., "HDFC", "ICICI", "Axis")
|
|
321
|
+
:param search_term: Optional search term to filter schemes within the AMC
|
|
322
|
+
:param limit: Maximum number of results to return (default: 10, use 0 for all)
|
|
323
|
+
:param as_json: Return data in JSON format (default: False)
|
|
324
|
+
:return: List of matching schemes with code and name
|
|
325
|
+
|
|
326
|
+
Example:
|
|
327
|
+
>>> mf = Mftool()
|
|
328
|
+
>>> # Get all HDFC schemes
|
|
329
|
+
>>> hdfc_schemes = mf.search_schemes_by_amc("HDFC")
|
|
330
|
+
>>>
|
|
331
|
+
>>> # Get HDFC midcap schemes
|
|
332
|
+
>>> hdfc_midcap = mf.search_schemes_by_amc("HDFC", "midcap")
|
|
333
|
+
"""
|
|
334
|
+
# Get all schemes from the AMC
|
|
335
|
+
amc_schemes = self.get_available_schemes(amc_name)
|
|
336
|
+
|
|
337
|
+
# If no search term, return AMC schemes
|
|
338
|
+
if not search_term:
|
|
339
|
+
results = [{'code': code, 'name': name} for code, name in amc_schemes.items()]
|
|
340
|
+
if limit > 0:
|
|
341
|
+
results = results[:limit]
|
|
342
|
+
return render_response(results, as_json)
|
|
343
|
+
|
|
344
|
+
# Filter by search term
|
|
345
|
+
search_term = search_term.lower().strip()
|
|
346
|
+
matches = []
|
|
347
|
+
|
|
348
|
+
for code, name in amc_schemes.items():
|
|
349
|
+
name_lower = name.lower()
|
|
350
|
+
if search_term in name_lower:
|
|
351
|
+
# Calculate relevance score
|
|
352
|
+
if name_lower == search_term:
|
|
353
|
+
score = 0
|
|
354
|
+
elif name_lower.startswith(search_term):
|
|
355
|
+
score = 1
|
|
356
|
+
elif f" {search_term} " in f" {name_lower} ":
|
|
357
|
+
score = 2
|
|
358
|
+
else:
|
|
359
|
+
score = 3
|
|
360
|
+
|
|
361
|
+
matches.append({
|
|
362
|
+
'code': code,
|
|
363
|
+
'name': name,
|
|
364
|
+
'score': score
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
# Sort by relevance
|
|
368
|
+
matches.sort(key=lambda x: (x['score'], x['name']))
|
|
369
|
+
|
|
370
|
+
# Remove score and apply limit
|
|
371
|
+
results = [{'code': m['code'], 'name': m['name']} for m in matches]
|
|
372
|
+
if limit > 0:
|
|
373
|
+
results = results[:limit]
|
|
374
|
+
|
|
375
|
+
return render_response(results, as_json)
|
|
376
|
+
|
|
377
|
+
def search_schemes_by_type(self, scheme_type: str, search_term: str = "",
|
|
378
|
+
limit: int = 10, as_json=False) -> Union[List[Dict[str, str]], str]:
|
|
379
|
+
"""
|
|
380
|
+
Search for schemes by type/category (Equity, Debt, Hybrid, etc.).
|
|
381
|
+
|
|
382
|
+
:param scheme_type: Type keywords like "equity", "debt", "hybrid", "elss", "index", "liquid"
|
|
383
|
+
:param search_term: Optional additional search term
|
|
384
|
+
:param limit: Maximum number of results (default: 10, use 0 for all)
|
|
385
|
+
:param as_json: Return data in JSON format (default: False)
|
|
386
|
+
:return: List of matching schemes with code and name
|
|
387
|
+
|
|
388
|
+
Example:
|
|
389
|
+
>>> mf = Mftool()
|
|
390
|
+
>>> # Find all ELSS schemes
|
|
391
|
+
>>> elss = mf.search_schemes_by_type("elss")
|
|
392
|
+
>>>
|
|
393
|
+
>>> # Find HDFC ELSS schemes
|
|
394
|
+
>>> hdfc_elss = mf.search_schemes_by_type("elss", "hdfc")
|
|
395
|
+
"""
|
|
396
|
+
all_schemes = self.get_scheme_codes(as_json=False)
|
|
397
|
+
scheme_type = scheme_type.lower().strip()
|
|
398
|
+
search_term = search_term.lower().strip() if search_term else ""
|
|
399
|
+
|
|
400
|
+
matches = []
|
|
401
|
+
for code, name in all_schemes.items():
|
|
402
|
+
name_lower = name.lower()
|
|
403
|
+
|
|
404
|
+
# Check if scheme type is in the name
|
|
405
|
+
if scheme_type in name_lower:
|
|
406
|
+
# If search term provided, check if it's also in the name
|
|
407
|
+
if search_term and search_term not in name_lower:
|
|
408
|
+
continue
|
|
409
|
+
|
|
410
|
+
# Calculate relevance score
|
|
411
|
+
score = 0
|
|
412
|
+
if search_term:
|
|
413
|
+
# Both type and search term match
|
|
414
|
+
if scheme_type in name_lower and search_term in name_lower:
|
|
415
|
+
score = 1
|
|
416
|
+
else:
|
|
417
|
+
# Only type matches
|
|
418
|
+
if name_lower.startswith(scheme_type):
|
|
419
|
+
score = 2
|
|
420
|
+
else:
|
|
421
|
+
score = 3
|
|
422
|
+
|
|
423
|
+
matches.append({
|
|
424
|
+
'code': code,
|
|
425
|
+
'name': name,
|
|
426
|
+
'score': score
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
# Sort by relevance
|
|
430
|
+
matches.sort(key=lambda x: (x['score'], x['name']))
|
|
431
|
+
|
|
432
|
+
# Remove score and apply limit
|
|
433
|
+
results = [{'code': m['code'], 'name': m['name']} for m in matches]
|
|
434
|
+
if limit > 0:
|
|
435
|
+
results = results[:limit]
|
|
436
|
+
|
|
437
|
+
return render_response(results, as_json)
|
|
438
|
+
|
|
439
|
+
def get_scheme_details(self, code, as_json=False):
|
|
440
|
+
"""
|
|
441
|
+
gets the scheme info for a given scheme code
|
|
442
|
+
:param code: scheme code
|
|
443
|
+
:param as_json: default false
|
|
444
|
+
:return: dict or None
|
|
445
|
+
:raises: HTTPError, URLError
|
|
446
|
+
"""
|
|
447
|
+
code = str(code)
|
|
448
|
+
if self.is_valid_code(code):
|
|
449
|
+
# Try to get from cache first
|
|
450
|
+
cache_key = f"details:{code}:{as_json}"
|
|
451
|
+
cached_result = self._cache.get(cache_key)
|
|
452
|
+
if cached_result is not None:
|
|
453
|
+
return cached_result
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
scheme_info = {}
|
|
457
|
+
url = self._get_scheme_url + code
|
|
458
|
+
response = self._session.get(url)
|
|
459
|
+
response.raise_for_status() # Raise exception for bad status codes
|
|
460
|
+
response_data = response.json()
|
|
461
|
+
|
|
462
|
+
scheme_data = response_data['meta']
|
|
463
|
+
scheme_info['fund_house'] = scheme_data['fund_house']
|
|
464
|
+
scheme_info['scheme_type'] = scheme_data['scheme_type']
|
|
465
|
+
scheme_info['scheme_category'] = scheme_data['scheme_category']
|
|
466
|
+
scheme_info['scheme_code'] = scheme_data['scheme_code']
|
|
467
|
+
scheme_info['scheme_name'] = scheme_data['scheme_name']
|
|
468
|
+
scheme_info['scheme_start_date'] = response_data['data'][int(len(response_data['data']) -1)]
|
|
469
|
+
|
|
470
|
+
result = render_response(scheme_info, as_json)
|
|
471
|
+
# Cache the result
|
|
472
|
+
self._cache.set(cache_key, result)
|
|
473
|
+
return result
|
|
474
|
+
except Exception as e:
|
|
475
|
+
# Return None on error, don't cache errors
|
|
476
|
+
return None
|
|
477
|
+
else:
|
|
478
|
+
return None
|
|
479
|
+
|
|
480
|
+
def get_scheme_historical_nav(self, code, as_json=False, as_Dataframe=False):
|
|
481
|
+
"""
|
|
482
|
+
gets the scheme historical data till last updated for a given scheme code
|
|
483
|
+
:param code: scheme-code
|
|
484
|
+
:param as_json: default false
|
|
485
|
+
:param as_Dataframe: default false
|
|
486
|
+
:return: dict or json or Dataframe or None
|
|
487
|
+
:raises: HTTPError, URLError
|
|
488
|
+
"""
|
|
489
|
+
code = str(code)
|
|
490
|
+
if self.is_valid_code(code):
|
|
491
|
+
# Try to get from cache first
|
|
492
|
+
cache_key = f"historical:{code}:{as_json}:{as_Dataframe}"
|
|
493
|
+
cached_result = self._cache.get(cache_key)
|
|
494
|
+
if cached_result is not None:
|
|
495
|
+
return cached_result
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
scheme_info = {}
|
|
499
|
+
url = self._get_scheme_url + code
|
|
500
|
+
response = self._session.get(url)
|
|
501
|
+
response.raise_for_status() # Raise exception for bad status codes
|
|
502
|
+
response_data = response.json()
|
|
503
|
+
|
|
504
|
+
scheme_data = response_data['meta']
|
|
505
|
+
scheme_info['fund_house'] = scheme_data['fund_house']
|
|
506
|
+
scheme_info['scheme_type'] = scheme_data['scheme_type']
|
|
507
|
+
scheme_info['scheme_category'] = scheme_data['scheme_category']
|
|
508
|
+
scheme_info['scheme_code'] = scheme_data['scheme_code']
|
|
509
|
+
scheme_info['scheme_name'] = scheme_data['scheme_name']
|
|
510
|
+
scheme_info['scheme_start_date'] = response_data['data'][int(len(response_data['data']) - 1)]
|
|
511
|
+
result = get_52_week_high_low(response_data['data'])
|
|
512
|
+
scheme_info['52_week_high'] = result['52_week_high']
|
|
513
|
+
scheme_info['52_week_low'] = result['52_week_low']
|
|
514
|
+
if response_data['data']:
|
|
515
|
+
scheme_info['data'] = response_data['data']
|
|
516
|
+
else:
|
|
517
|
+
scheme_info['data'] = "Underlying data not available"
|
|
518
|
+
|
|
519
|
+
final_result = render_response(scheme_info, as_json, as_Dataframe)
|
|
520
|
+
# Cache the result
|
|
521
|
+
self._cache.set(cache_key, final_result)
|
|
522
|
+
return final_result
|
|
523
|
+
except Exception as e:
|
|
524
|
+
# Return None on error, don't cache errors
|
|
525
|
+
return None
|
|
526
|
+
else:
|
|
527
|
+
return None
|
|
528
|
+
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def calculate_balance_units_value(self, code, balance_units, as_json=False):
|
|
532
|
+
"""
|
|
533
|
+
gets the market value of your balance units for a given scheme code
|
|
534
|
+
:param code: scheme code, balance_units : current balance units
|
|
535
|
+
:param balance_units: balance units
|
|
536
|
+
:param as_json: default false
|
|
537
|
+
:return: dict or None
|
|
538
|
+
"""
|
|
539
|
+
code = str(code)
|
|
540
|
+
if self.is_valid_code(code):
|
|
541
|
+
scheme_info = {}
|
|
542
|
+
scheme_info = self.get_scheme_quote(code)
|
|
543
|
+
market_value = float(balance_units)*float(scheme_info['nav'])
|
|
544
|
+
scheme_info.update(balance_units_value= "{0:.2f}".format(market_value))
|
|
545
|
+
return render_response(scheme_info, as_json)
|
|
546
|
+
else:
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
def calculate_returns(self, code, balanced_units, monthly_sip, investment_in_months, as_json=False):
|
|
550
|
+
"""
|
|
551
|
+
gets the market value of your balance units for a given scheme code
|
|
552
|
+
:param code: scheme-code,
|
|
553
|
+
:param balanced_units : current balance units
|
|
554
|
+
:param monthly_sip: monthly investment in scheme
|
|
555
|
+
:param investment_in_months: months
|
|
556
|
+
:param as_json: default false
|
|
557
|
+
:return: dict or None
|
|
558
|
+
:example: calculate_returns(119062,1718.925, 2000, 51)
|
|
559
|
+
"""
|
|
560
|
+
code = str(code)
|
|
561
|
+
if self.is_valid_code(code):
|
|
562
|
+
scheme_info = {}
|
|
563
|
+
scheme_info = self.get_scheme_quote(code)
|
|
564
|
+
initial_investment = int(investment_in_months) * float(monthly_sip)
|
|
565
|
+
years = investment_in_months / 12
|
|
566
|
+
market_value = float(float(balanced_units) * float(scheme_info['nav']))
|
|
567
|
+
total_return = market_value - initial_investment
|
|
568
|
+
absolute_return = ((market_value - initial_investment)/initial_investment) * 100
|
|
569
|
+
annualised_return = ((market_value / initial_investment) ** (1/years) - 1)*100
|
|
570
|
+
|
|
571
|
+
scheme_info.update(final_investment_value="{0:.2f}".format(market_value))
|
|
572
|
+
scheme_info.update(absolute_return="%.2f %%" %(absolute_return))
|
|
573
|
+
scheme_info.update(IRR_annualised_return="%.2f %%" %(annualised_return))
|
|
574
|
+
return render_response(scheme_info, as_json)
|
|
575
|
+
else:
|
|
576
|
+
return None
|
|
577
|
+
|
|
578
|
+
@deprecated(version='3.1',
|
|
579
|
+
reason="This function will be in deprecated from next release, use mf.history() to get data")
|
|
580
|
+
def get_scheme_historical_nav_for_dates(self, code, start_date, end_date, as_json=False, as_dataframe=False):
|
|
581
|
+
"""
|
|
582
|
+
gets the scheme historical data between start_date and end_date for a given scheme code
|
|
583
|
+
:param start_date: string '%Y-%m-%d'
|
|
584
|
+
:param end_date: string '%Y-%m-%d'
|
|
585
|
+
:param code: scheme code
|
|
586
|
+
:param as_json: default false
|
|
587
|
+
:param as_dataframe: default false
|
|
588
|
+
:return: dict or None
|
|
589
|
+
:raises: HTTPError, URLError
|
|
590
|
+
"""
|
|
591
|
+
code = str(code)
|
|
592
|
+
if self.is_valid_code(code):
|
|
593
|
+
# scheme_info = {}
|
|
594
|
+
data = []
|
|
595
|
+
start_date = datetime.datetime.strptime(start_date, '%d-%m-%Y').date()
|
|
596
|
+
end_date = datetime.datetime.strptime(end_date, '%d-%m-%Y').date()
|
|
597
|
+
nav = self.get_scheme_historical_nav(code)
|
|
598
|
+
scheme_info = self.get_scheme_details(code)
|
|
599
|
+
for dat in nav['data']:
|
|
600
|
+
navDate = dat['date']
|
|
601
|
+
d = datetime.datetime.strptime(navDate, '%d-%m-%Y')
|
|
602
|
+
if end_date >= d.date() >= start_date:
|
|
603
|
+
data.append(dat)
|
|
604
|
+
if len(data) == 0:
|
|
605
|
+
data.append({'Data is NOT available for selected range'})
|
|
606
|
+
|
|
607
|
+
scheme_info.update(data=data)
|
|
608
|
+
return render_response(scheme_info, as_json, as_dataframe)
|
|
609
|
+
else:
|
|
610
|
+
return None
|
|
611
|
+
|
|
612
|
+
def get_open_ended_equity_scheme_performance(self, report_date=None,as_json=False):
|
|
613
|
+
"""
|
|
614
|
+
gets the daily performance of open-ended equity schemes for all AMCs
|
|
615
|
+
:param report_date: date in 'DD-MMM-YYYY' format, if None then it will take last working day
|
|
616
|
+
:return: json format
|
|
617
|
+
:raises: HTTPError, URLError
|
|
618
|
+
"""
|
|
619
|
+
scheme_performance = {}
|
|
620
|
+
subCategory = self._open_ended_equity_category
|
|
621
|
+
for key in subCategory:
|
|
622
|
+
scheme_performance[subCategory[key]] = self._get_daily_scheme_performance(self._get_open_ended_equity_scheme_url,report_date,1, key)
|
|
623
|
+
return render_response(scheme_performance, as_json)
|
|
624
|
+
|
|
625
|
+
def get_open_ended_debt_scheme_performance(self, report_date=None, as_json=False):
|
|
626
|
+
"""
|
|
627
|
+
gets the daily performance of open-ended debt schemes for all AMCs
|
|
628
|
+
:param report_date: date in 'DD-MMM-YYYY' format, if None then it will take last working day
|
|
629
|
+
:return: json format
|
|
630
|
+
:raises: HTTPError, URLError
|
|
631
|
+
"""
|
|
632
|
+
subCategory = self._open_ended_debt_category
|
|
633
|
+
scheme_performance = {}
|
|
634
|
+
for key in subCategory:
|
|
635
|
+
scheme_performance[subCategory[key]] = self._get_daily_scheme_performance(self._get_open_ended_equity_scheme_url,report_date,2,key)
|
|
636
|
+
return render_response(scheme_performance, as_json)
|
|
637
|
+
|
|
638
|
+
def get_open_ended_hybrid_scheme_performance(self, report_date=None, as_json=False):
|
|
639
|
+
"""
|
|
640
|
+
gets the daily performance of open-ended hybrid schemes for all AMCs
|
|
641
|
+
:param report_date: date in 'DD-MMM-YYYY' format, if None then it will take last working day
|
|
642
|
+
:return: json format
|
|
643
|
+
:raises: HTTPError, URLError
|
|
644
|
+
"""
|
|
645
|
+
subCategory = self._open_ended_hybrid_category
|
|
646
|
+
scheme_performance = {}
|
|
647
|
+
for key in subCategory:
|
|
648
|
+
scheme_performance[subCategory[key]] = self._get_daily_scheme_performance(
|
|
649
|
+
self._get_open_ended_equity_scheme_url,report_date, 3, key)
|
|
650
|
+
return render_response(scheme_performance, as_json)
|
|
651
|
+
|
|
652
|
+
def get_open_ended_solution_scheme_performance(self, report_date=None, as_json=False):
|
|
653
|
+
"""
|
|
654
|
+
gets the daily performance of open-ended Solution-Oriented schemes for all AMCs
|
|
655
|
+
:param report_date: date in 'DD-MMM-YYYY' format, if None then it will take last working day
|
|
656
|
+
:return: json format
|
|
657
|
+
:raises: HTTPError, URLError
|
|
658
|
+
"""
|
|
659
|
+
subCategory = self._open_ended_solution_category
|
|
660
|
+
scheme_performance = {}
|
|
661
|
+
for key in subCategory:
|
|
662
|
+
scheme_performance[subCategory[key]] = self._get_daily_scheme_performance(
|
|
663
|
+
self._get_open_ended_equity_scheme_url, report_date,4, key)
|
|
664
|
+
return render_response(scheme_performance, as_json)
|
|
665
|
+
|
|
666
|
+
def get_open_ended_other_scheme_performance(self, report_date=None, as_json=False):
|
|
667
|
+
"""
|
|
668
|
+
gets the daily performance of open-ended index and FoF schemes for all AMCs
|
|
669
|
+
:param report_date: date in 'DD-MMM-YYYY' format, if None then it will take last working day
|
|
670
|
+
:return: json format
|
|
671
|
+
:raises: HTTPError, URLError
|
|
672
|
+
"""
|
|
673
|
+
subCategory = self._open_ended_other_category
|
|
674
|
+
scheme_performance = {}
|
|
675
|
+
for key in subCategory:
|
|
676
|
+
scheme_performance[subCategory[key]] = self._get_daily_scheme_performance(
|
|
677
|
+
self._get_open_ended_equity_scheme_url, report_date,5, key)
|
|
678
|
+
return render_response(scheme_performance, as_json)
|
|
679
|
+
|
|
680
|
+
def _get_daily_scheme_performance(self, performance_url,report_date, category,key, as_json=False):
|
|
681
|
+
fund_performance = []
|
|
682
|
+
if not report_date:
|
|
683
|
+
if is_holiday():
|
|
684
|
+
report_date = get_friday()
|
|
685
|
+
else:
|
|
686
|
+
report_date = get_today()
|
|
687
|
+
try:
|
|
688
|
+
data = {"maturityType": 1,"category": category,"subCategory": int(key),"mfid": 0,"reportDate": report_date}
|
|
689
|
+
html = httpx.post(performance_url,headers={"User-Agent":"Mozilla/5.0"},timeout=25, json=data)
|
|
690
|
+
for result in html.json()['data']:
|
|
691
|
+
scheme_details = {}
|
|
692
|
+
scheme_details['scheme_name'] = result['schemeName']
|
|
693
|
+
scheme_details['benchmark'] = result['benchmark']
|
|
694
|
+
scheme_details['latest NAV- Regular'] = result['navRegular']
|
|
695
|
+
scheme_details['latest NAV- Direct'] = result['navDirect']
|
|
696
|
+
scheme_details['1-Year Return(%)- Regular'] = result['return1YearRegular']
|
|
697
|
+
scheme_details['1-Year Return(%)- Direct'] = result['return1YearDirect']
|
|
698
|
+
scheme_details['3-Year Return(%)- Regular'] = result['return3YearRegular']
|
|
699
|
+
scheme_details['3-Year Return(%)- Direct'] = result['return3YearDirect']
|
|
700
|
+
scheme_details['5-Year Return(%)- Regular'] = result['return5YearRegular']
|
|
701
|
+
scheme_details['5-Year Return(%)- Direct'] = result['return5YearDirect']
|
|
702
|
+
fund_performance.append(scheme_details)
|
|
703
|
+
except Exception:
|
|
704
|
+
return render_response(['The underlying data is unavailable for Today'], as_json)
|
|
705
|
+
return render_response(fund_performance, as_json)
|
|
706
|
+
|
|
707
|
+
@deprecated(version='3.1',
|
|
708
|
+
reason="This function will be in deprecated from next release, use mf.history() to get data")
|
|
709
|
+
def get_all_amc_profiles(self, as_json=True):
|
|
710
|
+
"""
|
|
711
|
+
gets profiles for all Fund houses
|
|
712
|
+
:return: json format
|
|
713
|
+
:raises: HTTPError, URLError
|
|
714
|
+
"""
|
|
715
|
+
url = self._get_amc_details_url
|
|
716
|
+
amc_profiles = []
|
|
717
|
+
for amc in self._amc:
|
|
718
|
+
html = requests.post(url,{'Id':amc})
|
|
719
|
+
soup = BeautifulSoup(html.text, 'html.parser')
|
|
720
|
+
rows = soup.select("table tbody tr")
|
|
721
|
+
amc_details = {}
|
|
722
|
+
for row in rows:
|
|
723
|
+
if len(row.findAll('td')) > 1:
|
|
724
|
+
amc_details[row.select("td")[0].get_text()] = row.select("td")[1].get_text().strip()
|
|
725
|
+
amc_profiles.append(amc_details)
|
|
726
|
+
amc_details = None
|
|
727
|
+
return render_response(amc_profiles, as_json)
|
|
728
|
+
|
|
729
|
+
def get_average_aum(self, year_quarter, as_json=True):
|
|
730
|
+
"""
|
|
731
|
+
gets the Avearage AUM data for all Fund houses
|
|
732
|
+
:param as_json: True / False
|
|
733
|
+
:param year_quarter: string 'July - September 2020'
|
|
734
|
+
#quarter format should like - 'April - June 2020'
|
|
735
|
+
:return: json format
|
|
736
|
+
:raises: HTTPError, URLError
|
|
737
|
+
"""
|
|
738
|
+
all_funds_aum = []
|
|
739
|
+
url = self._get_avg_aum
|
|
740
|
+
html = requests.post(url,headers=self._user_agent,data={"AUmType":'F',"Year_Quarter":year_quarter})
|
|
741
|
+
soup = BeautifulSoup(html.text, 'html.parser')
|
|
742
|
+
rows = soup.select("table tbody tr")
|
|
743
|
+
for row in rows:
|
|
744
|
+
aum_fund = {}
|
|
745
|
+
if len(row.findAll('td')) > 1:
|
|
746
|
+
aum_fund['Fund Name']= row.select("td")[1].get_text().strip()
|
|
747
|
+
aum_fund['AAUM Overseas']= row.select("td")[2].get_text().strip()
|
|
748
|
+
aum_fund['AAUM Domestic'] = row.select("td")[3].get_text().strip()
|
|
749
|
+
all_funds_aum.append(aum_fund)
|
|
750
|
+
aum_fund = None
|
|
751
|
+
return render_response(all_funds_aum, as_json)
|
|
752
|
+
|
|
753
|
+
def history(self, code, start=None, end=None, period='5d', as_dataframe=True):
|
|
754
|
+
"""
|
|
755
|
+
gets the scheme historical data in DataFrame or json for a given scheme code, only use NEW codes
|
|
756
|
+
:Parameters:
|
|
757
|
+
code : str, list
|
|
758
|
+
Scheme code to download
|
|
759
|
+
period : str
|
|
760
|
+
Valid periods: 1d,5d,1mo,3mo,6mo,1y,2y,5y,10y,max
|
|
761
|
+
Either Use period parameter or use start and end
|
|
762
|
+
start: str
|
|
763
|
+
Download start date string (YYYY-MM-DD) or _datetime.
|
|
764
|
+
Default is None
|
|
765
|
+
end: str
|
|
766
|
+
Download end date string (YYYY-MM-DD) or _datetime.
|
|
767
|
+
Default is None
|
|
768
|
+
as_dataframe: boolen
|
|
769
|
+
download data format,
|
|
770
|
+
True : DataFrame, False : JSON
|
|
771
|
+
Default is True
|
|
772
|
+
:return: Dataframe or JSON or None
|
|
773
|
+
:raises: HTTPError, URLError
|
|
774
|
+
"""
|
|
775
|
+
code = str(code)
|
|
776
|
+
if self.is_code(code):
|
|
777
|
+
def get_Dataframe(df, as_dataframe):
|
|
778
|
+
df = df.drop(columns=['Open', 'High', 'Low','Volume'])
|
|
779
|
+
df = df.rename(columns={'Close': 'nav'})
|
|
780
|
+
df['dayChange'] = df['nav'].diff()
|
|
781
|
+
df = df.rename_axis('date')
|
|
782
|
+
df.index = df.index.strftime('%d-%m-%Y')
|
|
783
|
+
if not as_dataframe: # To get json format
|
|
784
|
+
df.reset_index(inplace=True)
|
|
785
|
+
return df.astype(str).to_json(orient = "index", date_format = "iso")
|
|
786
|
+
else:
|
|
787
|
+
return df
|
|
788
|
+
code = code + ".BO"
|
|
789
|
+
if start and end is not None:
|
|
790
|
+
response = yf.download(code,start=start,end=end)
|
|
791
|
+
elif period is not None:
|
|
792
|
+
response = yf.download(code,period=period)
|
|
793
|
+
return get_Dataframe(response, as_dataframe)
|
|
794
|
+
|
|
795
|
+
def get_scheme_info(self, code, as_json=True):
|
|
796
|
+
"""
|
|
797
|
+
gets the complete information for a given scheme code, only use NEW scheme codes
|
|
798
|
+
:Parameters:
|
|
799
|
+
code : str
|
|
800
|
+
Scheme code to download
|
|
801
|
+
as_json: True / False
|
|
802
|
+
Default is True
|
|
803
|
+
:return: JSON or None
|
|
804
|
+
:raises: HTTPError, URLError
|
|
805
|
+
"""
|
|
806
|
+
code = str(code)
|
|
807
|
+
if self.is_code(code):
|
|
808
|
+
code = code + ".BO"
|
|
809
|
+
mf = yf.Ticker(code)
|
|
810
|
+
response = mf.info
|
|
811
|
+
return render_response(response, as_json)
|
|
812
|
+
|
|
813
|
+
def compare_trend(self, codes, start_date, end_date):
|
|
814
|
+
"""
|
|
815
|
+
plot and Compare trend of mutual funds
|
|
816
|
+
:param start_date: string '%Y-%m-%d'
|
|
817
|
+
:param end_date: string '%Y-%m-%d'
|
|
818
|
+
:param code: scheme code
|
|
819
|
+
:param as_json: default false
|
|
820
|
+
:param as_dataframe: default false
|
|
821
|
+
:return: dict or None
|
|
822
|
+
:raises: HTTPError, URLError
|
|
823
|
+
"""
|
|
824
|
+
all_mf = pd.DataFrame()
|
|
825
|
+
for code in codes:
|
|
826
|
+
mf_data = self.get_scheme_historical_nav_for_dates(code, start_date, end_date, as_dataframe=True)
|
|
827
|
+
mf_data = mf_data.drop(columns=['dayChange'])
|
|
828
|
+
mf_name = self.get_scheme_details(code)['scheme_name']
|
|
829
|
+
mf_data[mf_name] = mf_data['nav'].astype(float)
|
|
830
|
+
mf_data['date'] = mf_data.index
|
|
831
|
+
all_mf[mf_name] = mf_data[mf_name]
|
|
832
|
+
all_mf['date'] = mf_data['date']
|
|
833
|
+
|
|
834
|
+
all_mf = all_mf[::-1]
|
|
835
|
+
all_mf.plot(x='date')
|
|
836
|
+
plt.title("Compare mutual funds")
|
|
837
|
+
plt.xlabel("Date")
|
|
838
|
+
plt.ylabel("NAV")
|
|
839
|
+
plt.show()
|
|
840
|
+
|
|
841
|
+
def clear_cache(self):
|
|
842
|
+
"""
|
|
843
|
+
Clear all cached data
|
|
844
|
+
:return: None
|
|
845
|
+
"""
|
|
846
|
+
self._cache.clear()
|
|
847
|
+
self._scheme_codes_cache.clear()
|
|
848
|
+
|
|
849
|
+
def get_cache_stats(self):
|
|
850
|
+
"""
|
|
851
|
+
Get cache statistics
|
|
852
|
+
:return: dict with cache stats
|
|
853
|
+
"""
|
|
854
|
+
return {
|
|
855
|
+
'nav_cache': self._cache.get_stats(),
|
|
856
|
+
'scheme_codes_cache': self._scheme_codes_cache.get_stats()
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
def disable_cache(self):
|
|
860
|
+
"""
|
|
861
|
+
Disable caching globally
|
|
862
|
+
:return: None
|
|
863
|
+
"""
|
|
864
|
+
self._cache.disable()
|
|
865
|
+
self._scheme_codes_cache.disable()
|
|
866
|
+
|
|
867
|
+
def enable_cache(self):
|
|
868
|
+
"""
|
|
869
|
+
Enable caching globally
|
|
870
|
+
:return: None
|
|
871
|
+
"""
|
|
872
|
+
self._cache.enable()
|
|
873
|
+
self._scheme_codes_cache.enable()
|
|
874
|
+
|
|
875
|
+
def calculate_portfolio_value(self, holdings: List[Dict[str, Union[str, float]]],
|
|
876
|
+
as_json=False) -> Dict[str, Union[float, dict]]:
|
|
877
|
+
"""
|
|
878
|
+
Calculate total portfolio value for multiple holdings concurrently.
|
|
879
|
+
|
|
880
|
+
:param holdings: List of dicts with 'scheme_code' and 'units' keys
|
|
881
|
+
:param as_json: Return data in JSON format (default: False)
|
|
882
|
+
:return: Dictionary with portfolio summary
|
|
883
|
+
|
|
884
|
+
Example:
|
|
885
|
+
>>> holdings = [
|
|
886
|
+
... {'scheme_code': '119597', 'units': 100},
|
|
887
|
+
... {'scheme_code': '119062', 'units': 50}
|
|
888
|
+
... ]
|
|
889
|
+
>>> portfolio = mf.calculate_portfolio_value(holdings)
|
|
890
|
+
>>> print(f"Total value: {portfolio['total_value']}")
|
|
891
|
+
"""
|
|
892
|
+
scheme_codes = [str(h['scheme_code']) for h in holdings]
|
|
893
|
+
|
|
894
|
+
# Fetch all quotes concurrently
|
|
895
|
+
quotes = self.get_bulk_quotes(scheme_codes, as_json=False)
|
|
896
|
+
|
|
897
|
+
portfolio_data = []
|
|
898
|
+
total_value = 0.0
|
|
899
|
+
|
|
900
|
+
for holding in holdings:
|
|
901
|
+
code = str(holding['scheme_code'])
|
|
902
|
+
units = float(holding['units'])
|
|
903
|
+
|
|
904
|
+
quote = quotes.get(code)
|
|
905
|
+
if quote and 'nav' in quote:
|
|
906
|
+
nav = float(quote['nav'])
|
|
907
|
+
value = units * nav
|
|
908
|
+
total_value += value
|
|
909
|
+
|
|
910
|
+
portfolio_data.append({
|
|
911
|
+
'scheme_code': code,
|
|
912
|
+
'scheme_name': quote.get('scheme_name', 'N/A'),
|
|
913
|
+
'units': units,
|
|
914
|
+
'nav': nav,
|
|
915
|
+
'current_value': round(value, 2),
|
|
916
|
+
'last_updated': quote.get('last_updated', 'N/A')
|
|
917
|
+
})
|
|
918
|
+
else:
|
|
919
|
+
portfolio_data.append({
|
|
920
|
+
'scheme_code': code,
|
|
921
|
+
'scheme_name': 'Error fetching data',
|
|
922
|
+
'units': units,
|
|
923
|
+
'nav': 0,
|
|
924
|
+
'current_value': 0,
|
|
925
|
+
'last_updated': 'N/A'
|
|
926
|
+
})
|
|
927
|
+
|
|
928
|
+
result = {
|
|
929
|
+
'total_value': round(total_value, 2),
|
|
930
|
+
'total_schemes': len(holdings),
|
|
931
|
+
'holdings': portfolio_data,
|
|
932
|
+
'currency': 'INR'
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return render_response(result, as_json)
|