Skip to content
Level 4 · Trading System ResearcherLessonPart 30 · page 1 of 832 min
32Minutes
12AFL functions
13Sources
StandardRequires
AFL functions taught here12

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.

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 tomorrow
Buy = Close > MA( Close, 50 ) AND Close > Ref( Close, -1 ); // reads yesterday

The 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

On Monday, the third row already contains Tuesday's close. Any rule that reads it is trading with tomorrow's newspaper.
BarMonTueWedThuFri
Close10.010.410.210.910.7
Ref(Close, 1)10.410.210.910.7Null
Ref(Close, -1)Null10.010.410.210.9
Ref(Close, 1) > Close1010Null
On Monday, the third row already contains Tuesday's close. Any rule that reads it is trading with tomorrow's newspaper. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

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 CLOSE
BuyPrice = Open; // filled at the SAME bar's OPEN
SetTradeDelays( 0, 0, 0, 0 ); // no delay: act on the signal bar

The 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, Short and Cover move. So BuyPrice = Open; with a delay of 1 means “the open of the bar the trade lands on”, which is what you want — but BuyPrice = Ref( Open, 1 ); with a delay of 1 means the open of the bar after that, which is a leak.
  • PositionSize and PositionScore are 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.

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 finished
TimeFrameSet( 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 Monday
WeekHigh = TimeFrameGetPrice( "H", inWeekly, -1 ); // safe: last completed week

Note 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; // leak
PositionScore = ROC( Close, 20 ); // causal
PositionScore = MA( Close * Volume, 50 ); // causal

The 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. StaticVarGenerateRanks ranks 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.

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 runnable AFL

look-ahead-side-by-side.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 today
PreviousClose = 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 Monday
WeeklyHighCausal = 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 );

Download look-ahead-side-by-side.afl134 lines

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.

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.

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.

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.

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.

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.

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

  1. Every Ref() call: is the second argument negative? Search for Ref( and read each one. A positive shift upstream of Buy, Sell, Short, Cover, PositionSize or PositionScore is a defect.
  2. Every TimeFrameGetPrice() call: is there an explicit negative shift? The default of 0 combined with the default expandFirst is unsafe for every field except "O".
  3. Every TimeFrameExpand() call: is the interval the frame the data came from? Is the mode expandLast, or deliberately something else for a documented reason?
  4. Every array created inside a TimeFrameSet() block: is it expanded before use? TimeFrameRestore() restores only Open, High, Low, Close, Volume, OpenInt and Avg. Everything else you computed stays compressed.
  5. PositionScore: is every input to it knowable at the decision bar? Watch for whole-history normalisations, percentile ranks and z-scores.
  6. 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.
  7. Repainting functions: no Zig(), Peak() or Trough() anywhere upstream of a signal.
  8. SetBarsRequired( sbrAll, sbrAll ) present if the formula uses timeframe functions.

In the settings

  1. 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.
  2. Allow same bar exit: off, unless entries genuinely happen at the open.
  3. Activate stops immediately: on only when entering at the open; off when entering at the close.
  4. Add artificial future bar: off for research runs.
  5. Periodicity matches the interval your formula assumes.

In the data

  1. Timestamps are start-of-interval. If the vendor stamps forward, was $TIMESHIFT applied on import?
  2. Padding and gap-filling: are repeated closes with zero volume being treated as tradable bars?

The behavioural test

  1. Does the equity curve look implausibly smooth, with a very high Winners percentage and a small Avg. 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

Question 1. Which of these introduce look-ahead bias into a trading rule? Select all that apply.
Show the answer and why

Answer: PositionScore = TimeFrameGetPrice("C", inWeekly);, BuyPrice = Open; SetTradeDelays(0,0,0,0); with Buy decided from the Close

The first ranks candidates on a weekly close that has not happened yet, because both TimeFrameGetPrice defaults — shift 0 and expandFirst — publish the whole period on its first bar. The third fills a close-based decision at a price printed that morning. The second correctly shifts the breakout level back one bar so it excludes the current high, and the fourth explicitly reads a completed weekly bar.

Question 2. A formula calls TimeFrameSet(inWeekly), computes three arrays, calls TimeFrameRestore(), and expands only one of them. AmiBroker raises no Warning 509. What does that tell you?
Show the answer and why

Answer: Only that TimeFrameExpand() appears somewhere in the formula

Warning 509 is raised when a formula uses timeframe functions and never calls TimeFrameExpand() at all. It is a presence check on the formula, not a per-array correctness check, so a formula that expands one array and forgets two passes silently. The two forgotten arrays are still compressed, and comparing them against base-interval data compares two different time scales.

Question 3. TimeFrameExpand() and TimeFrameGetPrice() have different default expansion modes. Which is which?
Show the answer and why

Answer: TimeFrameExpand defaults to expandLast; TimeFrameGetPrice defaults to expandFirst

TimeFrameExpand defaults to expandLast, which publishes a period value on the period’s last bar and is the causally safe choice. TimeFrameGetPrice defaults to expandFirst with shift 0, which publishes the whole period’s aggregate on its first bar. Writing what looks like the same idea with the two functions therefore gives two different alignments.

Question 4. A backtest reports a very high Winners percentage, an Avg. Bars Held of about 1, and an Avg. Profit/Loss % close to the average daily range of the universe. What is the first thing to check?
Show the answer and why

Answer: Whether the trade price and delay settings let a close-based decision fill at the same bar

Those three symptoms together are the signature of a same-bar leak: the trade is capturing the bar it was decided on rather than any subsequent move, which is why the average winner is about the size of one bar. Costs, universe composition and optimisation are all real problems, but none of them produce that particular combination.

Question 5. Why is SetBarsRequired( sbrAll, sbrAll ) recommended at the top of a multi-timeframe study?
Show the answer and why

Answer: It prevents QuickAFL from evaluating only part of the array, which the documentation lists as a case where results may differ

QuickAFL evaluates the in-range slice plus an estimated margin. The official QuickAFL notes list timeframe functions with much higher intervals than the base interval as a case where the estimate may not produce identical results, so the same formula can give different numbers for a date sub-range and for all quotations. Forcing all bars removes that variable; it makes the run slower, not faster.

Sources for this lesson

13 verified · checked 2026-08-31

  1. 01AFL Function Reference — Refamibroker.com/guide/afl/ref.html2026-08-31
  2. 02AFL Function Reference — TimeFrameExpandamibroker.com/guide/afl/timeframeexpand.html2026-08-31
  3. 03AFL Function Reference — TimeFrameGetPriceamibroker.com/guide/afl/timeframegetprice.html2026-08-31
  4. 04AFL Function Reference — TimeFrameCompressamibroker.com/guide/afl/timeframecompress.html2026-08-31
  5. 05AFL Function Reference — SetTradeDelaysamibroker.com/guide/afl/settradedelays.html2026-08-31
  6. 06AmiBroker User's Guide — Multiple Time Frame Supportamibroker.com/guide/h_timeframe.html2026-08-31
  7. 07AmiBroker User's Guide — System test settings window§ General tab, Portfolio tabamibroker.com/guide/w_settings.html2026-08-31
  8. 08AmiBroker User's Guide — Warning 509amibroker.com/guide/errors/509.html2026-08-31
  9. 09AmiBroker User's Guide — Common Coding Mistakes in AFLamibroker.com/guide/a_mistakes.html2026-08-31
  10. 10AFL Function Reference — AddToCompositeamibroker.com/guide/afl/addtocomposite.html2026-08-31
  11. 11AFL Function Reference — StaticVarGenerateRanksamibroker.com/guide/afl/staticvargenerateranks.html2026-08-31
  12. 12AmiBroker Knowledge Base — QuickAFLamibroker.com/kb/2008/07/03/quickafl2026-08-31
  13. 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.