Skip to main content

rebuilt backtesting pipeline - 10x faster parameter optimization

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:

  1. GPU acceleration for ML models (PyTorch on CUDA)
  2. distributed optimization across multiple servers
  3. Bayesian optimization instead of grid search
  4. real-time parameter tracking dashboard (Grafana)

code repo
#

not sharing full code yet (edge).

but approach:

  1. multiprocessing for parallelization
  2. shared memory for data
  3. vectorized calculations
  4. 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

Related

using python async for real-time market data
rewrote my market data pipeline to use async. 3x faster, way cleaner code. the problem # old synchronous code:
checking my backtests for overfitting
worried my strategies are overfit to historical data. spent today testing for it. been reading NexusFi backtesting threads about this exact problem. the problem # my backtests look great:
how i organize my trading code on github
got asked on r/algotrading how i organize my trading repos. here’s my setup after 4 months of refactoring. repo structure # i have 4 main repos:
position sizing is killing me - fixing it with code
been up since 3am coding a proper position sizing module. my current approach (fixed 2% risk per trade) is bleeding me out. down another $8k this week. total drawdown now -$48k since january. at this rate i’ll be broke by august.
modeling slippage the right way
the slippage problem is worse than i thought # after 2 weeks live trading (10 total trades), my average slippage is $6.40 per spread
first algo went live today
three years of paper trading, finally real money # started learning algo trading april 2020 during COVID lockdown. i was 16, bored af at home, discovered r/algotrading and fell into the rabbit hole