Stage 4 of 4, lesson 28 of 31. Improver
The look ahead mistake in a backtest signal
Write a signal as a function, write it again with a small slip that lets it see the future, and catch the slip with a test. It introduces look ahead bias and how to test for it.
Risk warning: CFDs and spread bets are complex instruments and come with a high risk of losing money rapidly due to leverage. You can lose money on these products, and each provider publishes its own loss figure under FCA rules. You should consider whether you understand how they work and whether you can afford to take the high risk of losing your money. Automated and API trading can amplify losses through bugs, connectivity failures and runaway orders.
You need: The lessons on backtesting and on testing your own functions. No broker account.
The backtest lesson warned against using today's close to decide about today. This lesson makes the mistake on purpose, so that you can see how it looks and how much it flatters a result.
The script holds two versions of one signal. The honest one decides about a day using only the closes from before it. The other includes that day's own close in its window. The two differ by a slice that is one position further along, and both run without any error.
Run it and compare the totals. On the same invented prices the look ahead version comes out far ahead, and every point of the difference comes from information that did not exist when the decision would have been made. A backtest that looks too good is more often this than a discovery.
The last lines show a test that catches it. Move one day's close a long way and ask whether the decision about that same day can change. An honest signal cannot be moved by a price it has not seen. The rule here is an illustration on invented prices, not something to follow.
Files for this lesson
- sample-candles.csv, sample data with invented values
The code
# A moving average signal as code, and the look ahead mistake that flatters it.
# Uses sample-candles.csv from the lesson on reading prices from a file.
# The prices are invented and the rule is an illustration, not something to follow.
import csv
closes = []
with open("sample-candles.csv", newline="") as f:
for row in csv.DictReader(f):
closes.append(float(row["close"]))
WINDOW = 10
def signal_honest(prices, day):
"""Decide about `day` using only closes from before it. This could really have been done."""
past = prices[day - WINDOW:day]
return prices[day - 1] > sum(past) / WINDOW
def signal_look_ahead(prices, day):
"""The mistake: the window includes the close of `day`, which nobody knows until the day is over."""
window = prices[day - WINDOW + 1:day + 1]
return prices[day] > sum(window) / WINDOW
def run(signal):
"""Collect the change over each day on which the signal said to be holding."""
days_held = 0
total = 0.0
for day in range(WINDOW, len(closes)):
if signal(closes, day):
days_held += 1
total += closes[day] - closes[day - 1]
return days_held, round(total, 2)
honest_days, honest_total = run(signal_honest)
cheat_days, cheat_total = run(signal_look_ahead)
print("Honest signal: held", honest_days, "days, total", honest_total, "points")
print("Look ahead signal: held", cheat_days, "days, total", cheat_total, "points")
print("Points that came from knowing the future:", round(cheat_total - honest_total, 2))
# A test that catches the mistake: move one day's close a long way up, then a long way down, and
# see whether the decision about that same day can change. An honest signal cannot be affected by
# a price it has not seen.
def depends_on_own_close(signal, day):
before = signal(closes, day)
for nudge in (50, -50):
changed = list(closes)
changed[day] = closes[day] + nudge
if signal(changed, day) != before:
return True
return False
print("Honest signal depends on the day's own close:", depends_on_own_close(signal_honest, 60))
print("Look ahead signal depends on the day's own close:", depends_on_own_close(signal_look_ahead, 60))What you should see
Honest signal: held 71 days, total -1.43 points Look ahead signal: held 71 days, total 17.24 points Points that came from knowing the future: 18.67 Honest signal depends on the day's own close: False Look ahead signal depends on the day's own close: True
Run with Python 3.12 on Windows against the sample files on this page. No network and no account.
Check yourself
Answer each question from the code above before you open it. The answers were written from the script on this page and what it printed.
What is the difference between the two slices?
The honest one is prices[day - WINDOW:day], which ends at the close before the day. The other is prices[day - WINDOW + 1:day + 1], which is the same length but ends on the day's own close.
Both signals held for 71 days. Why are the totals so different?
The honest signal totals -1.43 points and the look ahead one 17.24. The second decides about a day with that day's close already in its window, so it is choosing with the answer in hand. Nobody could have traded on it.
How does the test at the end tell the two apart?
It moves the close of day 60 up by 50, then down by 50, and asks each signal about day 60 again. The honest decision cannot change either way, so the script prints False for it. The look ahead decision does change, so it prints True.
Exercise
Change WINDOW from 10 to 20 and run it again. Does the gap between the two totals go away?
What the changed script prints
Honest signal: held 67 days, total 5.43 points Look ahead signal: held 68 days, total 12.52 points Points that came from knowing the future: 7.09 Honest signal depends on the day's own close: False Look ahead signal depends on the day's own close: True
Captured from a run of the solution on . Run with Python 3.12 on Windows against the sample files on this page. No network and no account.
One solution
# Exercise solution: the lesson script with the one change the exercise asks for.
# A moving average signal as code, and the look ahead mistake that flatters it.
# Uses sample-candles.csv from the lesson on reading prices from a file.
# The prices are invented and the rule is an illustration, not something to follow.
import csv
closes = []
with open("sample-candles.csv", newline="") as f:
for row in csv.DictReader(f):
closes.append(float(row["close"]))
WINDOW = 20
def signal_honest(prices, day):
"""Decide about `day` using only closes from before it. This could really have been done."""
past = prices[day - WINDOW:day]
return prices[day - 1] > sum(past) / WINDOW
def signal_look_ahead(prices, day):
"""The mistake: the window includes the close of `day`, which nobody knows until the day is over."""
window = prices[day - WINDOW + 1:day + 1]
return prices[day] > sum(window) / WINDOW
def run(signal):
"""Collect the change over each day on which the signal said to be holding."""
days_held = 0
total = 0.0
for day in range(WINDOW, len(closes)):
if signal(closes, day):
days_held += 1
total += closes[day] - closes[day - 1]
return days_held, round(total, 2)
honest_days, honest_total = run(signal_honest)
cheat_days, cheat_total = run(signal_look_ahead)
print("Honest signal: held", honest_days, "days, total", honest_total, "points")
print("Look ahead signal: held", cheat_days, "days, total", cheat_total, "points")
print("Points that came from knowing the future:", round(cheat_total - honest_total, 2))
# A test that catches the mistake: move one day's close a long way up, then a long way down, and
# see whether the decision about that same day can change. An honest signal cannot be affected by
# a price it has not seen.
def depends_on_own_close(signal, day):
before = signal(closes, day)
for nudge in (50, -50):
changed = list(closes)
changed[day] = closes[day] + nudge
if signal(changed, day) != before:
return True
return False
print("Honest signal depends on the day's own close:", depends_on_own_close(signal_honest, 60))
print("Look ahead signal depends on the day's own close:", depends_on_own_close(signal_look_ahead, 60))Your progress
Next: Settings from environment
Your marks are kept in this browser only, in its local storage. They are not sent anywhere, there is no account, and they will not follow you to another browser or device. To remove them, use Clear my progress on the learning track page, or clear this site's data in your browser's settings. Marking needs JavaScript. With scripts off the track reads the same, without the ticks.
This browser refused to store anything for this page, so marks cannot be kept here.
Where this sits in the path
Stage 4 of 4: Building things that survive. Failure, limits, repeated orders, logs, settings, tests and backtests that do not fool you: what separates a script from a tool.
- Retry and backoffDone
- Stay under a rate limitDone
- Handle an error replyDone
- Idempotent ordersDone
- Log what happenedDone
- Log your API callsDone
- Backtest an ideaDone
- The look ahead mistakeDoneYou are here
- Settings from environmentDone
- Test with saved repliesDone
- A command line toolDone
- The capstone projectDone
Repositories to read next
- microsoft/qlib49,155 stars, read . Qlib is an AI-oriented Quant investment platform that aims to use AI tech to empower Quant Research, from exploring ideas to implementing productions. Qlib supports diverse ML modeling paradigms, including supervised learning, market dynamics modeling, and RL, and is now equipped with https://github.com/microsoft/RD-Agent to automate R&D process.
- nautechsystems/nautilus_trader29,633 stars, read . Production-grade Rust-native trading engine with deterministic event-driven architecture
- QuantConnect/Lean21,864 stars, read . Lean Algorithmic Trading Engine by QuantConnect (Python, C#)
- AI4Finance-Foundation/FinRL16,555 stars, read . FinRLĀ®: Financial Reinforcement Learning.
- goldmansachs/gs-quant13,061 stars, read . Python toolkit for quantitative finance
- polakowo/vectorbt9,277 stars, read . The backtesting engine that gives you an unfair advantage. Run thousands of trading ideas before others finish one.