feat: Implement complete backtesting page with dynamic strategy configuration
This commit is contained in:
parent
3a053a0fb0
commit
ccb62b1cb9
@ -8,4 +8,275 @@ import itertools
|
||||
from datetime import datetime, timedelta
|
||||
from src.utils.common_utils import get_stock_data
|
||||
|
||||
[... rest of the backtesting_page.py content as provided above ...]
|
||||
class DynamicStrategy(Strategy):
|
||||
"""Dynamic strategy class that can be configured through the UI"""
|
||||
|
||||
def init(self):
|
||||
# Will be populated with indicator calculations
|
||||
self.indicators = {}
|
||||
|
||||
# Initialize all selected indicators
|
||||
for ind_name, ind_config in self.indicator_configs.items():
|
||||
if ind_config['type'] == 'SMA':
|
||||
self.indicators[ind_name] = self.I(ta.sma, self.data.Close, length=ind_config['params']['length'])
|
||||
elif ind_config['type'] == 'EMA':
|
||||
self.indicators[ind_name] = self.I(ta.ema, self.data.Close, length=ind_config['params']['length'])
|
||||
elif ind_config['type'] == 'RSI':
|
||||
self.indicators[ind_name] = self.I(ta.rsi, self.data.Close, length=ind_config['params']['length'])
|
||||
elif ind_config['type'] == 'MACD':
|
||||
macd = ta.macd(self.data.Close,
|
||||
fast=ind_config['params']['fast'],
|
||||
slow=ind_config['params']['slow'],
|
||||
signal=ind_config['params']['signal'])
|
||||
self.indicators[f"{ind_name}_macd"] = self.I(lambda x: x['MACD_12_26_9'], macd)
|
||||
self.indicators[f"{ind_name}_signal"] = self.I(lambda x: x['MACDs_12_26_9'], macd)
|
||||
elif ind_config['type'] == 'BB':
|
||||
bb = ta.bbands(self.data.Close,
|
||||
length=ind_config['params']['length'],
|
||||
std=ind_config['params']['std'])
|
||||
self.indicators[f"{ind_name}_upper"] = self.I(lambda x: x['BBU_20_2.0'], bb)
|
||||
self.indicators[f"{ind_name}_lower"] = self.I(lambda x: x['BBL_20_2.0'], bb)
|
||||
self.indicators[f"{ind_name}_middle"] = self.I(lambda x: x['BBM_20_2.0'], bb)
|
||||
|
||||
def next(self):
|
||||
# Simple example strategy - should be customized based on user input
|
||||
price = self.data.Close[-1]
|
||||
|
||||
# Example trading logic using indicators
|
||||
for ind_name, ind_config in self.indicator_configs.items():
|
||||
if ind_config['type'] == 'SMA':
|
||||
if price > self.indicators[ind_name][-1] and not self.position:
|
||||
self.buy()
|
||||
elif price < self.indicators[ind_name][-1] and self.position:
|
||||
self.position.close()
|
||||
|
||||
elif ind_config['type'] == 'MACD':
|
||||
macd = self.indicators[f"{ind_name}_macd"][-1]
|
||||
signal = self.indicators[f"{ind_name}_signal"][-1]
|
||||
prev_macd = self.indicators[f"{ind_name}_macd"][-2]
|
||||
prev_signal = self.indicators[f"{ind_name}_signal"][-2]
|
||||
|
||||
if macd > signal and prev_macd <= prev_signal and not self.position:
|
||||
self.buy()
|
||||
elif macd < signal and prev_macd >= prev_signal and self.position:
|
||||
self.position.close()
|
||||
|
||||
def get_available_indicators() -> Dict:
|
||||
"""Returns available indicators and their parameters"""
|
||||
return {
|
||||
'SMA': {
|
||||
'params': ['length'],
|
||||
'defaults': {'length': 20},
|
||||
'ranges': {'length': (5, 200)}
|
||||
},
|
||||
'EMA': {
|
||||
'params': ['length'],
|
||||
'defaults': {'length': 20},
|
||||
'ranges': {'length': (5, 200)}
|
||||
},
|
||||
'RSI': {
|
||||
'params': ['length'],
|
||||
'defaults': {'length': 14},
|
||||
'ranges': {'length': (5, 30)}
|
||||
},
|
||||
'MACD': {
|
||||
'params': ['fast', 'slow', 'signal'],
|
||||
'defaults': {'fast': 12, 'slow': 26, 'signal': 9},
|
||||
'ranges': {'fast': (8, 20), 'slow': (20, 40), 'signal': (5, 15)}
|
||||
},
|
||||
'BB': {
|
||||
'params': ['length', 'std'],
|
||||
'defaults': {'length': 20, 'std': 2.0},
|
||||
'ranges': {'length': (10, 50), 'std': (1.5, 3.0)}
|
||||
}
|
||||
}
|
||||
|
||||
def prepare_data_for_backtest(df: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Prepare the dataframe for backtesting"""
|
||||
# Ensure the dataframe has the required columns
|
||||
required_columns = ['Open', 'High', 'Low', 'Close', 'Volume']
|
||||
|
||||
# Rename columns if needed
|
||||
df = df.copy()
|
||||
df.columns = [c.capitalize() for c in df.columns]
|
||||
|
||||
# Set the date as index if it's not already
|
||||
if 'Date' in df.columns:
|
||||
df.set_index('Date', inplace=True)
|
||||
|
||||
# Verify all required columns exist
|
||||
missing_cols = [col for col in required_columns if col not in df.columns]
|
||||
if missing_cols:
|
||||
raise ValueError(f"Missing required columns: {missing_cols}")
|
||||
|
||||
return df
|
||||
|
||||
def backtesting_page():
|
||||
st.title("Strategy Backtesting")
|
||||
|
||||
# Sidebar controls
|
||||
st.sidebar.subheader("Backtest Settings")
|
||||
|
||||
# Ticker selection
|
||||
ticker = st.sidebar.text_input("Enter Ticker Symbol", value="AAPL").upper()
|
||||
|
||||
# Date range selection
|
||||
col1, col2 = st.sidebar.columns(2)
|
||||
with col1:
|
||||
start_date = st.date_input("Start Date",
|
||||
value=datetime.now() - timedelta(days=365))
|
||||
with col2:
|
||||
end_date = st.date_input("End Date")
|
||||
|
||||
# Indicator selection
|
||||
available_indicators = get_available_indicators()
|
||||
selected_indicators = st.sidebar.multiselect(
|
||||
"Select Technical Indicators",
|
||||
options=list(available_indicators.keys())
|
||||
)
|
||||
|
||||
# Parameter input/optimization section
|
||||
optimize = st.sidebar.checkbox("Optimize Parameters")
|
||||
|
||||
indicator_settings = {}
|
||||
for ind_name in selected_indicators:
|
||||
st.sidebar.subheader(f"{ind_name} Settings")
|
||||
ind_config = available_indicators[ind_name]
|
||||
|
||||
if optimize:
|
||||
params = {}
|
||||
for param in ind_config['params']:
|
||||
param_range = ind_config['ranges'][param]
|
||||
col1, col2, col3 = st.sidebar.columns(3)
|
||||
with col1:
|
||||
min_val = st.number_input(f"{param} Min",
|
||||
value=float(param_range[0]))
|
||||
with col2:
|
||||
max_val = st.number_input(f"{param} Max",
|
||||
value=float(param_range[1]))
|
||||
with col3:
|
||||
step = st.number_input(f"{param} Step",
|
||||
value=1.0 if param == 'std' else 1)
|
||||
params[param] = {'min': min_val, 'max': max_val, 'step': step}
|
||||
else:
|
||||
params = {}
|
||||
for param in ind_config['params']:
|
||||
default_val = ind_config['defaults'][param]
|
||||
params[param] = st.sidebar.number_input(f"{param}",
|
||||
value=float(default_val))
|
||||
|
||||
indicator_settings[ind_name] = {
|
||||
'type': ind_name,
|
||||
'params': params
|
||||
}
|
||||
|
||||
if st.sidebar.button("Run Backtest"):
|
||||
with st.spinner('Running backtest...'):
|
||||
# Fetch data
|
||||
df = get_stock_data(ticker, start_date, end_date, 'daily')
|
||||
|
||||
if df.empty:
|
||||
st.error("No data available for the selected period")
|
||||
return
|
||||
|
||||
try:
|
||||
df = prepare_data_for_backtest(df)
|
||||
|
||||
if optimize:
|
||||
results = run_optimization(df, indicator_settings)
|
||||
display_optimization_results(results)
|
||||
else:
|
||||
results = run_single_backtest(df, indicator_settings)
|
||||
display_backtest_results(results)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"Error during backtest: {str(e)}")
|
||||
|
||||
def run_optimization(df: pd.DataFrame, indicator_settings: Dict) -> List:
|
||||
"""Run optimization with different parameter combinations"""
|
||||
param_combinations = generate_param_combinations(indicator_settings)
|
||||
results = []
|
||||
|
||||
for params in param_combinations:
|
||||
# Configure strategy with current parameters
|
||||
DynamicStrategy.indicator_configs = params
|
||||
|
||||
# Run backtest
|
||||
bt = Backtest(df, DynamicStrategy, cash=100000, commission=.002)
|
||||
stats = bt.run()
|
||||
|
||||
results.append({
|
||||
'parameters': params,
|
||||
'Return [%]': stats['Return [%]'],
|
||||
'Sharpe Ratio': stats['Sharpe Ratio'],
|
||||
'Max Drawdown [%]': stats['Max Drawdown [%]'],
|
||||
'Win Rate [%]': stats['Win Rate [%]']
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
def run_single_backtest(df: pd.DataFrame, indicator_settings: Dict) -> Dict:
|
||||
"""Run a single backtest with fixed parameters"""
|
||||
DynamicStrategy.indicator_configs = indicator_settings
|
||||
bt = Backtest(df, DynamicStrategy, cash=100000, commission=.002)
|
||||
return bt.run()
|
||||
|
||||
def generate_param_combinations(settings: Dict) -> List[Dict]:
|
||||
"""Generate all possible parameter combinations for optimization"""
|
||||
param_space = {}
|
||||
for ind_name, ind_config in settings.items():
|
||||
for param_name, param_range in ind_config['params'].items():
|
||||
if isinstance(param_range, dict): # Optimization mode
|
||||
values = np.arange(
|
||||
param_range['min'],
|
||||
param_range['max'] + param_range['step'],
|
||||
param_range['step']
|
||||
)
|
||||
param_space[f"{ind_name}_{param_name}"] = values
|
||||
|
||||
# Generate all combinations
|
||||
keys = list(param_space.keys())
|
||||
values = list(param_space.values())
|
||||
combinations = list(itertools.product(*values))
|
||||
|
||||
# Convert to list of parameter dictionaries
|
||||
result = []
|
||||
for combo in combinations:
|
||||
params = {}
|
||||
for key, value in zip(keys, combo):
|
||||
ind_name, param_name = key.rsplit('_', 1)
|
||||
if ind_name not in params:
|
||||
params[ind_name] = {'type': ind_name, 'params': {}}
|
||||
params[ind_name]['params'][param_name] = value
|
||||
result.append(params)
|
||||
|
||||
return result
|
||||
|
||||
def display_optimization_results(results: List):
|
||||
"""Display optimization results in a formatted table"""
|
||||
df_results = pd.DataFrame(results)
|
||||
|
||||
# Format parameters column for better display
|
||||
df_results['parameters'] = df_results['parameters'].apply(str)
|
||||
|
||||
st.subheader("Optimization Results")
|
||||
st.dataframe(df_results.sort_values('Return [%]', ascending=False))
|
||||
|
||||
def display_backtest_results(results: Dict):
|
||||
"""Display single backtest results with metrics and plots"""
|
||||
st.subheader("Backtest Results")
|
||||
|
||||
# Display key metrics
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
with col1:
|
||||
st.metric("Return", f"{results['Return [%]']:.2f}%")
|
||||
with col2:
|
||||
st.metric("Sharpe Ratio", f"{results['Sharpe Ratio']:.2f}")
|
||||
with col3:
|
||||
st.metric("Max Drawdown", f"{results['Max Drawdown [%]']:.2f}%")
|
||||
with col4:
|
||||
st.metric("Win Rate", f"{results['Win Rate [%]']:.2f}%")
|
||||
|
||||
# Display full results in expandable section
|
||||
with st.expander("See detailed results"):
|
||||
st.write(results)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user