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)