How To Build a Python Backtester That Runs in Seconds
Introduction
There are largely 2 types of backtesters, an event-based backtester, and a vectorised backtester. Events-based are really complicated but, in exchange, they can get you the highest fidelity in terms of fills and transaction costs. On the other hand, vectorised backtesters are really simple and thus tend to make a lot of “loose” assumptions about fills and transaction costs.
Despite their flaws, many of the largest hedge funds in mid-frequency use variants of vectorised backtesters due to their ease of use and backtesting speed.
A vectorised backtester can process 500 tickers over 10 years in under one second. The same data in an event-driven framework takes 15-30 minutes. Here’s how to build one in Python that accounts for slippage, spreads, and costs.
A Short Plug
I’d like to take some time to answer a frequent question I get: “How does your content differ from the myriad quant content out there?“
Well, it’s simple and boils down to two simple things:
I actually eat my own cooking. I’ve managed money at scale and quite successfully. I just so happen to be in my leaking alpha for clout capture era. I don’t write about stuff I’ve never tried or thought deeply about. This means I do not flood you with BS.
My articles are going to be practical, to the point, and hopefully contains one insight that will make it useful. Most content are vacuous and do not contain any insights, therefore, they pad the content with “proofs”, “formulas” and “equations”. There is no need for that here. You will get the meat without the bones.
There’s going to be so much more to be done in 2026. If you don’t want to miss any of this, make sure you subscribe!
Architecture Overview
The fundamental vectorised backtesting pattern is simple. At its core, you work with “bars.” Each bar represents one time period in your data (daily bar = one trading day, hourly bar = one hour). If you’ve seen candlestick charts, each candle is one bar.
Python Implementation
What I am about to show you is something very hard hitting quant stuff. Are you ready for the working python code to implement a vectorised backtester?
This is going to be some rain man shit.
import numpy as np
import pandas as pd
# 1. Calculate returns using log returns
data['returns'] = np.log(data['price'] / data['price'].shift(1))
# 2. Generate signals (example: SMA crossover)
data['SMA1'] = data['price'].rolling(20).mean()
data['SMA2'] = data['price'].rolling(50).mean()
data['position'] = np.where(data['SMA1'] > data['SMA2'], 1, -1)
# 3. Shift signals by one bar (CRITICAL)
positions = data['position'].shift(1)
# 4. Calculate strategy returns
data['strategy'] = positions * data['returns']
# 5. Compute equity curve
equity_curve = data['strategy'].cumsum().apply(np.exp)I’m joking. That’s it.
The core formula for a vectorised backtester is strategy_returns = position.shift(1) * returns. Multiply the previous timestep’s position by this timestep’s returns.
Considerations
Why log returns instead of simple percentage returns? Log returns are additive across time. You can sum them to get cumulative returns, which is exactly what cumsum() does. Simple returns require multiplication, which is messier (and slower). For typical daily moves (under 5%), the difference is negligible anyway.
Why the shift(1)? Without it, you’re using information from bar T to trade on bar T, which is impossible in live trading. The signal is generated at bar close, but execution happens at the next bar’s open or close. This single line prevents look-ahead bias, the cardinal sin of backtesting where you accidentally use future information to make past decisions.
Is one-bar delay always correct? For daily bars on liquid instruments, yes. You can typically execute within the next trading day. For illiquid instruments or intraday bars where execution takes longer, use shift(2) or more. The principle is: shift by however many bars your actual execution takes.
This pattern extends naturally to multi-asset portfolios. Use a DataFrame where each column is a different asset, and the same operations apply column-wise. Position sizing across assets is just another column of weights. Multiply your signal by the weight before computing returns. The vectorised advantage scales: 500 assets is still one operation.
The Magic Of Vectorisation
The speed comes from NumPy’s optimised C code. When you write position * returns, NumPy doesn’t loop through each element in Python.
It passes the entire arrays to pre-compiled C routines that use SIMD instructions (Single Instruction, Multiple Data). A multi-asset backtest over 10 years of daily data completes in under a second because you’re not iterating through millions of data points. You’re executing a single array operation.
This speed advantage compounds when you test parameter combinations. Testing 20 fast-window values times 20 slow-window values gives 400 strategy variants. In a loop-based approach, that’s 400 separate backtests.
In a vectorised framework, you can actually stack the parameter combinations as DataFrame columns and run once. This seems to evade most practitioners even, so I thought it would be good to explicitly point it out!
Adding Transaction Costs
Vanilla vectorised backtesting ignores execution costs. It assumes fills with no friction. However, we need slippage, cost, and spread implementations so that we can separate realistic backtests from fantasy.
The basic cost implementation counts position changes:
cost_per_trade = 0.001 # 0.1%
trade_count = positions.diff().sum()
total_cost = trade_count * cost_per_tradeFrom here you can layer in different transaction costs in cost_per_trade. You can add in slippage costs, impact costs and even bid_ask_spread costs!
For simplicities’ sake, we are assuming a fixed costs here, but you should realize that this would also work if costs were dynamic. You just need to represent it as a series that can be subtracted from gross returns!
Slippage
For slippage costs, you want to model costs as starting small and rising exponentially with trading size in dollar volume relative to the market’s dollar volume. An example is: Orders under 5% of ADV incur 1 basis point. Orders between 5-10% incur 5 bps. Above 10%, you’re paying 10 bps or more.
adv = data[’volume’].rolling(20).mean()
This tiered approach captures the market impact reality: larger orders relative to available liquidity move prices against you. There’s a lot of literature on slippage/impact modelling, and I don’t want to make THIS article about this.
Bid-Ask Spread Handling
Vectorised backtesting is limited to market orders at the next bar’s open or close. It cannot model limit orders accurately because that requires knowing whether your limit price was hit during the bar, information you only have in hindsight.
For market orders, you pay the spread to cross. Why half the spread? Think of it this way: the bid-ask spread is the full gap between the best buy and sell prices. The “mid price” (roughly where the close price is) sits in the middle. To buy, you cross up to the ask, which is half the spread above mid. To sell, you cross down to the bid, half the spread below mid. Each side of the trade costs half.
You can bake in this cost by taking half the spread and adding it to costs.
Limitations
Vectorised backtesting applies execution costs post-hoc, after computing returns. Event-driven backtesting applies them during execution. This distinction matters when costs affect decisions.
If your strategy wouldn’t take a trade because slippage would make it unprofitable, vectorised backtesting cannot model that. The strategy logic cannot condition on execution cost outcomes because costs are calculated afterward.
Other limitations:
No partial fills. Vectorised backtesting assumes you get filled on your entire order. In reality, large orders relative to liquidity get partial fills.
Bar-level decisions only. Risk checks happen at bar boundaries, not intrabar. You cannot model intrabar stop-losses or position limits accurately.
Path dependency is hard. A strategy is path-dependent when today’s decision depends on what happened earlier, not just on today’s data. Trailing stops and dynamic position sizing fall into this category.
The practical response is a hybrid workflow. Use vectorised backtesting for rapid iteration and parameter exploration, where you might test thousands of parameter combinations in minutes. Then move promising strategies to an event-driven framework for final validation before deployment.
Think of vectorised backtesting as the screening phase. It tells you which ideas are worth investigating further. Event-driven backtesting is the validation phase. It tells you whether the execution realities kill the strategy.
Conclusion
Vectorised backtesting can process 500 tickers over 10 years in under one second. Use it for rapid iteration and parameter exploration. The core pattern is position.shift(1) * returns, but add transaction costs, tiered slippage, and spread costs or your returns will lie to you. Validate promising ideas in an event-driven framework before committing capital.

