Skip to content

Component 4: Multi-Timeframe Chart

One price pane carrying three horizons at once:

  • Context — a higher timeframe, drawn as a shaded range plus a trend state, telling you what the larger picture was doing.
  • Trading — the chart’s own interval, where the setup is judged.
  • Entry — the trigger level, drawn so you can see how close price is to it.

And the requirement that makes this component different from every multi-timeframe chart you have seen: the absence of look-ahead is provable, by measurement, not by assertion.

TimeFrameGetPrice’s full signature is:

Pseudocode — not valid AFL

TimeFrameGetPrice( pricefield, interval, shift = 0, mode = expandFirst )

and the official page states the consequence directly:

if shift = 0 compressed data may look into the future ( weekly high can be known on monday )

Every higher-timeframe value on this chart is read with an explicit shift of −1 — the previous completed higher-timeframe bar, which had finished before the current chart bar opened.

Complete runnable AFL

mtf-chart.afl
// mtf-chart.afl
// Capstone Component 4 - Multi-Timeframe Chart
//
// GOAL
// One price pane carrying three horizons at once:
// context a higher timeframe, drawn as a band and a trend state;
// trading the chart's own interval, where the setup is judged;
// entry the trigger level, drawn so you can see how close price is.
//
// And - the part that matters - built so that the absence of look-ahead is
// PROVABLE rather than asserted. See mtf-lookahead-proof.afl, which measures
// it instead of trusting this comment.
//
// THE RULE THAT MAKES IT SAFE
// Every higher-timeframe value is read with an EXPLICIT NEGATIVE SHIFT.
// TimeFrameGetPrice's signature is
// TimeFrameGetPrice( pricefield, interval, shift = 0, mode = expandFirst )
// and the official page states that with shift = 0 the compressed data may
// look into the future - the weekly high can be known on Monday. A shift of
// -1 reads the PREVIOUS COMPLETED higher-timeframe bar, which had finished
// before the current bar opened.
//
// The same reasoning applies to TimeFrameCompress/TimeFrameExpand: expandLast
// is the default and is the causally safe mode, because it publishes the
// compressed value on the LAST bar of the period. expandFirst publishes it on
// the first bar, which is what produces the leak.
//
// WHERE TO PLOT IT
// On the price pane, at your trading interval. The context interval must be
// HIGHER than the chart interval; the title says so if it is not.
//
// ASSUMPTIONS
// Interval any, as long as the context interval is higher.
// Warm-up the context averages need their full look-back IN CONTEXT
// BARS, which is many more chart bars. The title reports it.
// Not modelled costs, fills, size. This is a picture, not a system.
_SECTION_BEGIN( "Capstone multi-timeframe chart" );
SetChartOptions( 2, chartWrapTitle );
ContextChoice = ParamList( "Context interval", "Weekly|Monthly|Daily|Hourly", 0 );
ContextTrend = Param( "Context trend average (context bars)", 20, 2, 200, 1 );
TrendPeriod = Param( "Trading trend average (chart bars)", 50, 2, 400, 1 );
TriggerPeriod = Param( "Entry trigger look-back (chart bars)", 20, 2, 250, 1 );
AtrPeriod = Param( "ATR period", 14, 1, 100, 1 );
if( ContextChoice == "Weekly" ) ContextInterval = inWeekly;
else if( ContextChoice == "Monthly" ) ContextInterval = inMonthly;
else if( ContextChoice == "Hourly" ) ContextInterval = inHourly;
else ContextInterval = inDaily;
ChartInterval = Interval();
ContextIsHigher = ContextInterval > ChartInterval;
// ------------------------------------------------- 1. context, shifted back
// -1 on every call. Nothing here reads a higher-timeframe bar that had not
// finished when the current chart bar opened.
PriorContextHigh = TimeFrameGetPrice( "H", ContextInterval, -1 );
PriorContextLow = TimeFrameGetPrice( "L", ContextInterval, -1 );
PriorContextClose = TimeFrameGetPrice( "C", ContextInterval, -1 );
// A context moving average needs the compressed series. TimeFrameSet switches
// the price arrays to the compressed interval; TimeFrameRestore switches back;
// TimeFrameExpand then stretches the result back onto chart bars. expandLast is
// the default and the safe mode - the value appears on the LAST bar of each
// context period, so no chart bar sees a context value before it existed.
TimeFrameSet( ContextInterval );
ContextMa = MA( Close, ContextTrend );
TimeFrameRestore();
ContextMaOnChart = TimeFrameExpand( Ref( ContextMa, -1 ), ContextInterval, expandLast );
ContextUp = PriorContextClose > ContextMaOnChart;
ContextDown = PriorContextClose < ContextMaOnChart;
// ------------------------------------------------------- 2. trading state
TradingMa = MA( Close, TrendPeriod );
Setup = Close > TradingMa;
// --------------------------------------------------------- 3. entry level
// Shifted by one chart bar for the same reason: the current bar must not be
// part of the range it is trying to break.
TriggerLevel = Ref( HHV( High, TriggerPeriod ), -1 );
Distance = SafeDivide( TriggerLevel - Close, ATR( AtrPeriod ), Null );
// ------------------------------------------------------------- 4. drawing
// The context band is drawn as a shaded high/low range, so the higher
// timeframe is visible as a region rather than as another line to confuse with
// the trading average.
PlotOHLC( PriorContextHigh, PriorContextHigh, PriorContextLow, PriorContextLow,
"Previous " + ContextChoice + " range", colorLightGrey,
styleCloud | styleNoLabel );
Plot( Close, "Close", colorDefault, styleCandle );
Plot( ContextMaOnChart, ContextChoice + " average (completed bars only)",
colorBlueGrey, styleLine | styleThick | styleStaircase );
Plot( TradingMa, "Trading average", colorBlue, styleLine );
Plot( TriggerLevel, "Entry trigger level", colorOrange,
styleLine | styleStaircase );
// Context state as a ribbon along the foot of the pane. Colour never carries
// the meaning alone: the title states the same thing in words.
RibbonTint = IIf( IsNull( ContextMaOnChart ), colorLightGrey,
IIf( ContextUp, colorPaleGreen,
IIf( ContextDown, colorRose, colorLightYellow ) ) );
Plot( 1, "Context state", RibbonTint, styleArea | styleOwnScale | styleNoLabel,
0, 100 );
// ------------------------------------------------------------- 5. reading
ContextWord = WriteIf( IsNull( ContextMaOnChart ), "not enough context history",
WriteIf( ContextUp, "context up",
WriteIf( ContextDown, "context down",
"context neutral" ) ) );
SetupWord = WriteIf( Setup, "above trading average", "below trading average" );
WarningWord = WriteIf( ContextIsHigher, "",
"\nWARNING: the context interval is not higher than the "
+ "chart interval. Change one of them." );
_N( Title = StrFormat(
"%s chart %s context %s\n"
+ "Context: %s (previous completed %s bar: high %g low %g close %g)\n"
+ "Trading: %s Entry trigger %g, which is %g ATR above the close\n"
+ "Every higher-timeframe value on this chart is read with an explicit "
+ "shift of -1. Nothing here uses a period that had not finished.%s",
Name(), Interval( 2 ), ContextChoice,
ContextWord, ContextChoice,
SelectedValue( PriorContextHigh ), SelectedValue( PriorContextLow ),
SelectedValue( PriorContextClose ),
SetupWord, SelectedValue( TriggerLevel ), SelectedValue( Distance ),
WarningWord ) );
_SECTION_END();

Download mtf-chart.afl135 lines

Fragment — not a complete formula

PriorContextHigh = TimeFrameGetPrice( "H", ContextInterval, -1 );
PriorContextLow = TimeFrameGetPrice( "L", ContextInterval, -1 );
PriorContextClose = TimeFrameGetPrice( "C", ContextInterval, -1 );

Three calls, one field each, all shifted. Drawn as a cloud with PlotOHLC and styleCloud, which the Plot() reference documents as the area between the high and low arrays and specifically as being for use with PlotOHLC.

Drawing the context as a region rather than another line matters more than it sounds: a higher-timeframe moving average drawn as a line is easily confused with the trading average, and the two mean completely different things.

A moving average of the higher timeframe needs the compressed series, which is the documented three-step idiom:

Fragment — not a complete formula

TimeFrameSet( ContextInterval );
ContextMa = MA( Close, ContextTrend );
TimeFrameRestore();
ContextMaOnChart = TimeFrameExpand( Ref( ContextMa, -1 ), ContextInterval, expandLast );

TimeFrameSet replaces the price arrays with compressed bars, so MA( Close, 20 ) inside the block is a 20-context-bar average. TimeFrameRestore puts the chart’s own arrays back. And TimeFrameExpand is required to bring the result back onto chart bars — the User’s Guide’s own Common Coding Mistakes page lists forgetting it as one of the standard errors, because comparing a compressed array against an uncompressed one silently compares different time scales.

Note the belt and braces: Ref( ContextMa, -1 ) shifts within the compressed timeframe, and expandLast publishes on the last bar of each period. Either alone would be defensible; both together mean the chart is using an average of context bars that had all completed.

Fragment — not a complete formula

ChartInterval = Interval();
ContextIsHigher = ContextInterval > ChartInterval;

Interval() returns the bar interval in seconds, and the higher-timeframe constants are larger numbers — inDaily is 86,400. So a plain comparison tells you whether the configuration makes sense, and the title warns when it does not.

This matters because a “context” that is lower than the chart interval is not a warning AmiBroker will give you. You will simply get a picture that means nothing.

This is the deliverable that distinguishes this component. Do not skip it.

Complete runnable AFL

mtf-lookahead-proof.afl
// mtf-lookahead-proof.afl
// Capstone Component 4 - the look-ahead proof
//
// GOAL
// Prove, by measurement rather than by assertion, that the multi-timeframe
// chart's context values contain no information from the future. This is the
// evidence you attach to Component 4 of the capstone report.
//
// THE TEST
// For each chart bar it computes the SAME higher-timeframe high two ways:
//
// Leaky TimeFrameGetPrice( "H", interval ) - shift 0, expandFirst
// Safe TimeFrameGetPrice( "H", interval, -1 ) - previous COMPLETED bar
//
// Then it asks a question that has a definite right answer:
//
// On how many chart bars did the value EXCEED the highest high seen so far
// in the data up to and including that bar?
//
// A value that is legitimately derived from completed history can never do
// that. A value that has been given the current period's aggregate can, and
// does, whenever the period's high has not yet printed.
//
// The safe column must report ZERO. If it does not, something else in your
// configuration is leaking and you have found it before it reached a backtest.
//
// HOW TO RUN
// Analysis -> Apply to: your universe. Periodicity: your trading interval.
// Range: All quotations. Press EXPLORE. One row per symbol.
//
// ASSUMPTIONS
// Interval the chart/Analysis interval must be LOWER than the context
// interval, or both columns are meaningless.
// Warm-up bars before the first completed context period are excluded.
// Scope this tests ONE specific failure mode - a higher-timeframe value
// arriving early. It is not a general look-ahead detector.
SetBarsRequired( sbrAll, sbrAll );
ContextChoice = ParamList( "Context interval", "Weekly|Monthly|Daily", 0 );
if( ContextChoice == "Monthly" ) ContextInterval = inMonthly;
else if( ContextChoice == "Daily" ) ContextInterval = inDaily;
else ContextInterval = inWeekly;
// -------------------------------------------------------- the two versions
LeakyHigh = TimeFrameGetPrice( "H", ContextInterval ); // defaults
SafeHigh = TimeFrameGetPrice( "H", ContextInterval, -1 ); // completed only
// ------------------------------------------------ the yardstick: known past
// The highest high in the data up to AND INCLUDING the current bar. Nothing
// derived only from completed history can exceed this.
KnownHigh = Highest( High );
Measurable = Status( "barinrange" )
AND NOT IsNull( LeakyHigh )
AND NOT IsNull( SafeHigh )
AND NOT IsNull( KnownHigh );
// A small tolerance, because the comparison is between floating point numbers
// that took different routes to the same quantity.
Tolerance = 1e-6 * Max( KnownHigh, 1 );
LeakyAhead = Measurable AND LeakyHigh > KnownHigh + Tolerance;
SafeAhead = Measurable AND SafeHigh > KnownHigh + Tolerance;
BarsTested = Cum( Measurable );
LeakyCount = Cum( LeakyAhead );
SafeCount = Cum( SafeAhead );
// How far ahead the leaky version was, when it was ahead. This is the size of
// the advantage a backtest would have been handed.
LeakyExcess = IIf( LeakyAhead,
100 * SafeDivide( LeakyHigh - KnownHigh, KnownHigh, 0 ),
0 );
LeakySum = Cum( LeakyExcess );
LeakyWorst = Highest( LeakyExcess );
// ---------------------------------------------------------------- output
Filter = Status( "lastbarinrange" ) AND BarsTested > 0;
AddColumn( BarsTested, "Bars tested", 1.0 );
AddColumn( LeakyCount, "Leaky version ahead of known history", 1.0 );
AddColumn( 100 * SafeDivide( LeakyCount, BarsTested, Null ), "Leaky %", 1.1 );
AddColumn( SafeCount, "SAFE version ahead (must be 0)", 1.0 );
AddColumn( SafeDivide( LeakySum, LeakyCount, Null ), "Avg leak %", 1.3 );
AddColumn( LeakyWorst, "Worst leak %", 1.3 );
SetSortColumns( -4 );
// COUNT and AVERAGE. Read the "SAFE version ahead" column first: any non-zero
// value anywhere is a finding, and the whole point of the exploration.
AddSummaryRows( 2 | 8 | 16, 1.2 );

Download mtf-lookahead-proof.afl93 lines

For each chart bar it computes the same higher-timeframe high two ways — once with the leaky defaults and once with the safe shift — and then asks a question with a definite right answer:

On how many chart bars did the value exceed the highest high seen so far in the data, up to and including that bar?

A value legitimately derived from completed history can never do that. A value that has been handed the current period’s aggregate can, and does, whenever the period’s high has not yet printed.

Fragment — not a complete formula

KnownHigh = Highest( High ); // the highest high up to and including this bar
LeakyAhead = Measurable AND LeakyHigh > KnownHigh + Tolerance;
SafeAhead = Measurable AND SafeHigh > KnownHigh + Tolerance;

The tolerance is there because the two routes to the same quantity are different floating-point computations; comparing them with exact equality would produce spurious hits.

Run the proof exploration. Safe column zero, every symbol. This is the deliverable.

Watch the band boundaries. Scroll to a week boundary and confirm the band jumps there and nowhere else. Then confirm the value it jumps to matches the previous week’s actual high and low, read from a weekly chart of the same symbol.

Deliberately break it. Change one call to TimeFrameGetPrice( "H", ContextInterval ) — no shift — and re-run the proof. The safe column should now be non-zero for that series. Change it back. Seeing the test fail is what makes you trust it when it passes.

Check the expand mode. Change expandLast to expandFirst in the TimeFrameExpand call and watch the context average line. It should now step at the start of each period instead of the end. The proof will not catch this one — it only tests the TimeFrameGetPrice values — which is a useful demonstration of the previous callout’s point about narrow tests.

Check the interval warning. Set the context to Daily on a daily chart. The title must show the warning.

The band moves during a period. A shift is missing on one of the three TimeFrameGetPrice calls.

The context average is offset by one whole period. You applied both the Ref( ..., -1 ) and expandLast when you meant only one, or the context bars are not what you think. Compare against a chart of the context interval directly.

“Error 6” or a nonsensical picture from the TimeFrameSet block. Something inside the block assumes the chart’s own interval. Remember that everything between TimeFrameSet and TimeFrameRestore operates on compressed data, including built-in indicators.

The context values are Null for most of the chart. The context average needs its full lookback in context bars, which is many more chart bars — a 20-week average needs about 100 trading days before it exists at all. The title reports this; read it before assuming a bug.

The chart looks right and the proof fails. Trust the proof. The picture can look entirely plausible while carrying a leak, which is precisely why this component requires a measurement.

Everything is Null after changing the context interval. The new interval may not exist in your database’s base interval hierarchy. Compression works upward from the base interval, not downward.

  1. Add a third timeframe. Monthly context, weekly setup, daily entry. Each one shifted. Then re-run the proof against the new context series and confirm it is still zero.

  2. Add a “context agreement” note to the title stating whether the context state and the trading state agree. As with the Part 10 panel, resist turning it into a signal: the value is naming a state, and disagreement is the interesting case.

  3. Extend the proof to test the TimeFrameExpand path as well as the TimeFrameGetPrice path, using the same known-history yardstick. This closes the gap the validation step above exposed, and writing it is a genuine exercise in test design.

  4. Measure the cost of being right. Build a version with the leaky defaults, run both through Component 6’s backtest, and record the difference. That number is what look-ahead is worth in your universe, and it belongs in the report.

  • The three intervals and why you chose them.
  • The proof exploration’s output, with the safe column visible and zero.
  • A note stating exactly what the proof does and does not cover.
  • If you did extension 4: the size of the difference the leak would have made.

Check your understanding

Question 1. Which expand mode is the causally safe default, and why?
Show the answer and why

Answer: expandLast, because it publishes the compressed value on the LAST bar of each period — when it actually exists

expandFirst puts the whole period's aggregate on the period's first bar, which is what produces the weekly-high-known-on-Monday leak the documentation warns about. expandPoint writes only on the last bar and leaves the rest Null, which is useful for markers and useless for a per-bar condition.

Question 2. The proof exploration asks whether a value exceeded the highest high seen so far. Why does that settle the question?
KnownHigh  = Highest( High );
LeakyAhead = LeakyHigh > KnownHigh + Tolerance;
Show the answer and why

Answer: Because a value legitimately derived from completed history cannot exceed the highest price that has actually printed — but one handed the current period's aggregate can, whenever that period's high has not yet occurred

It is a bound, not a heuristic. Anything above the known-so-far high must have come from data that had not printed yet. The tolerance exists only because the two routes to the quantity are different floating-point computations.

Question 3. Why must TimeFrameExpand() be called after TimeFrameRestore()?
Show the answer and why

Answer: Because a compressed array and an uncompressed one are on different time scales — the User's Guide lists comparing them directly as one of the standard AFL mistakes

The Common Coding Mistakes page gives exactly this example: Cross( Close, MA14_Weekly ) without expanding is wrong, because the two arrays use different time scales. It also notes that the interval passed to TimeFrameExpand must be the one the variable was calculated in.

Question 4. The chart looks plausible but the proof reports a non-zero safe column. What should you conclude?
Show the answer and why

Answer: Something in the configuration is leaking. A chart can look entirely plausible while carrying a leak, which is why this component requires a measurement rather than an inspection

Trust the measurement. Leaked values look normal — that is what makes look-ahead dangerous. A tolerance that is too small would produce hits on the leaky column too, not selectively on the safe one.

Question 5. Which limitations of the proof should be stated when you attach it to the report? Select all that apply.
Show the answer and why

Answer: It tests one specific failure mode: a higher-timeframe value arriving before its period finished, It does not detect a positive Ref() shift elsewhere in the formula, It does not test the TimeFrameExpand path unless you extend it

A narrow test that is conclusive beats a broad one that is not — but only if its scope is stated. Claiming it proves the absence of all bias would be exactly the kind of overreach the rest of the course is about.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AFL Function Reference — TimeFrameGetPrice§ if shift = 0 compressed data may look into the futureamibroker.com/guide/afl/timeframegetprice.html2026-08-31
  2. 02AFL Function Reference — TimeFrameExpandamibroker.com/guide/afl/timeframeexpand.html2026-08-31
  3. 03AmiBroker User's Guide — Multiple time frame supportamibroker.com/guide/h_timeframe.html2026-08-31
  4. 04AmiBroker User's Guide — Common Coding Mistakes in AFL§ TimeFrameExpand() is required to match data with the original time frameamibroker.com/guide/a_mistakes.html2026-08-31
  5. 05AFL Function Reference — Intervalamibroker.com/guide/afl/interval.html2026-08-31
  6. 06AFL Function Reference — Plot§ styleCloudamibroker.com/guide/afl/plot.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.