Files changed (1) hide show
  1. app.py +563 -584
app.py CHANGED
@@ -1,584 +1,563 @@
1
- import streamlit as st
2
- import pandas as pd
3
- import numpy as np
4
- import matplotlib.pyplot as plt
5
- import datetime
6
- from dateutil.relativedelta import relativedelta
7
- import plotly.express as px
8
- import plotly.graph_objects as go
9
- import plotly.figure_factory as ff
10
- from plotly.subplots import make_subplots
11
- import yfinance as yf
12
- import seaborn as sns
13
- from scipy import stats
14
- from typing import Dict, Optional, List
15
- import warnings
16
- warnings.filterwarnings('ignore')
17
-
18
- # Try importing mftool, handle if not available
19
- try:
20
- from mftool import Mftool
21
- mftool_available = True
22
- except ImportError:
23
- mftool_available = False
24
- # Define a placeholder if needed, or ensure Mftool() isn't called if not available
25
- class Mftool: pass
26
-
27
- try:
28
- from yahooquery import Ticker
29
-
30
- yahooquery_available = True
31
- except ImportError:
32
- yahooquery_available = False
33
-
34
- # Set page configuration
35
- st.set_page_config(
36
- page_title="Mutual Fund Analytics Suite",
37
- page_icon="📈",
38
- layout="wide",
39
- initial_sidebar_state="expanded"
40
- )
41
-
42
- # Custom CSS styling
43
- st.markdown("""
44
- <style>
45
- .main {
46
- padding: 2rem;
47
- }
48
- .stButton>button {
49
- width: 100%;
50
- background-color: #1f77b4;
51
- color: white;
52
- }
53
- .reportview-container .main .block-container {
54
- padding-top: 2rem;
55
- }
56
- h1 {
57
- color: #1f77b4;
58
- }
59
- .stMetric {
60
- background-color: #f8f9fa;
61
- padding: 1rem;
62
- border-radius: 5px;
63
- box-shadow: 0 2px 4px rgba(0,0,0,0.1);
64
- }
65
- .stAlert {
66
- padding: 1rem;
67
- margin: 1rem 0;
68
- border-radius: 0.5rem;
69
- }
70
- </style>
71
- """, unsafe_allow_html=True)
72
-
73
- # Cache data fetching functions
74
- @st.cache_data(ttl=3600)
75
- def fetch_mutual_fund_data(mutual_fund_code: str) -> Optional[pd.DataFrame]:
76
- """Fetch mutual fund data from mftool."""
77
- if not mftool_available:
78
- st.error("mftool library is not installed. Cannot fetch Indian mutual fund data.")
79
- return None
80
-
81
- try:
82
- mf = Mftool()
83
- # Step 1: Fetch the data
84
- raw_df = mf.get_scheme_historical_nav(mutual_fund_code, as_Dataframe=True)
85
-
86
- # Step 2: Check if data was successfully fetched (is not None)
87
- if raw_df is not None and not raw_df.empty:
88
- # Step 3: Process the DataFrame only if it exists and is not empty
89
- df = (raw_df
90
- .reset_index()
91
- .assign(nav=lambda x: pd.to_numeric(x['nav'], errors='coerce'), # Use pd.to_numeric for safety
92
- date=lambda x: pd.to_datetime(x['date'], format='%d-%m-%Y', errors='coerce'))
93
- .dropna(subset=['nav', 'date']) # Remove rows where conversion failed
94
- .sort_values('date')
95
- .reset_index(drop=True))
96
-
97
- if df.empty:
98
- st.warning(f"No valid historical NAV data found for fund code {mutual_fund_code} after processing.")
99
- return None
100
- return df
101
- else:
102
- # Handle the case where mftool returned None or an empty DataFrame
103
- st.error(f"Could not fetch data for mutual fund code: {mutual_fund_code}. It might be invalid, contain no data, or data is unavailable from the source.")
104
- return None # Explicitly return None if fetching failed or returned empty
105
-
106
- except Exception as e:
107
- # Catch other potential exceptions during processing or Mftool instantiation
108
- st.error(f"An unexpected error occurred while fetching/processing data for {mutual_fund_code}: {str(e)}")
109
- return None
110
-
111
- @st.cache_data(ttl=3600)
112
- def load_yahoo_finance_data(ticker_symbol: str, start_date: datetime.date, end_date: datetime.date) -> Optional[pd.DataFrame]:
113
- """Fetch data from Yahoo Finance."""
114
- try:
115
- data = yf.download(ticker_symbol, start=start_date, end=end_date)
116
- data = data.reset_index()
117
- data = data.rename(columns={'Date': 'date', 'Close': 'nav', 'Volume': 'volume'})
118
- return data
119
- except Exception as e:
120
- st.error(f"Error fetching Yahoo Finance data: {str(e)}")
121
- return None
122
-
123
- def calculate_risk_metrics(returns: pd.Series) -> Dict[str, float]:
124
- """Calculate comprehensive risk metrics for the fund."""
125
- try:
126
- metrics = {
127
- 'volatility': returns.std() * np.sqrt(252),
128
- 'sharpe_ratio': (returns.mean() * 252) / (returns.std() * np.sqrt(252)),
129
- 'sortino_ratio': (returns.mean() * 252) / (returns[returns < 0].std() * np.sqrt(252)),
130
- 'max_drawdown': (1 - (1 + returns).cumprod() / (1 + returns).cumprod().cummax()).max(),
131
- 'skewness': stats.skew(returns),
132
- 'kurtosis': stats.kurtosis(returns),
133
- 'var_95': np.percentile(returns, 5),
134
- 'cvar_95': returns[returns <= np.percentile(returns, 5)].mean(),
135
- 'positive_days': (returns > 0).mean() * 100,
136
- 'negative_days': (returns < 0).mean() * 100,
137
- 'avg_gain': returns[returns > 0].mean(),
138
- 'avg_loss': returns[returns < 0].mean()
139
- }
140
- return metrics
141
- except Exception as e:
142
- st.error(f"Error calculating risk metrics: {str(e)}")
143
- return {}
144
-
145
- def plot_price_volume_chart(df: pd.DataFrame) -> go.Figure:
146
- """Create an interactive price and volume chart."""
147
- try:
148
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
149
- vertical_spacing=0.03,
150
- row_heights=[0.7, 0.3])
151
-
152
- fig.add_trace(go.Candlestick(x=df['date'],
153
- open=df['Open'],
154
- high=df['High'],
155
- low=df['Low'],
156
- close=df['nav'],
157
- name='Price'),
158
- row=1, col=1)
159
-
160
- fig.add_trace(go.Bar(x=df['date'],
161
- y=df['volume'],
162
- name='Volume'),
163
- row=2, col=1)
164
-
165
- fig.update_layout(
166
- title='Price and Volume Analysis',
167
- yaxis_title='Price',
168
- yaxis2_title='Volume',
169
- height=800,
170
- template='plotly_white'
171
- )
172
-
173
- return fig
174
- except Exception as e:
175
- st.error(f"Error creating price-volume chart: {str(e)}")
176
- return None
177
-
178
- def plot_returns_distribution(returns: pd.Series) -> go.Figure:
179
- """Create an interactive returns distribution plot."""
180
- try:
181
- fig = go.Figure()
182
-
183
- # Actual returns distribution
184
- fig.add_trace(go.Histogram(
185
- x=returns,
186
- name='Actual Returns',
187
- nbinsx=50,
188
- histnorm='probability'
189
- ))
190
-
191
- # Normal distribution overlay
192
- x_range = np.linspace(returns.min(), returns.max(), 100)
193
- normal_dist = stats.norm.pdf(x_range, returns.mean(), returns.std())
194
-
195
- fig.add_trace(go.Scatter(
196
- x=x_range,
197
- y=normal_dist,
198
- name='Normal Distribution',
199
- line=dict(color='red')
200
- ))
201
-
202
- fig.update_layout(
203
- title='Returns Distribution Analysis',
204
- xaxis_title='Returns',
205
- yaxis_title='Probability',
206
- barmode='overlay',
207
- showlegend=True,
208
- template='plotly_white'
209
- )
210
-
211
- return fig
212
- except Exception as e:
213
- st.error(f"Error creating returns distribution plot: {str(e)}")
214
- return None
215
-
216
- def plot_rolling_metrics(df: pd.DataFrame, window: int = 30) -> go.Figure:
217
- """Create rolling metrics visualization with confidence bands."""
218
- try:
219
- rolling_returns = df['daily_returns'].rolling(window=window)
220
- rolling_vol = rolling_returns.std() * np.sqrt(252)
221
- rolling_mean = rolling_returns.mean() * 252
222
- rolling_sharpe = rolling_mean / (rolling_returns.std() * np.sqrt(252))
223
-
224
- fig = go.Figure()
225
-
226
- # Add rolling volatility with confidence bands
227
- vol_std = rolling_vol.std()
228
- fig.add_trace(go.Scatter(
229
- x=df['date'],
230
- y=rolling_vol + 2*vol_std,
231
- fill=None,
232
- mode='lines',
233
- line_color='rgba(0,100,80,0.2)',
234
- name='Volatility Upper Band'
235
- ))
236
-
237
- fig.add_trace(go.Scatter(
238
- x=df['date'],
239
- y=rolling_vol - 2*vol_std,
240
- fill='tonexty',
241
- mode='lines',
242
- line_color='rgba(0,100,80,0.2)',
243
- name='Volatility Lower Band'
244
- ))
245
-
246
- fig.add_trace(go.Scatter(
247
- x=df['date'],
248
- y=rolling_vol,
249
- name='Rolling Volatility',
250
- line=dict(color='rgb(0,100,80)')
251
- ))
252
-
253
- fig.add_trace(go.Scatter(
254
- x=df['date'],
255
- y=rolling_sharpe,
256
- name='Rolling Sharpe Ratio',
257
- yaxis='y2',
258
- line=dict(color='rgb(200,30,30)')
259
- ))
260
-
261
- fig.update_layout(
262
- title=f'Rolling Metrics (Window: {window} days)',
263
- yaxis=dict(title='Annualized Volatility'),
264
- yaxis2=dict(title='Sharpe Ratio', overlaying='y', side='right'),
265
- showlegend=True,
266
- height=600,
267
- template='plotly_white'
268
- )
269
-
270
- return fig
271
- except Exception as e:
272
- st.error(f"Error creating rolling metrics plot: {str(e)}")
273
- return None
274
-
275
- def plot_comparative_analysis(dfs: Dict[str, pd.DataFrame]) -> List[go.Figure]:
276
- """Create comparative analysis plots."""
277
- try:
278
- # Normalize all fund values to 100
279
- normalized_dfs = {}
280
- for name, df in dfs.items():
281
- normalized_dfs[name] = df.copy()
282
- normalized_dfs[name]['normalized_nav'] = df['nav'] / df['nav'].iloc[0] * 100
283
-
284
- # Create comparative performance plot
285
- perf_fig = go.Figure()
286
- for name, df in normalized_dfs.items():
287
- perf_fig.add_trace(go.Scatter(
288
- x=df['date'],
289
- y=df['normalized_nav'],
290
- name=name,
291
- mode='lines'
292
- ))
293
-
294
- perf_fig.update_layout(
295
- title='Comparative Performance Analysis',
296
- xaxis_title='Date',
297
- yaxis_title='Normalized Value (Base=100)',
298
- template='plotly_white'
299
- )
300
-
301
- # Create correlation heatmap
302
- returns_df = pd.DataFrame()
303
- for name, df in dfs.items():
304
- returns_df[name] = df['nav'].pct_change()
305
-
306
- corr_matrix = returns_df.corr()
307
-
308
- corr_fig = go.Figure(data=go.Heatmap(
309
- z=corr_matrix,
310
- x=corr_matrix.columns,
311
- y=corr_matrix.columns,
312
- colorscale='RdBu',
313
- zmin=-1,
314
- zmax=1
315
- ))
316
-
317
- corr_fig.update_layout(
318
- title='Returns Correlation Matrix',
319
- template='plotly_white'
320
- )
321
-
322
- return [perf_fig, corr_fig]
323
- except Exception as e:
324
- st.error(f"Error creating comparative analysis plots: {str(e)}")
325
- return []
326
-
327
- def plot_risk_analytics(df: pd.DataFrame) -> List[go.Figure]:
328
- """Create risk analytics plots."""
329
- try:
330
- returns = df['nav'].pct_change()
331
-
332
- # Create drawdown plot
333
- cum_returns = (1 + returns).cumprod()
334
- rolling_max = cum_returns.cummax()
335
- drawdowns = (cum_returns - rolling_max) / rolling_max
336
-
337
- drawdown_fig = go.Figure()
338
- drawdown_fig.add_trace(go.Scatter(
339
- x=df['date'],
340
- y=drawdowns,
341
- fill='tozeroy',
342
- name='Drawdown'
343
- ))
344
-
345
- drawdown_fig.update_layout(
346
- title='Historical Drawdown Analysis',
347
- xaxis_title='Date',
348
- yaxis_title='Drawdown',
349
- template='plotly_white'
350
- )
351
-
352
- # Create risk-return scatter plot
353
- rolling_windows = [30, 60, 90, 180, 252]
354
- risk_return_data = []
355
-
356
- for window in rolling_windows:
357
- rolling_returns = returns.rolling(window=window)
358
- risk = rolling_returns.std() * np.sqrt(252)
359
- ret = rolling_returns.mean() * 252
360
- risk_return_data.append({
361
- 'window': f'{window} days',
362
- 'risk': risk.mean(),
363
- 'return': ret.mean()
364
- })
365
-
366
- risk_return_df = pd.DataFrame(risk_return_data)
367
-
368
- risk_return_fig = px.scatter(
369
- risk_return_df,
370
- x='risk',
371
- y='return',
372
- text='window',
373
- title='Risk-Return Analysis Across Different Time Windows'
374
- )
375
-
376
- risk_return_fig.update_traces(textposition='top center')
377
- risk_return_fig.update_layout(template='plotly_white')
378
-
379
- return [drawdown_fig, risk_return_fig]
380
- except Exception as e:
381
- st.error(f"Error creating risk analytics plots: {str(e)}")
382
- return []
383
-
384
- def main():
385
- st.title("📊 Advanced Mutual Fund Analytics Platform")
386
-
387
- st.markdown("""
388
- ### Professional-Grade Investment Analysis Tool
389
- This platform provides comprehensive mutual fund analytics with advanced risk metrics,
390
- interactive visualizations, and comparative analysis capabilities.
391
- """)
392
-
393
- # Sidebar controls
394
- st.sidebar.header("Analysis Controls")
395
-
396
- analysis_type = st.sidebar.selectbox(
397
- "Select Analysis Type",
398
- ["Single Fund Analysis", "Comparative Analysis", "Risk Analytics"]
399
- )
400
-
401
- # Date range selection
402
- col1, col2 = st.sidebar.columns(2)
403
- with col1:
404
- start_date = st.date_input(
405
- "Start Date",
406
- datetime.date.today() - relativedelta(years=3)
407
- )
408
- with col2:
409
- end_date = st.date_input(
410
- "End Date",
411
- datetime.date.today()
412
- )
413
-
414
- if analysis_type == "Single Fund Analysis":
415
- st.header("Single Fund Analysis")
416
-
417
- input_type = st.radio(
418
- "Select Input Type",
419
- ["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"]
420
- )
421
-
422
- if input_type == "Yahoo Finance Ticker":
423
- fund_id = st.text_input("Enter Yahoo Finance Ticker", "0P0000XW8F.BO")
424
- if st.button("Analyze Fund"):
425
- with st.spinner("Fetching and analyzing data..."):
426
- df = load_yahoo_finance_data(fund_id, start_date, end_date)
427
- if df is not None:
428
- df['daily_returns'] = df['nav'].pct_change()
429
-
430
- metrics = calculate_risk_metrics(df['daily_returns'].dropna())
431
-
432
- # Display metrics in a clean format
433
- col1, col2, col3, col4 = st.columns(4)
434
- with col1:
435
- st.metric("Annualized Volatility", f"{metrics['volatility']:.2%}")
436
- st.metric("Sharpe Ratio", f"{metrics['sharpe_ratio']:.2f}")
437
- with col2:
438
- st.metric("Maximum Drawdown", f"{metrics['max_drawdown']:.2%}")
439
- st.metric("Value at Risk (95%)", f"{metrics['var_95']:.2%}")
440
- with col3:
441
- st.metric("Positive Days", f"{metrics['positive_days']:.1f}%")
442
- st.metric("Average Daily Gain", f"{metrics['avg_gain']:.2%}")
443
- with col4:
444
- st.metric("Negative Days", f"{metrics['negative_days']:.1f}%")
445
- st.metric("Average Daily Loss", f"{metrics['avg_loss']:.2%}")
446
-
447
- # Create tabs for different visualizations
448
- tab1, tab2, tab3 = st.tabs(["Price Analysis", "Returns Analysis", "Risk Metrics"])
449
-
450
- with tab1:
451
- if 'Open' in df.columns:
452
- price_vol_fig = plot_price_volume_chart(df)
453
- if price_vol_fig:
454
- st.plotly_chart(price_vol_fig, use_container_width=True)
455
-
456
- with tab2:
457
- returns_dist_fig = plot_returns_distribution(df['daily_returns'].dropna())
458
- if returns_dist_fig:
459
- st.plotly_chart(returns_dist_fig, use_container_width=True)
460
-
461
- with tab3:
462
- window = st.slider("Rolling Window (days)", 10, 252, 30)
463
- rolling_fig = plot_rolling_metrics(df, window)
464
- if rolling_fig:
465
- st.plotly_chart(rolling_fig, use_container_width=True)
466
-
467
- else:
468
- fund_code = st.text_input("Enter Mutual Fund Code", "118989")
469
- if st.button("Analyze Fund"):
470
- with st.spinner("Fetching and analyzing data..."):
471
- df = fetch_mutual_fund_data(fund_code)
472
- if df is not None:
473
- df['daily_returns'] = df['nav'].pct_change()
474
- # Perform the same analysis as above
475
- metrics = calculate_risk_metrics(df['daily_returns'].dropna())
476
-
477
- # Display metrics and charts (same as above)
478
- col1, col2, col3, col4 = st.columns(4)
479
- with col1:
480
- st.metric("Annualized Volatility", f"{metrics['volatility']:.2%}")
481
- st.metric("Sharpe Ratio", f"{metrics['sharpe_ratio']:.2f}")
482
- with col2:
483
- st.metric("Maximum Drawdown", f"{metrics['max_drawdown']:.2%}")
484
- st.metric("Value at Risk (95%)", f"{metrics['var_95']:.2%}")
485
- with col3:
486
- st.metric("Positive Days", f"{metrics['positive_days']:.1f}%")
487
- st.metric("Average Daily Gain", f"{metrics['avg_gain']:.2%}")
488
- with col4:
489
- st.metric("Negative Days", f"{metrics['negative_days']:.1f}%")
490
- st.metric("Average Daily Loss", f"{metrics['avg_loss']:.2%}")
491
-
492
- tab1, tab2 = st.tabs(["Returns Analysis", "Risk Metrics"])
493
-
494
- with tab1:
495
- returns_dist_fig = plot_returns_distribution(df['daily_returns'].dropna())
496
- if returns_dist_fig:
497
- st.plotly_chart(returns_dist_fig, use_container_width=True)
498
-
499
- with tab2:
500
- window = st.slider("Rolling Window (days)", 10, 252, 30)
501
- rolling_fig = plot_rolling_metrics(df, window)
502
- if rolling_fig:
503
- st.plotly_chart(rolling_fig, use_container_width=True)
504
-
505
- elif analysis_type == "Comparative Analysis":
506
- st.header("Comparative Analysis")
507
-
508
- num_funds = st.number_input("Number of funds to compare", min_value=2, max_value=5, value=2)
509
-
510
- funds_data = {}
511
-
512
- for i in range(num_funds):
513
- st.subheader(f"Fund {i + 1}")
514
- input_type = st.radio(
515
- f"Select Input Type for Fund {i + 1}",
516
- ["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"],
517
- key=f"input_type_{i}"
518
- )
519
-
520
- if input_type == "Yahoo Finance Ticker":
521
- fund_id = st.text_input(f"Enter Yahoo Finance Ticker {i + 1}",
522
- value=f"0P0000XW8F.BO" if i == 0 else "",
523
- key=f"yahoo_{i}")
524
- fund_name = st.text_input(f"Enter Fund Name {i + 1}",
525
- value=f"Fund {i + 1}",
526
- key=f"name_{i}")
527
- funds_data[fund_name] = {'id': fund_id, 'type': 'yahoo'}
528
- else:
529
- fund_id = st.text_input(f"Enter Mutual Fund Code {i + 1}",
530
- value="118989" if i == 0 else "",
531
- key=f"mf_{i}")
532
- fund_name = st.text_input(f"Enter Fund Name {i + 1}",
533
- value=f"Fund {i + 1}",
534
- key=f"name_{i}")
535
- funds_data[fund_name] = {'id': fund_id, 'type': 'mf'}
536
-
537
- if st.button("Compare Funds"):
538
- with st.spinner("Fetching and comparing data..."):
539
- dfs = {}
540
- for name, info in funds_data.items():
541
- if info['type'] == 'yahoo':
542
- df = load_yahoo_finance_data(info['id'], start_date, end_date)
543
- else:
544
- df = fetch_mutual_fund_data(info['id'])
545
-
546
- if df is not None:
547
- dfs[name] = df
548
-
549
- if len(dfs) > 1:
550
- comparison_figs = plot_comparative_analysis(dfs)
551
- if comparison_figs:
552
- st.subheader("Comparative Performance")
553
- st.plotly_chart(comparison_figs[0], use_container_width=True)
554
-
555
- st.subheader("Correlation Analysis")
556
- st.plotly_chart(comparison_figs[1], use_container_width=True)
557
-
558
- else: # Risk Analytics
559
- st.header("Risk Analytics")
560
-
561
- input_type = st.radio(
562
- "Select Input Type",
563
- ["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"]
564
- )
565
-
566
- if input_type == "Yahoo Finance Ticker":
567
- fund_id = st.text_input("Enter Yahoo Finance Ticker", "0P0000XW8F.BO")
568
- else:
569
- fund_id = st.text_input("Enter Mutual Fund Code", "118989")
570
-
571
- if st.button("Analyze Risk"):
572
- with st.spinner("Performing risk analysis..."):
573
- df = load_yahoo_finance_data(fund_id, start_date, end_date) if input_type == "Yahoo Finance Ticker" else fetch_mutual_fund_data(fund_id)
574
-
575
- if df is not None:
576
- risk_figs = plot_risk_analytics(df)
577
- if risk_figs:
578
- st.subheader("Drawdown Analysis")
579
- st.plotly_chart(risk_figs[0], use_container_width=True)
580
-
581
- st.subheader("Risk-Return Analysis")
582
- st.plotly_chart(risk_figs[1], use_container_width=True)
583
- if __name__ == "__main__":
584
- main()
 
1
+ import streamlit as st
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import datetime
6
+ from dateutil.relativedelta import relativedelta
7
+ import plotly.express as px
8
+ import plotly.graph_objects as go
9
+ import plotly.figure_factory as ff
10
+ from plotly.subplots import make_subplots
11
+ import yfinance as yf
12
+ import seaborn as sns
13
+ from scipy import stats
14
+ from typing import Dict, Optional, List
15
+ import warnings
16
+ warnings.filterwarnings('ignore')
17
+
18
+ # Try importing mftool, handle if not available
19
+ try:
20
+ from mftool import Mftool
21
+
22
+ mftool_available = True
23
+ except ImportError:
24
+ mftool_available = False
25
+
26
+ try:
27
+ from yahooquery import Ticker
28
+
29
+ yahooquery_available = True
30
+ except ImportError:
31
+ yahooquery_available = False
32
+
33
+ # Set page configuration
34
+ st.set_page_config(
35
+ page_title="Mutual Fund Analytics Suite",
36
+ page_icon="📈",
37
+ layout="wide",
38
+ initial_sidebar_state="expanded"
39
+ )
40
+
41
+ # Custom CSS styling
42
+ st.markdown("""
43
+ <style>
44
+ .main {
45
+ padding: 2rem;
46
+ }
47
+ .stButton>button {
48
+ width: 100%;
49
+ background-color: #1f77b4;
50
+ color: white;
51
+ }
52
+ .reportview-container .main .block-container {
53
+ padding-top: 2rem;
54
+ }
55
+ h1 {
56
+ color: #1f77b4;
57
+ }
58
+ .stMetric {
59
+ background-color: #f8f9fa;
60
+ padding: 1rem;
61
+ border-radius: 5px;
62
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
63
+ }
64
+ .stAlert {
65
+ padding: 1rem;
66
+ margin: 1rem 0;
67
+ border-radius: 0.5rem;
68
+ }
69
+ </style>
70
+ """, unsafe_allow_html=True)
71
+
72
+ # Cache data fetching functions
73
+ @st.cache_data(ttl=3600)
74
+ def fetch_mutual_fund_data(mutual_fund_code: str) -> Optional[pd.DataFrame]:
75
+ """Fetch mutual fund data from mftool."""
76
+ try:
77
+ mf = Mftool()
78
+ df = (mf.get_scheme_historical_nav(mutual_fund_code, as_Dataframe=True)
79
+ .reset_index()
80
+ .assign(nav=lambda x: x['nav'].astype(float),
81
+ date=lambda x: pd.to_datetime(x['date'], format='%d-%m-%Y'))
82
+ .sort_values('date')
83
+ .reset_index(drop=True))
84
+ return df
85
+ except Exception as e:
86
+ st.error(f"Error fetching mutual fund data: {str(e)}")
87
+ return None
88
+
89
+ @st.cache_data(ttl=3600)
90
+ def load_yahoo_finance_data(ticker_symbol: str, start_date: datetime.date, end_date: datetime.date) -> Optional[pd.DataFrame]:
91
+ """Fetch data from Yahoo Finance."""
92
+ try:
93
+ data = yf.download(ticker_symbol, start=start_date, end=end_date)
94
+ data = data.reset_index()
95
+ data = data.rename(columns={'Date': 'date', 'Close': 'nav', 'Volume': 'volume'})
96
+ return data
97
+ except Exception as e:
98
+ st.error(f"Error fetching Yahoo Finance data: {str(e)}")
99
+ return None
100
+
101
+ def calculate_risk_metrics(returns: pd.Series) -> Dict[str, float]:
102
+ """Calculate comprehensive risk metrics for the fund."""
103
+ try:
104
+ metrics = {
105
+ 'volatility': returns.std() * np.sqrt(252),
106
+ 'sharpe_ratio': (returns.mean() * 252) / (returns.std() * np.sqrt(252)),
107
+ 'sortino_ratio': (returns.mean() * 252) / (returns[returns < 0].std() * np.sqrt(252)),
108
+ 'max_drawdown': (1 - (1 + returns).cumprod() / (1 + returns).cumprod().cummax()).max(),
109
+ 'skewness': stats.skew(returns),
110
+ 'kurtosis': stats.kurtosis(returns),
111
+ 'var_95': np.percentile(returns, 5),
112
+ 'cvar_95': returns[returns <= np.percentile(returns, 5)].mean(),
113
+ 'positive_days': (returns > 0).mean() * 100,
114
+ 'negative_days': (returns < 0).mean() * 100,
115
+ 'avg_gain': returns[returns > 0].mean(),
116
+ 'avg_loss': returns[returns < 0].mean()
117
+ }
118
+ return metrics
119
+ except Exception as e:
120
+ st.error(f"Error calculating risk metrics: {str(e)}")
121
+ return {}
122
+
123
+ def plot_price_volume_chart(df: pd.DataFrame) -> go.Figure:
124
+ """Create an interactive price and volume chart."""
125
+ try:
126
+ fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
127
+ vertical_spacing=0.03,
128
+ row_heights=[0.7, 0.3])
129
+
130
+ fig.add_trace(go.Candlestick(x=df['date'],
131
+ open=df['Open'],
132
+ high=df['High'],
133
+ low=df['Low'],
134
+ close=df['nav'],
135
+ name='Price'),
136
+ row=1, col=1)
137
+
138
+ fig.add_trace(go.Bar(x=df['date'],
139
+ y=df['volume'],
140
+ name='Volume'),
141
+ row=2, col=1)
142
+
143
+ fig.update_layout(
144
+ title='Price and Volume Analysis',
145
+ yaxis_title='Price',
146
+ yaxis2_title='Volume',
147
+ height=800,
148
+ template='plotly_white'
149
+ )
150
+
151
+ return fig
152
+ except Exception as e:
153
+ st.error(f"Error creating price-volume chart: {str(e)}")
154
+ return None
155
+
156
+ def plot_returns_distribution(returns: pd.Series) -> go.Figure:
157
+ """Create an interactive returns distribution plot."""
158
+ try:
159
+ fig = go.Figure()
160
+
161
+ # Actual returns distribution
162
+ fig.add_trace(go.Histogram(
163
+ x=returns,
164
+ name='Actual Returns',
165
+ nbinsx=50,
166
+ histnorm='probability'
167
+ ))
168
+
169
+ # Normal distribution overlay
170
+ x_range = np.linspace(returns.min(), returns.max(), 100)
171
+ normal_dist = stats.norm.pdf(x_range, returns.mean(), returns.std())
172
+
173
+ fig.add_trace(go.Scatter(
174
+ x=x_range,
175
+ y=normal_dist,
176
+ name='Normal Distribution',
177
+ line=dict(color='red')
178
+ ))
179
+
180
+ fig.update_layout(
181
+ title='Returns Distribution Analysis',
182
+ xaxis_title='Returns',
183
+ yaxis_title='Probability',
184
+ barmode='overlay',
185
+ showlegend=True,
186
+ template='plotly_white'
187
+ )
188
+
189
+ return fig
190
+ except Exception as e:
191
+ st.error(f"Error creating returns distribution plot: {str(e)}")
192
+ return None
193
+
194
+ def plot_rolling_metrics(df: pd.DataFrame, window: int = 30) -> go.Figure:
195
+ """Create rolling metrics visualization with confidence bands."""
196
+ try:
197
+ rolling_returns = df['daily_returns'].rolling(window=window)
198
+ rolling_vol = rolling_returns.std() * np.sqrt(252)
199
+ rolling_mean = rolling_returns.mean() * 252
200
+ rolling_sharpe = rolling_mean / (rolling_returns.std() * np.sqrt(252))
201
+
202
+ fig = go.Figure()
203
+
204
+ # Add rolling volatility with confidence bands
205
+ vol_std = rolling_vol.std()
206
+ fig.add_trace(go.Scatter(
207
+ x=df['date'],
208
+ y=rolling_vol + 2*vol_std,
209
+ fill=None,
210
+ mode='lines',
211
+ line_color='rgba(0,100,80,0.2)',
212
+ name='Volatility Upper Band'
213
+ ))
214
+
215
+ fig.add_trace(go.Scatter(
216
+ x=df['date'],
217
+ y=rolling_vol - 2*vol_std,
218
+ fill='tonexty',
219
+ mode='lines',
220
+ line_color='rgba(0,100,80,0.2)',
221
+ name='Volatility Lower Band'
222
+ ))
223
+
224
+ fig.add_trace(go.Scatter(
225
+ x=df['date'],
226
+ y=rolling_vol,
227
+ name='Rolling Volatility',
228
+ line=dict(color='rgb(0,100,80)')
229
+ ))
230
+
231
+ fig.add_trace(go.Scatter(
232
+ x=df['date'],
233
+ y=rolling_sharpe,
234
+ name='Rolling Sharpe Ratio',
235
+ yaxis='y2',
236
+ line=dict(color='rgb(200,30,30)')
237
+ ))
238
+
239
+ fig.update_layout(
240
+ title=f'Rolling Metrics (Window: {window} days)',
241
+ yaxis=dict(title='Annualized Volatility'),
242
+ yaxis2=dict(title='Sharpe Ratio', overlaying='y', side='right'),
243
+ showlegend=True,
244
+ height=600,
245
+ template='plotly_white'
246
+ )
247
+
248
+ return fig
249
+ except Exception as e:
250
+ st.error(f"Error creating rolling metrics plot: {str(e)}")
251
+ return None
252
+
253
+ def plot_comparative_analysis(dfs: Dict[str, pd.DataFrame]) -> List[go.Figure]:
254
+ """Create comparative analysis plots."""
255
+ try:
256
+ # Normalize all fund values to 100
257
+ normalized_dfs = {}
258
+ for name, df in dfs.items():
259
+ normalized_dfs[name] = df.copy()
260
+ normalized_dfs[name]['normalized_nav'] = df['nav'] / df['nav'].iloc[0] * 100
261
+
262
+ # Create comparative performance plot
263
+ perf_fig = go.Figure()
264
+ for name, df in normalized_dfs.items():
265
+ perf_fig.add_trace(go.Scatter(
266
+ x=df['date'],
267
+ y=df['normalized_nav'],
268
+ name=name,
269
+ mode='lines'
270
+ ))
271
+
272
+ perf_fig.update_layout(
273
+ title='Comparative Performance Analysis',
274
+ xaxis_title='Date',
275
+ yaxis_title='Normalized Value (Base=100)',
276
+ template='plotly_white'
277
+ )
278
+
279
+ # Create correlation heatmap
280
+ returns_df = pd.DataFrame()
281
+ for name, df in dfs.items():
282
+ returns_df[name] = df['nav'].pct_change()
283
+
284
+ corr_matrix = returns_df.corr()
285
+
286
+ corr_fig = go.Figure(data=go.Heatmap(
287
+ z=corr_matrix,
288
+ x=corr_matrix.columns,
289
+ y=corr_matrix.columns,
290
+ colorscale='RdBu',
291
+ zmin=-1,
292
+ zmax=1
293
+ ))
294
+
295
+ corr_fig.update_layout(
296
+ title='Returns Correlation Matrix',
297
+ template='plotly_white'
298
+ )
299
+
300
+ return [perf_fig, corr_fig]
301
+ except Exception as e:
302
+ st.error(f"Error creating comparative analysis plots: {str(e)}")
303
+ return []
304
+
305
+ def plot_risk_analytics(df: pd.DataFrame) -> List[go.Figure]:
306
+ """Create risk analytics plots."""
307
+ try:
308
+ returns = df['nav'].pct_change()
309
+
310
+ # Create drawdown plot
311
+ cum_returns = (1 + returns).cumprod()
312
+ rolling_max = cum_returns.cummax()
313
+ drawdowns = (cum_returns - rolling_max) / rolling_max
314
+
315
+ drawdown_fig = go.Figure()
316
+ drawdown_fig.add_trace(go.Scatter(
317
+ x=df['date'],
318
+ y=drawdowns,
319
+ fill='tozeroy',
320
+ name='Drawdown'
321
+ ))
322
+
323
+ drawdown_fig.update_layout(
324
+ title='Historical Drawdown Analysis',
325
+ xaxis_title='Date',
326
+ yaxis_title='Drawdown',
327
+ template='plotly_white'
328
+ )
329
+
330
+ # Create risk-return scatter plot
331
+ rolling_windows = [30, 60, 90, 180, 252]
332
+ risk_return_data = []
333
+
334
+ for window in rolling_windows:
335
+ rolling_returns = returns.rolling(window=window)
336
+ risk = rolling_returns.std() * np.sqrt(252)
337
+ ret = rolling_returns.mean() * 252
338
+ risk_return_data.append({
339
+ 'window': f'{window} days',
340
+ 'risk': risk.mean(),
341
+ 'return': ret.mean()
342
+ })
343
+
344
+ risk_return_df = pd.DataFrame(risk_return_data)
345
+
346
+ risk_return_fig = px.scatter(
347
+ risk_return_df,
348
+ x='risk',
349
+ y='return',
350
+ text='window',
351
+ title='Risk-Return Analysis Across Different Time Windows'
352
+ )
353
+
354
+ risk_return_fig.update_traces(textposition='top center')
355
+ risk_return_fig.update_layout(template='plotly_white')
356
+
357
+ return [drawdown_fig, risk_return_fig]
358
+ except Exception as e:
359
+ st.error(f"Error creating risk analytics plots: {str(e)}")
360
+ return []
361
+
362
+ def main():
363
+ st.title("📊 Advanced Mutual Fund Analytics Platform")
364
+
365
+ st.markdown("""
366
+ ### Professional-Grade Investment Analysis Tool
367
+ This platform provides comprehensive mutual fund analytics with advanced risk metrics,
368
+ interactive visualizations, and comparative analysis capabilities.
369
+ """)
370
+
371
+ # Sidebar controls
372
+ st.sidebar.header("Analysis Controls")
373
+
374
+ analysis_type = st.sidebar.selectbox(
375
+ "Select Analysis Type",
376
+ ["Single Fund Analysis", "Comparative Analysis", "Risk Analytics"]
377
+ )
378
+
379
+ # Date range selection
380
+ col1, col2 = st.sidebar.columns(2)
381
+ with col1:
382
+ start_date = st.date_input(
383
+ "Start Date",
384
+ datetime.date.today() - relativedelta(years=3)
385
+ )
386
+ with col2:
387
+ end_date = st.date_input(
388
+ "End Date",
389
+ datetime.date.today()
390
+ )
391
+
392
+ if analysis_type == "Single Fund Analysis":
393
+ st.header("Single Fund Analysis")
394
+
395
+ input_type = st.radio(
396
+ "Select Input Type",
397
+ ["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"]
398
+ )
399
+
400
+ if input_type == "Yahoo Finance Ticker":
401
+ fund_id = st.text_input("Enter Yahoo Finance Ticker", "0P0000XW8F.BO")
402
+ if st.button("Analyze Fund"):
403
+ with st.spinner("Fetching and analyzing data..."):
404
+ df = load_yahoo_finance_data(fund_id, start_date, end_date)
405
+ if df is not None:
406
+ df['daily_returns'] = df['nav'].pct_change()
407
+
408
+ metrics = calculate_risk_metrics(df['daily_returns'].dropna())
409
+
410
+ # Display metrics in a clean format
411
+ col1, col2, col3, col4 = st.columns(4)
412
+ with col1:
413
+ st.metric("Annualized Volatility", f"{metrics['volatility']:.2%}")
414
+ st.metric("Sharpe Ratio", f"{metrics['sharpe_ratio']:.2f}")
415
+ with col2:
416
+ st.metric("Maximum Drawdown", f"{metrics['max_drawdown']:.2%}")
417
+ st.metric("Value at Risk (95%)", f"{metrics['var_95']:.2%}")
418
+ with col3:
419
+ st.metric("Positive Days", f"{metrics['positive_days']:.1f}%")
420
+ st.metric("Average Daily Gain", f"{metrics['avg_gain']:.2%}")
421
+ with col4:
422
+ st.metric("Negative Days", f"{metrics['negative_days']:.1f}%")
423
+ st.metric("Average Daily Loss", f"{metrics['avg_loss']:.2%}")
424
+
425
+ # Create tabs for different visualizations
426
+ tab1, tab2, tab3 = st.tabs(["Price Analysis", "Returns Analysis", "Risk Metrics"])
427
+
428
+ with tab1:
429
+ if 'Open' in df.columns:
430
+ price_vol_fig = plot_price_volume_chart(df)
431
+ if price_vol_fig:
432
+ st.plotly_chart(price_vol_fig, use_container_width=True)
433
+
434
+ with tab2:
435
+ returns_dist_fig = plot_returns_distribution(df['daily_returns'].dropna())
436
+ if returns_dist_fig:
437
+ st.plotly_chart(returns_dist_fig, use_container_width=True)
438
+
439
+ with tab3:
440
+ window = st.slider("Rolling Window (days)", 10, 252, 30)
441
+ rolling_fig = plot_rolling_metrics(df, window)
442
+ if rolling_fig:
443
+ st.plotly_chart(rolling_fig, use_container_width=True)
444
+
445
+ else:
446
+ fund_code = st.text_input("Enter Mutual Fund Code", "118989")
447
+ if st.button("Analyze Fund"):
448
+ with st.spinner("Fetching and analyzing data..."):
449
+ df = fetch_mutual_fund_data(fund_code)
450
+ if df is not None:
451
+ df['daily_returns'] = df['nav'].pct_change()
452
+ # Perform the same analysis as above
453
+ metrics = calculate_risk_metrics(df['daily_returns'].dropna())
454
+
455
+ # Display metrics and charts (same as above)
456
+ col1, col2, col3, col4 = st.columns(4)
457
+ with col1:
458
+ st.metric("Annualized Volatility", f"{metrics['volatility']:.2%}")
459
+ st.metric("Sharpe Ratio", f"{metrics['sharpe_ratio']:.2f}")
460
+ with col2:
461
+ st.metric("Maximum Drawdown", f"{metrics['max_drawdown']:.2%}")
462
+ st.metric("Value at Risk (95%)", f"{metrics['var_95']:.2%}")
463
+ with col3:
464
+ st.metric("Positive Days", f"{metrics['positive_days']:.1f}%")
465
+ st.metric("Average Daily Gain", f"{metrics['avg_gain']:.2%}")
466
+ with col4:
467
+ st.metric("Negative Days", f"{metrics['negative_days']:.1f}%")
468
+ st.metric("Average Daily Loss", f"{metrics['avg_loss']:.2%}")
469
+
470
+ tab1, tab2 = st.tabs(["Returns Analysis", "Risk Metrics"])
471
+
472
+ with tab1:
473
+ returns_dist_fig = plot_returns_distribution(df['daily_returns'].dropna())
474
+ if returns_dist_fig:
475
+ st.plotly_chart(returns_dist_fig, use_container_width=True)
476
+
477
+ with tab2:
478
+ window = st.slider("Rolling Window (days)", 10, 252, 30)
479
+ rolling_fig = plot_rolling_metrics(df, window)
480
+ if rolling_fig:
481
+ st.plotly_chart(rolling_fig, use_container_width=True)
482
+
483
+ elif analysis_type == "Comparative Analysis":
484
+ st.header("Comparative Analysis")
485
+
486
+ num_funds = st.number_input("Number of funds to compare", min_value=2, max_value=5, value=2)
487
+
488
+ funds_data = {}
489
+
490
+ for i in range(num_funds):
491
+ st.subheader(f"Fund {i + 1}")
492
+ input_type = st.radio(
493
+ f"Select Input Type for Fund {i + 1}",
494
+ ["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"],
495
+ key=f"input_type_{i}"
496
+ )
497
+
498
+ if input_type == "Yahoo Finance Ticker":
499
+ fund_id = st.text_input(f"Enter Yahoo Finance Ticker {i + 1}",
500
+ value=f"0P0000XW8F.BO" if i == 0 else "",
501
+ key=f"yahoo_{i}")
502
+ fund_name = st.text_input(f"Enter Fund Name {i + 1}",
503
+ value=f"Fund {i + 1}",
504
+ key=f"name_{i}")
505
+ funds_data[fund_name] = {'id': fund_id, 'type': 'yahoo'}
506
+ else:
507
+ fund_id = st.text_input(f"Enter Mutual Fund Code {i + 1}",
508
+ value="118989" if i == 0 else "",
509
+ key=f"mf_{i}")
510
+ fund_name = st.text_input(f"Enter Fund Name {i + 1}",
511
+ value=f"Fund {i + 1}",
512
+ key=f"name_{i}")
513
+ funds_data[fund_name] = {'id': fund_id, 'type': 'mf'}
514
+
515
+ if st.button("Compare Funds"):
516
+ with st.spinner("Fetching and comparing data..."):
517
+ dfs = {}
518
+ for name, info in funds_data.items():
519
+ if info['type'] == 'yahoo':
520
+ df = load_yahoo_finance_data(info['id'], start_date, end_date)
521
+ else:
522
+ df = fetch_mutual_fund_data(info['id'])
523
+
524
+ if df is not None:
525
+ dfs[name] = df
526
+
527
+ if len(dfs) > 1:
528
+ comparison_figs = plot_comparative_analysis(dfs)
529
+ if comparison_figs:
530
+ st.subheader("Comparative Performance")
531
+ st.plotly_chart(comparison_figs[0], use_container_width=True)
532
+
533
+ st.subheader("Correlation Analysis")
534
+ st.plotly_chart(comparison_figs[1], use_container_width=True)
535
+
536
+ else: # Risk Analytics
537
+ st.header("Risk Analytics")
538
+
539
+ input_type = st.radio(
540
+ "Select Input Type",
541
+ ["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"]
542
+ )
543
+
544
+ if input_type == "Yahoo Finance Ticker":
545
+ fund_id = st.text_input("Enter Yahoo Finance Ticker", "0P0000XW8F.BO")
546
+ else:
547
+ fund_id = st.text_input("Enter Mutual Fund Code", "118989")
548
+
549
+ if st.button("Analyze Risk"):
550
+ with st.spinner("Performing risk analysis..."):
551
+ df = load_yahoo_finance_data(fund_id, start_date, end_date) if input_type == "Yahoo Finance Ticker" else fetch_mutual_fund_data(fund_id)
552
+
553
+ if df is not None:
554
+ risk_figs = plot_risk_analytics(df)
555
+ if risk_figs:
556
+ st.subheader("Drawdown Analysis")
557
+ st.plotly_chart(risk_figs[0], use_container_width=True)
558
+
559
+ st.subheader("Risk-Return Analysis")
560
+ st.plotly_chart(risk_figs[1], use_container_width=True)
561
+
562
+ if __name__ == "__main__":
563
+ main()