Skip to main content

Optimization

Optimization is the process of finding the best parameters for your strategy. But beware: aggressive optimization leads to overfitting.

The Optimization Paradox​

More optimization ≠ Better results

  • Too little: Suboptimal performance
  • Too much: Overfitting (great backtest, terrible live trading)
  • Just right: Robust, generalizable strategy

What to Optimize​

Good Candidates​

Parameters with economic rationale:

ParameterRationaleRange
MA periodsTrend timeframe10-200
RSI thresholdOverbought/oversold definition25-35, 65-75
Stop loss %Risk tolerance2-10%

Poor Candidates​

Parameters that just "fit the data":

  • Arbitrary numbers without meaning
  • Too many parameters simultaneously
  • Parameters that perfectly capture historical noise

Optimization Methods​

Test all combinations:

from vecalpha import GridSearch

params = {
'short_ma': [10, 20, 30],
'long_ma': [40, 50, 60],
'stop_loss': [0.03, 0.05, 0.07],
}

grid = GridSearch(strategy, data, params, metric='sharpe')
results = grid.run()

print(f"Best params: {results.best_params}")
print(f"Best Sharpe: {results.best_score:.2f}")

Pros: Exhaustive, finds global optimum Cons: Computationally expensive, easy to overfit

Sample random combinations:

from vecalpha import RandomSearch

random = RandomSearch(
strategy, data,
n_iterations=100, # Test 100 random combinations
param_distributions={
'short_ma': (10, 50),
'long_ma': (50, 200),
}
)

Pros: More efficient than grid for high dimensions Cons: May miss global optimum

Bayesian Optimization​

Intelligent search using probabilistic models:

from vecalpha import BayesianOptimization

bo = BayesianOptimization(
strategy, data,
n_iterations=50,
param_space={
'short_ma': (10, 50),
'long_ma': (50, 200),
}
)

Pros: Efficient, good for expensive-to-evaluate functions Cons: Complex, assumes smooth objective


Avoiding Overfitting​

1. Train/Test Split​

Never optimize and test on the same data:

# Split data
train_data = data['2020-01-01':'2022-12-31'] # 3 years for training
test_data = data['2023-01-01':'2023-12-31'] # 1 year for testing

# Optimize on train
best_params = optimize(strategy, train_data)

# Test on test (only once!)
final_sharpe = backtest(strategy, test_data, params=best_params).sharpe

2. Walk-Forward Optimization​

The robust approach:

from vecalpha import WalkForwardOptimization

wfo = WalkForwardOptimization(
train_period='2Y',
test_period='6M',
n_splits=5
)

results = wfo.run(strategy, data)

# Each split has its own optimized params
for i, split in enumerate(results.splits):
print(f"Split {i}: OOS Sharpe = {split.test_sharpe:.2f}")

# Average out-of-sample performance
print(f"Average OOS Sharpe: {results.avg_oos_sharpe:.2f}")

3. Parameter Stability​

Good parameters are stable:

# Check parameter sensitivity
base_params = {'ma_period': 50}
sensitivity_results = []

for delta in [-10, -5, 0, 5, 10]:
params = {'ma_period': base_params['ma_period'] + delta}
result = backtest(strategy, data, params)
sensitivity_results.append(result.sharpe)

# If Sharpe varies wildly with small param changes -> unstable
std_dev = np.std(sensitivity_results)
if std_dev > 0.3:
print("WARNING: Parameters are unstable")

4. Complexity Penalty​

More parameters = more risk:

# Information Criterion (AIC/BIC) penalizes complexity
n_params = count_parameters(strategy)
n_obs = len(data)

aic = -2 * log_likelihood + 2 * n_params
bic = -2 * log_likelihood + n_params * np.log(n_obs)

# Prefer strategies with lower AIC/BIC

Multi-Objective Optimization​

Optimize for multiple goals:

from vecalpha import MultiObjectiveOptimization

moo = MultiObjectiveOptimization(
strategy, data,
objectives=['return', 'sharpe', 'max_drawdown'],
weights=[0.4, 0.4, 0.2] # Priorities
)

# Returns Pareto-optimal solutions
pareto_front = moo.run()

Objective Trade-offs​

Return ↑Sharpe ↑Drawdown ↓
Higher riskSmoother returnsLower risk
More volatileLower absolute returnMay miss opportunities

Optimization Workflow​

Step 1: Define Parameter Space​

param_space = {
'short_window': (5, 30), # Short MA
'long_window': (30, 100), # Long MA
'stop_loss': (0.02, 0.10), # Stop loss %
'take_profit': (0.03, 0.15), # Take profit %
}

Step 2: Choose Metric​

# For trend strategies
metric = 'sharpe_ratio'

# For conservative strategies
metric = 'sortino_ratio'

# For risk-averse
metric = 'calmar_ratio'

Step 3: Run Walk-Forward​

wfo = WalkForwardOptimization(
train_period='2Y',
test_period='6M',
param_space=param_space,
metric=metric
)

results = wfo.run(strategy, data)

Step 4: Analyze Results​

# Check consistency across splits
for split in results.splits:
print(f"Train Sharpe: {split.train_sharpe:.2f}")
print(f"Test Sharpe: {split.test_sharpe:.2f}")
print(f"Degradation: {(1 - split.test_sharpe/split.train_sharpe)*100:.1f}%")

Step 5: Validate on Holdout​

# Final validation on unseen data
holdout_data = data['2024-01-01':]
final_result = backtest(strategy, holdout_data, params=results.best_params)

print(f"Holdout Sharpe: {final_result.sharpe:.2f}")

Red Flags​

Signs of overfitting:

SymptomMeaning
Train Sharpe >> Test SharpeFitted to noise
Performance cliff with small param changesUnstable
Too many optimized parametersData mining
Perfect equity curveUnrealistic
No losing monthsToo good to be true

Best Practices Summary​

  1. Start simple - Fewer parameters, clearer logic
  2. Use out-of-sample testing - Always hold back data
  3. Check parameter stability - Robust parameters survive small changes
  4. Expect degradation - Live performance < backtest performance
  5. Economic rationale - Parameters should make sense

Next Steps​