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.
The function
Section titled “The function”Fragment — not a complete formula
Expanded = TimeFrameExpand( CompressedArray, inWeekly ); // mode defaults to expandLastExpanded = 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 three modes, exactly as documented
Section titled “The three modes, exactly as documented”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
| Bar | Mon | Tue | Wed | Thu | Fri | Mon | Tue | Wed | Thu | Fri |
|---|---|---|---|---|---|---|---|---|---|---|
High (daily) | 10.2 | 10.5 | 10.7 | 11.0 | 10.8 | 11.1 | 11.4 | 12.0 | 11.6 | 11.7 |
Highest high so far this week | 10.2 | 10.5 | 10.7 | 11.0 | 11.0 | 11.1 | 11.4 | 12.0 | 12.0 | 12.0 |
expandLastthe default | Null | Null | Null | Null | 11.0 | 11.0 | 11.0 | 11.0 | 11.0 | 12.0 |
expandFirstreads the future | 11.0 | 11.0 | 11.0 | 11.0 | 11.0 | 12.0 | 12.0 | 12.0 | 12.0 | 12.0 |
expandPointdrawing only | Null | Null | Null | Null | 11.0 | Null | Null | Null | Null | 12.0 |
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 one case where expandFirst is correct
Section titled “The one case where expandFirst is correct”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 meansWeeklyCloseSeries = 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 is for drawing
Section titled “expandPoint is for drawing”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().
The two defaults disagree with each other
Section titled “The two defaults disagree with each other”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
- Pattern A — compute in the higher frameTimeFrameSet → compute → TimeFrameRestore → TimeFrameExpand with the default expandLast
- Pattern B — read a completed higher-frame priceTimeFrameGetPrice with a NEGATIVE shift, so the bar you read has finished
The residual same-bar caveat
Section titled “The residual same-bar caveat”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.
Proving that a formula does not leak
Section titled “Proving that a formula does not leak”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.
Procedure 1 — the arrival check
Section titled “Procedure 1 — the arrival check”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.
Procedure 2 — the causal bound
Section titled “Procedure 2 — the causal bound”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.
Procedure 3 — the truncation test
Section titled “Procedure 3 — the truncation test”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.
- 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.
- Change only the To date, moving it back to the Monday of that same week. Run again.
- 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.
The laboratory
Section titled “The laboratory”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 formula
Section titled “Complete formula”Complete runnable 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 );How it works
Section titled “How it works”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.
Key functions
Section titled “Key functions”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 wayBarIndex()can.HHV( array, periods )— the highest value over the last periods bars.periodsmay 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.
Expected result
Section titled “Expected result”Test it
Section titled “Test it”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.
Common errors
Section titled “Common errors”- Every row says ok. Check that the mode column really is
expandFirstand notexpandLast, 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.
Extension
Section titled “Extension”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.
What changes for you
Section titled “What changes for you”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
Sources for this lesson
6 verified · checked 2026-08-31
- 01AFL Function Reference — TimeFrameExpandamibroker.com/guide/afl/timeframeexpand.html2026-08-31
- 02AFL Function Reference — TimeFrameCompressamibroker.com/guide/afl/timeframecompress.html2026-08-31
- 03AFL Function Reference — TimeFrameGetPriceamibroker.com/guide/afl/timeframegetprice.html2026-08-31
- 04AmiBroker User's Guide — Multiple Time Frame Support in AFL§ Available modes and caveatamibroker.com/guide/h_timeframe.html2026-08-31
- 05AmiBroker User's Guide — Warning 509amibroker.com/guide/errors/509.html2026-08-31
- 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.