‹ Backtest Strategies Lesson 5 of 17
Contents Lesson 5 of 17

4 min read · professional

State a rule that cannot see the future

Everything in a backtest hangs on one discipline: at every bar, the rule may only use information that existed at that bar. Break it anywhere and every number downstream is fiction, and the fiction is always flattering.

Write the rule in English first

Before code, one sentence with no ambiguity left in it:

If the 20-day moving average of the adjusted close is above the 50-day, hold one unit of the instrument; otherwise hold nothing.

That sentence forces three answers a vague version hides. Which prices — adjusted, as lesson 3 settled. Measured when — at the close of each bar. Acted on when — the next lesson's subject, and the usual place the leak lives.

If your assistant can generate ten plausible implementations of your sentence, the sentence is not finished.

Where the leak gets in

A moving average at bar i must be computed from bars i-19 through i. Write it as a window ending at i and it is honest. Write it as a window centred on i, or fetch it from a library that pads at the end, and half the window is tomorrow.

The classic slip, and it is one character:

const window = bars.slice(i - 19, i + 20);   // centred: half of this is the future
const window = bars.slice(i - 19, i + 1);    // ending at i: honest

Both run. Both produce a curve. One of them is a time machine, and it will look like a brilliant strategy.

Two habits that make the leak visible

Compute signals in one pass, forwards. A single loop from oldest to newest, where each iteration may only read what earlier iterations already saw. Any code that indexes ahead of the cursor stands out immediately, which is the point — the shape of the loop is the check.

Never sort or reverse the series in place. Course 1 taught that Array.prototype.sort mutates. Here that bug does not just reorder a table, it silently reverses time. If your bars arrive newest-first from anywhere, normalise once at the boundary and assert the direction.

if (bars[0].date > bars[bars.length - 1].date) throw new Error("bars must be oldest-first");

One line, at the boundary, and it converts a whole class of catastrophic silent bug into a loud crash.

The test you write now, not later

This is the course's craft discipline arriving: bias detection is code review, and the best code review is a test.

Write one now, on a fixture you control. The fixture has to be long enough for the rule: a 50-day average needs fifty bars before it exists at all, so take 120 synthetic bars, compute the signal at bar 80, then change the prices of bars 81 through 120 and recompute. If the signal at bar 80 moved, your rule reads the future.

Check the window before you trust the result, because this is where the test lies to you. bars.slice(i - 19, i + 1) looks right and is right at bar 80. At bar 10 it is bars.slice(-9, 11), and JavaScript counts a negative start from the END of the array — on a twenty-bar fixture that resolves to slice(11, 11), an empty window. The signal is then garbage, garbage does not move when you change the future, and the test passes. Print the window length once and assert it: if (window.length !== 20) throw new Error("short window at bar " + i).

That test is a few lines, it never needs updating, and it catches the single most expensive bug in this domain.

Run it on every strategy you write for the rest of the course.

Try it now

Write the leak test before the strategy. Then ask your assistant for the moving-average signal and run the test against what it hands you. If it passes first time, deliberately break it — switch the slice to a centred window — and confirm the test actually fails. A test you have never seen fail is not evidence.