Files changed (1) hide show
  1. app.py +584 -563
app.py CHANGED
@@ -1,563 +1,584 @@
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()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()