Strategy Optimization

Find optimal parameters with grid search, Bayesian optimization, and walk-forward analysis

⚠️

Beware of Overfitting

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.

Grid Search

Exhaustively test all combinations of parameter values. Best for small parameter spaces.

grid_search.py
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))

Performance

81
Combinations tested
~3 sec
Total time (8 cores)
4.5M
Bars/sec throughput

Bayesian Optimization

Efficiently explore large parameter spaces using probabilistic models. Much faster than grid search for high-dimensional problems.

bayesian_optimize.py
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)

When to Use

✓ Use Bayesian

  • • 5+ parameters
  • • Continuous parameter spaces
  • • Expensive objective function
  • • Looking for global optimum

✓ Use Grid Search

  • • 2-3 parameters
  • • Discrete values only
  • • Need all results
  • • Simple parameter space

Walk-Forward Analysis

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.

walk_forward.py
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}")

Efficiency Ratio

Measures how well in-sample performance predicts out-of-sample results.

Efficiency Ratio = OOS Performance / IS Performance
< 50%
Overfit
50-80%
Acceptable
> 80%
Robust

Multi-Objective Optimization

Optimize for multiple objectives simultaneously (e.g., maximize Sharpe while minimizing drawdown).

multi_objective.py
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)

Best Practices

✓ Do

  • • Use walk-forward analysis
  • • Keep parameter count low (3-5)
  • • Test on multiple markets/periods
  • • Use realistic transaction costs
  • • Validate with paper trading

✗ Don't

  • • Optimize on full dataset
  • • Use too many parameters
  • • Ignore out-of-sample results
  • • Trust perfect backtests
  • • Skip robustness testing