Fin_Research / pages /2_Portfolio_Builder.py
RobertCastagna's picture
Update pages/2_Portfolio_Builder.py
4417139 verified
import pandas as pd
from openbb import obb
import riskfolio as rp
import os
import regex as re
from dotenv import load_dotenv
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
import plotly.graph_objs as go
import plotly.tools as tls
from plotly.subplots import make_subplots
import plotly.figure_factory as ff
import streamlit as st
import platform
import datetime
import asyncio
import nest_asyncio
import json
import requests
def open_nested_event_loop():
# Check if there is an existing event loop, if not, create a new one
nest_asyncio.apply()
try:
loop = asyncio.get_event_loop()
st.write("event loop exists")
except RuntimeError as ex:
if "There is no current event loop in thread" in str(ex):
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
st.write("event loop created")
return
open_nested_event_loop()
def get_positions(account):
positions = ib.portfolio(account)
tickers = []
for position in positions:
tickers.append([position.contract.symbol, position.position, position.marketPrice])
return tickers
def get_finnhub_data(example: str) -> json:
"""
Pass in the "example" string from the API documentation. It changes for every endpoint.
:param1 example: '/company-news?symbol=AAPL&from=2023-08-15&to=2023-08-20'
"""
base_url = 'https://finnhub.io/api/v1//'
token = f"&token={os.environ['finnhub_token']}"
request = requests.get(f"{base_url}{example}{token}")
return request.json()
@st.cache_data
def get_list_of_tickers():
comp_info = get_finnhub_data('/stock/symbol?exchange=US')
list_of_tickers = []
for i in range(len(comp_info)-1):
for key in comp_info[i].keys():
if key == 'symbol':
list_of_tickers.append(comp_info[i]['symbol'])
return list_of_tickers
load_dotenv()
obb.account.login(pat=os.environ['open_bb_pat'])
# -------------------------------- (Tickers) -------------------------------- #
if re.search('AuthenticAMD', platform.processor()) and 'tickers' not in st.session_state: # use live ibkr portfolio if ran from local machine
try:
# Initialize IB connection here
from ib_insync import *
with IB().connect('127.0.0.1', 7497, clientId=0, timeout=10) as ib:
# Assuming you're connecting to a local TWS instance with default settings
paper_acct = ib.managedAccounts()[0]
position_data = get_positions(paper_acct)
for pos in position_data:
tickers = [pos[0] for pos in position_data]
st.session_state.tickers = tickers
except Exception as e:
st.write('IB Workstation is not running. Please start the IB Workstation and try again.')
st.write('Error: ', e)
st.stop()
if 'tickers' not in st.session_state:
tickers = [
"SPY", "QQQ","BND","GLD","VTI"
]
st.session_state['tickers'] = tickers
if not platform.processor(): # take inputs from the user for hosted application
tickers = st.session_state['tickers']
list_of_tickers = get_list_of_tickers()
with st.form(key="selecting columns"):
tickers = st.multiselect(label='Enter Tickers Here. ', options=list_of_tickers, placeholder='...', default=st.session_state['tickers'])
submit_button = st.form_submit_button(label='Optimize Portfolio')
st.session_state['tickers'] = tickers
if submit_button:
# define range as today - 365 days to today
start_date = (datetime.datetime.now() - datetime.timedelta(days=365)).strftime('%Y-%m-%d')
end_date = datetime.datetime.now().strftime('%Y-%m-%d')
data = (
obb
.equity
.price
.historical(tickers, start_date=start_date, end_date=end_date, provider="yfinance")
.to_df()
.pivot(columns="symbol", values="close")
)
returns = data.pct_change().dropna()
# -------------------------- (Efficient Frontier Calculation) -------------------------------- #
st.title('Efficient Frontier Portfolio')
st.write("The efficient frontier is a set of optimal portfolios that offer the highest expected return for a defined level of risk or the lowest risk for a given level of expected return. Portfolios that lie below the efficient frontier are sub-optimal because they do not provide enough return for the level of risk. Portfolios that cluster to the right of the efficient frontier are also sub-optimal because they have a higher level of risk for the defined rate of return.")
port = rp.Portfolio(returns=returns)
# Step 2: Set portfolio optimization model
port.assets_stats(model='hist') # Using historical data for estimation
# Step 3: Configure the optimization model and calculate the efficient frontier
ef = port.efficient_frontier(model='Classic', rm='MV', points=50, rf=0.0406, hist=True)
w1 = port.optimization(model='Classic', rm='MV', obj='Sharpe', rf=0.0, hist=True)
mu = port.mu # Expected returns
cov = port.cov # Covariance matrix
# ---------------------------- (Portfolio Statistics) -------------------------------- #
st.write('**Portfolio Statistics Optimized for Max Sharpe Ratio:**')
spy_prices = obb.equity.price.historical(symbol = "spy", provider="yfinance", start_date=start_date, end_date=end_date).to_df()
# Calculate daily returns
# Ensure you're using the adjusted close prices for accurate return calculation
benchmark_returns = spy_prices['close'].pct_change().dropna()
port.rf = 0.000406 # Risk-free rate
portfolio_return = np.dot(w1, mu)
# market return
spy_daily_return = benchmark_returns
spy_expected_return = spy_daily_return.mean()
# portfolio's beta
covariance = returns.apply(lambda x: x.cov(spy_daily_return))
spy_variance = spy_daily_return.var()
beta_values = covariance / spy_variance
portfolio_beta = np.dot(w1['weights'], beta_values)
st.write('Portfolio Beta: ', np.round(portfolio_beta,3))
# jensens alpha
expected_return = port.rf + portfolio_beta * (spy_daily_return - port.rf)
st.write('Jensen\'s Alpha: ', np.round(expected_return.iloc[-1],3))
# treynor ratio
treynor_ratio = (expected_return - port.rf) / portfolio_beta
st.write('Treynor Ratio: ', np.round(treynor_ratio.iloc[-1],3))
# Portfolio volatility
portfolio_stddev = np.sqrt(np.dot(pd.Series(w1['weights']).T, np.dot(covariance, w1['weights'])))
st.write('Portfolio Volatility: ', np.round(np.mean(portfolio_stddev), 3))
# Sharpe Ratio, adjusted for the risk-free rate
sharpe_ratio = rp.RiskFunctions.Sharpe(
mu=mu,
cov=cov,
returns=returns,
rf=port.rf,
w=w1,
)
st.write('Sharpe Ratio: ', np.round(sharpe_ratio, 3))
# -------------------------- (Plotting) -------------------------------- #
# Step 4: Plot the efficient frontier
fig_ef, ax_ef = plt.subplots()
ax_ef = rp.plot_frontier(mu=mu, cov=cov, returns=port.returns, w=w1, rm='MV', w_frontier=ef, marker='*', label='Optimal Portfolio - Max. Sharpe' ,s=16)
st.pyplot(fig_ef)
st.write('**Asset Mix of Optimized Portfolio:**')
st.dataframe(w1.T, use_container_width=True)
# corr matrix
fig, ax = plt.subplots()
corr = returns.corr()
# Create a heatmap
heatmap = go.Heatmap(z=corr.values, x=corr.columns, y=corr.index, colorscale='RdYlBu')
layout = go.Layout(title='Correlation Matrix', autosize=True)
fig = go.Figure(data=[heatmap], layout=layout)
st.plotly_chart(fig)
# -------------------------- (HRP Portfolio) -------------------------------- #
st.title('Hierarchical Risk Parity Portfolio')
st.write("""
HRP is unlike traditional portfolio optimization methods. It can create an optimized portfolio when the covariance matrix is ill-degenerated or singular. This is impossible for quadratic optimizers.
Research has shown HRP to deliver lower out-of-sample variance than traditional optimization methods.
""")
fig1, ax1 = plt.subplots()
ax1 = rp.plot_clusters(returns=returns,
codependence='pearson',
linkage='single',
k=None,
max_k=10,
leaf_order=True,
dendrogram=True,
ax=None)
st.pyplot(fig1)
port = rp.HCPortfolio(returns=returns)#, w_max=0.3, w_min=0.05)
w = port.optimization(
model="HRP",
codependence="pearson",
obj='Sharpe',
rm='vol', # minimum variance
rf=0.000406,
linkage="single",
max_k=10,
leaf_order=True,
)
fig2, ax2 = plt.subplots()
ax2 = rp.plot_pie(
w=w,
title="HRP Naive Risk Parity",
others=0.05,
nrow=25,
cmap="tab20",
height=8,
width=10,
ax=None,
)
st.pyplot(fig2)
fig3, ax3 = plt.subplots()
ax3 = rp.plot_risk_con(
w=w,
cov=returns.cov(),
returns=returns,
rm="MV",
rf=0,
alpha=0.05,
color="tab:blue",
height=6,
width=10,
t_factor=252,
ax=None,
)
st.pyplot(fig3)
# ---------------------------- (Portfolio Statistics) -------------------------------- #
spy_prices = obb.equity.price.historical(symbol = "spy", provider="yfinance", start_date=start_date, end_date=end_date).to_df()
# Calculate daily returns
# Ensure you're using the adjusted close prices for accurate return calculation
benchmark_returns = spy_prices['close'].pct_change().dropna()
port.rf = 0.000406 # Risk-free rate
portfolio_return = np.dot(w, mu)
# market return
spy_daily_return = benchmark_returns
spy_expected_return = spy_daily_return.mean()
# portfolio's beta
covariance = returns.apply(lambda x: x.cov(spy_daily_return))
spy_variance = spy_daily_return.var()
beta_values = covariance / spy_variance
portfolio_beta = np.dot(w['weights'], beta_values)
st.write('Portfolio Beta: ', np.round(portfolio_beta,3))
# jensens alpha
expected_return = port.rf + portfolio_beta * (spy_daily_return - port.rf)
st.write('Jensen\'s Alpha: ', np.round(expected_return.iloc[-1],3))
# treynor ratio
treynor_ratio = (expected_return - port.rf) / portfolio_beta
st.write('Treynor Ratio: ', np.round(treynor_ratio.iloc[-1],3))
# Portfolio volatility
portfolio_stddev = np.sqrt(np.dot(pd.Series(w['weights']).T, np.dot(covariance, w['weights'])))
st.write('Portfolio Volatility: ', np.round(np.mean(portfolio_stddev), 3))
# Sharpe Ratio, adjusted for the risk-free rate
sharpe_ratio = rp.RiskFunctions.Sharpe(
mu=mu,
cov=cov,
returns=returns,
rf=port.rf,
w=w,
)
st.write('Sharpe Ratio: ', np.round(sharpe_ratio, 3))
# -------------------------- (Report) -------------------------------- #
# fig_report = rp.jupyter_report(returns,
# w=w1,
# rm='MV',
# rf=0.0406,
# nrow=25
# )
# st.pyplot(fig_report)