Davidchen8/Portfolio_Backtesting_MovingAverage
0
1import yfinance as yf2import pandas as pd3import numpy as np4import matplotlib.pyplot as plt5import gradio as gr6 7def categorize_stocks(tickers):8 """Categorize tickers into US and HK markets."""9 us_stocks = [ticker for ticker in tickers if not ticker.endswith(".HK")]10 hk_stocks = [ticker for ticker in tickers if ticker.endswith(".HK")]11 return us_stocks, hk_stocks12 13def download_stock_data(tickers, start_date, end_date):14 """Download stock data for multiple tickers from Yahoo Finance."""15 stock_data = {}16 for ticker in tickers:17 data = yf.download(ticker, start=start_date, end=end_date)18 if not data.empty:19 stock_data[ticker] = data20 return stock_data21 22def download_benchmark_data(start_date, end_date):23 """Download benchmark data for S&P 500 and Hang Seng Index."""24 benchmarks = {25 'S&P 500': yf.download('^GSPC', start=start_date, end=end_date),26 'Hang Seng Index': yf.download('^HSI', start=start_date, end=end_date)27 }28 return benchmarks29 30def calculate_performance_metrics(data):31 """Calculate performance metrics for a given data series."""32 if data.empty or len(data) < 2:33 raise ValueError("Insufficient data to calculate performance metrics.")34 cumulative_returns = (data.iloc[-1] / data.iloc[0] - 1) * 10035 max_drawdown = (data / data.cummax() - 1).min() * 10036 annualized_return = ((data.iloc[-1] / data.iloc[0]) ** (1 / (len(data) / 252)) - 1) * 10037 std_dev = np.std(data.pct_change()) * np.sqrt(252) * 10038 39 cumulative_returns = cumulative_returns if not isinstance(cumulative_returns, pd.Series) else cumulative_returns.iloc[0]40 max_drawdown = max_drawdown if not isinstance(max_drawdown, pd.Series) else max_drawdown.iloc[0]41 annualized_return = annualized_return if not isinstance(annualized_return, pd.Series) else annualized_return.iloc[0]42 std_dev = std_dev if not isinstance(std_dev, pd.Series) else std_dev.iloc[0]43 44 return round(cumulative_returns, 2), round(max_drawdown, 2), round(annualized_return, 2), round(std_dev, 2)45 46def parse_stock_tickers_and_weightings(input_string):47 """Parse stock tickers and weightings from user input."""48 tickers = []49 weightings = {}50 try:51 items = [item.strip() for item in input_string.split(",") if item.strip()]52 for item in items:53 if ":" in item:54 ticker, weight = item.split(":")55 ticker = ticker.strip()56 weight = float(weight.strip()) / 100 # Convert percentage to a decimal57 tickers.append(ticker)58 weightings[ticker] = weight59 else:60 raise ValueError61 if not tickers or not weightings:62 raise ValueError63 return tickers, weightings64 except Exception:65 raise ValueError("Invalid format. Use: Ticker1:Weight1, Ticker2:Weight2 (e.g., AAPL:15, MSFT:10)")66 67def simple_moving_average_strategy_with_volume(data, buy_short_window_1, buy_long_window_1, buy_condition, buy_short_window_2, buy_long_window_2, sell_short_window, sell_long_window, volume_window, volume_threshold, drawback_threshold):68 """Apply a simple moving average strategy to multiple stocks with volume filtering."""69 signals = {}70 for ticker, df in data.items():71 signal_df = pd.DataFrame(index=df.index)72 signal_df['price'] = df['Adj Close']73 signal_df['volume'] = df['Volume']74 signal_df['buy_short_mavg_1'] = df['Adj Close'].rolling(window=buy_short_window_1).mean()75 signal_df['buy_long_mavg_1'] = df['Adj Close'].rolling(window=buy_long_window_1).mean()76 signal_df['buy_short_mavg_2'] = df['Adj Close'].rolling(window=buy_short_window_2).mean()77 signal_df['buy_long_mavg_2'] = df['Adj Close'].rolling(window=buy_long_window_2).mean()78 signal_df['sell_short_mavg'] = df['Adj Close'].rolling(window=sell_short_window).mean()79 signal_df['sell_long_mavg'] = df['Adj Close'].rolling(window=sell_long_window).mean()80 signal_df['volume_mavg'] = df['Volume'].rolling(window=volume_window).mean()81 signal_df['signal'] = 0.082 signal_df['max_price_since_entry'] = np.nan83 84 # Generate buy/sell signals85 for i in range(max(buy_long_window_1, sell_long_window), len(signal_df)):86 signal_df.loc[signal_df.index[i], 'signal'] = signal_df['signal'].iloc[i - 1]87 88 if signal_df['signal'].iloc[i - 1] == 0.0: # No current position89 if (90 (buy_condition == "one" and91 signal_df['buy_short_mavg_1'].iloc[i] > signal_df['buy_long_mavg_1'].iloc[i] and92 signal_df['sell_short_mavg'].iloc[i] > signal_df['sell_long_mavg'].iloc[i] and93 signal_df['volume'].iloc[i] > (volume_threshold / 100) * signal_df['volume_mavg'].iloc[i])94 or95 (buy_condition == "two" and96 signal_df['buy_short_mavg_1'].iloc[i] > signal_df['buy_long_mavg_1'].iloc[i] and97 signal_df['buy_short_mavg_2'].iloc[i] > signal_df['buy_long_mavg_2'].iloc[i] and98 signal_df['sell_short_mavg'].iloc[i] > signal_df['sell_long_mavg'].iloc[i] and99 signal_df['volume'].iloc[i] > (volume_threshold / 100) * signal_df['volume_mavg'].iloc[i])100 ):101 signal_df.loc[signal_df.index[i], 'signal'] = 1.0102 signal_df.loc[signal_df.index[i], 'max_price_since_entry'] = signal_df['price'].iloc[i]103 104 # Maintain max price since entry105 if signal_df['signal'].iloc[i - 1] == 1.0:106 signal_df.loc[signal_df.index[i], 'max_price_since_entry'] = max(107 signal_df['max_price_since_entry'].iloc[i - 1], signal_df['price'].iloc[i])108 109 # Sell condition110 if signal_df['signal'].iloc[i - 1] == 1.0 and signal_df['sell_short_mavg'].iloc[i] < signal_df['sell_long_mavg'].iloc[i]:111 signal_df.loc[signal_df.index[i], 'signal'] = 0.0112 113 # Stop loss condition114 if signal_df['signal'].iloc[i - 1] == 1.0 and signal_df['price'].iloc[i] < (1 - drawback_threshold / 100) * signal_df['max_price_since_entry'].iloc[i]:115 signal_df.loc[signal_df.index[i], 'signal'] = 0.0116 117 signal_df['positions'] = signal_df['signal'].diff()118 signal_df.index = signal_df.index.tz_localize(None)119 signals[ticker] = signal_df120 121 return signals122 123def combine_trading_dates(signals):124 """Combine all unique trading dates from different exchanges into a master date index."""125 all_dates = pd.Index([])126 for signal_df in signals.values():127 all_dates = all_dates.union(signal_df.index)128 return pd.to_datetime(all_dates)129 130def fill_missing_prices(signal_df, master_dates):131 """Fill missing prices in the signal DataFrame with the previous closing price."""132 signal_df = signal_df.reindex(master_dates) # Reindex to the master dates133 signal_df['price'] = signal_df['price'].ffill() # Forward fill to handle closed days134 signal_df['price'] = signal_df['price'].fillna(0) # Forward fill to handle closed days135 signal_df['volume'] = signal_df['volume'].fillna(0) # Set volume to 0 on closed days136 signal_df['positions'] = signal_df['positions'].fillna(0) # No position change on closed days137 return signal_df138 139def fill_missing_prices_stocks(stock_df, master_dates):140 """Fill missing prices in the signal DataFrame with the previous closing price."""141 142 stock_df = stock_df.reindex(master_dates) # Reindex to the master dates143 stock_df['Adj Close'] = stock_df['Adj Close'].ffill() # Forward fill to handle closed days144 stock_df['Adj Close'] = stock_df['Adj Close'].fillna(0) # Forward fill to handle closed days145 stock_df['Volume'] = stock_df['Volume'].fillna(0) # Set volume to 0 on closed days146 147 df=pd.DataFrame(index=master_dates)148 df['price']=stock_df['Adj Close']149 df['volume']=stock_df['Volume'] 150 151 return df152 153def backtest_portfolio(signals, initial_capital, stock_weightings, us_exposure, master_dates):154 """Backtest the portfolio strategy with a combined trading calendar."""155 # Initialize cash and holdings156 cash = initial_capital157 holdings = {ticker: 0.0 for ticker in signals.keys()}158 holdings_over_time = pd.DataFrame(index=master_dates, columns=signals.keys(),dtype=float).fillna(0)159 160 # Initialize the portfolio value DataFrame with the master dates161 portfolio_value = pd.DataFrame(index=master_dates,dtype=float)162 portfolio_value['cash'] = 0.0163 portfolio_value['total'] = 0.0164 portfolio_value['cash'].iloc[0] = initial_capital165 portfolio_value['total'].iloc[0] = initial_capital166 167 # Categorize stocks into US and HK168 us_stocks, hk_stocks = categorize_stocks(signals.keys())169 max_us_allocation = initial_capital * us_exposure170 max_hk_allocation = initial_capital * (1 - us_exposure)171 us_allocation = 0.0172 hk_allocation = 0.0173 174 # Iterate over all master dates and perform backtesting175 for date in master_dates:176 daily_value = 0177 previous_date = master_dates[master_dates.get_loc(date) - 1] if master_dates.get_loc(date) > 0 else date178 for ticker, signal_df in signals.items():179 if date in signal_df.index:180 # Check for buy signal181 if signal_df.at[date, 'positions'] == 1.0:182 allocation = stock_weightings[ticker] * portfolio_value.at[previous_date, 'total']183 if ticker in us_stocks and us_allocation + allocation <= max_us_allocation:184 if cash >= allocation:185 shares_to_buy = allocation / signal_df.at[date, 'price']186 holdings[ticker] = shares_to_buy187 cash -= allocation188 us_allocation += allocation189 elif ticker in hk_stocks and hk_allocation + allocation <= max_hk_allocation:190 if cash >= allocation:191 shares_to_buy = allocation / signal_df.at[date, 'price']192 holdings[ticker] = shares_to_buy193 cash -= allocation194 hk_allocation += allocation195 # Check for sell signal196 elif signal_df.at[date, 'positions'] == -1.0:197 cash += holdings[ticker] * signal_df.at[date, 'price']198 if ticker in us_stocks:199 us_allocation -= holdings[ticker] * signal_df.at[date, 'price']200 elif ticker in hk_stocks:201 hk_allocation -= holdings[ticker] * signal_df.at[date, 'price']202 holdings[ticker] = 0.0203 204 daily_value += holdings[ticker] * signal_df.at[date, 'price']205 holdings_over_time.at[date, ticker] = holdings[ticker]206 207 # Update portfolio value208 portfolio_value.at[date, 'cash'] = float(cash) # Explicitly cast to float209 portfolio_value.at[date, 'total'] = float(daily_value) + float(cash) # Explicitly cast to float210 211 # Calculate returns212 portfolio_value['returns'] = portfolio_value['total'].pct_change()213 portfolio_value['returns'].iloc[0] = 0.0214 portfolio_value['cumulative_returns'] = (1 + portfolio_value['returns']).cumprod() - 1215 216 # Return both portfolio_value and holdings_over_time217 return portfolio_value, holdings_over_time218 219def run_strategy(tickers, start_date, end_date, initial_capital, buy_short_window_1, buy_long_window_1, buy_condition, buy_short_window_2, buy_long_window_2, sell_short_window, sell_long_window, volume_window, volume_threshold, drawback_threshold, stock_weightings, us_exposure):220 """Run the strategy with user inputs and market exposure constraints."""221 stock_data = download_stock_data(tickers, start_date, end_date)222 if not stock_data:223 return "No valid data found for the given tickers.", None, None, None224 225 signals = simple_moving_average_strategy_with_volume(226 stock_data, buy_short_window_1, buy_long_window_1, buy_condition, buy_short_window_2, buy_long_window_2, sell_short_window, sell_long_window, volume_window, volume_threshold, drawback_threshold227 )228 229 # Combine all trading dates from different exchanges230 master_dates = combine_trading_dates(signals)231 232 # Fill missing prices for each stock in the signals233 for ticker in signals:234 signals[ticker] = fill_missing_prices(signals[ticker], master_dates)235 236 portfolio_value, holdings_over_time = backtest_portfolio(signals, initial_capital, stock_weightings, us_exposure, master_dates)237 238 # Download benchmark data239 benchmark_data = download_benchmark_data(start_date, end_date)240 241 # Reindex benchmark data to match combined_dates and forward-fill missing values & calculate the performance metrics242 benchmark_metrics = {}243 for index in benchmark_data:244 benchmark_data[index] = fill_missing_prices_stocks(benchmark_data[index], master_dates)245 benchmark_metrics[index] = calculate_performance_metrics(benchmark_data[index]['price'])246 247 # Calculate portfolio performance metrics248 start_portfolio_value = portfolio_value['total'].iloc[0]249 final_value = portfolio_value['total'].iloc[-1]250 cumulative_returns = round((portfolio_value['total'].iloc[-1]/portfolio_value['total'].iloc[0]-1) * 100, 2)251 max_drawdown = round((portfolio_value['total']/portfolio_value['total'].cummax()-1).min()* 100, 2)252 annualized_return = round(((final_value / start_portfolio_value) ** (1 / (len(portfolio_value['total'])/ 252)) - 1) * 100, 2)253 portfolio_std = round(portfolio_value['returns'].std() * (252 ** 0.5) * 100, 2)254 255 256 # Create a comparison table257 comparison_table = pd.DataFrame({258 "Metric": ["Cumulative Returns (%)", "Maximum Drawdown (%)", "Annualized Return (%)", "Standard Deviation (%)"],259 "Portfolio": [cumulative_returns, max_drawdown, annualized_return, portfolio_std],260 "S&P 500": benchmark_metrics['S&P 500'],261 "Hang Seng Index": benchmark_metrics['Hang Seng Index']262 })263 264 # Categorize stocks into US and HK265 us_stocks, hk_stocks = categorize_stocks(signals.keys())266 267 # Plot the portfolio value over time with benchmarks268 fig = plot_portfolio_value(portfolio_value, signals, holdings_over_time, us_stocks, hk_stocks, benchmark_data)269 270 result_text = (271 f"Final Portfolio Value: ${final_value:,.2f}\n"272 f"Cumulative Returns: {cumulative_returns}%\n"273 f"Maximum Drawdown: {max_drawdown}%\n"274 f"Annualized Return: {annualized_return}%\n"275 f"Standard Deviation: {portfolio_std}%"276 )277 278 for ticker in stock_data:279 stock_data[ticker] = fill_missing_prices_stocks(stock_data[ticker], master_dates)280 281# Export the results to an Excel file282 excel_file_path = export_results_to_excel(portfolio_value, signals, holdings_over_time, stock_data, benchmark_data, master_dates)283 284 return result_text, comparison_table, fig, excel_file_path285 286def plot_portfolio_value(portfolio_value, signals, holdings_over_time, us_stocks, hk_stocks, benchmark_data):287 """Plot the portfolio value over time, including the value of US and HK stocks separately."""288 289 # Calculate the value of US and HK stock holdings for each date290 us_values = []291 hk_values = []292 total_values = []293 294 for date in portfolio_value.index:295 us_value = sum(296 holdings_over_time.at[date, ticker] * signals[ticker].at[date, 'price']297 for ticker in us_stocks if date in signals[ticker].index298 )299 hk_value = sum(300 holdings_over_time.at[date, ticker] * signals[ticker].at[date, 'price']301 for ticker in hk_stocks if date in signals[ticker].index302 )303 total_value = us_value + hk_value + portfolio_value.at[date, 'cash']304 305 us_values.append(us_value)306 hk_values.append(hk_value)307 total_values.append(total_value)308 309 # Rebase benchmark values to match the starting value of the portfolio310 initial_value = portfolio_value['total'].iloc[0]311 312 sp500_rebased = (benchmark_data['S&P 500']['price'] / benchmark_data['S&P 500']['price'].iloc[0]) * initial_value313 hsi_rebased = (benchmark_data['Hang Seng Index']['price'] / benchmark_data['Hang Seng Index']['price'].iloc[0]) * initial_value 314 315 # Plotting the portfolio value316 fig, ax = plt.subplots(figsize=(14, 7))317 ax.bar(portfolio_value.index, us_values, label='US Stocks Value', color='blue', alpha=0.6)318 ax.bar(portfolio_value.index, hk_values, bottom=us_values, label='HK Stocks Value', color='green', alpha=0.6)319 ax.plot(portfolio_value.index, total_values, label='Total Portfolio Value', color='black', linewidth=2)320 321 # Plot rebased benchmark values322 ax.plot(benchmark_data['S&P 500'].index, sp500_rebased, label='S&P 500 (Rebased)', color='red', linestyle='--')323 ax.plot(benchmark_data['Hang Seng Index'].index, hsi_rebased, label='Hang Seng Index (Rebased)', color='orange', linestyle='--')324 325 ax.set_title('Portfolio Value Over Time')326 ax.set_xlabel('Date')327 ax.set_ylabel('Value')328 ax.legend()329 plt.grid(True)330 331 return fig332 333def export_results_to_excel(portfolio_value, signals, holdings, stock_data, benchmark_data, combined_dates, file_path='trading_strategy_results.xlsx'):334 """Export the portfolio summary and trading signals to an Excel file."""335 # Calculate the daily returns for the portfolio and benchmarks336 portfolio_daily_return = portfolio_value['total'].pct_change().fillna(0) * 100 # Daily return in percentage337 sp500_daily_return = benchmark_data['S&P 500']['price'].pct_change().fillna(0) * 100338 hsi_daily_return = benchmark_data['Hang Seng Index']['price'].pct_change().fillna(0) * 100339 340 # Create the Portfolio Summary DataFrame341 portfolio_summary = pd.DataFrame(index=combined_dates)342 portfolio_summary['Portfolio Daily Return (%)'] = portfolio_daily_return343 portfolio_summary['Cash Value'] = portfolio_value['cash']344 portfolio_summary['Portfolio Value'] = portfolio_value['total']345 346 # Add benchmark daily returns (rebased to match portfolio start)347 start_value = portfolio_value['total'].iloc[0]348 sp500_rebased = (benchmark_data['S&P 500']['price'] / benchmark_data['S&P 500']['price'].iloc[0]) * start_value349 hsi_rebased = (benchmark_data['Hang Seng Index']['price'] / benchmark_data['Hang Seng Index']['price'].iloc[0]) * start_value350 portfolio_summary['S&P 500 (Rebased)'] = sp500_rebased351 portfolio_summary['S&P 500 Daily Return (%)'] = sp500_daily_return352 portfolio_summary['Hang Seng Index (Rebased)'] = hsi_rebased353 portfolio_summary['Hang Seng Index Daily Return (%)'] = hsi_daily_return354 355 # Add holdings and stock daily returns356 for ticker in holdings.columns:357 stockprice= stock_data[ticker]358 stock_return = stockprice['price'].pct_change().fillna(0) * 100359 portfolio_summary[f'{ticker} Holdings'] = holdings[ticker]360 portfolio_summary[f'{ticker} Daily Return (%)'] = stock_return361 362 # Make sure the portfolio_summary index and columns are timezone-naive363 364 # Export to Excel365 with pd.ExcelWriter(file_path) as writer:366 try:367 # Save the Portfolio Summary sheet368 portfolio_summary.to_excel(writer, sheet_name='Portfolio Summary')369 370 # Save each stock's signal data to individual sheets371 for ticker, signal_df in signals.items():372 signal_data = signal_df[['price', 'buy_short_mavg_1', 'buy_long_mavg_1', 'buy_short_mavg_2', 'buy_long_mavg_2',373 'sell_short_mavg', 'sell_long_mavg', 'volume', 'volume_mavg', 'signal', 'positions',374 'max_price_since_entry']].copy()375 signal_data.columns = ['Stock Price', 'S1: Short MA', 'S1: Long MA', 'S2: Short MA', 'S2: Long MA',376 'Sell: Short MA', 'Sell: Long MA', 'Volume', 'Volume MA', 'Signal',377 'Buy/Sell', 'Max Price Since Entry']378 signal_data.to_excel(writer, sheet_name=f'{ticker} Signals')379 except Exception as e:380 # If an error occurs, create a fallback sheet with the error message381 fallback_df = pd.DataFrame({"Error": [str(e)]})382 fallback_df.to_excel(writer, sheet_name='Error Summary')383 384 return file_path385 386# Gradio Interface387def gradio_interface(tickers_and_weightings, start_date, end_date, initial_capital, buy_short_window_1, buy_long_window_1, buy_condition, buy_short_window_2, buy_long_window_2, sell_short_window, sell_long_window, volume_window, volume_threshold, drawback_threshold, us_exposure):388 tickers_list, stock_weightings = parse_stock_tickers_and_weightings(tickers_and_weightings)389 result_text, comparison_table, portfolio_plot, excel_file_path = run_strategy(390 tickers_list, start_date, end_date, initial_capital, buy_short_window_1, buy_long_window_1, buy_condition, buy_short_window_2, buy_long_window_2, sell_short_window, sell_long_window, volume_window, volume_threshold, drawback_threshold, stock_weightings, us_exposure391 )392 return result_text, comparison_table, portfolio_plot, excel_file_path393 394# Gradio UI395iface = gr.Interface(396 fn=gradio_interface,397 inputs=[398 gr.Textbox(label="Stock Tickers and Weightings", value="AAPL:20, MSFT:20, 1810.HK:20"),399 gr.Textbox(label="Start Date (YYYY-MM-DD)", value="2020-01-01"),400 gr.Textbox(label="End Date (YYYY-MM-DD)", value="2024-10-20"),401 gr.Number(label="Initial Capital", value=10000),402 gr.Slider(1, 60, step=1, label="Buy Short Window 1", value=10),403 gr.Slider(20, 200, step=1, label="Buy Long Window 1", value=30),404 gr.Dropdown(choices=["one", "two"], label="Buy Condition", value="one"),405 gr.Slider(1, 60, step=1, label="Buy Short Window 2", value=5),406 gr.Slider(10, 200, step=1, label="Buy Long Window 2", value=10),407 gr.Slider(1, 60, step=1, label="Sell Short Window", value=10),408 gr.Slider(20, 200, step=1, label="Sell Long Window", value=20),409 gr.Slider(5, 60, step=5, label="Volume Window", value=5),410 gr.Slider(100, 300, step=10, label="Volume Threshold (%)", value=120),411 gr.Slider(5, 50, step=5, label="Drawback Threshold (%)", value=10),412 gr.Slider(0, 1, step=0.1, label="US Market Exposure", value=0.5)413 ],414 outputs=[415 gr.Textbox(label="Result"),416 gr.Dataframe(label="Comparison Table"),417 gr.Plot(label="Portfolio Value Over Time"),418 gr.File(label="Download Excel File")419 ],420 title="Multi-Stock Trading Strategy with Combined Ticker and Weighting Input",421 description="Apply a uniform moving average strategy to a portfolio of up to 20 stocks, with customizable weightings in a single input."422)423 424 425# Launch Gradio Interface426iface.launch()