Quant Memo
Core

Backtesting Path-Dependent Strategies

Vectorized backtesting is fast because it computes every period at once, which is exactly what breaks when today's position size depends on yesterday's running P&L. Path-dependent logic needs a sequential loop, and shortcuts to vectorize it tend to leak the future into the past.

Prerequisites: Look-Ahead Bias

Vectorized backtesting computes a whole return series in one pass — apply a signal column to a price column, multiply, sum. It's fast and it's the default in most research libraries, but it quietly assumes each period's outcome is independent of the path that got there. That assumption breaks the moment a strategy has memory: a trailing stop that only ratchets one direction, a pyramiding rule that adds size after a winning streak, a drawdown-based de-risking rule. Those all need to know what happened in every prior period, in order, before they can tell you what to do next — which is precisely what a single vectorized pass doesn't naturally give you.

Worked example: the trailing stop that gets vectorized wrong

A trailing stop that ratchets up with the highest price seen since entry, exiting if price falls 10% from that running peak. A common but broken shortcut computes the running peak with a vectorized rolling-max over the whole price series, then checks the 10% condition — which is fine only if the position is never re-entered. Once the strategy can exit and re-enter, the rolling-max computed over the whole series keeps referencing peaks from a previous trade, ratcheting a stop for a position that didn't exist yet.

ApproachRunning peak used for trade 2's stopEffect
Correct, sequential loopPeak since trade 2's own entryStop set relative to trade 2's actual path
Broken vectorized shortcutPeak since the start of the whole series (includes trade 1)Stop can trigger immediately if trade 1 peaked higher, or never trigger if trade 1's peak inflates the reference

In a concrete run, trade 1 peaks at $140 before being stopped out; trade 2 re-enters later at $100 and should trail a stop from its own high. The broken version still measures the 10% drawdown against $140, so a price of $127 — a perfectly normal level for trade 2 — reads as a 9.3% draw from $140 and never triggers, when it should already be well past its own 10%-from-peak exit relative to a realistic running high near $115.

trade 1 peak \$140 trade 2's own peak broken: stop still measured from \$140
Trade 2 should trail its own peak; the broken vectorized version keeps referencing trade 1's, silently distorting the exit.

Any rule that depends on the running state of the position itself — trailing peaks, streak counters, drawdown-based sizing — needs to be computed with a sequential, trade-scoped loop; a whole-series vectorized shortcut carries state across trade boundaries where it doesn't belong.

The bug is easy to miss because the vectorized code runs without error and produces a plausible-looking equity curve — there's no crash, just a wrong number, and it's usually optimistic because carried-over peaks tend to make stops trigger less often than they should.

When speed matters, vectorize everything path-independent (signal computation, price transforms) and isolate only the path-dependent state machine into a loop — that keeps most of the runtime benefit while getting the trade-scoped logic right.

Related concepts

Practice in interviews

Further reading

  • Pardo, The Evaluation and Optimization of Trading Strategies (ch. 6)
ShareTwitterLinkedIn