Spaces:
Sleeping
Sleeping
| 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) | |