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/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)