You are on page 1of 1

In 

[1]:
!pip install yfinance --upgrade --no-cache-dir -q > /dev/null 2>&1

In [2]:
!pip install jupyter-dash==0.4.1 -q > /dev/null 2>&1

In [3]:
!pip install jupyter-dash -q > /dev/null 2>&1

In [4]:
!pip install "notebook>=5.3" "ipywidgets>=7.5" -q > /dev/null 2>&1

In [5]:
!pip install dash-bootstrap-components -q > /dev/null 2>&1

In [6]:
!pip install dash-table -q > /dev/null 2>&1

In [7]:
!pip install copulas -q > /dev/null 2>&1

In [8]:
!pip install dash-bootstrap-templates -q > /dev/null 2>&1

In [9]:
!pip install pandas-datareader -q > /dev/null 2>&1

In [10]:
import pandas as pd
import numpy as np
import datetime as dt
import yfinance as yf
import plotly.express as px
import dash
from dash import dcc
from dash import html
from dash.dependencies import Input, Output, State
from datetime import datetime, timedelta
from scipy.stats import norm, multivariate_normal
from jupyter_dash import JupyterDash

#pip install dash-bootstrap-components


import dash_bootstrap_components as dbc
from datetime import date
from dash_bootstrap_templates import load_figure_template
from scipy.optimize import minimize
from scipy.optimize import *
from scipy.optimize import fmin

#Additionnal imports
import plotly.graph_objects as go
import dash_bootstrap_components as dbc
import dash_bootstrap_templates as dbt
import plotly.graph_objs as go
from dash import dash_table
from scipy.stats import norm, gaussian_kde
from copulas.bivariate import Frank, Gumbel, Clayton
from statsmodels.distributions.empirical_distribution import ECDF
import pandas_datareader as pdr
import plotly.figure_factory as ff

This code should download the historical stock prices for all the companies in the S&P500 for the last 10 years and store the data in a pandas dataframe called data. You can then use this dataframe to build your app.

In [11]:
# Define the date range
end_date = datetime.today().strftime('%Y-%m-%d')
start_date = (datetime.today() - timedelta(days=365 * 10)).strftime('%Y-%m-%d')

# Download historical stock prices for the S&P500 index


def get_sp500_symbols():
table = pd.read_html("https://en.wikipedia.org/wiki/List_of_S%26P_500_companies")[0]
return table['Symbol'].tolist()

sp500_symbols = get_sp500_symbols()

stock_data_cache = None

def download_and_cache_stock_data(symbols, start_date="2010-01-01", end_date=datetime.today().strftime('%Y-%m-%d')):


global stock_data_cache
data = yf.download(symbols, start=start_date, end=end_date)
adj_close = data["Adj Close"]
if isinstance(adj_close, pd.Series):
adj_close = adj_close.to_frame()
stock_data_cache = adj_close

download_and_cache_stock_data(sp500_symbols, start_date, end_date)

def get_stock_data(symbols, start_date="2010-01-01", end_date=datetime.today().strftime('%Y-%m-%d')):


global stock_data_cache
start_date = pd.to_datetime(start_date)
end_date = pd.to_datetime(end_date)

if stock_data_cache is not None:


try:
start_date_available = stock_data_cache.index.asof(start_date)
end_date_available = stock_data_cache.index.asof(end_date)

if start_date_available is pd.NaT or end_date_available is pd.NaT:


return pd.DataFrame()

return stock_data_cache.loc[start_date_available:end_date_available, symbols]


except (KeyError, TypeError):
return pd.DataFrame()
else:
return pd.DataFrame()

def get_risk_free_rate():
try:
end_date = datetime.today()
start_date = end_date - pd.Timedelta(days=365)
risk_free_rate_data = pdr.get_data_fred('GS3M', start_date, end_date)
return risk_free_rate_data.iloc[-1].item() / 100 # Convert to decimal
except Exception as e:
print(f"Error fetching risk-free rate: {e}")
return 0.02 # Default risk-free rate if fetching fails

[*********************100%***********************] 502 of 502 completed

2 Failed downloads:
- BRK.B: No timezone found, symbol may be delisted
- BF.B: No data found for this date range, symbol may be delisted

This code first calculates the percentage change in adjusted closing prices for each stock using the pct_change() function, and then drops the first row which will be NaN. The resulting DataFrame sp500_returns will contain the daily returns for each
stock in the S&P500.

In [12]:
# Calculate the daily returns for all the stocks in the S&P500
def get_stock_log_returns(portfolio, start_date, end_date):
stock_data = get_stock_data(portfolio, start_date=start_date, end_date=end_date)
stock_log_returns = np.log(stock_data / stock_data.shift(1)).dropna()
return stock_log_returns[portfolio]

Everything benchmark related

In [13]:
# Define the benchmark portfolio
benchmark_portfolio = sp500_symbols

# Download the S&P 500 index data


def get_sp500_index_data(start_date, end_date):
sp500_index_data = yf.download('^GSPC', start=start_date, end=end_date)["Adj Close"]
return sp500_index_data

sp500_index_data = get_sp500_index_data(start_date, end_date)

sp500_index_log_returns = np.log(sp500_index_data / sp500_index_data.shift(1)).dropna()

# Calculate benchmark_log_returns
benchmark_log_returns = sp500_index_log_returns.copy()
benchmark_log_returns_filtered = sp500_index_log_returns.loc[start_date:end_date]

[*********************100%***********************] 1 of 1 completed
Code pour tout ce qui est weight related

In [14]:
# Add this function to calculate equal weights
def equal_weights(n):
return np.ones(n) / n

# Add this function to calculate global minimum variance weights


def global_minimum_variance_weights(cov_matrix):
n = len(cov_matrix)
weights = np.ones(n)
constraints = [{"type": "eq", "fun": lambda x: np.sum(x) - 1}]
bounds = [(0, 1) for _ in range(n)]
result = minimize(lambda x: np.dot(x.T, np.dot(cov_matrix, x)), weights, constraints=constraints, bounds=bounds)
return result.x

# Add this function to calculate maximum sharpe ratio weights


def maximum_sharpe_ratio_weights(log_returns, cov_matrix, risk_free_rate=0.02):
n = len(cov_matrix)
weights = np.ones(n)
constraints = [{"type": "eq", "fun": lambda x: np.sum(x) - 1}]
bounds = [(0, 1) for _ in range(n)]
result = minimize(lambda x: -((np.dot(x, log_returns.mean()) - risk_free_rate) / np.sqrt(np.dot(x.T, np.dot(cov_matrix, x)))), weights, constraints=constraints, bounds=bounds)
return result.x

In the following code, we:

Do the correlation stuff


Calculate the VAR and ES of our Portfolio and show it graphically

In [15]:
def rolling_correlation(portfolio_log_returns, benchmark_log_returns, window=30):
df = pd.concat([portfolio_log_returns, benchmark_log_returns], axis=1)
df.columns = ["portfolio", "benchmark"]
rolling_corr = df.rolling(window=window).corr().xs("portfolio", level=1, drop_level=True)["benchmark"]
return rolling_corr

In [16]:
# Calculate the VaR and ES for a portfolio
def calculate_var_es(portfolio_returns, alpha=0.05):
if not portfolio_returns.empty:
mu = np.mean(portfolio_returns)
sigma = np.std(portfolio_returns)
z_score = norm.ppf(alpha)
var = -1 * (mu - z_score * sigma)
es = -1 * (mu + (sigma / alpha) * norm.pdf(z_score))
return round(var, 4), round(es, 4)
else:
return 0, 0

Everything about copulas

In [17]:
def transform_to_uniform(data):
transformed_data = pd.DataFrame(index=data.index)
for column in data.columns:
ecdf = ECDF(data[column])
transformed_data[column] = ecdf(data[column])
return transformed_data

def calculate_frank_copula(stock_log_returns, benchmark_log_returns):


data = pd.concat([stock_log_returns, benchmark_log_returns], axis=1, keys=['portfolio', 'benchmark'])
uniform_data = transform_to_uniform(data)
copula = Frank()
copula.fit(uniform_data.to_numpy())
return copula

def calculate_gumbel_copula(stock_log_returns, benchmark_log_returns):


data = pd.concat([stock_log_returns, benchmark_log_returns], axis=1, keys=['portfolio', 'benchmark'])
uniform_data = transform_to_uniform(data)
copula = Gumbel()
copula.fit(uniform_data.to_numpy())
return copula

def calculate_clayton_copula(stock_log_returns, benchmark_log_returns):


data = pd.concat([stock_log_returns, benchmark_log_returns], axis=1, keys=['portfolio', 'benchmark'])
uniform_data = transform_to_uniform(data)
copula = Clayton()
copula.fit(uniform_data.to_numpy())
return copula

Zone de code pour faire marcher les graphiques

In [18]:
def create_chart(portfolio_cumulative_returns, benchmark_cumulative_returns):
fig = go.Figure()
fig.add_trace(go.Scatter(x=portfolio_cumulative_returns.index, y= portfolio_cumulative_returns,
mode='lines', name='Portfolio Returns'))
fig.add_trace(go.Scatter(x=benchmark_cumulative_returns.index, y=benchmark_cumulative_returns,
mode='lines', name='Benchmark Returns', line=dict(color='red')))
fig.update_layout(title='Portfolio and Benchmark Returns Over Time',
xaxis_title='Date',
yaxis_title='Returns',
yaxis_tickformat=".0%",
showlegend=True)
return fig

def plot_density(returns, var, es, title):


hist_data = [returns]
group_labels = [title]

# Set the bin size for the histogram


bin_size = (max(returns) - min(returns)) / 100

fig = ff.create_distplot(
hist_data,
group_labels,
bin_size=[bin_size], # Provide the bin size as a list
histnorm="probability density",
show_hist=False,
show_rug=False,
)

fig.update_layout(
title=title,
xaxis_title="Returns",
)

fig.add_shape(
type="line",
x0=var,
x1=var,
y0=0,
y1=1,
yref="paper",
line=dict(color="red", width=2),
)

fig.add_shape(
type="line",
x0=es,
x1=es,
y0=0,
y1=1,
yref="paper",
line=dict(color="orange", width=2),
)

fig.add_trace(go.Scatter(x=[None], y=[None], mode='lines', name='VaR', line=dict(color='red', width=2)))


fig.add_trace(go.Scatter(x=[None], y=[None], mode='lines', name='ES', line=dict(color='orange', width=2)))

fig.update_layout(
showlegend=True,
legend=dict(
x=1,
y=1,
bgcolor="rgba(255, 255, 255, 0.5)",
bordercolor="rgba(255, 255, 255, 0.5)",
),
)

return fig

def create_copula_chart(copula):
copula_sample = copula.sample(500).T

fig = go.Figure()
fig.add_trace(go.Scatter(x=copula_sample[0], y=copula_sample[1], mode='markers', name='Copula'))
fig.update_layout(title='Copula Scatter Plot', xaxis_title='X', yaxis_title='Y')

return fig

Frontend stuff

In [19]:
# Define a CSS class with black text color
dropdown_style1 = {
'color': 'black',
'width': '40%',
'border-radius': '50px',
'margin-left': '5px',
'margin-bottom': '5px',
'margin-top': '-75px'
}

dropdown_style2 = {'color': 'black'}

button_style1 = {
'backgroundColor': 'transparent',
'color': 'white',
'border': '2px solid white',
'border-radius': '50px',
'margin-right': '545px'
}

button_style2 = {
'backgroundColor': 'transparent',
'color': 'white',
'border': '2px solid white',
'border-radius': '50px',
'margin-right': '0px',
'margin-top': '-90px'
}

case_style = {
'width': '20%',
'backgroundColor': 'white',
'color': 'black',
'border': '2px solid white',
'border-radius': '50px',
'margin-right': '10px',
'margin-top': '-90px',
'text-align': 'center', # Add this line to center the placeholder text
'justify-content': 'center' # Add this line to center the input content
}

With the following code we:

Put a dropdown menu allowing the user to select stocks from S&P500
Make an ADD button allowing the user to add them to his portfolio
Put a date dropdown menu to let the user choose the timeframe for his portfolio
Show portfolio returns over time on a graph

In [ ]:
# Initialize the app with the chosen theme
app = JupyterDash(__name__, suppress_callback_exceptions=True, external_stylesheets=[dbc.themes.QUARTZ])

# Define the layout


# Update the layout
app.layout = html.Div([
html.H1('P.R.O.F.I.T.', style={'text-align': 'center', 'margin-top': '40px', 'color': 'white', 'font-family': 'Castellar' }),
html.Div(children='Portfolio Risk Optimization and Financial Investment Tool',
style={'text-align': 'center', 'margin-top': '-20px', 'font-family': 'Castellar'}),

dcc.Dropdown(
id='stock-dropdown',
options=[{'label': ticker, 'value': ticker} for ticker in sp500_symbols],
style=dropdown_style1,
value=None,
),
dcc.DatePickerRange(
id='date-picker-range',
min_date_allowed=(datetime.today() - timedelta(days=365 * 10)).date(),
max_date_allowed=datetime.today().date(),
start_date=datetime.today().date() - timedelta(days=365),
end_date=datetime.today().date(),
display_format='YYYY-MM-DD',
persistence=True,
persisted_props=['start_date', 'end_date'],
style={'margin-right': '10px', 'margin-left': '5px', 'border-radius': '50px', 'margin-bottom': '5px', 'font-family': 'Castellar'}
),
html.Button('Add to Portfolio', id='add-button', n_clicks=0, style=button_style1),
dcc.Input(
id='investment-input',
type='number',
placeholder='Enter investment amount',
style=case_style
),
html.Button('Confirm Investment', id='confirm-investment-button', n_clicks=0,
style=button_style2),
dcc.Store(id='confirmed-investment-store', data=None),
html.Div(id='investment-output'),
dcc.Store(id='portfolio-store', data=[]),
html.Div(id='portfolio', children=[]),
dcc.Dropdown(
id='weighting-method-dropdown',
options=[
{'label': 'Equally Weighted', 'value': 'equal'},
{'label': 'Global Minimum Variance', 'value': 'min_variance'},
{'label': 'Maximum Sharpe Ratio', 'value': 'max_sharpe'}
],
style=dropdown_style2,
value='equal'),

dash_table.DataTable(
id='weights-table',
columns=[
{"name": "Stock", "id": "Stock"},
{"name": "Weight", "id": "Weight"},
],
style_header={
'backgroundColor': 'white',
'color': 'black', 'text-align': 'center'
},
style_data={
'color': 'black', 'text-align': 'center'},

),
dcc.Graph(id='portfolio-returns-chart'),
dcc.Graph(id='rolling-correlation-chart'),
dcc.Dropdown(
id='copula-dropdown',
options=[
{'label': 'Frank Copula', 'value': 'frank'},
{'label': 'Gumbel Copula', 'value': 'gumbel'},
{'label': 'Clayton Copula', 'value': 'clayton'}
],
style=dropdown_style2,
value='gumbel'),
dcc.Graph(id='copula-chart'),
dcc.Graph(id='portfolio-density-plot'),
dcc.Graph(id='benchmark-density-plot'),
])

# Update the callbacks


@app.callback(
Output('portfolio-store', 'data'),
[Input('add-button', 'n_clicks')],
[State('stock-dropdown', 'value'),
State('portfolio-store', 'data')]
)
def update_portfolio_store(n_clicks, selected_ticker, portfolio):
if n_clicks > 0 and selected_ticker is not None:
if selected_ticker not in portfolio:
portfolio.append(selected_ticker)
return portfolio

@app.callback(
[
Output("portfolio", "children"),
Output("portfolio-returns-chart", "figure"),
Output("portfolio-density-plot", "figure"),
Output("benchmark-density-plot", "figure"),
Output("investment-output", "children"),
Output("rolling-correlation-chart", "figure"),
Output("weights-table", "data"),
Output('copula-chart', 'figure'),
],
[
Input("portfolio-store", "data"),
Input("date-picker-range", "start_date"),
Input("date-picker-range", "end_date"),
Input("weighting-method-dropdown", "value"),
Input("copula-dropdown", "value"),
Input('confirm-investment-button', 'n_clicks') # Add the new button as input
],
[
State("investment-input", "value"),
State('confirmed-investment-store', 'data'), # Add the confirmed investment state
],
prevent_initial_call=True
)

def update_portfolio(portfolio, start_date, end_date, weighting_method, copula_type, n_clicks, investment_amount, confirmed_investment):


if n_clicks > 0 and investment_amount is not None:
confirmed_investment = investment_amount

start_date = pd.Timestamp(start_date)
end_date = pd.Timestamp(end_date)
risk_free_rate = get_risk_free_rate() # Fetch the risk-free rate

if not portfolio or len(portfolio) == 0:


return (
[],
go.Figure(), # Empty figure for portfolio returns chart
go.Figure(), # Empty figure for portfolio density plot
go.Figure(), # Empty figure for benchmark density plot
"Please add stocks to the portfolio.",
go.Figure(), # Empty figure for rolling correlation chart
[], # Empty data for the weights table
go.Figure() # Empty figure for copula chart
)
if len(portfolio) > 0:
# Calculate returns, VaR and ES for the user-built portfolio
stock_log_returns = get_stock_log_returns(portfolio, start_date, end_date)
stock_cov_matrix = stock_log_returns.cov()

if weighting_method == 'equal':
portfolio_weights = equal_weights(len(portfolio))
elif weighting_method == 'min_variance':
portfolio_weights = global_minimum_variance_weights(stock_cov_matrix)
elif weighting_method == 'max_sharpe':
portfolio_weights = maximum_sharpe_ratio_weights(stock_log_returns, stock_cov_matrix)

# Calculate additional metrics


portfolio_returns = stock_log_returns.loc[start_date:end_date, portfolio] * portfolio_weights
portfolio_log_returns = np.sum(portfolio_returns, axis=1)
portfolio_cumulative_returns = np.exp(portfolio_log_returns.cumsum()) - 1
portfolio_var, portfolio_es = calculate_var_es(portfolio_log_returns)
portfolio_variance = np.var(portfolio_log_returns)
portfolio_std_dev = np.sqrt(portfolio_variance)
sharpe_ratio = (np.mean(portfolio_log_returns) - risk_free_rate) / portfolio_std_dev

# Calculate returns, VaR and ES for the benchmark portfolio


benchmark_log_returns_filtered = sp500_index_log_returns.loc[start_date:end_date]
benchmark_cumulative_returns = (np.exp(np.cumsum(benchmark_log_returns_filtered)) - 1)
benchmark_variance = np.var(benchmark_log_returns_filtered)
benchmark_std_dev = np.sqrt(benchmark_variance)
benchmark_var, benchmark_es = calculate_var_es(benchmark_log_returns_filtered)
excess_return = portfolio_cumulative_returns[-1] - benchmark_cumulative_returns[-1]
excess_variance = portfolio_variance - benchmark_variance
# Calculate Information Ratio
information_ratio = (np.mean(portfolio_log_returns - benchmark_log_returns_filtered)) / np.std(portfolio_log_returns - benchmark_log_returns_filtered)

# Format the metrics as percentages


total_return_pct = f"{portfolio_cumulative_returns[-1] * 100:.2f}%"
variance_pct = f"{portfolio_variance * 100:.2f}%"
excess_return_pct = f"{excess_return * 100:.2f}%"
excess_variance_pct = f"{excess_variance * 100:.2f}%"
benchmark_return_pct = f"{benchmark_cumulative_returns[-1] * 100:.2f}%"
benchmark_variance_pct = f"{benchmark_variance * 100:.2f}%"

# Modify the portfolio_div to display two tables


portfolio_div = []
portfolio_div.append(html.H2('Portfolio'))
portfolio_div.append(html.Ul([html.Li(stock) for stock in portfolio]))

# Create the absolute analysis table


absolute_analysis_table = dbc.Table.from_dataframe(
pd.DataFrame({
'Metric': ['Total return', 'Variance', 'VaR', 'ES', 'Sharpe Ratio'],
'Value': [total_return_pct, variance_pct, portfolio_var, portfolio_es, sharpe_ratio]
}),
striped=True,
bordered=True,
hover=True,
size='sm'
)

# Create the relative analysis table


relative_analysis_table = dbc.Table.from_dataframe(
pd.DataFrame({
'Metric': ['Excess Return', 'Excess Variance', 'Information Ratio', 'Benchmark Return', 'Benchmark Variance', 'Benchmark VaR', 'Benchmark ES'],
'Value': [excess_return_pct, excess_variance_pct, information_ratio, benchmark_return_pct, benchmark_variance_pct, benchmark_var, benchmark_es]
}),
striped=True,
bordered=True,
hover=True,
size='sm'
)

# Add tables to the portfolio_div


portfolio_div.append(html.Div([
dbc.Row([
dbc.Col([
html.H4("Absolute Analysis"), # Add title for the first table
absolute_analysis_table
], width=6),
dbc.Col([
html.H4("Relative Analysis"), # Add title for the second table
relative_analysis_table
], width=6)
])
]))

# Create a chart for portfolio returns over time


returns_fig = create_chart(portfolio_cumulative_returns, benchmark_cumulative_returns)

# Create density plots


density_fig = plot_density(portfolio_log_returns, portfolio_var, portfolio_es, 'Portfolio returns density')
benchmark_density_fig = plot_density(sp500_index_log_returns, benchmark_var, benchmark_es, 'Benchmark returns density')

# Calculate and plot the rolling correlation


rolling_corr = rolling_correlation(portfolio_log_returns, benchmark_log_returns_filtered)
rolling_corr_fig = go.Figure()
rolling_corr_fig.add_trace(go.Scatter(x=rolling_corr.index, y=rolling_corr, mode='lines', name='Rolling Correlation'))
rolling_corr_fig.update_layout(title='Rolling Correlation between Portfolio and Benchmark', xaxis_title='Date', yaxis_title='Correlation')

# Calculate Gaussian copula


# Reindex portfolio and benchmark returns
common_dates = portfolio_log_returns.index.intersection(benchmark_log_returns_filtered.index)
portfolio_log_returns = portfolio_log_returns.reindex(common_dates)
benchmark_log_returns_filtered = benchmark_log_returns_filtered.reindex(common_dates)

# Calculate and plot the selected copula


copula = None
if copula_type == 'frank':
copula = calculate_frank_copula(portfolio_log_returns, benchmark_log_returns_filtered)
elif copula_type == 'gumbel':
copula = calculate_gumbel_copula(portfolio_log_returns, benchmark_log_returns_filtered)
elif copula_type == 'clayton':
copula = calculate_clayton_copula(portfolio_log_returns, benchmark_log_returns_filtered)

copula_chart = create_copula_chart(copula)

# Calculate investment output


if investment_amount is not None:
portfolio_total_return = np.exp(portfolio_log_returns.sum()) - 1
invested_amount = round(investment_amount * (1 + portfolio_total_return), 2)
investment_output = html.P(f'Investment output: ${invested_amount}')
else:
investment_output = html.P('Please enter the investment amount.')

# Create a table to display the weights


weights_table = pd.DataFrame({
"Stock": portfolio,
"Weight": portfolio_weights
})

return (
portfolio_div,
returns_fig,
density_fig,
benchmark_density_fig,
investment_output,
rolling_corr_fig,
weights_table.to_dict("records"),
copula_chart,
)
else:
return [], {}, {}, {}, 'Please add stocks to the portfolio.', {}, []

Adding update to the layout for VAR and ES

In [ ]:
JupyterDash.infer_jupyter_proxy_config()
app.run_server(port=2225)

In [ ]:

In [ ]:

You might also like