Skip to content
Level 3 · AFL DeveloperProjectPart 14 · page 5 of 550 min
50Minutes
12AFL functions
7Sources
StandardRequires
AFL functions taught here12

Project: Multi-Timeframe Scanner

A chart shows you one symbol and you can see when something is wrong with it. A screen runs over four hundred symbols in two seconds and shows you a list of twelve. Nothing in that list tells you that on three of them the weekly value came from the wrong week. This project is about building the screen and, more importantly, about building the evidence that the list can be believed.

Not “what should I buy”. The screen answers a narrower and far more useful question: on which instruments do the weekly picture and the daily picture currently agree, out of those liquid enough to act on?

What each stage removes

  1. UniverseA watch list you chose deliberately
  2. LiquidityAverage turnover above a floor
  3. Weekly contextWeekly close above a rising weekly EMA
  4. Daily triggerClose crosses above the daily average today
Each stage is a filter with its own failure mode. The audit columns exist because the third one is the only one you cannot check by eye.
  • Context, weekly. The weekly close is above a weekly exponential average of weekly closes, and that average is higher than it was one weekly bar ago.
  • Trigger, daily. Today’s close crosses above a daily moving average. A cross is an event on one bar, which is what makes it a trigger rather than a state.
  • Sanity, daily. Average turnover over the last fifty bars exceeds a floor, so the list contains instruments a position could actually be opened and closed in.
  • Optional, weekly. Today’s close is above the previous completed week’s high. Off by default, because it makes the screen very selective; useful when you want the strong form of agreement.

What is different about doing this across a universe

Section titled “What is different about doing this across a universe”

Three things get harder the moment you leave a single chart.

Calendars differ. Symbols in one watch list can trade on different exchanges, with different holidays and, occasionally, different session days. “Five daily bars in a weekly bar” is a property of one symbol’s data, not a rule. A screen that silently assumes it will be wrong on the symbols that matter — the recently listed, the thinly traded, the cross-listed.

You cannot see the error. On a chart, a band that tracks the current week is visible. In a table of twelve rows on one date, a weekly value that came from the wrong week looks exactly like one that did not.

Currency and scale differ. A turnover floor of one million means one thing on a London line and another on a Tokyo one. The formula states this assumption in its header rather than hiding it.

Complete runnable AFL

mtf-agreement-scanner.afl
// ===========================================================================
// Multi-timeframe agreement scanner
// Finds symbols where the weekly picture and the daily picture agree on the
// same day, and shows both timeframes side by side so the agreement can be
// inspected rather than assumed.
//
// THE RULE, IN WORDS
// Context (weekly) : weekly close above a weekly EMA, and that EMA rising.
// Trigger (daily) : today's close crosses above a daily moving average.
// Sanity (daily) : average daily turnover above a floor, so the result is
// a list of instruments you could actually trade.
// Optional (weekly) : today's close above the previous completed week's high.
//
// Every weekly value used in a decision is expanded with expandLast, or read
// with a negative shift, so the value attached to a given daily bar is one
// that a completed weekly bar had already produced.
//
// HOW TO RUN
// Analysis window -> Apply to: a watch list of a few hundred symbols.
// Range: 1 recent bar(s) for a live screen, or All quotations while you are
// auditing the formula. Analysis -> Settings -> Periodicity: Daily.
// Explore lists the table below. Scan reports the Buy signals.
//
// DEMONSTRATION MODE - READ THIS
// "Expansion mode" set to Unsafe deliberately reproduces the documented
// look-ahead: weekly values expanded with expandFirst and the CURRENT week's
// high instead of the previous one. It is here so that the audit columns can
// be shown to catch something. Never leave it on.
//
// ASSUMPTIONS, STATED SO THEY CAN BE CHECKED
// - Daily base data. Turnover is close times volume in whatever currency the
// symbol is quoted in; mixing currencies in one watch list makes the
// liquidity floor meaningless.
// - Weekly boundaries come from the database settings, so two people with
// different First-day-of-week settings can legitimately get different
// weekly values from identical daily data.
// - Signals are candidates for inspection, not instructions. Nothing here is
// tested for profitability; that work belongs in Parts 27 to 33.
// ===========================================================================
SetBarsRequired( sbrAll, sbrAll );
WeeklyAvgPeriod = Param( "Weekly EMA length (weekly bars)", 10, 3, 52, 1 );
DailyAvgPeriod = Param( "Daily MA length (daily bars)", 20, 5, 200, 1 );
TurnoverFloor = Param( "Minimum average daily turnover", 1000000, 0, 100000000, 50000 );
TurnoverLookback = Param( "Turnover lookback (daily bars)", 50, 10, 250, 5 );
RequireBreakout = ParamToggle( "Require close above previous week high", "No|Yes", 0 );
ShowAllRows = ParamToggle( "Rows", "Signals only|Every symbol", 0 );
ShowAudit = ParamToggle( "Audit columns", "Hide|Show", 1 );
UnsafeDemo = ParamToggle( "Expansion mode",
"Safe (causal)|Unsafe (demonstrates look-ahead)", 0 );
// ---------------------------------------------------------------------------
// Weekly context. Everything inside the block is weekly and compressed.
// ---------------------------------------------------------------------------
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;
}
WeeklyRefHigh = TimeFrameGetPrice( "H", inWeekly, BandShift );
WeeklyContext = ( WeeklyClose > WeeklyAvg ) AND ( WeeklySlope > 0 );
// ---------------------------------------------------------------------------
// Daily trigger and liquidity, both computed in the base frame.
// ---------------------------------------------------------------------------
DailyAvg = MA( Close, DailyAvgPeriod );
DailyTrigger = Cross( Close, DailyAvg );
Turnover = MA( Close * Volume, TurnoverLookback );
LiquidEnough = Turnover > TurnoverFloor;
BreakoutOk = IIf( RequireBreakout, Close > WeeklyRefHigh, True );
Buy = WeeklyContext AND DailyTrigger AND LiquidEnough AND BreakoutOk;
Sell = Cross( DailyAvg, Close );
// ---------------------------------------------------------------------------
// Per-symbol alignment bookkeeping. These columns answer "which weekly bar is
// this row actually using?" for every symbol independently, which matters as
// soon as the universe contains instruments with different trading calendars.
// ---------------------------------------------------------------------------
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;
HighKnownThisWeek = HHV( High, BarsIntoWeek + 1 );
HighKnownSincePrev = HHV( High, BarNumber - FirstBarOfPrevWeek + 1 );
if( UnsafeDemo )
CausalBound = HighKnownThisWeek;
else
CausalBound = HighKnownSincePrev;
CausalTolerance = 1.000001;
BandLeak = WeeklyRefHigh > CausalBound * CausalTolerance;
// ---------------------------------------------------------------------------
// Output.
// ---------------------------------------------------------------------------
Filter = IIf( ShowAllRows, 1, Buy );
SetOption( "NoDefaultColumns", True );
AddTextColumn( Name(), "Symbol", 1.0, colorDefault, colorDefault, 80 );
AddColumn( DateTime(), "Date", formatDateTimeISO, colorDefault, colorDefault, 110 );
AddColumn( WeeklyClose, "Weekly close", 1.2 );
AddColumn( WeeklyAvg, "Weekly EMA", 1.2 );
AddColumn( WeeklySlope, "Weekly EMA change", 1.3 );
AddColumn( WeeklyRefHigh, "Reference week high", 1.2 );
AddTextColumn( WriteIf( WeeklyContext, "weekly UP", "weekly not up" ),
"Weekly context", 1.0, colorDefault, colorDefault, 110 );
AddColumn( Close, "Daily close", 1.2 );
AddColumn( DailyAvg, "Daily MA", 1.2 );
AddTextColumn( WriteIf( DailyTrigger, "cross today", "-" ),
"Daily trigger", 1.0, colorDefault, colorDefault, 90 );
AddColumn( Turnover, "Avg turnover", 1.0 );
AddTextColumn( WriteIf( Buy, "AGREE", "-" ), "Both timeframes", 1.0,
colorDefault, colorDefault, 100 );
if( ShowAudit )
{
AddColumn( BarsIntoWeek, "Bar of week", 1.0 );
AddColumn( BarsSinceWeekClose, "Bars since weekly bar closed", 1.0 );
AddColumn( CausalBound, "Highest high legally available", 1.2 );
AddTextColumn( WriteIf( BandLeak, "IMPOSSIBLE - LOOK-AHEAD", "ok" ),
"Causal bound", 1.0, colorDefault, colorDefault, 170 );
}
// Column 11 is average turnover: the most liquid candidates first, so the top
// of the list is the part of it you could actually act on.
SetSortColumns( -11 );

Download mtf-agreement-scanner.afl160 lines

The weekly block is the same shape as the previous project: switch, compute, restore, expand with the default expandLast. The WeeklyContext Boolean is therefore a weekly verdict that changes only when a weekly bar completes, and that carries forward across the following week.

The daily section computes an ordinary moving average and a Cross(), both in the base frame, plus average turnover. Because the weekly Boolean has been expanded onto daily bars, WeeklyContext AND DailyTrigger is a legal comparison: both sides now have one value per daily bar and both describe the same day.

The reference-week high comes from TimeFrameGetPrice( "H", inWeekly, BandShift ), where BandShift is −1 in normal operation. It is displayed on every row and used in the rule only when the optional breakout filter is switched on.

The bookkeeping block at the end exists purely for the audit. Cum( 1 ) numbers the bars; compressing that with compressOpen and with compressLast and expanding each appropriately gives the bar on which the current week started and the bar on which the weekly bar in use finished. Those two numbers are what make per-symbol alignment inspectable.

The table is deliberately wide, and the column order is the argument:

Column group Columns What it tells you
Identity Symbol, Date Which instrument, which bar
Weekly Weekly close, Weekly EMA, Weekly EMA change, Reference week high, Weekly context The context, in numbers rather than as a verdict
Daily Daily close, Daily MA, Daily trigger, Avg turnover The trading timeframe
Verdict Both timeframes The conjunction
Audit Bar of week, Bars since weekly bar closed, Highest high legally available, Causal bound Whether the weekly columns can be trusted

Showing the weekly numbers and not merely the weekly verdict is what makes the screen auditable. A row that says “weekly UP” with a weekly close of 41.90 and a weekly EMA of 41.85 is telling you the context is marginal; the verdict alone would have hidden that.

Both, and they answer different questions.

  • Exploration produces the table above. Use it while you are working on the formula, with “Rows” set to Every symbol so that you can see the instruments the screen rejected and why.
  • Scan reports the Buy signals themselves. Use it once the formula is settled, with the range set to a few recent bars, when the output you want is a signal list rather than a research table.

The vocabulary matters here: a Scan reports where Buy and the other signal variables are true, while an Exploration produces whatever table Filter and AddColumn() define. This formula supplies both, which is why the same file works in either mode.

Run the Exploration with Range: All quotations, Periodicity: Daily, “Rows” set to Every symbol, and “Audit columns” showing. Then apply it to a single symbol first, and read three columns together.

“Bar of week” counts from 0 on each week’s first trading bar. On a normal week for a normal exchange it runs 0 to 4. Values above 4 mean the symbol’s data contains more than five bars in that weekly period; values that never reach 4 mean holidays or missing bars. Neither is an error in the formula, but both change what “the weekly value” summarises for that symbol.

“Bars since weekly bar closed” is 0 on the last trading bar of each week, then 1, 2, 3, 4 through the following week. This is the column that proves, per symbol, which weekly bar is currently in force. A value that jumps to 5 or more means a week with no trading bars at all in the data — a suspended symbol, a data gap, or a holiday week — and you should know that before you act on the row.

“Causal bound” should read ok on every row. It compares the reference week’s high against the largest high the symbol has actually printed within the window that value claims to summarise.

Across a universe the evidence has to be produced differently from a single chart. The instrument is the Export button: an Exploration result can be written to a CSV file, and two CSV files can be compared exactly rather than by eye.

Choose a date at least a month inside your history, and a week whose middle days were volatile. Call the Monday M and the Friday F.

Set Range to From-To dates, To date = M. Run the Exploration with “Rows” on Every symbol and “Audit columns” showing. Click Export and save the result as a CSV. This is the screen’s view of Monday using only data that existed on Monday.

Change only the To date, to F, and leave every other setting alone. Run again and export to a second CSV. This is the screen’s view of the same Monday with the rest of the week present.

Open both files and compare the rows for date M, symbol by symbol. Every weekly column — weekly close, weekly EMA, weekly EMA change, reference week high, weekly context — must be identical between the two files, and the same symbols must be flagged AGREE.

Any symbol whose weekly figures differ between the two files is a symbol on which the screen’s Monday answer depended on Tuesday-to-Friday data. That is look-ahead, located to the symbol, without any need to reason about the code.

Repeat all four steps with “Expansion mode” set to Unsafe. The two exports must now disagree, and they should disagree on most symbols: with the range ending on F, Monday’s reference week high is the whole week’s high, and with the range ending on M it cannot be. The AGREE list itself will usually differ too, because the weekly context Boolean is being evaluated from a weekly bar that has not closed.

If the exports match in Unsafe mode, your comparison is not doing what you think it is — most often because the two runs used different watch lists, different periodicity, or because SetBarsRequired( sbrAll, sbrAll ) was removed and QuickAFL changed the loaded history in both runs alike. Fix that before drawing any conclusion from the Safe run.

  • An empty result on a date you know had signals. Check Filter first: with “Rows” on Signals only, a single failing condition empties the table. Switch to Every symbol and read which column disagrees.
  • Every symbol shows the weekly context as true. The weekly variables were not expanded and are being compared while still compressed. The symptom is a table that looks confident and is meaningless; the fix is the TimeFrameExpand() call and the interval it came from.
  • Turnover floor filters out everything, or nothing. The floor is in the quote currency. A watch list mixing currencies needs either one floor per currency or a different liquidity measure, such as turnover expressed in average daily ranges.
  • The scan is much slower than expected. SetBarsRequired( sbrAll, sbrAll ) loads full history for every symbol. That is the right trade while auditing. For a daily production run over recent bars, measure whether a specific figure such as SetBarsRequired( 1000, 0 ) is sufficient, and re-run the truncation test after changing it.
  • Results differ between two machines. Compare File → Database Settings on both, especially the first-day-of-week setting. Identical formulas over identical quotes can produce different weekly bars if the databases disagree about where a week starts.
  • A symbol appears with a weekly EMA and no price history on the chart. The watch list contains a symbol whose data was removed or renamed. Clean the universe; a screen is only as trustworthy as the list it runs over.
  • Add a third horizon. A monthly gate in its own flat TimeFrameSet( inMonthly ) block, expanded with inMonthly. Then measure how many candidates the third horizon removes: if it removes almost none, it is decoration, and if it removes almost all, it is the only filter that is doing anything.
  • Record the screen daily and evaluate it later. Write each day’s list to a file and, after a few months, examine what happened to the flagged instruments over the following weeks. This is a genuinely out-of-sample record, and it is the cheapest one available.
  • Replace the daily trigger. A cross of a moving average was chosen for familiarity, not for merit. Swapping it for a break of the previous day’s high, or for a close above the previous week’s high alone, turns the screen into an instrument for comparing triggers under a fixed context.
  • Rank rather than filter. Instead of a Boolean context, score each symbol by how far the weekly close sits above the weekly average in units of weekly volatility, and sort. The ranking machinery in Part 13 turns that into a shortlist rather than a list.
  • Send it somewhere. Part 25 covers alerts; the same conditions can raise one. Prove the formula clean first — an alert on a look-ahead condition is a look-ahead condition that now wakes you up.

You can now build a screen that mixes two intervals and, separately, produce evidence that it does so honestly across an entire universe rather than on the one symbol you happened to check. The export-and-diff routine in this project is the universe-scale version of the truncation test, and it generalises: any formula whose output for a date changes when later data is added to the range is reading the future, whatever the mechanism.

That completes the multi-timeframe toolkit. Part 15 keeps the same discipline and changes the axis: instead of another interval, another symbol — an index used as a filter, a sector used as a benchmark — where the alignment problem reappears in a new form.

Check your understanding

Question 1. In the audit columns, "Bars since weekly bar closed" reads 7 on a Wednesday for one symbol. What is the most likely explanation?
Show the answer and why

Answer: That symbol has no trading bars in the previous weekly period, so the weekly value in force is older than a week

The counter measures how many bars have elapsed since the weekly bar in use finished. A large value means a week with no data for that symbol — suspension, a data gap, or a holiday week — which changes what the weekly value summarises.

Question 2. You export the Exploration twice with different To dates and diff the files. Which finding demonstrates look-ahead?
Show the answer and why

Answer: A row for the shared date has different weekly values in the two files

More rows and a longer runtime are expected consequences of a longer range. Only a change in a value for a date that both runs cover shows that data arriving later reached an earlier bar.

Question 3. Why does the scanner show weekly close and weekly EMA as numbers rather than only a "weekly UP" verdict?
Show the answer and why

Answer: So the strength of the context is visible and the columns can be audited against a chart

A Boolean verdict hides how marginal a decision was and cannot be cross-checked against anything. Displaying the inputs makes both possible.

Question 4. Which statements about running this formula in Scan mode rather than Exploration mode are correct? Select all that apply.
Show the answer and why

Answer: Scan reports the bars where Buy and the other signal variables are true, Exploration produces the table defined by Filter and AddColumn, Scan ignores the AddColumn calls when reporting its signal list

One formula serves both modes: the signal variables drive a Scan, while Filter and the AddColumn calls define what an Exploration prints.

Sources for this lesson

7 verified · checked 2026-08-31

  1. 01AFL Function Reference — TimeFrameExpandamibroker.com/guide/afl/timeframeexpand.html2026-08-31
  2. 02AFL Function Reference — TimeFrameGetPriceamibroker.com/guide/afl/timeframegetprice.html2026-08-31
  3. 03AFL Function Reference — AddColumnamibroker.com/guide/afl/addcolumn.html2026-08-31
  4. 04AFL Function Reference — SetSortColumnsamibroker.com/guide/afl/setsortcolumns.html2026-08-31
  5. 05AmiBroker User's Guide — Explorationamibroker.com/guide/h_exploration.html2026-08-31
  6. 06AmiBroker User's Guide — New Analysis window§ Defining the Date/Time Rangeamibroker.com/guide/h_newanalysis.html2026-08-31
  7. 07AmiBroker User's Guide — Notice 801amibroker.com/guide/errors/801.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.