CoolFace
Apppublic

Davidchen8/Portfolio_Backtesting_MovingAverage

sourceHugging Faceupdated 2y agoView on Hugging Face
0likes
app.py426 linesDownload Raw Back to root
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()