|
|
|
import streamlit as st |
|
from datetime import date |
|
import yfinance as yf |
|
import numpy as np |
|
import pandas as pd |
|
import plotly.express as px |
|
import plotly.graph_objs as go |
|
import plotly.subplots as sp |
|
from plotly.subplots import make_subplots |
|
import plotly.figure_factory as ff |
|
import plotly.io as pio |
|
from IPython.display import display |
|
from plotly.offline import init_notebook_mode |
|
init_notebook_mode(connected=True) |
|
|
|
|
|
import warnings |
|
warnings.filterwarnings('ignore') |
|
|
|
def perform_portfolio_analysis(df, tickers_weights): |
|
""" |
|
This function takes historical stock data and the weights of the securities in the portfolio, |
|
It calculates individual security returns, cumulative returns, volatility, and Sharpe Ratios. |
|
It then visualizes this data, showing historical performance and a risk-reward plot. |
|
|
|
Parameters: |
|
- df (pd.DataFrame): DataFrame containing historical stock data with securities as columns. |
|
- tickers_weights (dict): A dictionary where keys are ticker symbols (str) and values are their |
|
respective weights (float)in the portfolio. |
|
|
|
Returns: |
|
- fig1: A Plotly Figure with two subplots: |
|
1. Line plot showing the historical returns of each security in the portfolio. |
|
2. Plot showing the annualized volatility and last cumulative return of each security |
|
colored by their respective Sharpe Ratio. |
|
|
|
Notes: |
|
- The function assumes that 'pandas', 'numpy', and 'plotly.graph_objects' are imported as 'pd', 'np', and 'go' respectively. |
|
- The function also utilizes 'plotly.subplots.make_subplots' for creating subplots. |
|
- The risk-free rate is assumed to be 1% per annum for Sharpe Ratio calculation. |
|
""" |
|
|
|
|
|
individual_cumsum = pd.DataFrame() |
|
individual_vol = pd.Series(dtype=float) |
|
individual_sharpe = pd.Series(dtype=float) |
|
|
|
|
|
|
|
for ticker, weight in tickers_weights.items(): |
|
if ticker in df.columns: |
|
individual_returns = df[ticker].pct_change() |
|
individual_cumsum[ticker] = ((1 + individual_returns).cumprod() - 1) * 100 |
|
vol = (individual_returns.std() * np.sqrt(252)) * 100 |
|
individual_vol[ticker] = vol |
|
individual_excess_returns = individual_returns - 0.01 / 252 |
|
sharpe = (individual_excess_returns.mean() / individual_returns.std() * np.sqrt(252)).round(2) |
|
individual_sharpe[ticker] = sharpe |
|
|
|
|
|
fig1 = make_subplots(rows = 1, cols = 2, horizontal_spacing=0.2, |
|
column_titles=['Historical Performance Assets', 'Risk-Reward'], |
|
column_widths=[.55, .45], |
|
shared_xaxes=False, shared_yaxes=False) |
|
|
|
|
|
for ticker in individual_cumsum.columns: |
|
fig1.add_trace(go.Scatter(x=individual_cumsum.index, |
|
y=individual_cumsum[ticker], |
|
mode = 'lines', |
|
name = ticker, |
|
hovertemplate = '%{y:.2f}%', |
|
showlegend=False), |
|
row=1, col=1) |
|
|
|
|
|
sharpe_colors = [individual_sharpe[ticker] for ticker in individual_cumsum.columns] |
|
|
|
|
|
fig1.add_trace(go.Scatter(x=individual_vol.tolist(), |
|
y=individual_cumsum.iloc[-1].tolist(), |
|
mode='markers+text', |
|
marker=dict(size=75, color = sharpe_colors, |
|
colorscale = 'Bluered_r', |
|
colorbar=dict(title='Sharpe Ratio'), |
|
showscale=True), |
|
name = 'Returns', |
|
text = individual_cumsum.columns.tolist(), |
|
textfont=dict(color='white'), |
|
showlegend=False, |
|
hovertemplate = '%{y:.2f}%<br>Annualized Volatility: %{x:.2f}%<br>Sharpe Ratio: %{marker.color:.2f}', |
|
textposition='middle center'), |
|
row=1, col=2) |
|
|
|
|
|
fig1.update_layout(title={ |
|
'text': f'<b>Portfolio Analysis</b>', |
|
'font': {'size': 24} |
|
}, |
|
template = 'plotly_white', |
|
height = 650, width = 1250, |
|
hovermode = 'x unified') |
|
|
|
fig1.update_yaxes(title_text='Returns (%)', col=1) |
|
fig1.update_yaxes(title_text='Returns (%)', col = 2) |
|
fig1.update_xaxes(title_text = 'Date', col = 1) |
|
fig1.update_xaxes(title_text = 'Annualized Volatility (%)', col =2) |
|
|
|
return fig1 |
|
|
|
def portfolio_vs_benchmark(port_returns, benchmark_returns): |
|
|
|
""" |
|
This function calculates and displays the cumulative returns, annualized volatility, and Sharpe Ratios |
|
for both the portfolio and the benchmark. It provides a side-by-side comparison to assess the portfolio's |
|
performance relative to the benchmark. |
|
|
|
Parameters: |
|
- port_returns (pd.Series): A Pandas Series containing the daily returns of the portfolio. |
|
- benchmark_returns (pd.Series): A Pandas Series containing the daily returns of the benchmark. |
|
|
|
Returns: |
|
- fig2: A Plotly Figure object with two subplots: |
|
1. Line plot showing the cumulative returns of both the portfolio and the benchmark over time. |
|
2. Scatter plot indicating the annualized volatility and the last cumulative return of both the portfolio |
|
and the benchmark, colored by their respective Sharpe Ratios. |
|
|
|
Notes: |
|
- The function assumes that 'numpy' and 'plotly.graph_objects' are imported as 'np' and 'go' respectively. |
|
- The function also utilizes 'plotly.subplots.make_subplots' for creating subplots. |
|
- The risk-free rate is assumed to be 1% per annum for Sharpe Ratio calculation. |
|
""" |
|
|
|
|
|
portfolio_cumsum = (((1 + port_returns).cumprod() - 1) * 100).round(2) |
|
benchmark_cumsum = (((1 + benchmark_returns).cumprod() - 1) * 100).round(2) |
|
|
|
|
|
port_vol = ((port_returns.std() * np.sqrt(252)) * 100).round(2) |
|
benchmark_vol = ((benchmark_returns.std() * np.sqrt(252)) * 100).round(2) |
|
|
|
|
|
excess_port_returns = port_returns - 0.01 / 252 |
|
port_sharpe = (excess_port_returns.mean() / port_returns.std() * np.sqrt(252)).round(2) |
|
exces_benchmark_returns = benchmark_returns - 0.01 / 252 |
|
benchmark_sharpe = (exces_benchmark_returns.mean() / benchmark_returns.std() * np.sqrt(252)).round(2) |
|
|
|
|
|
fig2 = make_subplots(rows = 1, cols = 2, horizontal_spacing=0.2, |
|
column_titles=['Cumulative Returns', 'Portfolio Risk-Reward'], |
|
column_widths=[.55, .45], |
|
shared_xaxes=False, shared_yaxes=False) |
|
|
|
|
|
fig2.add_trace(go.Scatter(x=portfolio_cumsum.index, |
|
y = portfolio_cumsum, |
|
mode = 'lines', name = 'Portfolio', showlegend=False, |
|
hovertemplate = '%{y:.2f}%'), |
|
row=1,col=1) |
|
|
|
|
|
fig2.add_trace(go.Scatter(x=benchmark_cumsum.index, |
|
y = benchmark_cumsum, |
|
mode = 'lines', name = 'Benchmark', showlegend=False, |
|
hovertemplate = '%{y:.2f}%'), |
|
row=1,col=1) |
|
|
|
|
|
|
|
fig2.add_trace(go.Scatter(x = [port_vol, benchmark_vol], y = [portfolio_cumsum.iloc[-1], benchmark_cumsum.iloc[-1]], |
|
mode = 'markers+text', |
|
marker=dict(size = 75, |
|
color = [port_sharpe, benchmark_sharpe], |
|
colorscale='Bluered_r', |
|
colorbar=dict(title='Sharpe Ratio'), |
|
showscale=True), |
|
name = 'Returns', |
|
text=['Portfolio', 'Benchmark'], textposition='middle center', |
|
textfont=dict(color='white'), |
|
hovertemplate = '%{y:.2f}%<br>Annualized Volatility: %{x:.2f}%<br>Sharpe Ratio: %{marker.color:.2f}', |
|
showlegend=False), |
|
row = 1, col = 2) |
|
|
|
|
|
|
|
fig2.update_layout(title={ |
|
'text': f'<b>Portfolio vs Benchmark</b>', |
|
'font': {'size': 24} |
|
}, |
|
template = 'plotly_white', |
|
height = 650, width = 1250, |
|
hovermode = 'x unified') |
|
|
|
fig2.update_yaxes(title_text='Cumulative Returns (%)', col=1) |
|
fig2.update_yaxes(title_text='Cumulative Returns (%)', col = 2) |
|
fig2.update_xaxes(title_text = 'Date', col = 1) |
|
fig2.update_xaxes(title_text = 'Annualized Volatility (%)', col =2) |
|
|
|
return fig2 |
|
|
|
|
|
def portfolio_returns(tickers_and_values, start_date, end_date, benchmark): |
|
|
|
""" |
|
This function downloads historical stock data, calculates the weighted returns to build a portfolio, |
|
and compares these returns to a benchmark. |
|
It also displays the portfolio allocation and the performance of the portfolio against the benchmark. |
|
|
|
Parameters: |
|
- tickers_and_values (dict): A dictionary where keys are ticker symbols (str) and values are the current |
|
amounts (float) invested in each ticker. |
|
- start_date (str): The start date for the historical data in the format 'YYYY-MM-DD'. |
|
- end_date (str): The end date for the historical data in the format 'YYYY-MM-DD'. |
|
- benchmark (str): The ticker symbol for the benchmark against which to compare the portfolio's performance. |
|
|
|
Returns: |
|
- Displays three plots: |
|
1. A pie chart showing the portfolio allocation by ticker. |
|
2. A plot to analyze historical returns and volatility of each security |
|
in the portfolio. (Not plotted if portfolio only has one security) |
|
2. A comparison between portfolio returns and volatility against the benchmark over the specified period. |
|
|
|
Notes: |
|
- The function assumes that 'yfinance', 'pandas', 'plotly.graph_objects', and 'plotly.express' are imported |
|
as 'yf', 'pd', 'go', and 'px' respectively. |
|
- For single security portfolios, the function calculates returns without weighting. |
|
- The function utilizes a helper function 'portfolio_vs_benchmark' for comparing portfolio returns with |
|
the benchmark, which needs to be defined separately. |
|
- Another helper function 'perform_portfolio_analysis' is called for portfolios with more than one security, |
|
which also needs to be defined separately. |
|
""" |
|
|
|
|
|
df = yf.download(tickers=list(tickers_and_values.keys()), |
|
start=start_date, end=end_date) |
|
|
|
|
|
if isinstance(df.columns, pd.MultiIndex): |
|
missing_data_tickers = [] |
|
for ticker in tickers_and_values.keys(): |
|
first_valid_index = df['Adj Close'][ticker].first_valid_index() |
|
if first_valid_index is None or first_valid_index.strftime('%Y-%m-%d') > start_date: |
|
missing_data_tickers.append(ticker) |
|
|
|
if missing_data_tickers: |
|
error_message = f"No data available for the following tickers starting from {start_date}: {', '.join(missing_data_tickers)}" |
|
return "error", error_message |
|
else: |
|
|
|
first_valid_index = df['Adj Close'].first_valid_index() |
|
if first_valid_index is None or first_valid_index.strftime('%Y-%m-%d') > start_date: |
|
error_message = f"No data available for the ticker starting from {start_date}" |
|
return "error", error_message |
|
|
|
|
|
total_portfolio_value = sum(tickers_and_values.values()) |
|
|
|
|
|
tickers_weights = {ticker: value / total_portfolio_value for ticker, value in tickers_and_values.items()} |
|
|
|
|
|
if isinstance(df.columns, pd.MultiIndex): |
|
df = df['Adj Close'].fillna(df['Close']) |
|
|
|
|
|
if len(tickers_weights) > 1: |
|
weights = list(tickers_weights.values()) |
|
weighted_returns = df.pct_change().mul(weights, axis = 1) |
|
port_returns = weighted_returns.sum(axis=1) |
|
|
|
else: |
|
df = df['Adj Close'].fillna(df['Close']) |
|
port_returns = df.pct_change() |
|
|
|
|
|
benchmark_df = yf.download(benchmark, |
|
start=start_date, end=end_date) |
|
|
|
benchmark_df = benchmark_df['Adj Close'].fillna(benchmark_df['Close']) |
|
|
|
|
|
benchmark_returns = benchmark_df.pct_change() |
|
|
|
|
|
|
|
fig = go.Figure(data=[go.Pie( |
|
labels=list(tickers_weights.keys()), |
|
values=list(tickers_weights.values()), |
|
hoverinfo='label+percent', |
|
textinfo='label+percent', |
|
hole=.65, |
|
marker=dict(colors=px.colors.qualitative.G10) |
|
)]) |
|
|
|
|
|
fig.update_layout(title={ |
|
'text': '<b>Portfolio Allocation</b>', |
|
'font': {'size': 24} |
|
}, height=550, width=1250) |
|
|
|
|
|
fig2 = portfolio_vs_benchmark(port_returns, benchmark_returns) |
|
|
|
|
|
|
|
|
|
|
|
fig1 = None |
|
if len(tickers_weights) > 1: |
|
fig1 = perform_portfolio_analysis(df, tickers_weights) |
|
|
|
|
|
|
|
return "success", (fig, fig1, fig2) |
|
|
|
|
|
st.set_page_config( |
|
page_title = "Investment Portfolio Management", |
|
page_icon=":heavy_dollar_sign:", |
|
layout='wide', |
|
initial_sidebar_state='expanded' |
|
) |
|
|
|
if 'num_pairs' not in st.session_state: |
|
st.session_state['num_pairs'] = 1 |
|
|
|
def add_input_pair(): |
|
st.session_state['num_pairs'] += 1 |
|
|
|
title = '<h1 style="font-family:Didot; font-size: 64px; text-align:left">Investment Portfolio Management</h1>' |
|
st.markdown(title, unsafe_allow_html=True) |
|
|
|
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. |
|
|
|
Here's what you can do: |
|
|
|
• Enter the ticker symbols and the total amount invested for each security in your portfolio. |
|
|
|
• Set a benchmark to compare your portfolio's performance against market indices or other chosen standards. |
|
|
|
• Select the start and end dates for the period you wish to analyze and gain historical insights. |
|
|
|
• Click "Run Analysis" to visualize historical returns, obtain volatility metrics, and unveil the allocation percentages of your portfolio. |
|
|
|
Empower your investment strategy with cutting-edge financial APIs and visualization tools. <br>Start making informed decisions to elevate your financial future today. |
|
<br><br> |
|
Demo: |
|
<br><br></p>""" |
|
st.markdown(text, unsafe_allow_html=True) |
|
|
|
|
|
tickers_and_values = {} |
|
for n in range(st.session_state['num_pairs']): |
|
col1, col2 = st.columns(2) |
|
with col1: |
|
ticker = st.text_input(f"Ticker {n+1}", key=f"ticker_{n+1}", placeholder="Enter the symbol for a security.") |
|
with col2: |
|
value = st.number_input(f"Value Invested in Ticker {n+1} ($)", min_value = 0.0, format="%.2f", key=f"value_{n+1}") |
|
tickers_and_values[ticker] = value |
|
|
|
st.button("Add Another Ticker", on_click=add_input_pair) |
|
|
|
benchmark = st.text_input("Benchmark", placeholder="Enter the symbol for a benchmark.") |
|
|
|
col1, col2 = st.columns(2) |
|
with col1: |
|
start_date = st.date_input( |
|
"Start Date", value=date.today().replace(year=date.today().year-1), |
|
min_value=date(1900, 1, 1) |
|
) |
|
with col2: |
|
end_date = st.date_input( |
|
"End Date", value=date.today(), |
|
min_value=date(1900,1,1) |
|
) |
|
|
|
if st.button("Run Analysis"): |
|
tickers_and_values = {k: v for k,v in tickers_and_values.items() if k and v > 0} |
|
|
|
if not benchmark: |
|
st.error("Please enter a benchmark ticker before running the analysis.") |
|
elif not tickers_and_values: |
|
st.error("Please add at least one ticker with a non-zero investment value before running the analysis.") |
|
else: |
|
start_date_str=start_date.strftime('%Y-%m-%d') |
|
end_date_str=end_date.strftime('%Y-%m-%d') |
|
|
|
status, result = portfolio_returns(tickers_and_values, start_date_str, end_date_str, benchmark) |
|
|
|
if status == "error": |
|
st.error(result) |
|
else: |
|
fig, fig1, fig2 = result |
|
|
|
if fig is not None: |
|
st.plotly_chart(fig) |
|
if fig1 is not None: |
|
st.plotly_chart(fig1) |
|
if fig2 is not None: |
|
st.plotly_chart(fig2) |
|
|
|
signature_html = """ |
|
<hr style="border: 0; height: 1px; border-top: 0.85px solid #b2b2b2"> |
|
<div style="text-align: left; color: #8d8d8d; padding-left: 15px; font-size: 14.25px;"> |
|
Luis Fernando Torres, 2024<br><br> |
|
Let's connect!🔗<br> |
|
<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> |
|
</div> |
|
<div style="text-align: center; margin-top: 50px; color: #8d8d8d; padding-left: 15px; font-size: 14.25px;"> |
|
<b>Like my content? Feel free to <a href="https://www.buymeacoffee.com/luuisotorres" target="_blank">Buy Me a Coffee ☕</a></b> |
|
</div> |
|
<div style="text-align: center; margin-top: 80px; color: #8d8d8d; padding-left: 15px; font-size: 14.25px;"> |
|
<b><a href="https://luuisotorres.github.io/" target="_blank">https://luuisotorres.github.io/</a></b> |
|
</div> |
|
""" |
|
|
|
st.markdown(signature_html, unsafe_allow_html=True) |
|
|