File size: 14,064 Bytes
1918fd3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244

from IPython.display import HTML
import gradio as gr
import yfinance as yf  # Import yfinance for downloading data
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() + timedelta(days=1)).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"<h2>Should you trust us? </h2>"
      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"<h3>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</h3>"
      instructions_html_portfolio = "<p style='font-size: medium;'>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.</p>"
      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 = '<a href="https://finance.yahoo.com/quote/{}">{}</a>'.format(ticker, ticker)
          results_df_html = results_df_html.replace('<td>{}</td>'.format(ticker), '<td>{}</td>'.format(hyperlink))

      # Add instructions in smaller letters
      instructions_html = "<p style='font-size: medium;'>Click on each stock symbol to view more information about the company.</p>"
      header_html = "<h2>Our future predictions</h2>"
      small_header_html = "<h3>Portfolio Summary formulated from 2022 to the present day, leveraging the same sectors and volatility parameters you have chosen.</h3>"  # 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)
    # 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)

    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, maximum=120, show_label=True, container=True),
        gr.Number(label="Investment Size (USD)", 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()