Component 4: Multi-Timeframe Chart
Design
Section titled “Design”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.
The rule that makes it safe
Section titled “The rule that makes it safe”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.
The complete formula
Section titled “The complete formula”Complete runnable 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 stateTradingMa = 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. readingContextWord = 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();How it works
Section titled “How it works”The context range
Section titled “The context range”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.
The context average
Section titled “The context average”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.
The interval check
Section titled “The interval check”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.
The look-ahead proof
Section titled “The look-ahead proof”This is the deliverable that distinguishes this component. Do not skip it.
Complete runnable 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 versionsLeakyHigh = TimeFrameGetPrice( "H", ContextInterval ); // defaultsSafeHigh = 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 );
// ---------------------------------------------------------------- outputFilter = 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 );What it measures
Section titled “What it measures”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 barLeakyAhead = 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.
Expected result
Section titled “Expected result”Validation
Section titled “Validation”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.
Common errors
Section titled “Common errors”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.
Extensions
Section titled “Extensions”-
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.
-
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.
-
Extend the proof to test the
TimeFrameExpandpath as well as theTimeFrameGetPricepath, 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. -
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.
What to record for the report
Section titled “What to record for 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
Sources for this lesson
6 verified · checked 2026-08-31
- 01AFL Function Reference — TimeFrameGetPrice§ if shift = 0 compressed data may look into the futureamibroker.com/guide/afl/timeframegetprice.html2026-08-31
- 02AFL Function Reference — TimeFrameExpandamibroker.com/guide/afl/timeframeexpand.html2026-08-31
- 03AmiBroker User's Guide — Multiple time frame supportamibroker.com/guide/h_timeframe.html2026-08-31
- 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
- 05AFL Function Reference — Intervalamibroker.com/guide/afl/interval.html2026-08-31
- 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.