luisotorres commited on
Commit
7660838
β€’
1 Parent(s): 2da6be9

Updated Code

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