Spaces:
Sleeping
Sleeping
import streamlit as st | |
import pandas as pd | |
import numpy as np | |
import matplotlib.pyplot as plt | |
import datetime | |
from dateutil.relativedelta import relativedelta | |
import plotly.express as px | |
import plotly.graph_objects as go | |
import plotly.figure_factory as ff | |
from plotly.subplots import make_subplots | |
import yfinance as yf | |
import seaborn as sns | |
from scipy import stats | |
from typing import Dict, Optional, List | |
import warnings | |
warnings.filterwarnings('ignore') | |
# Try importing mftool, handle if not available | |
try: | |
from mftool import Mftool | |
mftool_available = True | |
except ImportError: | |
mftool_available = False | |
try: | |
from yahooquery import Ticker | |
yahooquery_available = True | |
except ImportError: | |
yahooquery_available = False | |
# Set page configuration | |
st.set_page_config( | |
page_title="Mutual Fund Analytics Suite", | |
page_icon="π", | |
layout="wide", | |
initial_sidebar_state="expanded" | |
) | |
# Custom CSS styling | |
st.markdown(""" | |
<style> | |
.main { | |
padding: 2rem; | |
} | |
.stButton>button { | |
width: 100%; | |
background-color: #1f77b4; | |
color: white; | |
} | |
.reportview-container .main .block-container { | |
padding-top: 2rem; | |
} | |
h1 { | |
color: #1f77b4; | |
} | |
.stMetric { | |
background-color: #f8f9fa; | |
padding: 1rem; | |
border-radius: 5px; | |
box-shadow: 0 2px 4px rgba(0,0,0,0.1); | |
} | |
.stAlert { | |
padding: 1rem; | |
margin: 1rem 0; | |
border-radius: 0.5rem; | |
} | |
</style> | |
""", unsafe_allow_html=True) | |
# Cache data fetching functions | |
def fetch_mutual_fund_data(mutual_fund_code: str) -> Optional[pd.DataFrame]: | |
"""Fetch mutual fund data from mftool.""" | |
try: | |
mf = Mftool() | |
df = (mf.get_scheme_historical_nav(mutual_fund_code, as_Dataframe=True) | |
.reset_index() | |
.assign(nav=lambda x: x['nav'].astype(float), | |
date=lambda x: pd.to_datetime(x['date'], format='%d-%m-%Y')) | |
.sort_values('date') | |
.reset_index(drop=True)) | |
return df | |
except Exception as e: | |
st.error(f"Error fetching mutual fund data: {str(e)}") | |
return None | |
def load_yahoo_finance_data(ticker_symbol: str, start_date: datetime.date, end_date: datetime.date) -> Optional[pd.DataFrame]: | |
"""Fetch data from Yahoo Finance.""" | |
try: | |
data = yf.download(ticker_symbol, start=start_date, end=end_date) | |
data = data.reset_index() | |
data = data.rename(columns={'Date': 'date', 'Close': 'nav', 'Volume': 'volume'}) | |
return data | |
except Exception as e: | |
st.error(f"Error fetching Yahoo Finance data: {str(e)}") | |
return None | |
def calculate_risk_metrics(returns: pd.Series) -> Dict[str, float]: | |
"""Calculate comprehensive risk metrics for the fund.""" | |
try: | |
metrics = { | |
'volatility': returns.std() * np.sqrt(252), | |
'sharpe_ratio': (returns.mean() * 252) / (returns.std() * np.sqrt(252)), | |
'sortino_ratio': (returns.mean() * 252) / (returns[returns < 0].std() * np.sqrt(252)), | |
'max_drawdown': (1 - (1 + returns).cumprod() / (1 + returns).cumprod().cummax()).max(), | |
'skewness': stats.skew(returns), | |
'kurtosis': stats.kurtosis(returns), | |
'var_95': np.percentile(returns, 5), | |
'cvar_95': returns[returns <= np.percentile(returns, 5)].mean(), | |
'positive_days': (returns > 0).mean() * 100, | |
'negative_days': (returns < 0).mean() * 100, | |
'avg_gain': returns[returns > 0].mean(), | |
'avg_loss': returns[returns < 0].mean() | |
} | |
return metrics | |
except Exception as e: | |
st.error(f"Error calculating risk metrics: {str(e)}") | |
return {} | |
def plot_price_volume_chart(df: pd.DataFrame) -> go.Figure: | |
"""Create an interactive price and volume chart.""" | |
try: | |
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, | |
vertical_spacing=0.03, | |
row_heights=[0.7, 0.3]) | |
fig.add_trace(go.Candlestick(x=df['date'], | |
open=df['Open'], | |
high=df['High'], | |
low=df['Low'], | |
close=df['nav'], | |
name='Price'), | |
row=1, col=1) | |
fig.add_trace(go.Bar(x=df['date'], | |
y=df['volume'], | |
name='Volume'), | |
row=2, col=1) | |
fig.update_layout( | |
title='Price and Volume Analysis', | |
yaxis_title='Price', | |
yaxis2_title='Volume', | |
height=800, | |
template='plotly_white' | |
) | |
return fig | |
except Exception as e: | |
st.error(f"Error creating price-volume chart: {str(e)}") | |
return None | |
def plot_returns_distribution(returns: pd.Series) -> go.Figure: | |
"""Create an interactive returns distribution plot.""" | |
try: | |
fig = go.Figure() | |
# Actual returns distribution | |
fig.add_trace(go.Histogram( | |
x=returns, | |
name='Actual Returns', | |
nbinsx=50, | |
histnorm='probability' | |
)) | |
# Normal distribution overlay | |
x_range = np.linspace(returns.min(), returns.max(), 100) | |
normal_dist = stats.norm.pdf(x_range, returns.mean(), returns.std()) | |
fig.add_trace(go.Scatter( | |
x=x_range, | |
y=normal_dist, | |
name='Normal Distribution', | |
line=dict(color='red') | |
)) | |
fig.update_layout( | |
title='Returns Distribution Analysis', | |
xaxis_title='Returns', | |
yaxis_title='Probability', | |
barmode='overlay', | |
showlegend=True, | |
template='plotly_white' | |
) | |
return fig | |
except Exception as e: | |
st.error(f"Error creating returns distribution plot: {str(e)}") | |
return None | |
def plot_rolling_metrics(df: pd.DataFrame, window: int = 30) -> go.Figure: | |
"""Create rolling metrics visualization with confidence bands.""" | |
try: | |
rolling_returns = df['daily_returns'].rolling(window=window) | |
rolling_vol = rolling_returns.std() * np.sqrt(252) | |
rolling_mean = rolling_returns.mean() * 252 | |
rolling_sharpe = rolling_mean / (rolling_returns.std() * np.sqrt(252)) | |
fig = go.Figure() | |
# Add rolling volatility with confidence bands | |
vol_std = rolling_vol.std() | |
fig.add_trace(go.Scatter( | |
x=df['date'], | |
y=rolling_vol + 2*vol_std, | |
fill=None, | |
mode='lines', | |
line_color='rgba(0,100,80,0.2)', | |
name='Volatility Upper Band' | |
)) | |
fig.add_trace(go.Scatter( | |
x=df['date'], | |
y=rolling_vol - 2*vol_std, | |
fill='tonexty', | |
mode='lines', | |
line_color='rgba(0,100,80,0.2)', | |
name='Volatility Lower Band' | |
)) | |
fig.add_trace(go.Scatter( | |
x=df['date'], | |
y=rolling_vol, | |
name='Rolling Volatility', | |
line=dict(color='rgb(0,100,80)') | |
)) | |
fig.add_trace(go.Scatter( | |
x=df['date'], | |
y=rolling_sharpe, | |
name='Rolling Sharpe Ratio', | |
yaxis='y2', | |
line=dict(color='rgb(200,30,30)') | |
)) | |
fig.update_layout( | |
title=f'Rolling Metrics (Window: {window} days)', | |
yaxis=dict(title='Annualized Volatility'), | |
yaxis2=dict(title='Sharpe Ratio', overlaying='y', side='right'), | |
showlegend=True, | |
height=600, | |
template='plotly_white' | |
) | |
return fig | |
except Exception as e: | |
st.error(f"Error creating rolling metrics plot: {str(e)}") | |
return None | |
def plot_comparative_analysis(dfs: Dict[str, pd.DataFrame]) -> List[go.Figure]: | |
"""Create comparative analysis plots.""" | |
try: | |
# Normalize all fund values to 100 | |
normalized_dfs = {} | |
for name, df in dfs.items(): | |
normalized_dfs[name] = df.copy() | |
normalized_dfs[name]['normalized_nav'] = df['nav'] / df['nav'].iloc[0] * 100 | |
# Create comparative performance plot | |
perf_fig = go.Figure() | |
for name, df in normalized_dfs.items(): | |
perf_fig.add_trace(go.Scatter( | |
x=df['date'], | |
y=df['normalized_nav'], | |
name=name, | |
mode='lines' | |
)) | |
perf_fig.update_layout( | |
title='Comparative Performance Analysis', | |
xaxis_title='Date', | |
yaxis_title='Normalized Value (Base=100)', | |
template='plotly_white' | |
) | |
# Create correlation heatmap | |
returns_df = pd.DataFrame() | |
for name, df in dfs.items(): | |
returns_df[name] = df['nav'].pct_change() | |
corr_matrix = returns_df.corr() | |
corr_fig = go.Figure(data=go.Heatmap( | |
z=corr_matrix, | |
x=corr_matrix.columns, | |
y=corr_matrix.columns, | |
colorscale='RdBu', | |
zmin=-1, | |
zmax=1 | |
)) | |
corr_fig.update_layout( | |
title='Returns Correlation Matrix', | |
template='plotly_white' | |
) | |
return [perf_fig, corr_fig] | |
except Exception as e: | |
st.error(f"Error creating comparative analysis plots: {str(e)}") | |
return [] | |
def plot_risk_analytics(df: pd.DataFrame) -> List[go.Figure]: | |
"""Create risk analytics plots.""" | |
try: | |
returns = df['nav'].pct_change() | |
# Create drawdown plot | |
cum_returns = (1 + returns).cumprod() | |
rolling_max = cum_returns.cummax() | |
drawdowns = (cum_returns - rolling_max) / rolling_max | |
drawdown_fig = go.Figure() | |
drawdown_fig.add_trace(go.Scatter( | |
x=df['date'], | |
y=drawdowns, | |
fill='tozeroy', | |
name='Drawdown' | |
)) | |
drawdown_fig.update_layout( | |
title='Historical Drawdown Analysis', | |
xaxis_title='Date', | |
yaxis_title='Drawdown', | |
template='plotly_white' | |
) | |
# Create risk-return scatter plot | |
rolling_windows = [30, 60, 90, 180, 252] | |
risk_return_data = [] | |
for window in rolling_windows: | |
rolling_returns = returns.rolling(window=window) | |
risk = rolling_returns.std() * np.sqrt(252) | |
ret = rolling_returns.mean() * 252 | |
risk_return_data.append({ | |
'window': f'{window} days', | |
'risk': risk.mean(), | |
'return': ret.mean() | |
}) | |
risk_return_df = pd.DataFrame(risk_return_data) | |
risk_return_fig = px.scatter( | |
risk_return_df, | |
x='risk', | |
y='return', | |
text='window', | |
title='Risk-Return Analysis Across Different Time Windows' | |
) | |
risk_return_fig.update_traces(textposition='top center') | |
risk_return_fig.update_layout(template='plotly_white') | |
return [drawdown_fig, risk_return_fig] | |
except Exception as e: | |
st.error(f"Error creating risk analytics plots: {str(e)}") | |
return [] | |
def main(): | |
st.title("π Advanced Mutual Fund Analytics Platform") | |
st.markdown(""" | |
### Professional-Grade Investment Analysis Tool | |
This platform provides comprehensive mutual fund analytics with advanced risk metrics, | |
interactive visualizations, and comparative analysis capabilities. | |
""") | |
# Sidebar controls | |
st.sidebar.header("Analysis Controls") | |
analysis_type = st.sidebar.selectbox( | |
"Select Analysis Type", | |
["Single Fund Analysis", "Comparative Analysis", "Risk Analytics"] | |
) | |
# Date range selection | |
col1, col2 = st.sidebar.columns(2) | |
with col1: | |
start_date = st.date_input( | |
"Start Date", | |
datetime.date.today() - relativedelta(years=3) | |
) | |
with col2: | |
end_date = st.date_input( | |
"End Date", | |
datetime.date.today() | |
) | |
if analysis_type == "Single Fund Analysis": | |
st.header("Single Fund Analysis") | |
input_type = st.radio( | |
"Select Input Type", | |
["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"] | |
) | |
if input_type == "Yahoo Finance Ticker": | |
fund_id = st.text_input("Enter Yahoo Finance Ticker", "0P0000XW8F.BO") | |
if st.button("Analyze Fund"): | |
with st.spinner("Fetching and analyzing data..."): | |
df = load_yahoo_finance_data(fund_id, start_date, end_date) | |
if df is not None: | |
df['daily_returns'] = df['nav'].pct_change() | |
metrics = calculate_risk_metrics(df['daily_returns'].dropna()) | |
# Display metrics in a clean format | |
col1, col2, col3, col4 = st.columns(4) | |
with col1: | |
st.metric("Annualized Volatility", f"{metrics['volatility']:.2%}") | |
st.metric("Sharpe Ratio", f"{metrics['sharpe_ratio']:.2f}") | |
with col2: | |
st.metric("Maximum Drawdown", f"{metrics['max_drawdown']:.2%}") | |
st.metric("Value at Risk (95%)", f"{metrics['var_95']:.2%}") | |
with col3: | |
st.metric("Positive Days", f"{metrics['positive_days']:.1f}%") | |
st.metric("Average Daily Gain", f"{metrics['avg_gain']:.2%}") | |
with col4: | |
st.metric("Negative Days", f"{metrics['negative_days']:.1f}%") | |
st.metric("Average Daily Loss", f"{metrics['avg_loss']:.2%}") | |
# Create tabs for different visualizations | |
tab1, tab2, tab3 = st.tabs(["Price Analysis", "Returns Analysis", "Risk Metrics"]) | |
with tab1: | |
if 'Open' in df.columns: | |
price_vol_fig = plot_price_volume_chart(df) | |
if price_vol_fig: | |
st.plotly_chart(price_vol_fig, use_container_width=True) | |
with tab2: | |
returns_dist_fig = plot_returns_distribution(df['daily_returns'].dropna()) | |
if returns_dist_fig: | |
st.plotly_chart(returns_dist_fig, use_container_width=True) | |
with tab3: | |
window = st.slider("Rolling Window (days)", 10, 252, 30) | |
rolling_fig = plot_rolling_metrics(df, window) | |
if rolling_fig: | |
st.plotly_chart(rolling_fig, use_container_width=True) | |
else: | |
fund_code = st.text_input("Enter Mutual Fund Code", "118989") | |
if st.button("Analyze Fund"): | |
with st.spinner("Fetching and analyzing data..."): | |
df = fetch_mutual_fund_data(fund_code) | |
if df is not None: | |
df['daily_returns'] = df['nav'].pct_change() | |
# Perform the same analysis as above | |
metrics = calculate_risk_metrics(df['daily_returns'].dropna()) | |
# Display metrics and charts (same as above) | |
col1, col2, col3, col4 = st.columns(4) | |
with col1: | |
st.metric("Annualized Volatility", f"{metrics['volatility']:.2%}") | |
st.metric("Sharpe Ratio", f"{metrics['sharpe_ratio']:.2f}") | |
with col2: | |
st.metric("Maximum Drawdown", f"{metrics['max_drawdown']:.2%}") | |
st.metric("Value at Risk (95%)", f"{metrics['var_95']:.2%}") | |
with col3: | |
st.metric("Positive Days", f"{metrics['positive_days']:.1f}%") | |
st.metric("Average Daily Gain", f"{metrics['avg_gain']:.2%}") | |
with col4: | |
st.metric("Negative Days", f"{metrics['negative_days']:.1f}%") | |
st.metric("Average Daily Loss", f"{metrics['avg_loss']:.2%}") | |
tab1, tab2 = st.tabs(["Returns Analysis", "Risk Metrics"]) | |
with tab1: | |
returns_dist_fig = plot_returns_distribution(df['daily_returns'].dropna()) | |
if returns_dist_fig: | |
st.plotly_chart(returns_dist_fig, use_container_width=True) | |
with tab2: | |
window = st.slider("Rolling Window (days)", 10, 252, 30) | |
rolling_fig = plot_rolling_metrics(df, window) | |
if rolling_fig: | |
st.plotly_chart(rolling_fig, use_container_width=True) | |
elif analysis_type == "Comparative Analysis": | |
st.header("Comparative Analysis") | |
num_funds = st.number_input("Number of funds to compare", min_value=2, max_value=5, value=2) | |
funds_data = {} | |
for i in range(num_funds): | |
st.subheader(f"Fund {i + 1}") | |
input_type = st.radio( | |
f"Select Input Type for Fund {i + 1}", | |
["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"], | |
key=f"input_type_{i}" | |
) | |
if input_type == "Yahoo Finance Ticker": | |
fund_id = st.text_input(f"Enter Yahoo Finance Ticker {i + 1}", | |
value=f"0P0000XW8F.BO" if i == 0 else "", | |
key=f"yahoo_{i}") | |
fund_name = st.text_input(f"Enter Fund Name {i + 1}", | |
value=f"Fund {i + 1}", | |
key=f"name_{i}") | |
funds_data[fund_name] = {'id': fund_id, 'type': 'yahoo'} | |
else: | |
fund_id = st.text_input(f"Enter Mutual Fund Code {i + 1}", | |
value="118989" if i == 0 else "", | |
key=f"mf_{i}") | |
fund_name = st.text_input(f"Enter Fund Name {i + 1}", | |
value=f"Fund {i + 1}", | |
key=f"name_{i}") | |
funds_data[fund_name] = {'id': fund_id, 'type': 'mf'} | |
if st.button("Compare Funds"): | |
with st.spinner("Fetching and comparing data..."): | |
dfs = {} | |
for name, info in funds_data.items(): | |
if info['type'] == 'yahoo': | |
df = load_yahoo_finance_data(info['id'], start_date, end_date) | |
else: | |
df = fetch_mutual_fund_data(info['id']) | |
if df is not None: | |
dfs[name] = df | |
if len(dfs) > 1: | |
comparison_figs = plot_comparative_analysis(dfs) | |
if comparison_figs: | |
st.subheader("Comparative Performance") | |
st.plotly_chart(comparison_figs[0], use_container_width=True) | |
st.subheader("Correlation Analysis") | |
st.plotly_chart(comparison_figs[1], use_container_width=True) | |
else: # Risk Analytics | |
st.header("Risk Analytics") | |
input_type = st.radio( | |
"Select Input Type", | |
["Yahoo Finance Ticker", "Mutual Fund Code (Indian)"] | |
) | |
if input_type == "Yahoo Finance Ticker": | |
fund_id = st.text_input("Enter Yahoo Finance Ticker", "0P0000XW8F.BO") | |
else: | |
fund_id = st.text_input("Enter Mutual Fund Code", "118989") | |
if st.button("Analyze Risk"): | |
with st.spinner("Performing risk analysis..."): | |
df = load_yahoo_finance_data(fund_id, start_date, end_date) if input_type == "Yahoo Finance Ticker" else fetch_mutual_fund_data(fund_id) | |
if df is not None: | |
risk_figs = plot_risk_analytics(df) | |
if risk_figs: | |
st.subheader("Drawdown Analysis") | |
st.plotly_chart(risk_figs[0], use_container_width=True) | |
st.subheader("Risk-Return Analysis") | |
st.plotly_chart(risk_figs[1], use_container_width=True) | |
if __name__ == "__main__": | |
main() | |