Space22 / app.py
QuantumLearner's picture
Update app.py
eccc942 verified
import numpy as np
import streamlit as st
import pandas as pd
import yfinance as yf
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from sklearn.preprocessing import QuantileTransformer
from copulae import StudentCopula
from scipy.stats import rankdata
from copulas.bivariate import Clayton
# Streamlit app
st.set_page_config(layout="wide")
st.title("Portfolio Analysis with Copulas")
st.write("""
This app evaluates how a portfolio performs under significant market changes by using copula models.
It simulates various market scenarios to analyze the dependencies between different assets and their combined impact on the portfolio.
Users can assess potential outcomes in terms of best, worst, and mean case scenarios.
""")
with st.expander("Methodology", expanded=False):
st.markdown("## Simulating Market Drop Scenarios: Best, Worst, and Mean Cases")
#st.markdown("## Transforming Returns")
st.markdown("""
To prepare the returns for copula modeling, we transform them to a uniform distribution using quantile transformation.
This is to ensure that the data fits within the range required by the copula model.
""")
st.latex(r"""
u_i = \frac{\text{rank}(R_i)}{n + 1}
""")
#st.markdown("## Fitting the Copula Model")
st.markdown("""
Next, we fit a multivariate Student-t copula to the transformed data. The copula model captures the dependencies between the stock returns to understand the joint behavior of the assets.
""")
st.latex(r"""
C_{\nu}(u_1, u_2, \ldots, u_d) = t_{\nu, \Sigma}(t_{\nu}^{-1}(u_1), t_{\nu}^{-1}(u_2), \ldots, t_{\nu}^{-1}(u_d))
""")
#st.markdown("## Simulating Scenarios")
st.markdown("""
Using the fitted copula model, we simulate a large number of return scenarios. This simulation helps us explore potential future outcomes
and assess the portfolio's risk under various market conditions.
""")
#st.markdown("## Market Drop Scenarios")
st.markdown("""
We identify scenarios where the specified ticker drops by the given percentage. By analyzing these scenarios, we can evaluate how the portfolio performs under stress.
""")
#st.markdown("## Portfolio Returns")
st.markdown("""
For each identified scenario, we calculate the portfolio returns. The portfolio return is a weighted sum of the individual stock returns.
""")
st.latex(r"""
R_p = \sum_{i=1}^{n} w_i R_i
""")
#st.markdown("## Visualizing Results")
st.markdown("""
We visualize the simulated portfolio returns using histograms, cumulative distribution functions, and kernel density estimates.
These visualizations help us understand the distribution and characteristics of potential returns.
""")
#st.markdown("## Portfolio Price Trajectory")
st.markdown("""
Finally, we visualize the portfolio's price trajectory under the worst-case, best-case, and mean scenarios.
This helps in understanding the potential impact of market drops on the portfolio value.
""")
st.sidebar.title("Input Parameters")
# Sidebar: How to Use (closed by default)
with st.sidebar.expander("How to Use", expanded=False):
st.write("""
1. Select the date range for the analysis.
2. Adjust the market drop percentage.
3. Enter the tickers and their respective weights.
4. Select the ticker for the drop scenario.
5. Click on 'Run Analysis' to execute.
""")
# Sidebar: Ticker and Dates (open by default)
with st.sidebar.expander("Ticker and Dates", expanded=True):
portfolio_tickers = st.text_area("Enter Tickers (comma-separated)", "^GSPC,MSFT,AAPL,GOOGL,ASML",
help="Enter the tickers for the assets in the portfolio, separated by commas.").split(',')
portfolio_weights = st.text_area("Enter Weights (comma-separated) (Must add to 1)", "0.1,0.3,0.3,0.2,0.1",
help="Enter the weights for each ticker, separated by commas. Make sure they sum to 1.").split(',')
portfolio_weights = [float(weight) for weight in portfolio_weights]
start_date = st.date_input("Start Date", value=pd.to_datetime("2020-01-01"),
help="Select the start date for the analysis period.")
end_date = st.date_input("End Date", value=pd.to_datetime(pd.Timestamp.now().date() + pd.Timedelta(days=1)),
help="Select the end date for the analysis period.")
# Sidebar: Market Drop Settings (open by default)
with st.sidebar.expander("Market Drop Settings", expanded=True):
drop_ticker = st.text_input("Enter Drop Ticker", "^GSPC",
help="Enter the ticker of the asset that will experience a drop.")
market_drop_percentage = st.slider("Market Drop Percentage", min_value=-0.1, max_value=0.0, step=0.01, value=-0.05,
help="Set the percentage drop in the market for the scenario.")
num_simulations = st.number_input("Number of Simulations", min_value=1000, max_value=100000, step=1000, value=50000,
help="Set the number of simulations to run.")
rolling_window = st.number_input("Rolling Window", min_value=7, max_value=1000, step=10, value=250,
help="Set the rolling window size for tail dependence analysis.")
if len(portfolio_tickers) != len(portfolio_weights):
st.sidebar.error("The number of tickers must match the number of weights.")
run_button = st.sidebar.button("Run Analysis")
if run_button and len(portfolio_tickers) == len(portfolio_weights):
# Explanation of the Simulation and Copula Models
#st.markdown("## Simulating Market Drop Scenarios: Best, Worst, and Mean Cases")
# Define the portfolio and allocation
portfolio_allocation = dict(zip(portfolio_tickers, portfolio_weights))
symbols = [drop_ticker] + portfolio_tickers
# Fetch real data
data = yf.download(symbols, start=start_date, end=end_date)["Close"]
# Compute daily returns
returns = data.pct_change().dropna()
# Transform returns to [0,1] range using quantile transformation
quantile_transformers = {}
data_uniform = pd.DataFrame()
for symbol in returns.columns:
qt = QuantileTransformer(output_distribution='uniform')
data_uniform[symbol] = qt.fit_transform(returns[[symbol]]).flatten()
quantile_transformers[symbol] = qt
# Fit a multivariate Student-t copula
copula = StudentCopula(dim=len(data_uniform.columns))
copula.fit(data_uniform.values)
# Simulate scenarios
simulated_uniform = copula.random(num_simulations)
simulated_returns = pd.DataFrame(index=range(num_simulations), columns=data_uniform.columns)
# Transform back to the original scale
for i, symbol in enumerate(data_uniform.columns):
simulated_returns[symbol] = quantile_transformers[symbol].inverse_transform(simulated_uniform[:, i].reshape(-1, 1)).flatten()
# Identify scenarios where the specified ticker drops by the specified percentage
drop_ticker_decrease_scenarios = simulated_returns[simulated_returns[drop_ticker] <= market_drop_percentage]
if drop_ticker_decrease_scenarios.empty:
st.write(f"No scenarios found where {drop_ticker} drops by {market_drop_percentage*100:.2f}%.")
else:
# Compute portfolio returns from individual stock returns
weights = np.array([portfolio_allocation[ticker] for ticker in portfolio_tickers])
portfolio_returns = np.dot(drop_ticker_decrease_scenarios[portfolio_tickers], weights)
# Visualization for the Portfolio
last_known_prices = data[portfolio_tickers].iloc[-1]
portfolio_last_known_price = np.dot(last_known_prices, weights)
min_return = np.min(portfolio_returns)
max_return = np.max(portfolio_returns)
mean_return = np.mean(portfolio_returns)
final_min_price = portfolio_last_known_price * (1 + min_return)
final_max_price = portfolio_last_known_price * (1 + max_return)
final_mean_price = portfolio_last_known_price * (1 + mean_return)
simulated_dates = pd.date_range(start=data.index[-1], periods=31, freq='D')[1:]
min_price_trajectory = [portfolio_last_known_price] + [final_min_price] * (len(simulated_dates) - 1)
max_price_trajectory = [portfolio_last_known_price] + [final_max_price] * (len(simulated_dates) - 1)
mean_price_trajectory = [portfolio_last_known_price] + [final_mean_price] * (len(simulated_dates) - 1)
# Create subplots
fig = make_subplots(rows=1, cols=3, subplot_titles=("Histogram of Returns", "CDF of Returns", "KDE of Returns"))
# Plot 1: Histogram of Simulated Returns
fig.add_trace(
go.Histogram(x=portfolio_returns, nbinsx=50, marker=dict(color='blue'), opacity=0.75),
row=1, col=1
)
fig.update_layout(
shapes=[
dict(type="line", x0=min_return, x1=min_return, y0=0, y1=1, xref='x', yref='paper', line=dict(color="red", width=2)),
dict(type="line", x0=max_return, x1=max_return, y0=0, y1=1, xref='x', yref='paper', line=dict(color="green", width=2)),
dict(type="line", x0=mean_return, x1=mean_return, y0=0, y1=1, xref='x', yref='paper', line=dict(color="blue", width=2))
],
annotations=[
dict(x=min_return, y=0.95, xref='x', yref='paper', text="Worst", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="red")),
dict(x=max_return, y=0.95, xref='x', yref='paper', text="Best", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="green")),
dict(x=mean_return, y=0.95, xref='x', yref='paper', text="Mean", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="blue"))
]
)
# Plot 2: CDF of Simulated Returns
fig.add_trace(
go.Histogram(x=portfolio_returns, nbinsx=100, cumulative_enabled=True, histnorm='probability density', marker=dict(color='blue'), opacity=0.75),
row=1, col=2
)
fig.update_layout(
shapes=[
dict(type="line", x0=min_return, x1=min_return, y0=0, y1=1, xref='x', yref='paper', line=dict(color="red", width=2)),
dict(type="line", x0=max_return, x1=max_return, y0=0, y1=1, xref='x', yref='paper', line=dict(color="green", width=2)),
dict(type="line", x0=mean_return, x1=mean_return, y0=0, y1=1, xref='x', yref='paper', line=dict(color="blue", width=2))
],
annotations=[
dict(x=min_return, y=0.95, xref='x', yref='paper', text="Worst", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="red")),
dict(x=max_return, y=0.95, xref='x', yref='paper', text="Best", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="green")),
dict(x=mean_return, y=0.95, xref='x', yref='paper', text="Mean", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="blue"))
]
)
# Plot 3: KDE of Simulated Returns
fig.add_trace(
go.Histogram(x=portfolio_returns, nbinsx=100, histnorm='density', marker=dict(color='blue'), opacity=0.75),
row=1, col=3
)
fig.add_vline(x=min_return, line_width=2, line_dash="dash", line_color="red", row=1, col=3)
fig.add_vline(x=max_return, line_width=2, line_dash="dash", line_color="green", row=1, col=3)
fig.add_vline(x=mean_return, line_width=2, line_dash="dash", line_color="blue", row=1, col=3)
fig.add_annotation(x=min_return, y=0.95, xref='x', yref='paper', text="Worst", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="red"))
fig.add_annotation(x=max_return, y=0.95, xref='x', yref='paper', text="Best", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="green"))
fig.add_annotation(x=mean_return, y=0.95, xref='x', yref='paper', text="Mean", showarrow=True, arrowhead=7, ax=0, ay=-40, font=dict(color="blue"))
fig.update_layout(
title_text=f"Simulated Portfolio Returns Analysis given {market_drop_percentage*100:.2f}% drop in {drop_ticker}",
showlegend=False,
xaxis=dict(title="Returns"),
yaxis=dict(title="Frequency"),
xaxis2=dict(title="Returns"),
yaxis2=dict(title="Cumulative Probability"),
xaxis3=dict(title="Returns"),
yaxis3=dict(title="Density")
)
st.plotly_chart(fig)
# Plot Portfolio Price Trajectory
fig_price = go.Figure()
fig_price.add_trace(go.Scatter(x=data.index, y=(data[portfolio_tickers] * weights).sum(axis=1), mode='lines', name='Original Prices'))
fig_price.add_trace(go.Scatter(x=simulated_dates, y=min_price_trajectory, mode='lines', name='Worst-Case Scenario', line=dict(dash='dash', color='red')))
fig_price.add_trace(go.Scatter(x=simulated_dates, y=max_price_trajectory, mode='lines', name='Best-Case Scenario', line=dict(dash='dash', color='green')))
fig_price.add_trace(go.Scatter(x=simulated_dates, y=mean_price_trajectory, mode='lines', name='Mean Scenario', line=dict(dash='dash', color='blue')))
fig_price.update_layout(
title=f"Portfolio Original vs. Worst, Best, and Mean Case Scenarios given {market_drop_percentage*100:.2f}% drop in {drop_ticker}",
xaxis_title="Date",
yaxis_title="Portfolio Price"
)
fig_price.add_annotation(x=simulated_dates[-10], y=final_min_price * 0.98, text=f"{min_return*100:.2f}% (Worst Scenario)", showarrow=False, font=dict(color='red'))
fig_price.add_annotation(x=simulated_dates[-10], y=final_max_price * 1.02, text=f"{max_return*100:.2f}% (Best Scenario)", showarrow=False, font=dict(color='green'))
fig_price.add_annotation(x=simulated_dates[-10], y=final_mean_price, text=f"{mean_return*100:.2f}% (Mean Scenario)", showarrow=False, font=dict(color='blue'))
st.plotly_chart(fig_price)
# Tail Dependence Analysis
st.markdown("## Tail Dependence Analysis")
st.markdown("""
Tail dependence measures the likelihood of extreme returns occurring simultaneously across different stocks in the portfolio.
We use the Clayton copula to model the tail dependence. The parameter θ of the Clayton copula is related to the tail dependence by:
""")
with st.expander("Tail Dependence Methodology", expanded=False):
st.latex(r"""
\lambda_{L} = 2^{1/\theta} - 1
""")
st.markdown("""
Where:
- λ1 is the lower tail dependence.
- θ is the parameter of the Clayton copula.
A higher λ1 value indicates a stronger relationship in the tails, meaning that extreme losses in one stock are more likely to coincide with extreme losses in another.
""")
def to_uniform(column):
n = len(column)
return rankdata(column) / (n + 1)
uniform_data = returns.apply(to_uniform)
# Constructing the Tail Dependence Matrix
st.write("This matrix shows the tail dependence between different stocks in the portfolio.")
tail_dep_matrix = np.zeros((len(portfolio_tickers), len(portfolio_tickers)))
for i in range(len(portfolio_tickers)):
for j in range(len(portfolio_tickers)):
if i != j:
copula_ij = Clayton()
copula_ij.fit(uniform_data[[portfolio_tickers[i], portfolio_tickers[j]]].values)
tail_dep_matrix[i, j] = copula_ij.theta
# Create annotations for the heatmap
annotations = []
for i in range(len(portfolio_tickers)):
for j in range(len(portfolio_tickers)):
if i != j:
annotations.append(
go.layout.Annotation(
text=str(round(tail_dep_matrix[i, j], 2)),
x=portfolio_tickers[j],
y=portfolio_tickers[i],
xref='x1',
yref='y1',
showarrow=False,
font=dict(color="black" if tail_dep_matrix[i, j] < np.max(tail_dep_matrix)/2 else "white")
)
)
# Tail Dependence Matrix Heatmap
fig2 = go.Figure(data=go.Heatmap(
z=tail_dep_matrix,
x=portfolio_tickers,
y=portfolio_tickers,
colorscale='YlGnBu',
zmin=0,
zmax=np.max(tail_dep_matrix),
showscale=True
))
fig2.update_layout(
title="Tail Dependence Matrix",
xaxis_title="Stocks",
yaxis_title="Stocks",
annotations=annotations
)
st.plotly_chart(fig2)
st.markdown("## Rolling Window Analysis")
st.markdown("""
We now perform a rolling window analysis to examine how tail dependence changes over time.
""")
# Rolling window analysis
tail_parameters = []
for start in range(0, len(uniform_data) - rolling_window):
window_data = uniform_data.iloc[start:start + rolling_window]
copula = Clayton()
copula.fit(window_data.values)
# Extract tail parameter (for Clayton, the theta parameter)
tail_parameters.append(copula.theta)
# Tail Dependence Over Time
fig1 = go.Figure()
fig1.add_trace(go.Scatter(x=returns.index[rolling_window:], y=tail_parameters, mode='lines', name='Tail Dependence Parameter'))
fig1.update_layout(
title="Tail Dependence Over Time",
xaxis_title="Date",
yaxis_title="Tail Dependence Parameter"
)
st.plotly_chart(fig1)
# Hide the Streamlit style elements
hide_streamlit_style = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_streamlit_style, unsafe_allow_html=True)