spent last 3 days rebuilding backtest optimization pipeline.
went from 6 hours to 35 minutes for full parameter sweep.
the problem #
old approach: sequential parameter testing.
# Old slow approach
for lookback in range(10, 60, 5):
for vol_threshold in np.arange(0.5, 3.0, 0.1):
for profit_target in np.arange(0.3, 0.8, 0.05):
results = run_backtest(lookback, vol_threshold, profit_target)
store_results(results)
testing 10 × 25 × 10 = 2,500 parameter combinations.
each backtest: ~8.5 seconds (includes data loading, calculations, metrics).
total time: 2,500 × 8.5s = 5.9 hours.
can’t iterate fast enough.
the solution #
1. parallelize parameter testing
using python’s multiprocessing to run tests in parallel:
from multiprocessing import Pool
from functools import partial
import numpy as np
class BacktestOptimizer:
def __init__(self, strategy_class, data, n_workers=8):
self.strategy_class = strategy_class
self.data = data
self.n_workers = n_workers
def optimize_parameters(self, param_grid):
"""Run parallel parameter optimization"""
# Generate all parameter combinations
param_combinations = self._generate_combinations(param_grid)
# Create partial function with data pre-loaded
backtest_func = partial(
self._run_single_backtest,
strategy_class=self.strategy_class,
data=self.data
)
# Run parallel backtests
with Pool(processes=self.n_workers) as pool:
results = pool.map(backtest_func, param_combinations)
return self._rank_results(results)
@staticmethod
def _run_single_backtest(params, strategy_class, data):
"""Single backtest execution"""
strategy = strategy_class(**params)
engine = BacktestEngine(strategy, data)
# Run backtest
trades = engine.run()
# Calculate metrics
sharpe = calculate_sharpe(trades)
max_dd = calculate_max_drawdown(trades)
win_rate = calculate_win_rate(trades)
profit_factor = calculate_profit_factor(trades)
return {
'params': params,
'sharpe': sharpe,
'max_dd': max_dd,
'win_rate': win_rate,
'profit_factor': profit_factor,
'total_trades': len(trades),
'score': sharpe * (1 - max_dd/100) # Custom fitness
}
2. pre-load market data once
class DataCache:
def __init__(self):
self.cache = {}
def load_symbol_data(self, symbol, start_date, end_date):
"""Load and cache market data"""
cache_key = f"{symbol}_{start_date}_{end_date}"
if cache_key not in self.cache:
df = self._fetch_from_db(symbol, start_date, end_date)
df = self._calculate_indicators(df)
self.cache[cache_key] = df
return self.cache[cache_key]
def _calculate_indicators(self, df):
"""Pre-calculate common indicators"""
df['sma_20'] = df['close'].rolling(20).mean()
df['sma_50'] = df['close'].rolling(50).mean()
df['volatility'] = df['close'].pct_change().rolling(20).std()
df['atr'] = self._calculate_atr(df)
return df
old approach loaded data fresh for each backtest (2,500 times).
new approach loads once, shares across all workers.
3. vectorized calculations
class VectorizedBacktest:
"""Ultra-fast vectorized backtesting"""
def __init__(self, data: pd.DataFrame):
self.data = data
def run_strategy(self, lookback: int, vol_threshold: float):
"""Vectorized strategy execution"""
df = self.data.copy()
# Calculate signals vectorized
df['signal'] = 0
df['vol'] = df['close'].pct_change().rolling(lookback).std()
# Entry conditions (all at once)
long_entry = (
(df['sma_20'] > df['sma_50']) &
(df['vol'] < vol_threshold) &
(df['vol'].shift(1) >= vol_threshold)
)
df.loc[long_entry, 'signal'] = 1
# Calculate returns vectorized
df['strategy_returns'] = df['signal'].shift(1) * df['close'].pct_change()
df['equity_curve'] = (1 + df['strategy_returns']).cumprod()
return self._calculate_metrics(df)
def _calculate_metrics(self, df):
"""Fast metric calculation"""
returns = df['strategy_returns'].dropna()
sharpe = np.sqrt(252) * returns.mean() / returns.std()
# Vectorized drawdown calculation
cumulative = (1 + returns).cumprod()
running_max = cumulative.expanding().max()
drawdown = (cumulative - running_max) / running_max
max_dd = drawdown.min()
return {
'sharpe': sharpe,
'max_dd': max_dd * 100,
'total_return': (cumulative.iloc[-1] - 1) * 100
}
loops are slow. pandas vectorization is fast AF.
4. smart parameter pruning
class AdaptiveOptimizer:
"""Prunes unpromising parameter regions early"""
def __init__(self, param_grid, n_rounds=3):
self.param_grid = param_grid
self.n_rounds = n_rounds
def optimize(self, backtest_func):
"""Multi-round optimization with pruning"""
current_grid = self.param_grid
for round_num in range(self.n_rounds):
print(f"Round {round_num + 1}: testing {len(current_grid)} combinations")
# Test current grid
results = self._parallel_backtest(current_grid, backtest_func)
# Keep top 30% performers
top_performers = self._get_top_n_percent(results, 30)
if round_num < self.n_rounds - 1:
# Narrow grid around top performers
current_grid = self._create_refined_grid(top_performers)
return top_performers[0] # Best overall
def _create_refined_grid(self, top_results):
"""Create refined grid around best parameters"""
refined = []
for result in top_results[:5]: # Top 5 only
params = result['params']
# Create tighter grid around these params
for key, value in params.items():
delta = self._get_delta(key)
refined.extend([
{**params, key: value - delta},
{**params, key: value},
{**params, key: value + delta}
])
return refined
don’t test every parameter combination.
test coarse grid → identify promising regions → refine.
reduces search space by 70%.
results #
old pipeline:
- 2,500 combinations tested
- 6 hours total
- sequential execution
new pipeline:
- round 1: 500 combinations (coarse grid)
- round 2: 300 combinations (refined)
- round 3: 100 combinations (fine-tuned)
- 35 minutes total
- 8 parallel workers
10x speedup. can now test ideas same day instead of overnight.
infrastructure #
running on my dell server (8 cores, 64GB RAM).
all cores hit 100% during optimization.
beautiful sight watching htop during parameter sweep.
real impact #
before:
- test idea friday night
- wait until saturday afternoon for results
- can’t iterate fast
after:
- test idea friday night
- results in 35 minutes
- test 3-4 variations same night
speed = more iterations = better strategies.
next improvements #
already planning:
- GPU acceleration for ML models (PyTorch on CUDA)
- distributed optimization across multiple servers
- Bayesian optimization instead of grid search
- real-time parameter tracking dashboard (Grafana)
code repo #
not sharing full code yet (edge).
but approach:
- multiprocessing for parallelization
- shared memory for data
- vectorized calculations
- adaptive grid refinement
all standard python: multiprocessing, pandas, numpy.
nothing fancy. just applied correctly.
lessons #
1. profile before optimizing
spent 2 hours profiling old code.
found: 80% time in data loading, 15% in metrics calculation.
optimized those two areas first.
2. parallelize embarrassingly parallel tasks
backtests are independent. perfect for parallelization.
3. vectorize everything possible
loops in python are slow. pandas/numpy operations are fast.
rewrite logic to use vector operations.
impact on trading #
already used new pipeline to optimize vol detection parameters.
found better thresholds in 40 minutes vs old 7-hour approach.
updated live system friday. already seeing better trade filtering.
4:22am sunday. rebuilt optimization pipeline. 10x faster parameter testing. can iterate way faster now.
-AK