Spaces:
Sleeping
Sleeping
File size: 17,908 Bytes
410ac44 5e0f4f7 ed9c068 2b4bdc8 bbaf2ce c152d1f ed9c068 c152d1f ed9c068 c152d1f 2b4bdc8 ed9c068 eccc942 ed9c068 c152d1f ed9c068 c152d1f ed9c068 c152d1f ed9c068 c152d1f ed9c068 dc888a9 c152d1f dc888a9 c152d1f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 | 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)
|