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)