Project: Multi-Timeframe Trend Indicator
You are going to build a daily chart that carries three pieces of weekly information, and then spend as long proving it honest as you spent writing it. That ratio is not an accident. A multi-timeframe indicator that is subtly misaligned looks better than a correct one — the weekly line turns earlier, the bands sit more helpfully — which is precisely why it needs an audit rather than an opinion.
What you are building
Section titled “What you are building”Three weekly overlays on daily candles:
- A weekly exponential moving average of weekly closes, drawn as a staircase so that its flat sections make the weekly cadence visible.
- The previous completed week’s high and low, as a step band. This is the weekly range the market has just finished trading through, and it is the most concrete piece of weekly context there is.
- A weekly regime label — up, down or mixed — shown both as a colour ribbon and, more importantly, as words in the chart title.
The design contract
Section titled “The design contract”Before any code, write down what the formula must obey. This is the part that makes the audit possible later, because an audit needs something to audit against.
- Every value that leaves a
TimeFrameSet()block is expanded, with the interval it came from, before it is plotted or compared. - Every weekly value used for a decision is causal: it is either expanded with the
default
expandLast, or read withTimeFrameGetPrice()using a negative shift. - No weekly value describing the current, unfinished week appears anywhere. The band shows last week, not this week.
- The output does not change with the analysis range or the chart zoom. QuickAFL is disabled in the formula, because the guide names high interval ratios as a case where its estimate can change results.
- Meaning is never carried by colour alone. The ribbon has a text equivalent in the title, so the chart survives being printed in grey or read by someone who does not distinguish the two colours.
- A deliberate defect is available on demand. The Parameters dialog can switch the formula into the documented look-ahead behaviour, so the audit can be shown to detect something.
That last item deserves a word. It is tempting to leave broken code out of a finished formula. But a check you have never seen fire tells you nothing, and the cheapest way to give a check teeth is to keep a known-bad configuration one click away.
The formula
Section titled “The formula”Complete runnable AFL
// ===========================================================================// Weekly trend context on a daily chart// Draws three pieces of weekly information over daily candles, each one// expanded so that it becomes visible only after the market could actually// have known it://// 1. a weekly exponential moving average of weekly closes;// 2. the previous COMPLETED week's high and low, as a step band;// 3. a weekly regime ribbon, labelled in words in the chart title.//// HOW TO RUN// Formula Editor -> Apply Indicator on a DAILY chart of any liquid symbol.// Right-click the pane -> Parameters to change the weekly average length// and to switch the demonstration mode described below.//// DEMONSTRATION MODE - READ THIS// The Parameters dialog carries a switch labelled "Expansion mode". Leave it// on "Safe". The "Unsafe" setting deliberately expands the weekly values// with expandFirst and reads the CURRENT week's high and low instead of the// previous week's, which is exactly the look-ahead the official// documentation warns about. It exists so that the audit in the companion// Exploration has something to catch: a test that never fails on a formula// you know to be broken is not evidence of anything.//// ASSUMPTIONS, STATED SO THEY CAN BE CHECKED// - Daily base data, so inWeekly is reachable by compression. From weekly// data you could not get daily bars: compression only goes upwards.// - Weekly bars are built by AmiBroker from the database settings, so the// week's boundaries follow File -> Database Settings, not this formula.// - SetBarsRequired( sbrAll, sbrAll ) turns QuickAFL off. Time-frame// functions with an interval much higher than the base interval are a// documented case where the QuickAFL estimate can change results, and a// context indicator that changes when you zoom is worse than useless.// ===========================================================================
_SECTION_BEGIN( "Weekly trend context" );
SetBarsRequired( sbrAll, sbrAll );
WeeklyAvgPeriod = Param( "Weekly average length (weekly bars)", 10, 3, 52, 1 );ShowBand = ParamToggle( "Previous week high/low band", "Hide|Show", 1 );UnsafeDemo = ParamToggle( "Expansion mode", "Safe (causal)|Unsafe (demonstrates look-ahead)", 0 );
// ---------------------------------------------------------------------------// Step 1 - compute in the weekly frame.// Inside the block, Close is the WEEKLY close, so EMA() is a weekly average of// weekly closes, not a 10-week-long average of daily closes. Those are two// different indicators and the difference is the whole point of the exercise.// ---------------------------------------------------------------------------TimeFrameSet( inWeekly );
WeeklyCloseRaw = Close;WeeklyAvgRaw = EMA( Close, WeeklyAvgPeriod );WeeklySlopeRaw = WeeklyAvgRaw - Ref( WeeklyAvgRaw, -1 ); // one weekly bar of change
TimeFrameRestore();
// ---------------------------------------------------------------------------// Step 2 - expand every value that leaves the block.// TimeFrameRestore() restores only Open, High, Low, Close, Volume, OpenInt and// Avg. The three variables above are still compressed. The interval argument// must be inWeekly - the frame they came FROM - not inDaily.// ---------------------------------------------------------------------------if( UnsafeDemo ){ // Deliberately wrong. Writes each completed week's value onto that week's // FIRST bar, which is Monday's bar for a week that has not happened yet. WeeklyClose = TimeFrameExpand( WeeklyCloseRaw, inWeekly, expandFirst ); WeeklyAvg = TimeFrameExpand( WeeklyAvgRaw, inWeekly, expandFirst ); WeeklySlope = TimeFrameExpand( WeeklySlopeRaw, inWeekly, expandFirst ); BandShift = 0; // the CURRENT week's extremes: not yet knowable}else{ // expandLast is the documented default and the causal choice: a completed // week's value appears on that week's last bar and carries forward into the // following week until the next weekly bar completes. WeeklyClose = TimeFrameExpand( WeeklyCloseRaw, inWeekly, expandLast ); WeeklyAvg = TimeFrameExpand( WeeklyAvgRaw, inWeekly, expandLast ); WeeklySlope = TimeFrameExpand( WeeklySlopeRaw, inWeekly, expandLast ); BandShift = -1; // the PREVIOUS week, complete before this one began}
// A negative shift asks TimeFrameGetPrice for a completed higher-timeframe bar.// The previous week is finished before the current week opens, so showing it// from the current week's first bar reads nothing that has not happened.PrevWeekHigh = TimeFrameGetPrice( "H", inWeekly, BandShift );PrevWeekLow = TimeFrameGetPrice( "L", inWeekly, BandShift );
// ---------------------------------------------------------------------------// Step 3 - classify the weekly regime. Three states, not two: refusing to// call a direction is a legitimate answer and keeps the ribbon honest.// ---------------------------------------------------------------------------WeeklyUp = ( WeeklyClose > WeeklyAvg ) AND ( WeeklySlope > 0 );WeeklyDown = ( WeeklyClose < WeeklyAvg ) AND ( WeeklySlope < 0 );WeeklyMixed = NOT WeeklyUp AND NOT WeeklyDown;
RegimeText = WriteIf( WeeklyUp, "weekly UP", WriteIf( WeeklyDown, "weekly DOWN", "weekly MIXED" ) );RegimeColour = IIf( WeeklyUp, colorSeaGreen, IIf( WeeklyDown, colorDarkRed, colorLightGrey ) );
// ---------------------------------------------------------------------------// Step 4 - draw. Every weekly series on this chart has been expanded, so each// daily bar is being compared with a weekly number that existed on that day.// ---------------------------------------------------------------------------Plot( Close, "Daily close", colorDefault, styleCandle );Plot( WeeklyAvg, "Weekly EMA(" + NumToStr( WeeklyAvgPeriod, 1.0 ) + ") expanded", colorBlue, styleLine | styleThick | styleStaircase );
if( ShowBand ){ Plot( PrevWeekHigh, "Previous week high", colorOrange, styleStaircase | styleNoRescale ); Plot( PrevWeekLow, "Previous week low", colorOrange, styleStaircase | styleNoRescale );}
// The ribbon repeats what the title already says in words, so the chart is// still readable if the colours are not distinguishable to the reader.Plot( 1, "", RegimeColour, styleArea | styleOwnScale | styleNoLabel, 0, 8 );
Title = Name() + " " + DateTimeToStr( SelectedValue( DateTime() ), 1 ) + " daily close " + NumToStr( Close, 1.2 ) + " | " + RegimeText + " weekly EMA " + NumToStr( WeeklyAvg, 1.2 ) + " previous week " + NumToStr( PrevWeekLow, 1.2 ) + " to " + NumToStr( PrevWeekHigh, 1.2 ) + WriteIf( UnsafeDemo, " *** UNSAFE DEMONSTRATION MODE - DO NOT TRADE THIS ***", "" );
_SECTION_END();How it works
Section titled “How it works”The formula runs in four movements.
Compute in the weekly frame. Inside TimeFrameSet( inWeekly ), Close is the weekly
close, so EMA( Close, WeeklyAvgPeriod ) is an average of weekly closes over that many
weeks. The slope is the change over one weekly bar, obtained with Ref( …, -1 ) while still
inside the block — inside a weekly frame, Ref steps by weeks.
Expand on the way out. TimeFrameRestore() returns the seven built-in price arrays and
nothing else, so all three weekly variables are expanded explicitly with inWeekly. The
safe branch takes the default expandLast; the demonstration branch takes expandFirst and
sets the band shift to zero, which is the anti-pattern the reference manual describes.
Read the completed week’s extremes. TimeFrameGetPrice( "H", inWeekly, -1 ) returns the
previous weekly bar’s high. Because the shift is negative, the bar being read is finished
before the current week opens, so the function’s own expandFirst default causes no
difficulty: it makes last week’s figure available from this week’s first bar, which is
exactly when it becomes known.
Classify and draw. Three regime states rather than two, because “the weekly average is
rising and price is above it” and “the weekly average is falling and price is below it”
do not exhaust the possibilities, and a chart that pretends they do will label every
sideways week as a trend. The plots then use styleStaircase for the weekly series, which
makes the flat-then-step shape of a correctly expanded weekly value obvious at a glance.
Key functions and arguments
Section titled “Key functions and arguments”Plot( array, name, color, style, minvalue, maxvalue, … )— the ribbon usesstyleArea | styleOwnScale | styleNoLabelwith an explicit 0-to-8 range, so it occupies a strip at the bottom of the pane instead of rescaling the price axis.ParamToggle( name, values, defaultval = 0 )— the pipe-separated list supplies the two labels. It returns a number, which is why the branches below it are ordinaryifstatements rather thanIIf().NumToStr( array, format )— used in the title. The reference page marks the olderWriteVal()as obsolete and points to this function instead.DateTimeToStr( number, mode )— withSelectedValue( DateTime() )it prints the date of the bar under the cursor. Mode 1 prints the date portion only.
What you should see
Section titled “What you should see”If the band moves in the middle of a week, or the EMA steps on a Monday, something is wrong and the next two sections will find it.
Verifying the alignment visually
Section titled “Verifying the alignment visually”The fastest independent check uses AmiBroker itself as the second opinion, because a weekly chart reaches the same weekly bars by a different route.
- Open a second chart window on the same symbol and set its interval to Weekly.
- Apply a plain
EMA( Close, 10 )to it, matching the indicator’s default length. - Pick a specific week — say the week ending some Friday two months back — and read the weekly chart’s EMA value for that weekly bar.
- Go back to the daily chart and put the cursor on that same Friday. The title’s weekly EMA figure should be the same number.
- Now move the cursor to the following Monday, Tuesday and Wednesday. The figure should not move: those days are still governed by the week that ended on that Friday.
A second visual check needs no other window. Set the band to show, then look at any week where price made a decisive new high. The band’s upper line should sit at the previous week’s high for the whole of the following week, below the new highs being made. If the upper line tracks the current week’s highs as they are made, the band is being read with a shift of zero and the formula is showing you information from later in the week.
Proving there is no leak
Section titled “Proving there is no leak”Visual checks catch gross errors. They do not settle the question, because a value that is one weekly bar early still steps on a Friday and still looks perfectly plausible. For that you need the audit Exploration, which recomputes exactly what the chart draws and adds the columns the three procedures need.
Complete runnable AFL
// ===========================================================================// Multi-timeframe alignment audit// The companion Exploration for the "Weekly trend context" indicator. It// recomputes exactly what the indicator draws, then adds the columns that let// you decide - from evidence rather than from confidence - whether any weekly// number reaches a daily bar before the market could have produced it.//// It supports three checks, described in the lesson:// 1. ARRIVAL CHECK - on which bar of the week does a new weekly value// appear? A completed week's figure arriving on the// week's first bar arrived too early.// 2. CAUSAL BOUND - is the weekly high on screen larger than the highest// high that has actually printed within the window the// value claims to summarise? If so it cannot have been// computed from data that existed.// 3. TRUNCATION TEST - run this Exploration twice over different From-To// ranges and compare the rows the two runs share. A// causal formula returns identical numbers for a given// date whether or not later data is present.//// HOW TO RUN// Analysis window -> Apply to: Current symbol. Range: All quotations (for// checks 1 and 2) or From-To dates (for check 3).// Analysis -> Settings -> Periodicity: Daily.// Set "Expansion mode" in the Parameters dialog to match the setting you are// auditing on the chart.//// ASSUMPTIONS, STATED SO THEY CAN BE CHECKED// - Daily base data; weekly bars come from AmiBroker's own compression.// - The causal bound is a one-sided test. It fires when the number on screen// is impossible, and stays quiet when the future value happens to equal// something already known. Passing it is necessary, not sufficient, which// is why check 3 exists.// - The first few weeks of the database are blank in the weekly columns.// That is the leading-Null region of the compressed array, not a fault.// ===========================================================================
SetBarsRequired( sbrAll, sbrAll );
WeeklyAvgPeriod = Param( "Weekly average length (weekly bars)", 10, 3, 52, 1 );UnsafeDemo = ParamToggle( "Expansion mode", "Safe (causal)|Unsafe (demonstrates look-ahead)", 0 );
// ---------------------------------------------------------------------------// The weekly calculations, identical to the indicator.// ---------------------------------------------------------------------------TimeFrameSet( inWeekly );
WeeklyCloseRaw = Close;WeeklyAvgRaw = EMA( Close, WeeklyAvgPeriod );WeeklySlopeRaw = WeeklyAvgRaw - Ref( WeeklyAvgRaw, -1 );
TimeFrameRestore();
if( UnsafeDemo ){ WeeklyClose = TimeFrameExpand( WeeklyCloseRaw, inWeekly, expandFirst ); WeeklyAvg = TimeFrameExpand( WeeklyAvgRaw, inWeekly, expandFirst ); WeeklySlope = TimeFrameExpand( WeeklySlopeRaw, inWeekly, expandFirst ); BandShift = 0;}else{ WeeklyClose = TimeFrameExpand( WeeklyCloseRaw, inWeekly, expandLast ); WeeklyAvg = TimeFrameExpand( WeeklyAvgRaw, inWeekly, expandLast ); WeeklySlope = TimeFrameExpand( WeeklySlopeRaw, inWeekly, expandLast ); BandShift = -1;}
WeeklyHighBand = TimeFrameGetPrice( "H", inWeekly, BandShift );WeeklyLowBand = TimeFrameGetPrice( "L", inWeekly, BandShift );
WeeklyUp = ( WeeklyClose > WeeklyAvg ) AND ( WeeklySlope > 0 );WeeklyDown = ( WeeklyClose < WeeklyAvg ) AND ( WeeklySlope < 0 );RegimeText = WriteIf( WeeklyUp, "UP", WriteIf( WeeklyDown, "DOWN", "MIXED" ) );
// ---------------------------------------------------------------------------// Bar bookkeeping. Cum(1) numbers the bars of the AFL array itself, which is// what we want here: it does not depend on QuickAFL's view of the database.// compressOpen picks the first value inside each weekly period and compressLast// the final one, both of which are ordinary documented compressions.// ---------------------------------------------------------------------------BarNumber = Cum( 1 );FirstBarRaw = TimeFrameCompress( BarNumber, inWeekly, compressOpen );LastBarRaw = TimeFrameCompress( BarNumber, inWeekly, compressLast );
FirstBarOfWeek = TimeFrameExpand( FirstBarRaw, inWeekly, expandFirst );FirstBarOfPrevWeek = TimeFrameExpand( Ref( FirstBarRaw, -1 ), inWeekly, expandFirst );WeeklyBarEndsAt = TimeFrameExpand( LastBarRaw, inWeekly, expandLast );
BarsIntoWeek = BarNumber - FirstBarOfWeek;BarsSinceWeekClose = BarNumber - WeeklyBarEndsAt;
// Highest high inside the window that each version of the band claims to cover.HighKnownThisWeek = HHV( High, BarsIntoWeek + 1 );HighKnownSincePrev = HHV( High, BarNumber - FirstBarOfPrevWeek + 1 );
if( UnsafeDemo ) CausalBound = HighKnownThisWeek; // the band claims THIS week's extremeselse CausalBound = HighKnownSincePrev; // the band claims LAST week's extremes
CausalTolerance = 1.000001;BandLeak = WeeklyHighBand > CausalBound * CausalTolerance;
// Check 1: where does a new weekly number first appear?NewWeeklyValue = WeeklyAvg != Ref( WeeklyAvg, -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( BarsSinceWeekClose, "Bars since weekly bar closed", 1.0 );AddColumn( Close, "Daily close", 1.2 );AddColumn( High, "Daily high", 1.2 );AddColumn( WeeklyClose, "Weekly close in use", 1.2 );AddColumn( WeeklyAvg, "Weekly EMA in use", 1.2 );AddTextColumn( RegimeText, "Weekly regime", 1.0, colorDefault, colorDefault, 80 );AddColumn( WeeklyLowBand, "Band low", 1.2 );AddColumn( WeeklyHighBand, "Band high", 1.2 );AddColumn( CausalBound, "Highest high the band may legally use", 1.2 );
AddTextColumn( WriteIf( NewWeeklyValue, "new weekly value", "-" ), "Check 1: arrival", 1.0, colorDefault, colorDefault, 120 );AddTextColumn( WriteIf( BandLeak, "IMPOSSIBLE - LOOK-AHEAD", "ok" ), "Check 2: causal bound", 1.0, colorDefault, colorDefault, 170 );Procedure 1 — the arrival check
Section titled “Procedure 1 — the arrival check”Run the Exploration on the current symbol, Range: All quotations, Periodicity: Daily, with “Expansion mode” on Safe. Sort ascending by date and look at the “Check 1: arrival” column.
Every “new weekly value” marker should fall on a row whose “Bars since weekly bar closed” reads 0 — that is, on the last trading bar of a week. Not one of them should fall on a row where “Bar of week” is 0. If a new weekly value arrives on a week’s first bar, the formula is publishing a completed week’s figure before the week has completed.
Procedure 2 — the causal bound
Section titled “Procedure 2 — the causal bound”Same run, “Check 2: causal bound” column. Every row should read ok.
The column compares the band’s high against the largest high the market has actually printed inside the window that the band claims to describe. In Safe mode the band describes the previous week, so the bound is the highest high since that week began — a window that is entirely in the past by the time the value is displayed. A value above that bound would be arithmetically impossible from data that existed.
Note what this test cannot do. If this week’s high happens to be lower than last week’s, an impossible value can hide beneath the bound and the column will read ok anyway. Procedure 2 is a detector of impossibility, not a certificate of causality, which is why it is not the last word.
Procedure 3 — the truncation test
Section titled “Procedure 3 — the truncation test”This is the one that settles it, and it is worth running slowly the first time.
- Choose a week in the middle of your history — pick one where the weekly range was wide, because a flat week hides everything. Note the Monday and Friday dates.
- In the Analysis window set Range to From-To dates, with the To date on that Friday. Run the Exploration. Write down, or export, the row for the Monday of that week: the weekly close in use, the weekly EMA in use, the regime, the band low and the band high.
- Change only the To date, moving it to the Monday of that same week. Run again.
- Compare the Monday row from run 3 with the Monday row from run 2.
Every figure should be identical. The formula’s answer for Monday must not depend on whether Tuesday through Friday exist in the range, because on Monday they had not happened.
The positive control
Section titled “The positive control”Now show that all three procedures can fail. Open the Parameters dialog, set “Expansion mode” to Unsafe, and repeat every step above.
If any of the three procedures stays quiet in Unsafe mode, fix the procedure before trusting it in Safe mode. Then set the switch back to Safe and leave it there.
Common errors
Section titled “Common errors”- The weekly EMA steps on Mondays. An explicit
expandFirsthas crept into aTimeFrameExpand()call, or the mode switch is set to Unsafe. Procedure 1 catches this in seconds. - The band tracks the current week. The shift argument is 0 rather than −1. Remember
that
TimeFrameGetPrice()defaults to a shift of 0, so an omitted third argument produces exactly this defect. - Everything is Null for the first year of the chart. Expected. The compressed array begins with Nulls, and an exponential average of weekly closes needs weekly bars before it produces anything.
- The weekly figures differ between a zoomed-in and a zoomed-out chart.
SetBarsRequired()has been removed. Put it back; the guide names this exact case. - Weeks with four bars, or six. Holidays, half-days and exchanges with weekend sessions. The formula does not assume five, and neither should any rule you build on it.
- Two people get different weekly values from the same daily data. Check File → Database Settings, particularly the first-day-of-week setting and, on intraday databases, the daily-compression basis. Weekly boundaries are a property of the database, not of the formula.
Extensions
Section titled “Extensions”- Add a second context interval. A monthly regime alongside the weekly one, in a
separate
TimeFrameSet( inMonthly )block with its own restore. Remember that the blocks must be flat, not nested, and that the monthly values expand withinMonthly. - Show the distance to the band in units of volatility. Divide the gap between the close
and the previous week’s high by
ATR( 20 ), so “close to the band” means the same thing on a share and on an index. - Make the regime definition swappable. A
ParamList()offering two or three definitions of “weekly up” turns the indicator into an instrument for asking which definition changes the picture and by how much — the beginning of the sensitivity work in Part 31. - Extend the audit to the EMA. Procedure 2 currently bounds only the band. An exponential average has no simple arithmetic bound, so the honest route is Procedure 3, run at several dates rather than one. Automating that comparison is a good use of the batch tools in Part 36.
What changes for you
Section titled “What changes for you”You have a weekly context indicator you can defend line by line, and — worth more — a three-procedure routine that applies to any multi-timeframe formula, including ones you did not write. The routine works because it never asks whether the code looks correct. It asks whether a number arrived before it could have existed, whether it exceeds what the market had printed, and whether it changes when the future is removed.
The next project applies the same discipline where it is harder to see: across a whole universe at once, where you cannot eyeball a chart and where a single misaligned symbol will never announce itself.
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 — TimeFrameGetPriceamibroker.com/guide/afl/timeframegetprice.html2026-08-31
- 03AFL Function Reference — TimeFrameSetamibroker.com/guide/afl/timeframeset.html2026-08-31
- 04AFL Function Reference — Plotamibroker.com/guide/afl/plot.html2026-08-31
- 05AmiBroker User's Guide — New Analysis window§ Defining the Date/Time Rangeamibroker.com/guide/h_newanalysis.html2026-08-31
- 06AmiBroker Knowledge Base — QuickAFL factsamibroker.com/kb/2008/07/03/quickafl2026-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.