NEWSPCE / src /app.py
pranit144's picture
Update src/app.py
04a8c30 verified
raw
history blame
21.5 kB
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
@st.cache_data(ttl=3600)
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
@st.cache_data(ttl=3600)
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()