import gradio as gr import yfinance as yf import numpy as np import pandas as pd from datetime import datetime, timedelta from numpy.linalg import inv import pathlib import matplotlib.pyplot as plt def plot_portfolio_cumulative_return_daily(df, portfolio_weights): returns = df.pct_change() weighted_returns = returns * portfolio_weights portfolio_returns = weighted_returns.sum(axis=1) portfolio_cumulative_return = (1 + portfolio_returns).cumprod() - 1 sp500 = yf.download('^GSPC', start=df.index.min(), end=df.index.max()) sp500_returns = sp500['Adj Close'].pct_change() sp500_cumulative_return = (1 + sp500_returns).cumprod() - 1 # Create the plot fig = plt.figure(figsize=(10, 6)) ax = fig.add_subplot(111) # Plot portfolio cumulative returns ax.plot(portfolio_cumulative_return.index, portfolio_cumulative_return, label='Portfolio Cumulative Return', color='blue') # Plot S&P 500 cumulative returns ax.plot(sp500_cumulative_return.index, sp500_cumulative_return, label='S&P 500 Cumulative Return', color='red') ax.set_title('Portfolio Cumulative Return vs. S&P 500 Cumulative Return (Daily)') ax.set_xlabel('Date') ax.set_ylabel('Cumulative Return') ax.legend() fig.tight_layout() return fig def generate_portfolios(age, investment_size, volatility, risk_flag, selected_sectors,button_ignore, num_stocks): try: payload = pd.read_html('https://en.wikipedia.org/wiki/List_of_S%26P_500_companies') df = pd.DataFrame(payload[0]) #tickers_symbols = df.values.tolist() stock_symbols = df[df['GICS Sector'].isin(selected_sectors)]['Symbol'].tolist() fig = plt.figure(figsize=(10, 6)) empty_df_html = pd.DataFrame().to_html(index=False) if len(stock_symbols) < num_stocks: return (empty_df_html,"Not enough stocks in those sectors please add sectors",empty_df_html,fig) if (age > 50 and volatility > 0.5 and not risk_flag): return (empty_df_html,"Its high risk for your age decrease the risk or check the Activate risk button",empty_df_html,fig) End_date = (datetime.today()).strftime('%Y-%m-%d') Start_date = '2021-01-01' end_training_date = '2023-01-01' Start_training_date_future_predictions = '2022-01-01' data= yf.download(stock_symbols, start=Start_date, end=End_date) close = data['Adj Close'].dropna(axis=1).dropna() stock_symbols = close.columns.tolist() returns = close.pct_change(1).dropna() ################ Calculations related to past predictions results old_portfolio = weighted_portfolio(returns,stock_symbols,volatility,Start_date,end_training_date) old_portfolio, old_num_stocks_symbols = shrink_portfolio(old_portfolio,stock_symbols,num_stocks) old_portfolio_returns_df, old_asset_returns = calculate_portfolio_return(close[close.index > end_training_date][old_num_stocks_symbols],old_portfolio) old_results_df, old_exact_sum = calculate_portfolio_allocation(old_num_stocks_symbols, old_portfolio, investment_size, close[old_num_stocks_symbols]) fig = plot_portfolio_cumulative_return_daily(close[close.index > end_training_date][old_num_stocks_symbols],old_portfolio) header_html_portfolio = f"

Should you trust us?

" ration = old_portfolio_returns_df['Portfolio Total Return'].iloc[0] - old_portfolio_returns_df['S&P 500 Total Return'].iloc[0] ration = np.round(ration,2) header_html_portfolio_small = f"

Portfolio return per year (%) - calculated based on data from 2021 to 2022 and evaluated from 2023 to present. Excess return of {ration} % for our portfolio relative to the s&p500

" instructions_html_portfolio = "

The portfolio is formulated using stock returns from 2021 to 2022 and assessed from 2023 to the present day. It aligns with your chosen sectors and volatility preferences, employing the following portfolio allocations.

" portfolio_returns_df_html = header_html_portfolio + header_html_portfolio_small + old_portfolio_returns_df.to_html(index=False) + instructions_html_portfolio #transposed_old_results_df.to_html(index=False) ################ Calculations related to future predictions portfolio = weighted_portfolio(returns,stock_symbols,volatility,Start_training_date_future_predictions,End_date) portfolio, num_stocks_symbols = shrink_portfolio(portfolio,stock_symbols,num_stocks) portfolio_returns_df, asset_returns = calculate_portfolio_return(close[num_stocks_symbols],portfolio) results_df, exact_sum = calculate_portfolio_allocation(num_stocks_symbols, portfolio, investment_size, close[close.index > end_training_date][num_stocks_symbols]) one_year_ago_date = (datetime.today() - timedelta(days=365)).strftime('%Y-%m-%d') results_df[f'1Y stock return (%) ({one_year_ago_date} - {End_date})'] = np.round(np.array(asset_returns.tolist()) * 100, 1) results_df_html = results_df.to_html(index=False, escape=False) # Iterate over each row in the DataFrame to add hyperlinks for index, row in results_df.iterrows(): ticker = row['Symbol'] # Assuming 'Ticker' is the column name containing stock tickers hyperlink = '{}'.format(ticker, ticker) results_df_html = results_df_html.replace('{}'.format(ticker), '{}'.format(hyperlink)) # Add instructions in smaller letters instructions_html = "

Click and open in a new window on each stock symbol to view more information about the company.

" header_html = "

Our future predictions

" small_header_html = "

Portfolio Summary formulated from 2022 to the present day, leveraging the same sectors and volatility parameters you have chosen.

" # Smaller header HTML results_df_html = header_html + small_header_html + results_df_html + instructions_html return (results_df_html , exact_sum,portfolio_returns_df_html ,fig) except Exception as e: error_message = str(e) return (np.nan, error_message, np.nan, np.nan) def shrink_portfolio(portfolio,stock_symbols,num_stocks): if len(stock_symbols) == num_stocks: return portfolio else: sorted_indices = np.argsort(portfolio) num_stocks_weights_indices = sorted_indices[-num_stocks:][::-1] # num_stocks biggest weights indices num_stocks_weights = portfolio[num_stocks_weights_indices] # num_stocks biggest weights values num_stocks_symbols = [stock_symbols[i] for i in num_stocks_weights_indices] #num_stocks biggest weights symbols non_selected_sum = np.sum(portfolio[sorted_indices[:-num_stocks]]) num_stocks_weights /= (1 - non_selected_sum) # normalize weights to 1 portfolio_updated = num_stocks_weights return portfolio_updated, num_stocks_symbols def weighted_portfolio(returns,stock_symbols,volatility,start_training_date,end_training_date): min_var_portfolio,C,C_inv = min_variance(returns[(returns.index < end_training_date)&(returns.index >= start_training_date)]) best_basket_portfolio = best_basket(returns[(returns.index < end_training_date)&(returns.index >= start_training_date)],C,C_inv) portfolio = (1-volatility)*min_var_portfolio + volatility*best_basket_portfolio return portfolio def min_variance(returns): C = returns.cov() C_inv = np.linalg.inv(C) e = np.ones(C.shape[0]) min_var = np.matmul(C_inv, e) / np.matmul(np.matmul(e.T, C_inv), e) return min_var,C,C_inv def best_basket(returns,C,C_inv): #C = returns.cov().to_numpy() e = np.ones((np.shape(C)[0],1), dtype=int) #C_inv = inv(C) mean_returns = np.array(returns.mean().values) divider = e.T@C_inv@mean_returns best_basket = C_inv@mean_returns / divider return best_basket def get_sp500_cumulative_return(start_date, end_date): sp500_data = yf.download('^GSPC', start=start_date, end=end_date) sp500_returns = sp500_data['Adj Close'].pct_change() cumulative_return = (1 + sp500_returns).prod() - 1 cumulative_return_percentage = np.round(cumulative_return * 100, 2) return cumulative_return_percentage def calculate_portfolio_return(close_prices_df, weights): annual_returns = close_prices_df.groupby(close_prices_df.index.year).apply(lambda x: x.iloc[-1] / x.iloc[0] - 1) # Calculate the weighted returns for each asset weighted_annual_returns = annual_returns * weights # Calculate the overall portfolio return portfolio_return_per_year = weighted_annual_returns.sum(axis=1) overall_return = np.prod(portfolio_return_per_year+1) - 1 portfolio_return_per_year = np.round(portfolio_return_per_year * 100, 2) years = close_prices_df.index.year.unique().tolist() # Create a DataFrame with a single row containing the portfolio return for each year portfolio_return_per_year_df = pd.DataFrame([portfolio_return_per_year], columns=years) portfolio_return_per_year_df['Portfolio Total Return'] = np.round(overall_return * 100 , 2) portfolio_return_per_year_df['S&P 500 Total Return'] = get_sp500_cumulative_return(close_prices_df.index.min(), close_prices_df.index.max()) Start_date = (datetime.today() - timedelta(days=365)).strftime('%Y-%m-%d') last_year_close_prices_df = close_prices_df[close_prices_df.index>Start_date] last_year_asset_returns = last_year_close_prices_df.iloc[-1] / last_year_close_prices_df.iloc[0] - 1 return portfolio_return_per_year_df, last_year_asset_returns def calculate_portfolio_allocation(symbols, weights, investment_amount, close_stocks_df): # Calculate percent of portfolio for each symbol portfolio_allocation = [round(weight * 100, 2) for weight in weights] # Get the last row of the close_stocks_df to get the current stock prices last_prices = close_stocks_df.iloc[-1] # Calculate number of shares and value of shares for each symbol num_shares = [] exact_values = [] for symbol, stock_weight in zip(symbols, weights): stock_price = last_prices[symbol] shares = int(np.floor((stock_weight * investment_amount) / stock_price)) exact_value = shares * stock_price num_shares.append(shares) exact_values.append(exact_value) exact_sum = np.round(sum(exact_values),2) # Calculate portfolio allocation # Create a DataFrame with the results results_df = pd.DataFrame({ 'Symbol': symbols, 'Shares to buy': num_shares, 'Purchase amount in USD': np.round(np.array(exact_values), 2), 'Portfolio Allocation (%)': portfolio_allocation, }).sort_values(by='Portfolio Allocation (%)', ascending=False) results_df['Portfolio Allocation (%)'] = round((results_df['Purchase amount in USD'] / exact_sum) * 100, 2) return results_df,exact_sum interface = gr.Interface( title='Stock Guide', description="This app utilizes interactive features to gather information from users, such as their level of volatility tolerance,\ preferred sectors, amount of investment, and desired number of stocks in the portfolio. Using this information, \ the app generates a selection of stocks that align with the user's criteria. By providing tailored portfolio suggestions and educational resources,\ the app aims to empower users to make informed investment decisions that align with their financial objectives. We're here to create the best investment portfolio for you.\ Let's start by asking you a few questions to understand your financial goals and preferences. The results are only a hypothesis and do not constitute a recommendation. Created by Idan Salutsky & Tomer Eichler", fn=generate_portfolios, inputs=[ gr.Number(label="Age, minimum 18", minimum=18, maximum=120, show_label=True, container=True), gr.Number(label="Investment amount (USD) minimum 10000$", minimum=10000 , info="We would like to know the amount of money that the investor would like to invest in order to correctly allocate investments in the portfolio.", show_label=True, container=True), gr.Slider(label="Volatility of the investment Portfolio", minimum=0, maximum=1, show_label=True, container=True, info="Your volatility choice reflects how much risk and potential return you're comfortable with. Higher volatility means embracing market fluctuations for potentially greater returns, while lower volatility prioritizes stability and risk mitigation. Understanding and aligning your volatility preference with your financial goals helps tailor your portfolio to suit your needs. Low volatility - [0-0.2], medium - [0.2-0.8], high - [0.8-1]"), gr.Checkbox(label="Activate risk", info="If you are over 50 years old and have chosen a volatility level greater than 0.5, you must understand that this is a high risk for your age, if you want to continue press the button"), gr.CheckboxGroup(choices=['Industrials', 'Health Care', 'Information Technology', 'Utilities', 'Financials', 'Materials', 'Consumer Discretionary', 'Real Estate', 'Communication Services', 'Consumer Staples', 'Energy'], label="Choose Sectors from the GICS Sectors", info="Choose your preferred sectors"), gr.Button(value="Click for learning about GICS Sectors !", link="https://en.wikipedia.org/wiki/Global_Industry_Classification_Standard"), gr.Number(label="Number of Stocks", minimum=1, maximum=20, info="Deciding on the number of stocks in your portfolio is critical for achieving a balance between risk and return. More stocks increase diversification but demand more research and monitoring. Conversely, fewer stocks lead to concentration, potentially amplifying individual stock impacts. Balancing diversification and concentration aligns with your objectives and ensures effective risk management in your investment portfolio. (maximum 20)"), ], outputs=[ gr.HTML(), gr.Textbox(label="Total budget used"), gr.HTML(), gr.Plot(), ], ) interface.launch()