- Solve real problems with our hands-on interface
- Progress from basic puts and calls to advanced strategies

Posted June 18, 2025 at 6:57 pm
Backtesting evaluates a trading strategy using historical data. Two paradigms exist: Vector- Based Backtesting, which processes data in fixed time-step batches (e.g., daily or minute bars) and uses vectorized operations to compute signals across all assets simultaneously, and Event-Based Backtesting, which simulates a live environment by sequentially handling discrete market data events (ticks, bar closes, etc.) in an event loop. Vector-based frameworks are fast and simple, suited for rapid prototyping and low-frequency strategies, but they assume fills at the next bar’s open or close and ignore intra-bar details (slippage, partial fills, bid-ask spreads). Event-based frameworks are more complex and computationally intensive but provide higher fidelity by modeling realistic order types, slippage, partial fills, and real-time risk checks. They also facilitate a smoother production transition. The choice depends on strategy time horizon, execution complexity, and data availability.
Backtesting applies a trading strategy’s rules to historical market data to estimate performance—returns, drawdowns, and risk metrics—as if the strategy had run live. It is essential for validating logic before deploying capital.
Historical data is loaded into arrays or dataframes where each row is a fixed interval (one day or one minute) and each column is an asset’s price or indicator series. At each bar ti, indicators are computed via vectorized operations (e.g., pandas’ .rolling(), NumPy functions) up to ti.
At bar ti, the strategy applies logic such as moving-average crossovers or mean-reversion tests using vectorized calculations (e.g., price.rolling(window=20).mean()) to decide long, short, or flat for the next bar. All signals for all assets are computed simultaneously. The engine then issues hypothetical market orders filled at the next bar’s open or close, rebalancing positions in a single batch.
import pandas as pd # 1. Load historical data into a DataFrame # Index: DateTime; Columns: [’AssetA’, ’AssetB’, ...] prices = pd.read_csv(’historical_prices.csv’, index_col=’date’, parse_dates=True) # 2. Compute indicators vectorized ma_short = prices[’AssetA’].rolling(window=20).mean() # 20-bar moving average ma_long = prices[’AssetA’].rolling(window=50).mean() # 50-bar moving average # 3. Generate signals signals = pd.DataFrame(index=prices.index) signals[’signal’] = 0 signals[’signal’][ma_short > ma_long] = 1 # Go long when 20-MA > 50-MA signals[’signal’][ma_short < ma_long] = -1 # Go short when 20-MA < 50-MA # 4. Shift signals to simulate next-bar execution positions = signals[’signal’].shift(1) # 5. Compute bar returns returns = prices[’AssetA’].pct_change() # 6. Calculate strategy returns strategy_returns = positions * returns equity_curve = (1 + strategy_returns).cumprod() # 7. (Optional) Apply transaction costs # cost_per_trade = 0.001 # 0.1% # trade_count = (positions != positions.shift(1)).sum() # total_cost = trade_count * cost_per_trade
Event-based frameworks use a chronological event queue. The core loop processes events in order:
Market data is introduced only when available, avoiding look-ahead bias and enabling realistic execution simulation.
Market Data Handler: Streams historical tick or bar data into the event queue chronologically.
Prevents look-ahead by releasing data only when appropriate.
Strategy Module: Subscribes to data events. Maintains internal state (e.g., rolling indicators updated incrementally) and emits signal events when conditions are met (e.g., price crosses above a moving average).
Portfolio/Execution Handler: Receives signal events and creates order events (market, limit, stop). Manages cash, positions, and risk exposure, enforcing predefined risk limits (e.g., maximum notional exposure).
Broker/Exchange Simulator: Processes order events by matching them against market data (quotes or reconstructed order book). Models realistic slippage, partial fills, and commissions, issuing fill events back to the portfolio.
Risk & Compliance Module: Monitors limits such as intraday VaR, position caps, and short- sale constraints. Triggers forced liquidations or cancels orders if thresholds are breached.
Performance Recorder: Logs each fill event—timestamp, fill price, quantity, slippage, commis- sion—for post-mortem analysis and performance metrics calculation.
from collections import deque
import abc
# Event base classes
class Event:
pass
class MarketEvent(Event):
def init (self, symbol, timestamp, price):
self.type = ’MARKET’
self.symbol = symbol
self.timestamp = timestamp
self.price = price
class SignalEvent(Event):
def init (self, symbol, timestamp, signal_type, quantity):
self.type = ’SIGNAL’
self.symbol = symbol
self.timestamp = timestamp
self.signal_type = signal_type # ’LONG’ or ’SHORT’
self.quantity = quantity
class OrderEvent(Event):
def init (self, symbol, order_type, quantity, price=None):
self.type = ’ORDER’
self.symbol = symbol
self.order_type = order_type # ’MARKET’, ’LIMIT’, ’STOP’
self.quantity = quantity
self.price = price # For limit/stop orders
class FillEvent(Event):
def init (self, symbol, timestamp, fill_price, quantity, commission):
self.type = ’FILL’
self.symbol = symbol
self.timestamp = timestamp
self.fill_price = fill_price
self.quantity = quantity
self.commission = commission
# Abstract handler interfaces
class DataHandler(abc.ABC):
@abc.abstractmethod
def get_next_market_event(self):
raise NotImplementedError
class Strategy(abc.ABC):
@abc.abstractmethod
def on_market_event(self, event):
raise NotImplementedError
class Portfolio(abc.ABC):
@abc.abstractmethod
def on_signal(self, signal):
raise NotImplementedError
@abc.abstractmethod
def on_fill(self, fill):
raise NotImplementedError
class Broker(abc.ABC):
@abc.abstractmethod
def execute_order(self, order):
raise NotImplementedError
# Simple implementations
class HistoricalCSVDataHandler(DataHandler):
def init (self, event_queue, csv_file):
self.event_queue = event_queue
self.data = self._load_csv(csv_file)
self.index = 0
def _load_csv(self, file):
import pandas as pd
df = pd.read_csv(file, parse_dates=[’timestamp’])
df.sort_values(’timestamp’, inplace=True)
return df.to_dict(’records’)
def get_next_market_event(self):
if self.index < len(self.data):
row = self.data[self.index]
event = MarketEvent(
symbol=row[’symbol’],
timestamp=row[’timestamp’],
price=row[’price’]
)
self.index += 1
return event
else:
return None
class MovingAverageCrossStrategy(Strategy):
def init (self, data_handler, short_window=20, long_window=50):
self.data_handler = data_handler
self.short_window = short_window
self.long_window = long_window
self.prices = {}
self.signals = deque()
def on_market_event(self, event):
symbol = event.symbol
price = event.price
# Append price to history
if symbol not in self.prices:
self.prices[symbol] = []
self.prices[symbol].append(price)
# Only generate signals after long_window prices
if len(self.prices[symbol]) >= self.long_window:
short_ma = sum(self.prices[symbol][-self.short_window:]) / self.short_window
long_ma = sum(self.prices[symbol][-self.long_window:]) / self.long_window
if short_ma > long_ma:
signal = SignalEvent(symbol, event.timestamp, ’LONG’, 100)
self.signals.append(signal)
elif short_ma < long_ma:
signal = SignalEvent(symbol, event.timestamp, ’SHORT’, 100)
self.signals.append(signal)
def get_signals(self):
signals = list(self.signals)
self.signals.clear()
return signals
class SimplePortfolio(Portfolio):
def _init_(self, event_queue, initial_capital=100000):
self.event_queue = event_queue
self.initial_capital = initial_capital
self.cash = initial_capital
self.positions = {}
def on_signal(self, signal):
order = None
if signal.signal_type == ’LONG’:
order = OrderEvent(signal.symbol, ’MARKET’, signal.quantity)
elif signal.signal_type == ’SHORT’:
order = OrderEvent(signal.symbol, ’MARKET’, -signal.quantity)
return order
def on_fill(self, fill):
qty = fill.quantity
cost = fill.fill_price * qty
self.cash -= cost + fill.commission
self.positions[fill.symbol] = self.positions.get(fill.symbol, 0) +
class SimulatedBroker(Broker):
def init (self, event_queue):
self.event_queue = event_queue
def execute_order(self, order):
# Assume immediate fill at last known price minus 1 tick slippage
fill_price = order.price if order.price else 100.0 # placeholder commission = 1.0 # flat commission
fill = FillEvent(
symbol=order.symbol,
timestamp="now", # placeholder
fill_price=fill_price,
quantity=order.quantity,
commission=commission
)
return fill
# Main event loop
def backtest(event_queue, data_handler, strategy, portfolio,
# Prime the data handler
market_event = data_handler.get_next_market_event()
if market_event:
event_queue.append(market_event)
while event_queue:
event = event_queue.popleft()
if isinstance(event, MarketEvent):
strategy.on_market_event(event)
signals = strategy.get_signals()
for signal in signals:
event_queue.append(signal)
elif isinstance(event, SignalEvent):
order = portfolio.on_signal(event)
if order:
event_queue.append(order)
elif isinstance(event, OrderEvent):
fill = broker.execute_order(order)
event_queue.append(fill)
elif isinstance(event, FillEvent):
portfolio.on_fill(event)
# Fetch next market event if queue is empty
if not event_queue:
next_event = data_handler.get_next_market_event() if next_event:
event_queue.append(next_event)
if name == " main ":
from collections import deque
event_queue = deque()
data_handler = HistoricalCSVDataHandler(event_queue, ’historical_tick_data.csv’)
strategy = MovingAverageCrossStrategy(data_handler)
portfolio = SimplePortfolio(event_queue, initial_capital=100000) broker = SimulatedBroker(event_queue)
backtest(event_queue, data_handler, strategy, portfolio, broker)
# After completion, analyze portfolio.cash and portfolio.positionsDefinition: Look-ahead bias happens when future information inadvertently influences past signal generation. For example, using adjusted prices that “know” about splits or dividends before they occur inflates performance. Mitigation:
Issue: If signals are generated on bar T and executed on the same bar’s close, you implicitly assume knowledge of that close price before it is available. Mitigation: Always shift signals by one bar (e.g., signals = signals.shift(1)) so execution on bar T + 1 uses only information available at bar T .
Issue: Vector-based backtests often apply a flat slippage rate (e.g., 5 bps per trade) uniformly, ignoring volume, liquidity, and bid-ask dynamics, misrepresenting P&L in less liquid securities. Mitigation:
Issue: Inefficient event queues become bottlenecks, slowing simulations over tick data significantly.
Issue: Without detailed logs of fills (timestamp, price, quantity, slippage, commission), diagnosing discrepancies between vectorized and event-driven results is difficult. Mitigation:
| Factor | Vector-Based | Event-Based |
| Core Loop | Fixed time-step loop (daily/minute bars) | Event-driven loop (market data to sig- nals to orders to fills) |
| Execution | Granularity Bar-level only (one price per bar) | Tick- or intra-bar level, modeling bid- ask and volume |
| Order Types Supported | Market orders at next bar open/close; lim- ited stop/limit simula- tion via bar high/low checks | Market, limit, stop, stop-limit, iceberg, TWAP, VWAP, OCO |
| Slippage Modeling | Post-hoc flat slippage assumptions or simple tiers | Dynamic slippage from bid-ask data, volume, order-book depth |
| Realism | Lower fidelity (as- sumes synchronized bar rebalances, no intra-bar execution nuances) | Higher fidelity (simu- lates realistic fills, par- tial fills, asynchronous signals) |
| Speed & Scalability | Very fast (seconds for decades of data across hundreds of symbols via vectorization) | Slower (minutes to hours for minute/tick data via event loops) |
| Code | Complexity Simple (few hundred lines, DataFrame oper- ations) | Complex (multi- module event loop, 500–1,000+ lines, intricate event/state management) |
| Data Requirements | OHLCV bars (daily or minute) | Tick or sub-minute data, order-book reconstruction, exten- sive cleaning |
| Risk Management Granularity | Bar-level only (risk checks at bar close) Event-level (real-time risk checks, margin calls, intraday draw- down enforcement) | Event-level (real-time risk checks, margin calls, intraday draw- down enforcement) |
| Fidelity to Production Systems | Limited (needs rewrit- ing for live streaming data) | High (can often be reused in production with minimal changes) |
| Ideal Use Cases | Low-frequency, factor research, rapid proto- typing | Intra-day, high- frequency, algorithmic execution, market- making, institutional infrastructure |
Vector-based backtesting excels at rapid prototyping for low-frequency, bar-level strategies by lever- aging optimized NumPy and pandas routines to compute P&L across the entire dataset in seconds. It is ideal for factor research or strategies where intra-bar dynamics are negligible. However, it risks look-ahead bias and cannot model realistic execution aspects—slippage, partial fills, bid-ask spreads—or enforce intraday risk controls.
Event-based backtesting provides a high-fidelity simulation of live trading: modeling tick-level updates, sequential event processing, realistic slippage, partial fills, and dynamic risk controls. This accuracy comes at the cost of greater complexity, more extensive data requirements (tick or sub-minute), and longer runtimes. Event-driven engines are essential for intra-day or execution- sensitive strategies—market-making, high-frequency trading, complex order logic—and facilitate a smoother transition from backtest code to production trading systems.
Many quant teams adopt a hybrid approach: using vectorized backtests to screen and narrow a universe of candidates, then migrating promising designs into an event-driven framework for final validation before live deployment. By understanding trade-offs among speed, realism, data requirements, and strategy complexity, practitioners can tailor their backtesting infrastructure to meet research, development, and production needs effectively.
Information posted on IBKR Campus that is provided by third-parties does NOT constitute a recommendation that you should contract for the services of that third party. Third-party participants who contribute to IBKR Campus are independent of Interactive Brokers and Interactive Brokers does not make any representations or warranties concerning the services offered, their past or future performance, or the accuracy of the information provided by the third party. Past performance is no guarantee of future results.
This material is from Quant Insider and is being posted with its permission. The views expressed in this material are solely those of the author and/or Quant Insider and Interactive Brokers is not endorsing or recommending any investment or trading discussed in the material. This material is not and should not be construed as an offer to buy or sell any security. It should not be construed as research or investment advice or a recommendation to buy, sell or hold any security or commodity. This material does not and is not intended to take into account the particular financial conditions, investment objectives or requirements of individual customers. Before acting on this material, you should consider whether it is suitable for your particular circumstances and, as necessary, seek professional advice.
Join The Conversation
For specific platform feedback and suggestions, please submit it directly to our team using these instructions.
If you have an account-specific question or concern, please reach out to Client Services.
We encourage you to look through our FAQs before posting. Your question may already be covered!