From 282b74014494ae9151181f4e7e8dfb58b3b3d605 Mon Sep 17 00:00:00 2001 From: "Bobby (aider)" Date: Fri, 14 Feb 2025 00:00:53 -0800 Subject: [PATCH] feat: Add multi-ticker backtest support with performance filtering --- src/pages/backtesting/backtesting_page.py | 150 +++++++++++++++++++--- 1 file changed, 129 insertions(+), 21 deletions(-) diff --git a/src/pages/backtesting/backtesting_page.py b/src/pages/backtesting/backtesting_page.py index 7347b68..c504464 100644 --- a/src/pages/backtesting/backtesting_page.py +++ b/src/pages/backtesting/backtesting_page.py @@ -232,8 +232,26 @@ def backtesting_page(): with left_col: st.subheader("Backtest Settings") - # Ticker selection - ticker = st.text_input("Enter Ticker Symbol", value="AAPL").upper() + # Add radio button for single/multiple ticker mode + test_mode = st.radio("Testing Mode", ["Single Ticker", "Multiple Tickers"]) + + if test_mode == "Single Ticker": + # Single ticker input + ticker = st.text_input("Enter Ticker Symbol", value="AAPL").upper() + tickers = [ticker] + else: + # Multiple ticker input + ticker_input = st.text_area( + "Enter Ticker Symbols (one per line)", + value="AAPL\nMSFT\nGOOG" + ) + tickers = [t.strip().upper() for t in ticker_input.split('\n') if t.strip()] + + # Add minimum performance filters + st.subheader("Performance Filters") + min_return = st.number_input("Minimum Return (%)", value=10.0) + min_sharpe = st.number_input("Minimum Sharpe Ratio", value=1.0) + max_drawdown = st.number_input("Maximum Drawdown (%)", value=-20.0) # Date range selection col1, col2 = st.columns(2) @@ -287,30 +305,79 @@ def backtesting_page(): if st.button("Run Backtest"): with st.spinner('Running backtest...'): - # Fetch data - # Convert date to datetime start_datetime = datetime.combine(start_date, datetime.min.time()) end_datetime = datetime.combine(end_date, datetime.min.time()) - df = get_stock_data(ticker, start_datetime, end_datetime, 'daily') - if df.empty: - st.error("No data available for the selected period") - return - - try: - df = prepare_data_for_backtest(df) + if test_mode == "Single Ticker": + # Single ticker logic + df = get_stock_data(ticker, start_datetime, end_datetime, 'daily') + if df.empty: + st.error("No data available for the selected period") + return - if optimize: - results = run_optimization(df, indicator_settings) - with right_col: - display_optimization_results(results) - else: - results = run_single_backtest(df, indicator_settings) - with right_col: - display_backtest_results(results) + try: + df = prepare_data_for_backtest(df) + if optimize: + results = run_optimization(df, indicator_settings) + with right_col: + display_optimization_results(results) + else: + results = run_single_backtest(df, indicator_settings) + with right_col: + display_backtest_results(results) + except Exception as e: + st.error(f"Error during backtest: {str(e)}") + + else: + # Multiple ticker logic + try: + results_df = run_multi_ticker_backtest( + tickers, start_datetime, end_datetime, indicator_settings + ) - except Exception as e: - st.error(f"Error during backtest: {str(e)}") + # Apply performance filters + filtered_df = results_df[ + (results_df['Return [%]'] >= min_return) & + (results_df['Sharpe Ratio'] >= min_sharpe) & + (results_df['Max Drawdown [%]'] >= max_drawdown) + ] + + with right_col: + st.subheader("Multi-Ticker Results") + + # Display summary statistics + st.write("### Summary Statistics") + summary = pd.DataFrame({ + 'Metric': ['Average Return', 'Average Sharpe', 'Average Drawdown', 'Success Rate'], + 'Value': [ + f"{results_df['Return [%]'].mean():.2f}%", + f"{results_df['Sharpe Ratio'].mean():.2f}", + f"{results_df['Max Drawdown [%]'].mean():.2f}%", + f"{(len(filtered_df) / len(results_df) * 100):.1f}%" + ] + }) + st.table(summary) + + # Display full results + st.write("### All Results") + st.dataframe(results_df.sort_values('Return [%]', ascending=False)) + + # Display filtered results + st.write("### Filtered Results (Meeting Criteria)") + st.dataframe(filtered_df.sort_values('Return [%]', ascending=False)) + + # Create a downloadable CSV + csv = results_df.to_csv(index=False) + st.download_button( + "Download Results CSV", + csv, + "backtest_results.csv", + "text/csv", + key='download-csv' + ) + + except Exception as e: + st.error(f"Error during multi-ticker backtest: {str(e)}") def run_optimization(df: pd.DataFrame, indicator_settings: Dict) -> List: """Run optimization with different parameter combinations""" @@ -390,6 +457,47 @@ def display_optimization_results(results: List): st.subheader("Optimization Results") st.dataframe(df_results.sort_values('Return [%]', ascending=False)) +def run_multi_ticker_backtest(tickers: list, start_date: datetime, end_date: datetime, indicator_settings: Dict) -> pd.DataFrame: + """Run backtest across multiple tickers and aggregate results""" + all_results = [] + + for ticker in tickers: + try: + print(f"\nTesting strategy on {ticker}") + df = get_stock_data(ticker, start_date, end_date, 'daily') + + if df.empty: + print(f"No data available for {ticker}") + continue + + df = prepare_data_for_backtest(df) + + # Run backtest + DynamicStrategy.indicator_configs = indicator_settings + bt = Backtest(df, DynamicStrategy, cash=100000, commission=.002) + stats = bt.run() + + # Store results + result = { + 'Ticker': ticker, + 'Return [%]': stats['Return [%]'], + 'Sharpe Ratio': stats['Sharpe Ratio'], + 'Max Drawdown [%]': stats['Max. Drawdown [%]'], + 'Win Rate [%]': stats['Win Rate [%]'], + 'Number of Trades': stats['# Trades'] + } + all_results.append(result) + + print(f"{ticker} - Return: {stats['Return [%]']:.2f}%, " + f"Sharpe: {stats['Sharpe Ratio']:.2f}, " + f"Drawdown: {stats['Max. Drawdown [%]']:.2f}%") + + except Exception as e: + print(f"Error processing {ticker}: {str(e)}") + continue + + return pd.DataFrame(all_results) + def display_backtest_results(results: Dict): """Display single backtest results with metrics and plots""" st.subheader("Backtest Results")