|
|
|
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) |