Skip to content
Course contents
From backtest to live strategy

Two ways to run a strategy

The vectorised backtest from Python for Trading is fast but cannot run live; an event-driven engine processes one bar at a time and can drive both a backtest and a live session with the same code. This chapter contrasts the two, explains why sharing one code path removes a class of bugs, and shows an event-driven backtest reproducing the vectorised result.

10 min readChapter 15 of 28
What you will learn
  • Contrast vectorised and event-driven strategy execution and their trade-offs
  • Explain why running the same strategy code in backtest and live reduces error
  • Verify that an event-driven backtest matches the earlier vectorised one

There are two ways to run a strategy over data, and the difference between them decides whether your live results will match your tests. The backtest you wrote in Python for Trading was vectorised: it computed the whole strategy across the entire price series in a few array operations, all at once. It is fast and compact, and it is also, in a specific way, a cheat: it can only work because it already has the whole future in memory. A live strategy never does. So live trading needs the other way, event-driven, and the two must agree.

Vectorised: fast, but it has all of history

A vectorised backtest is fast but cannot run live; an event-driven engine processes one bar at a time and drives both backtest and live with the same code.
A vectorised backtest is fast but cannot run live; an event-driven engine processes one bar at a time and drives both backtest and live with the same code.

A vectorised backtest treats the price history as one big table and computes on whole columns at once: all the moving averages, all the signals, all the returns, in a handful of operations. This is why it is fast and why it reads so cleanly. But it can do this only because every row is already present. The line that computes today's return sits in the same array as tomorrow's, and it takes real discipline, the shift by one day you learned in order to avoid lookahead, to stop the computation from peeking at data that, live, would not exist yet. A vectorised backtest is a fine tool for testing an idea and a hopeless one for trading, because you cannot vectorise a future that has not arrived.

Event-driven: one bar at a time, like real life

An event-driven engine runs the strategy the way life delivers data: one bar at a time, in order, with no knowledge of what comes next. It feeds the strategy bar one, takes its orders, then bar two, and so on to the end. This is slower and wordier than the vectorised version, but it has one decisive property. It processes each bar knowing only the past, exactly as a live system does. In fact the same event-driven loop that replays history can, unchanged, run on a live feed. The bars just arrive from the market instead of from a file.

The same logic must give the same answer

Here is the test that ties the two together and earns your trust in a live engine. Take the identical crossover, run it both ways on the same data, and check that they agree.

ExampleThe same strategy, run both ways, agrees to the decimalch15/event_vs_vectorised.py
# Two ways to run the same strategy on the same data. The vectorised backtest
# (from Python for Trading) computes everything at once. The event-driven engine
# steps through one bar at a time, the way a live loop must. If the logic is the
# same, the answer must be the same, and it is. Only the event-driven form can
# also run live.
import pandas as pd

df = pd.read_csv("sample_prices.csv", parse_dates=["Date"], index_col="Date")
cost = 0.001                              # 0.1% per trade, as in Python for Trading

# --- Vectorised: the whole series at once ---
fast = df["Close"].rolling(20).mean()
slow = df["Close"].rolling(50).mean()
signal = (fast > slow).astype(int)
position = signal.shift(1).fillna(0)      # act on yesterday's signal (no lookahead)
market_return = df["Close"].pct_change().fillna(0)
strategy_return = position * market_return - position.diff().abs().fillna(0) * cost
vectorised = (1 + strategy_return).prod() - 1

# --- Event-driven: one bar at a time ---
closes = df["Close"].tolist()
fast_ma = fast.tolist()
slow_ma = slow.tolist()
equity, prev_position, trades = 1.0, 0, 0
for i in range(1, len(closes)):
    f, s = fast_ma[i - 1], slow_ma[i - 1]                 # yesterday's averages
    pos = 1 if (f == f and s == s and f > s) else 0       # f == f is False for NaN
    day_return = pos * (closes[i] / closes[i - 1] - 1)
    if pos != prev_position:
        day_return -= cost
        trades += 1
    equity *= (1 + day_return)
    prev_position = pos
event_driven = equity - 1

print(f"Vectorised total return:    {vectorised * 100:.2f}%")
print(f"Event-driven total return:  {event_driven * 100:.2f}%")
print(f"Difference: {abs(vectorised - event_driven) * 100:.6f}%")
print(f"Event-driven trades: {trades}")
print("Same logic, same answer. Only the event-driven form can also run live.")
Output
Vectorised total return:    -8.11%
Event-driven total return:  -8.11%
Difference: 0.000000%
Event-driven trades: 5
Same logic, same answer. Only the event-driven form can also run live.

They match exactly. Both report minus 8.11%, the very number from Python for Trading, with a difference of zero, and the same 5 trades. That agreement is not a nicety; it is the thing that lets you believe a live run. If your event-driven engine reproduces your vectorised backtest to the decimal, you know the engine that will trade live computes the same strategy you tested. If the two disagree, something is wrong, a lookahead in one, a timing slip in the other, and you must find it before a rupee is at stake.

One code path, fewer lies

The deeper lesson is to run the same strategy code in both the backtest and the live engine, not two copies. The most dangerous backtests are the ones whose logic quietly differs from what runs live, so the test says one thing and reality does another. When a single strategy object is fed by a backtest loop one day and a live loop the next, as in the last chapter, there is only one logic to be right or wrong, and the backtest genuinely predicts what live will do. Two separate implementations are two chances to disagree, and they will.

What to carry forward

There are two ways to run a strategy: vectorised, which computes on all of history at once and is perfect for testing but cannot trade live, and event-driven, which processes one bar at a time knowing only the past, so the same code can both backtest and run live. You saw the event-driven engine reproduce the vectorised backtest exactly, both losing 8.11% over the same 5 trades, which is what lets you trust that a live run computes the strategy you tested. Run one strategy through both paths, never two copies. Next, the step between a passing backtest and real money that most beginners skip: forward-testing the whole system in the sandbox.