Look-Ahead Bias
A formula has look-ahead bias when it uses a number that was not available at the moment the decision it drives would have been made. That is the entire definition, and it is worth stating so plainly because the error is almost never introduced by somebody who does not understand it. It is introduced by people who understand it perfectly and did not notice that a particular line had it.
By the end of this lesson you should be able to name six distinct mechanisms by which AFL can read the future, recognise each one on sight, reproduce the leak on your own data so that you can see its size, and run a fixed checklist over any backtest before you believe a single number in its report.
The information set, and why it is the only test
Section titled “The information set, and why it is the only test”Every bar in your database has a timestamp. Every decision your system makes has a moment. The question you are always asking is: at that moment, which numbers existed?
For a daily bar dated Tuesday, the Open existed at the opening auction. The High and the Low were not final until the session closed. The Close existed at the close. Volume existed at the close. Every indicator computed from the Close of Tuesday’s bar — a moving average, an RSI reading, a breakout level that includes Tuesday’s high — existed only after Tuesday’s session ended.
Look-ahead bias is the most dangerous of the backtesting errors for one reason: it does not merely flatter a result, it decouples the result from the rule entirely. A survivorship-biased test still tests your rule, on the wrong universe. An overfitted test still tests your rule, on data you already searched. A test with a future leak stops testing your rule at all. It tests the leak. That is why a leaky system’s equity curve so often looks like a straight line: you are not looking at a trading strategy, you are looking at a plot of information advantage.
Mechanism 1: the positive shift
Section titled “Mechanism 1: the positive shift”Ref( ARRAY, period ) is documented as follows: a positive period references n
periods in the future; a negative period references n periods ago. The function
reference says so directly, and even supplies the example ref( C, 12 ) with the
note that it means looking up the future.
Fragment — not a complete formula
Buy = Close > MA( Close, 50 ) AND Ref( Close, 1 ) > Close; // reads tomorrowBuy = Close > MA( Close, 50 ) AND Close > Ref( Close, -1 ); // reads yesterdayThe first line is not a subtle bug. It is a one-character difference from the second, and it converts a mediocre trend filter into a rule that only ever buys before an up day.
One character, two different universes
| Bar | Mon | Tue | Wed | Thu | Fri |
|---|---|---|---|---|---|
Close | 10.0 | 10.4 | 10.2 | 10.9 | 10.7 |
Ref(Close, 1) | 10.4 | 10.2 | 10.9 | 10.7 | Null |
Ref(Close, -1) | Null | 10.0 | 10.4 | 10.2 | 10.9 |
Ref(Close, 1) > Close | 1 | 0 | 1 | 0 | Null |
Positive shifts have a legitimate home: measurement. The base-rate study in Part 7
uses Ref( Close, Horizon ) deliberately, because it is measuring what followed a
pattern, and there is no other way to do that. The rule is simple and absolute: a
positive shift may appear in an exploration column, and it may never appear
anywhere upstream of Buy, Sell, Short, Cover, PositionSize or
PositionScore.
The same warning applies to anything built on a positive shift. TimeFrameGetPrice
takes a shift argument where positive values reference future higher-timeframe
bars. So does any helper function of your own that wraps Ref(). Grep your formula
for Ref( and read every call’s second argument; it takes a minute and it is the
cheapest audit on this page.
Mechanism 2: deciding on the close and filling at the open
Section titled “Mechanism 2: deciding on the close and filling at the open”This is the mechanism that produces most real-world impossible backtests, and it contains no suspicious code at all. Every line is ordinary. The leak lives in the relationship between the formula and two settings.
Fragment — not a complete formula
Buy = Cross( Close, MA( Close, 50 ) ); // decided from the bar's CLOSEBuyPrice = Open; // filled at the SAME bar's OPENSetTradeDelays( 0, 0, 0, 0 ); // no delay: act on the signal barThe signal is evaluated using Tuesday’s close. The fill happens at Tuesday’s open, which printed six and a half hours earlier. The backtester does exactly what you asked and reports the trade. Nothing warns you. On a universe of volatile symbols the difference between the open and the close of the signal bar is a substantial edge handed over for free on every single trade.
AmiBroker’s own documentation is explicit about the settings side of this. On the General tab, the description of Allow same bar exit states that the option may be turned on only if you are entering trades on the open, and that if you enter at any other time than the bar’s open the option should be turned off to avoid looking into the future. The same reasoning governs entries: if the decision uses the close, the earliest honest fill is the next bar.
There are only two internally consistent execution models on daily bars, and it is worth writing them out because most muddles are a blend of the two:
| Decide on | Delays | Trade price | Fill happens | |
|---|---|---|---|---|
| Next-open model | this bar’s Close | 1 | Open | next session’s opening |
| On-close model | this bar’s Close | 0 | Close | this session’s closing auction |
The next-open model is the one this course uses by default, because it is the one
you can actually operate: you read the close after the session, you place an order
overnight, it fills in the morning. The on-close model is defensible if you can
genuinely get a closing auction fill, and it must then be paired with
ActivateStopsImmediately turned off, because stops cannot fire on a bar you only
entered at its close.
SetTradeDelays() is worth understanding mechanically rather than as a checkbox.
The documented behaviour is that the backtester internally performs
Buy = Ref( Buy, -buydelay ); and the equivalent for the other three arrays, after
your formula has run. Two consequences follow, both of which cause errors:
- The price arrays are not shifted. Only
Buy,Sell,ShortandCovermove. SoBuyPrice = Open;with a delay of 1 means “the open of the bar the trade lands on”, which is what you want — butBuyPrice = Ref( Open, 1 );with a delay of 1 means the open of the bar after that, which is a leak. PositionSizeandPositionScoreare not shifted either. If your sizing or ranking reads values from the same bar as the undelayed signal, and the fill is a bar later, the size and the rank were computed from information the fill bar had not yet produced.
Mechanism 3: higher-timeframe expansion
Section titled “Mechanism 3: higher-timeframe expansion”Multi-timeframe code is where careful people leak, because the leak is a default.
AmiBroker’s timeframe functions compress an array so that the first N elements
become Null and the compressed values sit at the end. TimeFrameExpand() puts them
back onto base-interval bars, and its third argument decides at which bar of the
period the value becomes visible. That single choice is the entire causality
question:
| Mode | Documented behaviour | Safe in a rule? |
|---|---|---|
expandLast |
the value appears from the last bar in the period — a weekly close is available on Friday’s bar | yes, and it is TimeFrameExpand’s default |
expandFirst |
the value appears from the first bar in the period — a weekly open is available from Monday’s bar | only for the period’s Open |
expandPoint |
non-empty only on the period’s last bar, Null everywhere else | for drawing, not for rules |
The official caveat on expandFirst is unambiguous and appears on both the
function page and in the tutorial chapter: used on a price other than the open it
may look into the future, and the example given is that a weekly High expanded with
expandFirst lets you know on Monday what the high was for the entire week.
Fragment — not a complete formula
// Safe: default expandLast, value published when the week has finishedTimeFrameSet( inWeekly );WeeklyMA = MA( Close, 14 );TimeFrameRestore();Buy = Cross( Close, TimeFrameExpand( WeeklyMA, inWeekly ) );Two more traps live in the same neighbourhood.
TimeFrameGetPrice() has unsafe defaults on both arguments. Its signature is
TimeFrameGetPrice( pricefield, interval, shift = 0, mode = expandFirst ). The
documentation states that it is equivalent to a compress, a Ref() by shift, and
an expand with expandFirst, and warns that with shift = 0 the compressed data
may look into the future — a weekly high can be known on Monday — and that a
trading system should reference past data with a negative shift.
Fragment — not a complete formula
WeekHigh = TimeFrameGetPrice( "H", inWeekly ); // leak: this week, known MondayWeekHigh = TimeFrameGetPrice( "H", inWeekly, -1 ); // safe: last completed weekNote the asymmetry: TimeFrameExpand() defaults to the safe mode, and
TimeFrameGetPrice() defaults to the unsafe one. Both defaults are documented.
Memorise both, because writing TimeFrameExpand( x, inWeekly ) and
TimeFrameGetPrice( "C", inWeekly ) in the same formula gives you two different
alignments for what looks like the same idea.
The interval argument names the frame the data came from. The Common Coding
Mistakes chapter gives the exact wrong/right pair: after TimeFrameSet( inWeekly ),
expanding with TimeFrameExpand( MA14_Weekly, inDaily ) is wrong; it must be
inWeekly. Getting this wrong produces a plausible-looking, misaligned series with
no error message at all.
There is one more residual caveat with expandLast that the documentation does not
spell out but which follows directly from what it does say. On the period’s last
bar, the expanded value embeds that bar’s own close, high and low. A rule that
reads a weekly value on Friday and fills at Friday’s open is still cheating.
Fill at that bar’s close, or at the next bar’s open, or shift the higher-timeframe
value back by one period.
Finally, QuickAFL. The official QuickAFL notes list, among the cases where the
optimisation may not give identical results, “TimeFrame functions with much higher
intervals than base interval”. A weekly indicator computed from daily bars is
precisely that case. Put SetBarsRequired( sbrAll, sbrAll ); at the top of any
multi-timeframe study, or your results will change depending on the date range and
the chart zoom, and you will spend an afternoon believing AmiBroker is broken.
Mechanism 4: ranking on end-of-period data
Section titled “Mechanism 4: ranking on end-of-period data”In a portfolio backtest, PositionScore decides which candidates get the available
slots when more symbols signal than there is capital or MaxOpenPositions allows.
It is ranked, by default, on its absolute value. It is also the single easiest
place in AFL to leak the future without touching Buy at all.
Fragment — not a complete formula
PositionScore = TimeFrameGetPrice( "C", inWeekly ) - Close; // leakPositionScore = ROC( Close, 20 ); // causalPositionScore = MA( Close * Volume, 50 ); // causalThe first line ranks candidates by how far the price sits below the closing price
of the week that has not yet finished. The Buy array can be immaculate; the
selection is still made with information from the future, and in a portfolio run
with a tight position limit the selection is doing most of the work.
The same applies to any score built from a monthly or quarterly aggregate that is
expanded with expandFirst, and to any score computed from a full-history
statistic — a percentile rank, a z-score, a normalisation — where the statistic was
computed over the whole array including bars after the decision bar. If you
normalise momentum by dividing by its own standard deviation over all bars, every
bar’s score depends on every other bar’s value, and that includes the ones from
next year.
Mechanism 5: composites and static variables
Section titled “Mechanism 5: composites and static variables”These two are grouped because they share a failure mode: information crosses from one execution pass into another, and the second pass has no idea when the first pass’s numbers became knowable.
Composites. AddToComposite() accumulates a per-symbol array into a synthetic
ticker during a Scan, and you then read it back with Foreign( "~MyIndex", "C" ).
The composite is stored data, computed by a scan over the whole date range you
ran the scan on. If you build a breadth composite over 2010–2025 and then backtest
a rule that reads it, every bar of the backtest has access to a series that was
constructed with full knowledge of 2010–2025.
That is not always a leak. A breadth composite whose value on each bar is a sum of
per-bar values is fine, because bar i of the composite only ever contained
contributions from bar i of each symbol. It becomes a leak the moment the
contributed array itself looks forward — for example if you contribute a normalised
score whose normalisation spans the whole history, or if the universe you scanned
was itself chosen with hindsight. The KB also notes that AddToComposite
internally executes SetBarsRequired( sbrAll, sbrAll ), which is why Tools →
Check and Profile reports that such a formula references future bars; the KB
states this particular alarm is a false one caused by sbrAll.
Static variables. The documented ranking recipe writes scores for every symbol
under Status( "stocknum" ) == 0, calls StaticVarGenerateRanks() once, then reads
the ranks per symbol. Two leaks live in that pattern:
- Stale statics. Nothing clears static variables for you. If the removal step
(
StaticVarRemove( "score*" );— with the wildcard) is skipped, last run’s scores, computed over a different date range, silently participate in this run’s ranking. The official example’s comment shouts about the asterisk for exactly this reason. - Whole-history scores. The scores written in pass one are arrays over the full
loaded history. If the score is itself a whole-history statistic, the rank on bar
i depends on data after bar i.
StaticVarGenerateRanksranks bar by bar, so the ranking mechanism is causal; whether the input is causal is entirely up to you.
Mechanism 6: leaks that live in the data, not the formula
Section titled “Mechanism 6: leaks that live in the data, not the formula”Three that are worth knowing because no amount of formula discipline will fix them.
Forward-stamped timestamps. AmiBroker expects a bar’s timestamp to be the
start of the interval: a 9:30 one-minute bar covers 9:30:00 to 9:30:59.999. Some
vendors stamp the end instead. If you import forward-stamped data without
correcting it, every compressed bar is misaligned and the misalignment points
forward. The documented fix is a negative $TIMESHIFT on import, in hours — one
minute is −0.01666667.
“Add artificial future bar”. This checkbox on the Settings → Portfolio tab adds a bar after the last real one, with an incremented date, zero volume, and all four OHLC fields set to the last real bar’s Close. Its purpose is to let a one-bar-delay system show you tomorrow’s recommendation. It is a live-trading convenience. Leave it on during a research backtest and you have added a synthetic bar to the end of the series.
Repainting indicators. Zig(), Peak() and Trough() locate turning points by
looking at what came afterwards, which is why the line moves when new bars arrive.
Part 7 demonstrates this directly. Any function whose plotted history changes as
new data arrives cannot be used in a rule.
Making the leak visible on your own data
Section titled “Making the leak visible on your own data”Reading about a leak and seeing one are different experiences. This formula puts four leaky constructions next to their causal equivalents on the same bars, and counts how often they disagree, so that the size of each leak becomes a number rather than a warning.
Complete formula
Section titled “Complete formula”Complete runnable AFL
// look-ahead-side-by-side.afl// Part 30 - Look-Ahead Bias//// PURPOSE// Puts four leaky constructions next to their causal equivalents on the same// bars, so the leak stops being a warning in a book and becomes a column of// numbers you can read. Nothing here is a trading system. Every "leak" column// is deliberately wrong and is shown only so you can recognise it in your own// formulas.//// HOW TO RUN// Analysis window -> Apply to: *Current* symbol (one liquid symbol is enough)// Periodicity: Daily. Range: All quotations. Press EXPLORE.// Sort by Date ascending and read a few consecutive weeks.//// ASSUMPTIONS AND LIMITS// - Daily base interval. The weekly columns need at least one full week of// history before they mean anything.// - SetBarsRequired(-2,-2) forces AmiBroker to use all bars. Without it,// QuickAFL may evaluate only part of the array, and the documented QuickAFL// caveat about "TimeFrame functions with much higher intervals than base// interval" applies directly to the weekly columns below.// - The "leak edge" column measures a price difference, not a return after// costs. It is a size-of-the-lie measure, nothing more.//// Every value labelled LEAK below is information that was NOT available at the// moment the corresponding decision would have been taken.
SetBarsRequired( -2, -2 ); // -2 is sbrAll: switch QuickAFL off for this study
TrendPeriod = Param( "Trend MA period", 50, 5, 200, 5 );
// =====================================================================// MECHANISM 1 - a positive shift in Ref()// ---------------------------------------------------------------------// Ref( array, +n ) is documented as "n periods in the future". It exists for// measurement work (see the base-rate study in Part 7). In a Buy expression it// is a bug, and it is invisible: the formula compiles, runs and reports.// =====================================================================NextClose = Ref( Close, 1 ); // LEAK - tomorrow, read todayPreviousClose = Ref( Close, -1 ); // causal - yesterday, read today
LeakRule1 = Close > MA( Close, TrendPeriod ) AND NextClose > Close;CausalRule1 = Close > MA( Close, TrendPeriod ) AND Close > PreviousClose;
// =====================================================================// MECHANISM 2 - deciding on the Close and filling at the same bar's Open// ---------------------------------------------------------------------// Settings -> Trades with buy price = Open and buy delay = 0 fills a signal// computed from that same bar's Close at a price printed hours earlier.// The gap between the two is free money that no order could have captured.// =====================================================================Signal2 = Cross( Close, MA( Close, TrendPeriod ) );SameBarEdgePct = 100 * SafeDivide( Close - Open, Open, 0 );LeakEdge2 = IIf( Signal2, SameBarEdgePct, 0 );
// =====================================================================// MECHANISM 3 - TimeFrameGetPrice() with its own defaults// ---------------------------------------------------------------------// The documented default is shift = 0 AND mode = expandFirst. Both point the// wrong way. With those defaults the whole week's High is written onto the// week's FIRST bar, so a Monday rule can read Friday's high.// A negative shift reads a COMPLETED higher-timeframe bar instead.// =====================================================================WeeklyHighLeak = TimeFrameGetPrice( "H", inWeekly ); // LEAK - this week, known MondayWeeklyHighCausal = TimeFrameGetPrice( "H", inWeekly, -1 ); // causal - last completed week
// True on any bar where the "current week high" is above every high printed so// far, which is only possible because the value came from later in the week.HighSoFarThisWeek = HighestSince( DayOfWeek() < Ref( DayOfWeek(), -1 ), High );KnowsFutureHigh = WeeklyHighLeak > HighSoFarThisWeek + 0.000001;
// =====================================================================// MECHANISM 4 - expandFirst versus the expandLast default// ---------------------------------------------------------------------// TimeFrameExpand()'s own default is expandLast, which publishes a period's// value on the period's LAST bar. expandFirst publishes it on the FIRST bar -// the documented look-ahead mode. Compressing with compressLast and expanding// both ways makes the difference visible bar by bar.// =====================================================================WeeklyCloseCompressed = TimeFrameCompress( Close, inWeekly, compressLast );WeeklyCloseExpandLast = TimeFrameExpand( WeeklyCloseCompressed, inWeekly, expandLast );WeeklyCloseLeak = TimeFrameExpand( WeeklyCloseCompressed, inWeekly, expandFirst );
// The two disagree on every bar of a week except the one where the week's close// is genuinely known. Counting the disagreements measures how much of the test// period a formula using expandFirst would have been cheating on.Disagrees4 = NOT IsNull( WeeklyCloseLeak ) AND NOT IsNull( WeeklyCloseExpandLast ) AND abs( WeeklyCloseLeak - WeeklyCloseExpandLast ) > 0.000001;
// =====================================================================// Running counts, so the last row summarises the whole history// =====================================================================Measurable = Status( "barinrange" );Bars = Cum( Measurable );LeakSignals1 = Cum( Measurable AND LeakRule1 );CausalSignals1 = Cum( Measurable AND CausalRule1 );Signals2 = Cum( Measurable AND Signal2 );TotalLeakEdge2 = Cum( IIf( Measurable, LeakEdge2, 0 ) );FutureHighBars = Cum( Measurable AND KnowsFutureHigh );Disagree4Bars = Cum( Measurable AND Disagrees4 );
// =====================================================================// Output// =====================================================================Filter = Measurable;
AddColumn( DateTime(), "Date", formatDateTime );AddColumn( Open, "Open", 1.2 );AddColumn( Close, "Close", 1.2 );
AddColumn( NextClose, "LEAK 1: next Close", 1.2 );AddColumn( PreviousClose, "causal: prev Close", 1.2 );AddColumn( LeakRule1, "LEAK 1 signal", 1.0 );AddColumn( CausalRule1, "causal 1 signal", 1.0 );
AddColumn( Signal2, "Signal on Close", 1.0 );AddColumn( LeakEdge2, "LEAK 2: C-O edge %", 1.2 );
AddColumn( WeeklyHighLeak, "LEAK 3: this week H", 1.2 );AddColumn( WeeklyHighCausal, "causal 3: last week H", 1.2 );AddColumn( KnowsFutureHigh, "LEAK 3 active", 1.0 );
AddColumn( WeeklyCloseLeak, "LEAK 4: expandFirst", 1.2 );AddColumn( WeeklyCloseExpandLast, "causal 4: expandLast", 1.2 );
AddColumn( Bars, "Bars", 1.0 );AddColumn( LeakSignals1, "Cum LEAK 1 sigs", 1.0 );AddColumn( CausalSignals1, "Cum causal 1 sigs", 1.0 );AddColumn( Signals2, "Cum signals 2", 1.0 );AddColumn( TotalLeakEdge2, "Cum LEAK 2 edge %", 1.2 );AddColumn( FutureHighBars, "Bars LEAK 3 active", 1.0 );AddColumn( Disagree4Bars, "Bars LEAK 4 differs", 1.0 );How it works
Section titled “How it works”Four independent sections, each self-contained. The first computes a trend rule two
ways, once with Ref( Close, 1 ) and once with Ref( Close, -1 ), and counts the
signals each produces. The second measures the same-bar leak directly: on every bar
where a crossover fires, it records the percentage distance from that bar’s open to
its close, which is exactly the amount a delay-zero, fill-at-open backtest would
have handed you before the trade began. The third contrasts
TimeFrameGetPrice( "H", inWeekly ) with the same call given a shift of −1, and
flags every bar where the “current week’s high” exceeds every high printed so far —
which can only happen if the value came from later in the week. The fourth
compresses the close to weekly and expands it both ways, so you can read
expandFirst and expandLast side by side down a column.
The running Cum() totals at the end mean the final row summarises the whole
history: how many bars each leak was active on, and how much free edge the same-bar
fill accumulated.
Key functions
Section titled “Key functions”HighestSince( EXPRESSION, ARRAY ) returns the highest value of the array since
the expression was last true; here the expression is a week boundary, detected by
the day-of-week number dropping. DayOfWeek() returns 0 for Sunday through 6 for
Saturday, so a drop marks a new week — a heuristic that a holiday-shortened week
handles correctly and a data gap may not. SetBarsRequired( -2, -2 ) passes the
documented sbrAll value and switches QuickAFL off, which the weekly columns need.
Expected result
Section titled “Expected result”One row per bar. The LEAK 1 and causal 1 signal columns will differ on a large
minority of bars. The LEAK 2: C-O edge % column will be non-zero on every
crossover bar, and its cumulative total is the size of that particular lie over the
whole history. LEAK 3 active will be 1 on most bars of most weeks. The two
weekly-close columns will hold different numbers on every bar except the last of
each week.
Test it
Section titled “Test it”Sort by date and read one calendar week at a time. On the Monday row, compare
LEAK 3: this week H against the highs printed on Monday, Tuesday and Wednesday.
If the Monday value is larger than any of them, you are looking at a number that
did not exist on Monday. Then confirm the arithmetic of the same-bar leak by hand
on one row: the LEAK 2 column should equal (Close − Open) / Open × 100.
Common errors
Section titled “Common errors”Running on a symbol with fewer than a few months of history leaves the weekly
columns Null, because compression needs completed periods. Running on an intraday
database without adjusting the interval constants produces weekly columns that do
not mean what you expect. Leaving QuickAFL on — that is, removing the
SetBarsRequired line — makes the counts change when you change the date range,
which is the documented QuickAFL interaction rather than a bug in the formula.
Extension
Section titled “Extension”Add a fifth section that compares PositionScore computed from a whole-history
StDev() normalisation against a rolling one, and count the bars on which the two
would rank the same symbol differently.
The look-ahead audit checklist
Section titled “The look-ahead audit checklist”Run this over any backtest before you read its report. It takes about ten minutes and it is the highest-value ten minutes in the whole research process.
In the formula
- Every
Ref()call: is the second argument negative? Search forRef(and read each one. A positive shift upstream ofBuy,Sell,Short,Cover,PositionSizeorPositionScoreis a defect. - Every
TimeFrameGetPrice()call: is there an explicit negativeshift? The default of 0 combined with the defaultexpandFirstis unsafe for every field except"O". - Every
TimeFrameExpand()call: is theintervalthe frame the data came from? Is the modeexpandLast, or deliberately something else for a documented reason? - Every array created inside a
TimeFrameSet()block: is it expanded before use?TimeFrameRestore()restores onlyOpen,High,Low,Close,Volume,OpenIntandAvg. Everything else you computed stays compressed. PositionScore: is every input to it knowable at the decision bar? Watch for whole-history normalisations, percentile ranks and z-scores.- Breakout and channel levels: does the level include the current bar’s own high
or low?
HHV( High, 50 )includes today;Ref( HHV( High, 50 ), -1 )does not. - Repainting functions: no
Zig(),Peak()orTrough()anywhere upstream of a signal. SetBarsRequired( sbrAll, sbrAll )present if the formula uses timeframe functions.
In the settings
- Trade price and delays: do they form one of the two consistent execution models? A close-based decision with a delay of 0 and a trade price of Open is a leak.
- Allow same bar exit: off, unless entries genuinely happen at the open.
- Activate stops immediately: on only when entering at the open; off when entering at the close.
- Add artificial future bar: off for research runs.
- Periodicity matches the interval your formula assumes.
In the data
- Timestamps are start-of-interval. If the vendor stamps forward, was
$TIMESHIFTapplied on import? - Padding and gap-filling: are repeated closes with zero volume being treated as tradable bars?
The behavioural test
- Does the equity curve look implausibly smooth, with a very high
Winnerspercentage and a smallAvg. Bars Held? Do not reason about whether it could be real. Go back to step 1.
Look-ahead bias is one idea — using a number before it existed — expressed through
at least six distinct mechanisms. The positive shift is the obvious one and the
rarest in practice. The same-bar fill is the common one, and it lives in the
relationship between a formula and two settings rather than in any single line.
Multi-timeframe expansion leaks through documented defaults that point in opposite
directions in the two functions you are most likely to use. Ranking leaks through
PositionScore while Buy stays clean. Composites and static variables leak
across execution passes. And some leaks are in the data before your formula ever
runs.
What changes for you is the order of operations. You no longer read the report first. You run the checklist first, and only then decide whether the report is worth reading at all.
The next lesson moves to the error that no amount of formula discipline can fix, because the formula is not where it lives: the universe you tested on.
Check your understanding
Sources for this lesson
13 verified · checked 2026-08-31
- 01AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
- 02AFL Function Reference — TimeFrameExpandamibroker.com/guide/afl/timeframeexpand.html2026-08-31
- 03AFL Function Reference — TimeFrameGetPriceamibroker.com/guide/afl/timeframegetprice.html2026-08-31
- 04AFL Function Reference — TimeFrameCompressamibroker.com/guide/afl/timeframecompress.html2026-08-31
- 05AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
- 06AmiBroker User's Guide — Multiple Time Frame Supportamibroker.com/guide/h_timeframe.html2026-08-31
- 07AmiBroker User's Guide — System test settings window§ General tab, Portfolio tabamibroker.com/guide/w_settings.html2026-08-31
- 08AmiBroker User's Guide — Warning 509amibroker.com/guide/errors/509.html2026-08-31
- 09AmiBroker User's Guide — Common Coding Mistakes in AFLamibroker.com/guide/a_mistakes.html2026-08-31
- 10AFL Function Reference — AddToCompositeamibroker.com/guide/afl/addtocomposite.html2026-08-31
- 11AFL Function Reference — StaticVarGenerateRanksamibroker.com/guide/afl/staticvargenerateranks.html2026-08-31
- 12AmiBroker Knowledge Base — QuickAFLamibroker.com/kb/2008/07/03/quickafl2026-08-31
- 13AmiBroker Knowledge Base — How to correct forward-looking timestampsamibroker.com/kb/2014/10/25/how-to-correct-forward-looking-timestamps2026-08-31
Every technical claim on this page was checked against the official AmiBroker documentation on the date shown. Where the course disagrees with folklore, the source is how you can tell which one to trust.