Skip to content
Level 3 · AFL DeveloperLessonPart 14 · page 3 of 534 min
34Minutes
7AFL functions
6Sources
StandardRequires
AFL functions taught here7

Expansion Modes and the Look-Ahead Trap

A completed weekly bar produces one number. That number has to be written onto five daily bars. Which five — the week it describes, or the week that follows it — is the entire question, and AmiBroker settles it with one optional argument that most formulas never mention. This lesson makes sure you never write that argument by accident again.

Fragment — not a complete formula

Expanded = TimeFrameExpand( CompressedArray, inWeekly ); // mode defaults to expandLast
Expanded = TimeFrameExpand( CompressedArray, inWeekly, expandLast );

Documented syntax: TimeFrameExpand( array, interval, mode = expandLast ), returning an ARRAY. The interval argument “must match the value used in TimeFrameCompress or TimeFrameSet”.

That sentence is worth reading twice, because the natural mistake is the opposite reading. The interval names the frame the data came from, not the frame you want it in. The Common Coding Mistakes chapter has the pair explicitly: after TimeFrameSet( inWeekly ), writing TimeFrameExpand( MA14_Weekly, inDaily ) is wrong; it must be TimeFrameExpand( MA14_Weekly, inWeekly ). “I want it in daily” reads plausibly and is the wrong instruction.

The reference page defines them in one sentence each, and the wording repays close attention.

  • expandLast — “the compressed value is expanded starting from last bar within given period (so for example weekly close/high/low is available on Friday’s bar).”
  • expandFirst — “the compressed value is expanded starting from first bar within given period (so for example weekly open is available from Monday’s bar).”
  • expandPoint — “the resulting array gets not empty values only for the last bar within given period (all remaining bars are Null (empty)).”

Notice what is not different between the first two. The value is the same in both cases: the aggregate of the whole completed period. Only its starting bar moves. expandFirst does not give you a partial, in-progress figure; it gives you the finished figure, backdated to the beginning of the period it describes.

One weekly high, written onto daily bars three different ways

Illustrative weekly highs of 11.0 and 12.0. Compare the expandFirst row with the row above it: on Monday of week 1 it already reads 11.0, a high that does not print until Thursday.
BarMonTueWedThuFriMonTueWedThuFri
High (daily)10.210.510.711.010.811.111.412.011.611.7
Highest high so far this week10.210.510.711.011.011.111.412.012.012.0
expandLastthe defaultNullNullNullNull11.011.011.011.011.012.0
expandFirstreads the future11.011.011.011.011.012.012.012.012.012.0
expandPointdrawing onlyNullNullNullNull11.0NullNullNullNull12.0
Illustrative weekly highs of 11.0 and 12.0. Compare the expandFirst row with the row above it: on Monday of week 1 it already reads 11.0, a high that does not print until Thursday.

Why expandFirst is the one that reads the future

Section titled “Why expandFirst is the one that reads the future”

The official caveat appears in the same words on the function page and in the User’s Guide chapter:

expandFirst used on price different than open may look into the future. For example if you create weekly HIGH series, expanding it to daily interval using expandFirst will enable you to know on MONDAY what was the high for entire week.

Work through the diagram against that sentence. Week 1’s high is 11.0 and it prints on Thursday. With expandFirst, Monday’s cell already contains 11.0. On Monday the market has produced exactly one bar, whose high is 10.2. There is no arrangement of the available data from which 11.0 can be computed on Monday, because the trade that made it has not happened. A Buy rule reading that cell on Monday is reading Thursday’s tape.

With expandLast, week 1’s 11.0 first appears on Friday — the last bar of the week it describes, by which point every trade that contributed to it has occurred — and it is then the value in force through the following week until week 2 completes and posts 12.0 on the next Friday. That is exactly the behaviour a causal trading rule needs: at any moment, the most recent completed weekly figure.

The period’s open genuinely is known at the period’s first bar — the week’s opening price is set on Monday morning and never revised. That is why the caveat says “on price different than open”, and why the guide’s own paired example is:

Fragment — not a complete formula

// Documented pairing: the mode must match what the value means
WeeklyCloseSeries = TimeFrameExpand( TimeFrameCompress( Close, inWeekly, compressLast ), inWeekly, expandLast );
WeeklyOpenSeries = TimeFrameExpand( TimeFrameCompress( Open, inWeekly, compressOpen ), inWeekly, expandFirst );

The test generalises beyond prices: expandFirst is legitimate for any quantity that is fully determined at the period’s first bar. The lab formula below uses it for one such quantity — the bar number on which the current week started — and for nothing else.

expandPoint leaves Null everywhere except the last bar of each period, so any comparison against it is undefined on four days out of five. It is not a safer expandLast; it is a different tool. Its documented use is overlaying higher-timeframe candles on a lower interval chart: the guide’s second example builds weekly candles over a daily line with TimeFrameGetPrice( "O", inWeekly, 0, expandPoint ) and friends, feeding PlotOHLC().

This is the single fact most worth memorising in this part.

Call Default mode Default shift Safe for a decision?
TimeFrameExpand( x, inWeekly ) expandLast n/a Yes
TimeFrameGetPrice( "H", inWeekly ) expandFirst 0 No

Written with no optional arguments at all, those two calls align their output differently. The TimeFrameGetPrice() page is explicit about the consequence: because the function behaves like TimeFrameExpand( Ref( TimeFrameCompress( … ), shift ), interval, expandFirst ), “if shift = 0 compressed data may look into the future ( weekly high can be known on monday )”, and “if you want to write a trading system using this function please make sure to reference PAST data by using negative shift value”.

So there are two documented safe patterns, and it is worth being able to write both from memory:

Two safe routes to a higher-timeframe value

  1. Pattern A — compute in the higher frameTimeFrameSet → compute → TimeFrameRestore → TimeFrameExpand with the default expandLast
  2. Pattern B — read a completed higher-frame priceTimeFrameGetPrice with a NEGATIVE shift, so the bar you read has finished
Anything else needs an argument for why it is causal, written down where the next reader will find it.

expandLast places a period’s value on that period’s last bar, and that value includes that bar’s own high, low and close. So on Friday itself, the weekly figure embeds Friday’s trading. Reading it on Friday and filling at Friday’s open is still same-bar cheating, even though the expansion mode is correct.

The remedy is the ordinary one from Part 30’s vocabulary: act at the close of the bar that produced the signal, or at the next bar’s open, and set the backtester’s trade delays to match. Nothing about multi-timeframe work exempts a formula from that discipline; it simply adds a second, larger scale on which to get it wrong.

TimeFrameCompress(), and choosing the right aggregation

Section titled “TimeFrameCompress(), and choosing the right aggregation”

TimeFrameCompress( array, interval, mode = compressLast ) is the surgical alternative to TimeFrameSet(). It compresses one array and touches nothing else — the current frame stays where it was, so after wc = TimeFrameCompress( Close, inWeekly ); a subsequent MA( C, 14 ) is still a daily average while MA( wc, 14 ) is a weekly one.

The documented modes are:

Mode Meaning
compressLast last (close) value within the interval — the default
compressOpen first (open) value within the interval
compressHigh highest value within the interval
compressLow lowest value within the interval
compressVolume sum of the values within the interval

Compression mode and expansion mode are independent decisions and both must be deliberate. A useful habit: say out loud what the value means before you write the call. “The highest high of each completed week, visible from the week’s last bar” translates directly into compressHigh and expandLast, and the sentence is also the comment the line deserves.

Note that a compressed array is still compressed. TimeFrameCompress() output has the same leading-Null layout as anything produced inside a TimeFrameSet() block, and Warning 509 covers it too.

Argument settles nothing here. Three procedures do, and you should be able to run all three. Two of them are in the lab formula below; the third needs only the Analysis window.

Ask on which bar of each period a new higher-timeframe value first appears. If a completed week’s figure appears on the week’s first bar, it appeared before the week finished. This costs one comparison against Ref( value, -1 ) and it catches the mode error immediately.

For a value that claims to summarise a period, compare it against what the market has actually printed so far within that period. A weekly high can never legitimately exceed the highest high seen so far this week. If the number on screen is larger, it cannot have been computed from data that existed at that moment. This is a one-sided test: it fires when the value is impossible and stays quiet when the future value happens to match something already known, so passing it is necessary rather than sufficient.

This one is decisive and it works on any formula, not only on period aggregates. A causal formula’s output for a given date depends only on data up to that date. So run the analysis twice, over ranges with different end dates, and compare the rows the two runs share.

  1. Set the Analysis Range to From-To dates, ending on the Friday of some week in the middle of your history. Run and keep the results.
  2. Change only the To date, moving it back to the Monday of that same week. Run again.
  3. Compare the Monday row in both runs.

If the two runs disagree about Monday, the formula’s Monday output depends on data that arrived after Monday. That is look-ahead, demonstrated rather than argued. Keep SetBarsRequired( sbrAll, sbrAll ) in the formula while doing this, so the only difference between the runs is the end date and not QuickAFL’s estimate of how many bars to load.

Put the three expansion modes and the two relevant compression modes on one table against real dates, and add a column that decides — per bar, from the data — whether the expandFirst value could have been known.

Complete runnable AFL

compress-expand-lab.afl
// ===========================================================================
// Compress / expand laboratory
// Puts the three documented expansion modes and two compression modes side by
// side on one daily table, and adds a column that DETECTS look-ahead rather
// than asking you to take anybody's word for it.
//
// The official caveat this laboratory exists to demonstrate:
// "expandFirst used on price different than open may look into the future.
// For example if you create weekly HIGH series, expanding it to daily
// interval using expandFirst will enable you to know on MONDAY what was the
// high for entire week."
//
// HOW TO RUN
// Analysis window -> Apply to: Current symbol. Range: All quotations.
// Analysis -> Settings -> Periodicity: Daily.
// Sort by date ascending and read a handful of consecutive weeks.
//
// ASSUMPTIONS, STATED SO THEY CAN BE CHECKED
// - Daily base data. Weekly bars are built by AmiBroker's own compression,
// which is governed by the database settings, not by this formula.
// - The causal-bound test below is valid for a CURRENT-period aggregate
// only. It asks a single question: on this bar, is the number on screen
// larger than the largest high that has actually printed so far in this
// week? If it is, the number cannot have been computed from past data.
// ===========================================================================
SetBarsRequired( sbrAll, sbrAll );
// ---------------------------------------------------------------------------
// The weekly high, compressed correctly, then expanded three different ways.
// compressHigh is required here: the default compressLast would give the LAST
// daily high of the week, which is a different number entirely.
// ---------------------------------------------------------------------------
WeeklyHighRaw = TimeFrameCompress( High, inWeekly, compressHigh );
HighLast = TimeFrameExpand( WeeklyHighRaw, inWeekly, expandLast );
HighFirst = TimeFrameExpand( WeeklyHighRaw, inWeekly, expandFirst );
HighPoint = TimeFrameExpand( WeeklyHighRaw, inWeekly, expandPoint );
// The same series compressed with the WRONG mode, for comparison. This is the
// silent bug: no warning, no error, just a plausible column of wrong numbers.
WrongCompression = TimeFrameExpand( TimeFrameCompress( High, inWeekly ), inWeekly );
// ---------------------------------------------------------------------------
// How far into the current week each bar is, computed without a loop.
// The bar number of the week's FIRST bar is genuinely knowable on that first
// bar, so compressOpen with expandFirst is legitimate here. This is the one
// shape of value for which expandFirst carries no future information.
// ---------------------------------------------------------------------------
BarNumber = Cum( 1 );
FirstBarOfWeek = TimeFrameExpand( TimeFrameCompress( BarNumber, inWeekly, compressOpen ),
inWeekly, expandFirst );
BarsIntoWeek = BarNumber - FirstBarOfWeek;
// The highest high that has actually printed so far in this week. Anything
// larger than this is information the market has not produced yet.
HighKnownSoFar = HHV( High, BarsIntoWeek + 1 );
// A small relative tolerance keeps floating-point noise out of the comparison.
CausalTolerance = 1.000001;
LeakFirst = HighFirst > HighKnownSoFar * CausalTolerance;
// On which bar does each expansion first show a NEW weekly value? The answer
// is the whole argument in one column: a completed week's high that appears on
// the week's first bar appeared before the week had finished.
NewValueFirst = HighFirst != Ref( HighFirst, -1 );
NewValueLast = HighLast != Ref( HighLast, -1 );
DayName = WriteIf( DayOfWeek() == 1, "Mon",
WriteIf( DayOfWeek() == 2, "Tue",
WriteIf( DayOfWeek() == 3, "Wed",
WriteIf( DayOfWeek() == 4, "Thu",
WriteIf( DayOfWeek() == 5, "Fri", "other" ) ) ) ) );
Filter = 1;
SetOption( "NoDefaultColumns", True );
AddColumn( DateTime(), "Date", formatDateTimeISO, colorDefault, colorDefault, 110 );
AddTextColumn( DayName, "Day", 1.0, colorDefault, colorDefault, 50 );
AddColumn( BarsIntoWeek, "Bar of week", 1.0 );
AddColumn( High, "Daily high", 1.2 );
AddColumn( HighKnownSoFar, "Highest high so far this week", 1.2 );
AddColumn( HighLast, "Weekly high expandLast", 1.2 );
AddColumn( HighFirst, "Weekly high expandFirst", 1.2 );
AddColumn( HighPoint, "Weekly high expandPoint", 1.2 );
AddColumn( WrongCompression, "compressLast on a High series", 1.2 );
// Reported as words, not as colour, so the verdict survives being printed,
// pasted into a spreadsheet or read by a screen reader.
AddTextColumn( WriteIf( LeakFirst, "LEAK", "ok" ),
"expandFirst causal?", 1.0, colorDefault, colorDefault, 100 );
AddTextColumn( WriteIf( NewValueFirst, "new value here", "-" ),
"expandFirst changes on", 1.0, colorDefault, colorDefault, 120 );
AddTextColumn( WriteIf( NewValueLast, "new value here", "-" ),
"expandLast changes on", 1.0, colorDefault, colorDefault, 120 );

Download compress-expand-lab.afl95 lines

The first block compresses the daily High to weekly with compressHigh and expands the same compressed array three times, once per mode, so the three columns differ only in the mode argument. The second block repeats the compression with the default compressLast to show what the wrong aggregation looks like next to the right one.

The third block builds the causal bound without a loop. Cum( 1 ) numbers the bars of the array; compressing that with compressOpen and expanding with expandFirst yields, on every bar, the bar number on which the current week began — a quantity that genuinely is known on the week’s first bar, which is why expandFirst is legitimate here and nowhere else in the formula. Subtracting gives how many bars into the week we are, and HHV( High, BarsIntoWeek + 1 ) is then the highest high printed so far this week.

The verdict column compares the expandFirst value against that bound with a small relative tolerance, so that floating-point noise does not produce spurious verdicts. The two arrival columns implement Procedure 1 for both modes.

  • Cum( array ) — a running total. Cum( 1 ) gives 1, 2, 3 … across the array, which is a bar counter that does not depend on QuickAFL’s view of the underlying database the way BarIndex() can.
  • HHV( array, periods ) — the highest value over the last periods bars. periods may itself be an array, which is what lets the lookback grow as the week progresses.
  • Ref( array, -1 ) — the previous bar’s value, used here only to detect a change.

Change compressHigh to compressLow and the HHV() call to LLV(), and invert the comparison in the leak test. The behaviour should mirror exactly: expandFirst on the weekly low is impossible on the days before the week’s low prints. If your edited version reports no leaks at all, you have made an error in the inversion rather than discovered a safe mode.

  • Every row says ok. Check that the mode column really is expandFirst and not expandLast, and that the Periodicity is Daily. Weekly-on-weekly compression has nothing to detect.
  • The bar-of-week column counts past 4. Perfectly normal on some exchanges and in weeks with a Saturday session; it also happens if the database’s first-day-of-week setting is not Monday. It is a reminder that “five daily bars per weekly bar” is an assumption about a database, not a fact about markets.
  • The first rows are blank. That is the leading-Null region. Nothing is wrong.

Add two more columns: TimeFrameGetPrice( "H", inWeekly ) with all defaults, and TimeFrameGetPrice( "H", inWeekly, -1 ). Run the causal-bound test against the first. It should behave identically to the expandFirst column, because the documented equivalence says it is the same computation — which turns a sentence in the reference manual into something you have verified on your own data.

You now know which mode does what, in the documentation’s own words, and you can explain why the mode named “first” is the dangerous one rather than the cautious one. More usefully, you have three ways to settle the question on a formula you did not write, including one — the truncation test — that needs no understanding of the formula at all.

Everything from here is application. The next two pages build a chart indicator and a scanner, and both of them end by proving they are clean rather than asserting it.

Check your understanding

Question 1. Which expansion mode is documented as possibly looking into the future?
Show the answer and why

Answer: expandFirst

The reference page states that expandFirst used on a price other than the open may look into the future, giving the weekly high known on Monday as its example. It is the mode that backdates a completed period aggregate to the period’s first bar.

Question 2. After TimeFrameSet( inWeekly ), which expansion is correct?
TimeFrameSet( inWeekly );
w = MA( Close, 14 );
TimeFrameRestore();
Show the answer and why

Answer: TimeFrameExpand( w, inWeekly )

The interval argument names the frame the data came from. TimeFrameRestore returns only the seven built-in price arrays, so w is still compressed and must be expanded with inWeekly.

Question 3. Why is compressOpen paired with expandFirst in the official example?
Show the answer and why

Answer: Because a period’s open is fully determined at the period’s first bar, so backdating it to that bar reveals nothing unknown

expandFirst is safe exactly when the compressed value is already knowable on the period’s first bar. The open qualifies; the high, low and close do not.

Question 4. Which of these calls are unsafe as written in a trading rule? Select all that apply.
Show the answer and why

Answer: TimeFrameGetPrice( "H", inWeekly ), TimeFrameExpand( wHigh, inWeekly, expandFirst )

TimeFrameGetPrice defaults to shift 0 with expandFirst, which the docs warn can be forward-looking. TimeFrameExpand defaults to expandLast, so the third call is fine; the fourth overrides that default with the unsafe mode.

Question 5. Your formula produces a different value for 3 June depending on whether the analysis range ends on 3 June or on 30 June. What has that shown?
Show the answer and why

Answer: That the output for 3 June depends on data that arrived after 3 June

With SetBarsRequired( sbrAll, sbrAll ) removing QuickAFL as a confound, the only difference between the runs is the presence of later data. If the earlier date’s value moves, later data is reaching it.

Question 6. TimeFrameCompress( Volume, inWeekly ) with default arguments returns the total volume traded during the week.
Show the answer and why

Answer: False

False. The default is compressLast, which returns the last daily volume of the week. Summing requires compressVolume, the only mode documented as adding values rather than selecting one.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AFL Function Reference — TimeFrameExpandamibroker.com/guide/afl/timeframeexpand.html2026-08-31
  2. 02AFL Function Reference — TimeFrameCompressamibroker.com/guide/afl/timeframecompress.html2026-08-31
  3. 03AFL Function Reference — TimeFrameGetPriceamibroker.com/guide/afl/timeframegetprice.html2026-08-31
  4. 04AmiBroker User's Guide — Multiple Time Frame Support in AFL§ Available modes and caveatamibroker.com/guide/h_timeframe.html2026-08-31
  5. 05AmiBroker User's Guide — Warning 509amibroker.com/guide/errors/509.html2026-08-31
  6. 06AmiBroker User's Guide — Common coding mistakesamibroker.com/guide/a_mistakes.html2026-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.