luisotorres commited on
Commit
e974cb2
1 Parent(s): 5dd254b

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +418 -0
  2. requirements.txt +5 -0
app.py ADDED
@@ -0,0 +1,418 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Importing necessary libraries
2
+ import streamlit as st
3
+ from datetime import date
4
+ import yfinance as yf
5
+ import numpy as np
6
+ import pandas as pd
7
+ import plotly.express as px
8
+ import plotly.graph_objs as go
9
+ import plotly.subplots as sp
10
+ from plotly.subplots import make_subplots
11
+ import plotly.figure_factory as ff
12
+ import plotly.io as pio
13
+ from IPython.display import display
14
+ from plotly.offline import init_notebook_mode
15
+ init_notebook_mode(connected=True)
16
+
17
+ # Hiding Warnings
18
+ import warnings
19
+ warnings.filterwarnings('ignore')
20
+
21
+ def perform_portfolio_analysis(df, tickers_weights):
22
+ """
23
+ This function takes historical stock data and the weights of the securities in the portfolio,
24
+ It calculates individual security returns, cumulative returns, volatility, and Sharpe Ratios.
25
+ It then visualizes this data, showing historical performance and a risk-reward plot.
26
+
27
+ Parameters:
28
+ - df (pd.DataFrame): DataFrame containing historical stock data with securities as columns.
29
+ - tickers_weights (dict): A dictionary where keys are ticker symbols (str) and values are their
30
+ respective weights (float)in the portfolio.
31
+
32
+ Returns:
33
+ - fig1: A Plotly Figure with two subplots:
34
+ 1. Line plot showing the historical returns of each security in the portfolio.
35
+ 2. Plot showing the annualized volatility and last cumulative return of each security
36
+ colored by their respective Sharpe Ratio.
37
+
38
+ Notes:
39
+ - The function assumes that 'pandas', 'numpy', and 'plotly.graph_objects' are imported as 'pd', 'np', and 'go' respectively.
40
+ - The function also utilizes 'plotly.subplots.make_subplots' for creating subplots.
41
+ - The risk-free rate is assumed to be 1% per annum for Sharpe Ratio calculation.
42
+ """
43
+
44
+ # Starting DataFrame and Series
45
+ individual_cumsum = pd.DataFrame()
46
+ individual_vol = pd.Series(dtype=float)
47
+ individual_sharpe = pd.Series(dtype=float)
48
+
49
+
50
+ # Iterating through tickers and weights in the tickers_weights dictionary
51
+ for ticker, weight in tickers_weights.items():
52
+ if ticker in df.columns: # Confirming that the tickers are available
53
+ individual_returns = df[ticker].pct_change() # Computing individual daily returns for each ticker
54
+ individual_cumsum[ticker] = ((1 + individual_returns).cumprod() - 1) * 100 # Computing cumulative returns over the period for each ticker
55
+ vol = (individual_returns.std() * np.sqrt(252)) * 100 # Computing annualized volatility
56
+ individual_vol[ticker] = vol # Adding annualized volatility for each ticker
57
+ individual_excess_returns = individual_returns - 0.01 / 252 # Computing the excess returns
58
+ sharpe = (individual_excess_returns.mean() / individual_returns.std() * np.sqrt(252)).round(2) # Computing Sharpe Ratio
59
+ individual_sharpe[ticker] = sharpe # Adding Sharpe Ratio for each ticker
60
+
61
+ # Creating subplots for comparison across securities
62
+ fig1 = make_subplots(rows = 1, cols = 2, horizontal_spacing=0.2,
63
+ column_titles=['Historical Performance Assets', 'Risk-Reward'],
64
+ column_widths=[.55, .45],
65
+ shared_xaxes=False, shared_yaxes=False)
66
+
67
+ # Adding the historical returns for each ticker on the first subplot
68
+ for ticker in individual_cumsum.columns:
69
+ fig1.add_trace(go.Scatter(x=individual_cumsum.index,
70
+ y=individual_cumsum[ticker],
71
+ mode = 'lines',
72
+ name = ticker,
73
+ hovertemplate = '%{y:.2f}%',
74
+ showlegend=False),
75
+ row=1, col=1)
76
+
77
+ # Defining colors for markers on the second subplot
78
+ sharpe_colors = [individual_sharpe[ticker] for ticker in individual_cumsum.columns]
79
+
80
+ # Adding markers for each ticker on the second subplot
81
+ fig1.add_trace(go.Scatter(x=individual_vol.tolist(),
82
+ y=individual_cumsum.iloc[-1].tolist(),
83
+ mode='markers+text',
84
+ marker=dict(size=75, color = sharpe_colors,
85
+ colorscale = 'Bluered_r',
86
+ colorbar=dict(title='Sharpe Ratio'),
87
+ showscale=True),
88
+ name = 'Returns',
89
+ text = individual_cumsum.columns.tolist(),
90
+ textfont=dict(color='white'),
91
+ showlegend=False,
92
+ hovertemplate = '%{y:.2f}%<br>Annualized Volatility: %{x:.2f}%<br>Sharpe Ratio: %{marker.color:.2f}',
93
+ textposition='middle center'),
94
+ row=1, col=2)
95
+
96
+ # Updating layout
97
+ fig1.update_layout(title={
98
+ 'text': f'<b>Portfolio Analysis</b>',
99
+ 'font': {'size': 24}
100
+ },
101
+ template = 'plotly_white',
102
+ height = 650, width = 1250,
103
+ hovermode = 'x unified')
104
+
105
+ fig1.update_yaxes(title_text='Returns (%)', col=1)
106
+ fig1.update_yaxes(title_text='Returns (%)', col = 2)
107
+ fig1.update_xaxes(title_text = 'Date', col = 1)
108
+ fig1.update_xaxes(title_text = 'Annualized Volatility (%)', col =2)
109
+
110
+ return fig1 # Returning figure
111
+
112
+ def portfolio_vs_benchmark(port_returns, benchmark_returns):
113
+
114
+ """
115
+ This function calculates and displays the cumulative returns, annualized volatility, and Sharpe Ratios
116
+ for both the portfolio and the benchmark. It provides a side-by-side comparison to assess the portfolio's
117
+ performance relative to the benchmark.
118
+
119
+ Parameters:
120
+ - port_returns (pd.Series): A Pandas Series containing the daily returns of the portfolio.
121
+ - benchmark_returns (pd.Series): A Pandas Series containing the daily returns of the benchmark.
122
+
123
+ Returns:
124
+ - fig2: A Plotly Figure object with two subplots:
125
+ 1. Line plot showing the cumulative returns of both the portfolio and the benchmark over time.
126
+ 2. Scatter plot indicating the annualized volatility and the last cumulative return of both the portfolio
127
+ and the benchmark, colored by their respective Sharpe Ratios.
128
+
129
+ Notes:
130
+ - The function assumes that 'numpy' and 'plotly.graph_objects' are imported as 'np' and 'go' respectively.
131
+ - The function also utilizes 'plotly.subplots.make_subplots' for creating subplots.
132
+ - The risk-free rate is assumed to be 1% per annum for Sharpe Ratio calculation.
133
+ """
134
+
135
+ # Computing the cumulative returns for the portfolio and the benchmark
136
+ portfolio_cumsum = (((1 + port_returns).cumprod() - 1) * 100).round(2)
137
+ benchmark_cumsum = (((1 + benchmark_returns).cumprod() - 1) * 100).round(2)
138
+
139
+ # Computing the annualized volatility for the portfolio and the benchmark
140
+ port_vol = ((port_returns.std() * np.sqrt(252)) * 100).round(2)
141
+ benchmark_vol = ((benchmark_returns.std() * np.sqrt(252)) * 100).round(2)
142
+
143
+ # Computing Sharpe Ratio for the portfolio and the benchmark
144
+ excess_port_returns = port_returns - 0.01 / 252
145
+ port_sharpe = (excess_port_returns.mean() / port_returns.std() * np.sqrt(252)).round(2)
146
+ exces_benchmark_returns = benchmark_returns - 0.01 / 252
147
+ benchmark_sharpe = (exces_benchmark_returns.mean() / benchmark_returns.std() * np.sqrt(252)).round(2)
148
+
149
+ # Creating a subplot to compare portfolio performance with the benchmark
150
+ fig2 = make_subplots(rows = 1, cols = 2, horizontal_spacing=0.2,
151
+ column_titles=['Cumulative Returns', 'Portfolio Risk-Reward'],
152
+ column_widths=[.55, .45],
153
+ shared_xaxes=False, shared_yaxes=False)
154
+
155
+ # Adding the cumulative returns for the portfolio
156
+ fig2.add_trace(go.Scatter(x=portfolio_cumsum.index,
157
+ y = portfolio_cumsum,
158
+ mode = 'lines', name = 'Portfolio', showlegend=False,
159
+ hovertemplate = '%{y:.2f}%'),
160
+ row=1,col=1)
161
+
162
+ # Adding the cumulative returns for the benchmark
163
+ fig2.add_trace(go.Scatter(x=benchmark_cumsum.index,
164
+ y = benchmark_cumsum,
165
+ mode = 'lines', name = 'Benchmark', showlegend=False,
166
+ hovertemplate = '%{y:.2f}%'),
167
+ row=1,col=1)
168
+
169
+
170
+ # Creating risk-reward plot for the benchmark and the portfolio
171
+ fig2.add_trace(go.Scatter(x = [port_vol, benchmark_vol], y = [portfolio_cumsum.iloc[-1], benchmark_cumsum.iloc[-1]],
172
+ mode = 'markers+text',
173
+ marker=dict(size = 75,
174
+ color = [port_sharpe, benchmark_sharpe],
175
+ colorscale='Bluered_r',
176
+ colorbar=dict(title='Sharpe Ratio'),
177
+ showscale=True),
178
+ name = 'Returns',
179
+ text=['Portfolio', 'Benchmark'], textposition='middle center',
180
+ textfont=dict(color='white'),
181
+ hovertemplate = '%{y:.2f}%<br>Annualized Volatility: %{x:.2f}%<br>Sharpe Ratio: %{marker.color:.2f}',
182
+ showlegend=False),
183
+ row = 1, col = 2)
184
+
185
+
186
+ # Configuring layout
187
+ fig2.update_layout(title={
188
+ 'text': f'<b>Portfolio vs Benchmark</b>',
189
+ 'font': {'size': 24}
190
+ },
191
+ template = 'plotly_white',
192
+ height = 650, width = 1250,
193
+ hovermode = 'x unified')
194
+
195
+ fig2.update_yaxes(title_text='Cumulative Returns (%)', col=1)
196
+ fig2.update_yaxes(title_text='Cumulative Returns (%)', col = 2)
197
+ fig2.update_xaxes(title_text = 'Date', col = 1)
198
+ fig2.update_xaxes(title_text = 'Annualized Volatility (%)', col =2)
199
+
200
+ return fig2 # Returning subplots
201
+
202
+
203
+ def portfolio_returns(tickers_and_values, start_date, end_date, benchmark):
204
+
205
+ """
206
+ This function downloads historical stock data, calculates the weighted returns to build a portfolio,
207
+ and compares these returns to a benchmark.
208
+ It also displays the portfolio allocation and the performance of the portfolio against the benchmark.
209
+
210
+ Parameters:
211
+ - tickers_and_values (dict): A dictionary where keys are ticker symbols (str) and values are the current
212
+ amounts (float) invested in each ticker.
213
+ - start_date (str): The start date for the historical data in the format 'YYYY-MM-DD'.
214
+ - end_date (str): The end date for the historical data in the format 'YYYY-MM-DD'.
215
+ - benchmark (str): The ticker symbol for the benchmark against which to compare the portfolio's performance.
216
+
217
+ Returns:
218
+ - Displays three plots:
219
+ 1. A pie chart showing the portfolio allocation by ticker.
220
+ 2. A plot to analyze historical returns and volatility of each security
221
+ in the portfolio. (Not plotted if portfolio only has one security)
222
+ 2. A comparison between portfolio returns and volatility against the benchmark over the specified period.
223
+
224
+ Notes:
225
+ - The function assumes that 'yfinance', 'pandas', 'plotly.graph_objects', and 'plotly.express' are imported
226
+ as 'yf', 'pd', 'go', and 'px' respectively.
227
+ - For single security portfolios, the function calculates returns without weighting.
228
+ - The function utilizes a helper function 'portfolio_vs_benchmark' for comparing portfolio returns with
229
+ the benchmark, which needs to be defined separately.
230
+ - Another helper function 'perform_portfolio_analysis' is called for portfolios with more than one security,
231
+ which also needs to be defined separately.
232
+ """
233
+
234
+ # Obtaining tickers data with yfinance
235
+ df = yf.download(tickers=list(tickers_and_values.keys()),
236
+ start=start_date, end=end_date)
237
+
238
+ # Checking if there is data available in the given date range
239
+ if isinstance(df.columns, pd.MultiIndex):
240
+ missing_data_tickers = []
241
+ for ticker in tickers_and_values.keys():
242
+ first_valid_index = df['Adj Close'][ticker].first_valid_index()
243
+ if first_valid_index is None or first_valid_index.strftime('%Y-%m-%d') > start_date:
244
+ missing_data_tickers.append(ticker)
245
+
246
+ if missing_data_tickers:
247
+ error_message = f"No data available for the following tickers starting from {start_date}: {', '.join(missing_data_tickers)}"
248
+ return "error", error_message
249
+ else:
250
+ # For a single ticker, simply check the first valid index
251
+ first_valid_index = df['Adj Close'].first_valid_index()
252
+ if first_valid_index is None or first_valid_index.strftime('%Y-%m-%d') > start_date:
253
+ error_message = f"No data available for the ticker starting from {start_date}"
254
+ return "error", error_message
255
+
256
+ # Calculating portfolio value
257
+ total_portfolio_value = sum(tickers_and_values.values())
258
+
259
+ # Calculating the weights for each security in the portfolio
260
+ tickers_weights = {ticker: value / total_portfolio_value for ticker, value in tickers_and_values.items()}
261
+
262
+ # Checking if dataframe has MultiIndex columns
263
+ if isinstance(df.columns, pd.MultiIndex):
264
+ df = df['Adj Close'].fillna(df['Close']) # If 'Adjusted Close' is not available, use 'Close'
265
+
266
+ # Checking if there are more than just one security in the portfolio
267
+ if len(tickers_weights) > 1:
268
+ weights = list(tickers_weights.values()) # Obtaining weights
269
+ weighted_returns = df.pct_change().mul(weights, axis = 1) # Computed weighted returns
270
+ port_returns = weighted_returns.sum(axis=1) # Sum weighted returns to build portfolio returns
271
+ # If there is only one security in the portfolio...
272
+ else:
273
+ df = df['Adj Close'].fillna(df['Close']) # Obtaining 'Adjusted Close'. If not available, use 'Close'
274
+ port_returns = df.pct_change() # Computing returns without weights
275
+
276
+ # Obtaining benchmark data with yfinance
277
+ benchmark_df = yf.download(benchmark,
278
+ start=start_date, end=end_date)
279
+ # Obtaining 'Adjusted Close'. If not available, use 'Close'.
280
+ benchmark_df = benchmark_df['Adj Close'].fillna(benchmark_df['Close'])
281
+
282
+ # Computing benchmark returns
283
+ benchmark_returns = benchmark_df.pct_change()
284
+
285
+
286
+ # Plotting a pie plot
287
+ fig = go.Figure(data=[go.Pie(
288
+ labels=list(tickers_weights.keys()), # Obtaining tickers
289
+ values=list(tickers_weights.values()), # Obtaining weights
290
+ hoverinfo='label+percent',
291
+ textinfo='label+percent',
292
+ hole=.65,
293
+ marker=dict(colors=px.colors.qualitative.G10)
294
+ )])
295
+
296
+ # Defining layout
297
+ fig.update_layout(title={
298
+ 'text': '<b>Portfolio Allocation</b>',
299
+ 'font': {'size': 24}
300
+ }, height=550, width=1250)
301
+
302
+ # Running function to compare portfolio and benchmark
303
+ fig2 = portfolio_vs_benchmark(port_returns, benchmark_returns)
304
+
305
+ #fig.show() # Displaying Portfolio Allocation plot
306
+
307
+ # If we have more than one security in the portfolio,
308
+ # we run function to evaluate each security individually
309
+ fig1 = None
310
+ if len(tickers_weights) > 1:
311
+ fig1 = perform_portfolio_analysis(df, tickers_weights)
312
+ #fig1.show()
313
+ # Displaying Portfolio vs Benchmark plot
314
+ #fig2.show()
315
+ return "success", (fig, fig1, fig2)
316
+
317
+ # Defining page settings
318
+ st.set_page_config(
319
+ page_title = "Investment Portfolio Management",
320
+ page_icon=":heavy_dollar_sign:",
321
+ layout='wide',
322
+ initial_sidebar_state='expanded'
323
+ )
324
+
325
+ if 'num_pairs' not in st.session_state:
326
+ st.session_state['num_pairs'] = 1
327
+
328
+ def add_input_pair():
329
+ st.session_state['num_pairs'] += 1
330
+
331
+ title = '<h1 style="font-family:Didot; font-size: 64px; text-align:left">Investment Portfolio Management</h1>'
332
+ st.markdown(title, unsafe_allow_html=True)
333
+
334
+ text = """<p style="font-size: 18px; text-align: left;"><br>Welcome to <b>Investment Portfolio Management</b>, the intuitive app that streamlines your investment tracking and analysis. Effortlessly monitor your assets, benchmark against market standards, and discover valuable insights with just a few clicks.
335
+
336
+ Here's what you can do:
337
+
338
+ • Enter the ticker symbols and the total amount invested for each security in your portfolio.
339
+
340
+ • Set a benchmark to compare your portfolio's performance against market indices or other chosen standards.
341
+
342
+ • Select the start and end dates for the period you wish to analyze and gain historical insights.
343
+
344
+ • Click "Run Analysis" to visualize historical returns, obtain volatility metrics, and unveil the allocation percentages of your portfolio.
345
+
346
+ Empower your investment strategy with cutting-edge financial APIs and visualization tools. <br>Start making informed decisions to elevate your financial future today.
347
+ <br><br>
348
+ Demo:
349
+ <br><br></p>"""
350
+ st.markdown(text, unsafe_allow_html=True)
351
+
352
+
353
+ tickers_and_values = {}
354
+ for n in range(st.session_state['num_pairs']):
355
+ col1, col2 = st.columns(2)
356
+ with col1:
357
+ ticker = st.text_input(f"Ticker {n+1}", key=f"ticker_{n+1}", placeholder="Enter the symbol for a security.")
358
+ with col2:
359
+ value = st.number_input(f"Value Invested in Ticker {n+1} ($)", min_value = 0.0, format="%.2f", key=f"value_{n+1}")
360
+ tickers_and_values[ticker] = value
361
+
362
+ st.button("Add Another Ticker", on_click=add_input_pair)
363
+
364
+ benchmark = st.text_input("Benchmark", placeholder="Enter the symbol for a benchmark.")
365
+
366
+ col1, col2 = st.columns(2)
367
+ with col1:
368
+ start_date = st.date_input(
369
+ "Start Date", value=date.today().replace(year=date.today().year-1),
370
+ min_value=date(1900, 1, 1)
371
+ )
372
+ with col2:
373
+ end_date = st.date_input(
374
+ "End Date", value=date.today(),
375
+ min_value=date(1900,1,1)
376
+ )
377
+
378
+ if st.button("Run Analysis"):
379
+ tickers_and_values = {k: v for k,v in tickers_and_values.items() if k and v > 0}
380
+
381
+ if not benchmark:
382
+ st.error("Please enter a benchmark ticker before running the analysis.")
383
+ elif not tickers_and_values:
384
+ st.error("Please add at least one ticker with a non-zero investment value before running the analysis.")
385
+ else:
386
+ start_date_str=start_date.strftime('%Y-%m-%d')
387
+ end_date_str=end_date.strftime('%Y-%m-%d')
388
+
389
+ status, result = portfolio_returns(tickers_and_values, start_date_str, end_date_str, benchmark)
390
+
391
+ if status == "error":
392
+ st.error(result)
393
+ else:
394
+ fig, fig1, fig2 = result
395
+
396
+ if fig is not None:
397
+ st.plotly_chart(fig)
398
+ if fig1 is not None:
399
+ st.plotly_chart(fig1)
400
+ if fig2 is not None:
401
+ st.plotly_chart(fig2)
402
+
403
+ signature_html = """
404
+ <hr style="border: 0; height: 1px; border-top: 0.85px solid #b2b2b2">
405
+ <div style="text-align: left; color: #8d8d8d; padding-left: 15px; font-size: 14.25px;">
406
+ Luis Fernando Torres, 2024<br><br>
407
+ Let's connect!🔗<br>
408
+ <a href="https://www.linkedin.com/in/luuisotorres/" target="_blank">LinkedIn</a> • <a href="https://medium.com/@luuisotorres" target="_blank">Medium</a> • <a href="https://www.kaggle.com/lusfernandotorres" target="_blank">Kaggle</a><br><br>
409
+ </div>
410
+ <div style="text-align: center; margin-top: 50px; color: #8d8d8d; padding-left: 15px; font-size: 14.25px;">
411
+ <b>Like my content? Feel free to <a href="https://www.buymeacoffee.com/luuisotorres" target="_blank">Buy Me a Coffee ☕</a></b>
412
+ </div>
413
+ <div style="text-align: center; margin-top: 80px; color: #8d8d8d; padding-left: 15px; font-size: 14.25px;">
414
+ <b><a href="https://luuisotorres.github.io/" target="_blank">https://luuisotorres.github.io/</a></b>
415
+ </div>
416
+ """
417
+
418
+ st.markdown(signature_html, unsafe_allow_html=True)
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ numpy==1.26.1
2
+ pandas==2.1.1
3
+ plotly==5.17.0
4
+ streamlit==1.31.0
5
+ yfinance==0.2.31