Find optimal parameters with grid search, Bayesian optimization, and walk-forward analysis
Optimization can lead to curve fitting if not done carefully. Always use out-of-sample testing, walk-forward analysis, and keep the number of parameters small. A strategy that works "perfectly" on historical data may fail completely in live trading.
Exhaustively test all combinations of parameter values. Best for small parameter spaces.
from rlxbt import TradingEngine
from rlxbt.optimize import GridSearch
import pandas as pd
df = pd.read_csv("BTCUSDT_1h.csv")
# Define parameter grid
param_grid = {
"rsi_oversold": [25, 30, 35],
"rsi_overbought": [65, 70, 75],
"stop_loss_pct": [0.01, 0.02, 0.03],
"take_profit_pct": [0.02, 0.04, 0.06]
}
def objective(params, data):
"""Function to optimize - returns Sharpe ratio"""
df_copy = data.copy()
df_copy["signal"] = 0
df_copy.loc[df_copy["RSI_14"] < params["rsi_oversold"], "signal"] = 1
df_copy.loc[df_copy["RSI_14"] > params["rsi_overbought"], "signal"] = -1
engine = TradingEngine(
initial_capital=100_000,
stop_loss_pct=params["stop_loss_pct"],
take_profit_pct=params["take_profit_pct"]
)
result = engine.run_backtest(df_copy)
return result.sharpe_ratio
# Run grid search
optimizer = GridSearch(
param_grid=param_grid,
objective=objective,
maximize=True,
n_jobs=-1 # Use all CPU cores
)
results = optimizer.fit(df)
print("Best Parameters:", results.best_params)
print("Best Sharpe:", results.best_score)
print("\\nTop 5 Combinations:")
print(results.top_results(5))Efficiently explore large parameter spaces using probabilistic models. Much faster than grid search for high-dimensional problems.
from rlxbt.optimize import BayesianOptimizer
# Define continuous parameter space
param_space = {
"rsi_oversold": (20, 40), # (min, max)
"rsi_overbought": (60, 80),
"stop_loss_pct": (0.005, 0.05),
"take_profit_pct": (0.01, 0.10),
"sma_fast": (10, 50),
"sma_slow": (50, 200)
}
optimizer = BayesianOptimizer(
param_space=param_space,
objective=objective,
maximize=True,
n_iterations=100,
n_initial=10, # Random samples before modeling
acquisition="ei" # Expected Improvement
)
results = optimizer.fit(df)
print("Best Parameters:", results.best_params)
print("Convergence History:")
print(results.convergence_df)The gold standard for strategy validation. Optimize on in-sample data, validate on out-of-sample, then roll forward and repeat. Simulates real-world conditions where you can't peek into the future.
from rlxbt.optimize import WalkForwardOptimizer
optimizer = WalkForwardOptimizer(
param_grid=param_grid,
objective=objective,
# Walk-forward windows
in_sample_size=0.7, # 70% for optimization
out_of_sample_size=0.3, # 30% for validation
n_splits=5, # Number of periods
# Options
anchored=False, # Rolling vs expanding window
metric="sharpe_ratio"
)
results = optimizer.fit(df)
print("Walk-Forward Results:")
print(f"In-Sample Sharpe: {results.avg_in_sample_score:.2f}")
print(f"Out-of-Sample Sharpe: {results.avg_out_of_sample_score:.2f}")
print(f"Efficiency Ratio: {results.efficiency_ratio:.1%}")
# Per-period breakdown
print("\\nPer-Period Results:")
for i, period in enumerate(results.periods):
print(f"Period {i+1}: IS={period.in_sample_score:.2f} OOS={period.out_of_sample_score:.2f}")Measures how well in-sample performance predicts out-of-sample results.
Optimize for multiple objectives simultaneously (e.g., maximize Sharpe while minimizing drawdown).
from rlxbt.optimize import MultiObjectiveOptimizer
def multi_objective(params, data):
"""Returns multiple objectives"""
result = run_backtest(params, data)
return {
"sharpe": result.sharpe_ratio,
"drawdown": -result.max_drawdown_pct, # Negate to maximize
"win_rate": result.win_rate
}
optimizer = MultiObjectiveOptimizer(
param_space=param_space,
objectives=multi_objective,
weights={"sharpe": 0.5, "drawdown": 0.3, "win_rate": 0.2},
n_iterations=100
)
results = optimizer.fit(df)
# Pareto frontier
print("Pareto-optimal solutions:")
for sol in results.pareto_front:
print(sol)