feat: Add multi-ticker backtest support with performance filtering
This commit is contained in:
parent
32907718a1
commit
282b740144
@ -232,8 +232,26 @@ def backtesting_page():
|
|||||||
with left_col:
|
with left_col:
|
||||||
st.subheader("Backtest Settings")
|
st.subheader("Backtest Settings")
|
||||||
|
|
||||||
# Ticker selection
|
# Add radio button for single/multiple ticker mode
|
||||||
ticker = st.text_input("Enter Ticker Symbol", value="AAPL").upper()
|
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
|
# Date range selection
|
||||||
col1, col2 = st.columns(2)
|
col1, col2 = st.columns(2)
|
||||||
@ -287,30 +305,79 @@ def backtesting_page():
|
|||||||
|
|
||||||
if st.button("Run Backtest"):
|
if st.button("Run Backtest"):
|
||||||
with st.spinner('Running backtest...'):
|
with st.spinner('Running backtest...'):
|
||||||
# Fetch data
|
|
||||||
# Convert date to datetime
|
|
||||||
start_datetime = datetime.combine(start_date, datetime.min.time())
|
start_datetime = datetime.combine(start_date, datetime.min.time())
|
||||||
end_datetime = datetime.combine(end_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:
|
if test_mode == "Single Ticker":
|
||||||
st.error("No data available for the selected period")
|
# Single ticker logic
|
||||||
return
|
df = get_stock_data(ticker, start_datetime, end_datetime, 'daily')
|
||||||
|
if df.empty:
|
||||||
try:
|
st.error("No data available for the selected period")
|
||||||
df = prepare_data_for_backtest(df)
|
return
|
||||||
|
|
||||||
if optimize:
|
try:
|
||||||
results = run_optimization(df, indicator_settings)
|
df = prepare_data_for_backtest(df)
|
||||||
with right_col:
|
if optimize:
|
||||||
display_optimization_results(results)
|
results = run_optimization(df, indicator_settings)
|
||||||
else:
|
with right_col:
|
||||||
results = run_single_backtest(df, indicator_settings)
|
display_optimization_results(results)
|
||||||
with right_col:
|
else:
|
||||||
display_backtest_results(results)
|
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:
|
# Apply performance filters
|
||||||
st.error(f"Error during backtest: {str(e)}")
|
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:
|
def run_optimization(df: pd.DataFrame, indicator_settings: Dict) -> List:
|
||||||
"""Run optimization with different parameter combinations"""
|
"""Run optimization with different parameter combinations"""
|
||||||
@ -390,6 +457,47 @@ def display_optimization_results(results: List):
|
|||||||
st.subheader("Optimization Results")
|
st.subheader("Optimization Results")
|
||||||
st.dataframe(df_results.sort_values('Return [%]', ascending=False))
|
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):
|
def display_backtest_results(results: Dict):
|
||||||
"""Display single backtest results with metrics and plots"""
|
"""Display single backtest results with metrics and plots"""
|
||||||
st.subheader("Backtest Results")
|
st.subheader("Backtest Results")
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user