Skip to content
Level 2 · AmiBroker AnalystChallengePart 12 · page 7 of 935 min
35Minutes
12AFL functions
6Sources
StandardRequires
AFL functions taught here12

Challenge: The Scan That Returns Nothing

An empty result list is the least informative output a program can produce. It looks like an answer, it arrives without an error message, and it is consistent with at least a dozen different faults — most of which are not in the code at all.

This challenge gives you a real one. Work it before reading the solution; the value is entirely in the procedure, and the procedure only sticks if you have used it once under mild frustration.

A colleague sends you this, with a note:

“Liquid stocks that are above their 200-day average, that made a new 50-day high today on at least twice their normal volume, and that are not already overbought. I get maybe twenty or thirty of these a week. Yesterday it returned forty rows. Today it returns nothing at all and I have not touched the code.”

The database is a broad equity universe of about 1,900 symbols with daily bars going back roughly fifteen years. Several hundred of the symbols were added in the last few months and have short histories.

Complete runnable AFL

empty-scan-broken.afl
// empty-scan-broken.afl
// Part 12 - Challenge: The Scan That Returns Nothing
//
// This is the formula as its author wrote it. Do not fix it yet.
//
// The author's stated intent, in their own words:
//
// "Liquid stocks that are above their 200-day average, that made a new
// 50-day high today on at least twice their normal volume, and that are
// not already overbought. I get maybe twenty or thirty of these a week.
// Yesterday it returned forty rows. Today it returns nothing at all and I
// have not touched the code."
//
// The database is a broad equity universe of about 1,900 symbols with daily
// bars going back roughly fifteen years. Several hundred of the symbols were
// added to the database in the last few months and have short histories.
_SECTION_BEGIN( "Momentum screen" );
MinTurnover = 20000000;
TrendPeriod = 200;
BreakPeriod = 50;
VolPeriod = 50;
VolMultiple = 2;
RsiCeiling = 70;
AvgTurnover = MA( Close * Volume, 200 );
RelVolume = Volume / MA( Volume, VolPeriod );
Liquid = AvgTurnover > MinTurnover;
UpTrend = Close > MA( Close, TrendPeriod );
NewHigh = Close > HHV( High, BreakPeriod );
HeavyVolume = RelVolume > VolMultiple;
NotExtended = RSI( 14 ) < RsiCeiling;
Buy = Liquid AND UpTrend AND NewHigh AND HeavyVolume AND NotExtended;
Filter = Buy AND Status( "lastbarinrange" );
AddColumn( Close, "Close", 1.2 );
AddColumn( RelVolume, "Rel volume", 1.2 );
AddColumn( AvgTurnover, "Turnover", 1.0 );
AddColumn( RSI( 14 ), "RSI(14)", 1.0 );
_SECTION_END();

Download empty-scan-broken.afl45 lines

Additional observations from the person reporting it:

  • The formula compiles. Check syntax reports no errors.
  • The Analysis run completes. The Info tab shows it examined symbols and finished normally.
  • No warning, no message box, no entry in the log.
  • Re-running it produces the same nothing.
  • Charting the same conditions on a single symbol “looks about right”.

Before touching the formula, collect five pieces of evidence. Each one is cheap and each one eliminates a whole family of causes.

  1. Which button was pressed. Scan or Explore. This formula assigns both Buy and Filter, so the two buttons honour different halves of it and produce different — possibly both empty — outputs.
  2. What Apply to is set to. All symbols, Current symbol, or Filter. If it is Filter, which watch list, and how many symbols does that list contain right now?
  3. What Range is set to. In particular, whether it is a From-To range and what those dates are.
  4. How many symbols the run examined. Not how many are in the database — how many this run actually saw. This is the number people assume and never measure.
  5. Whether each individual condition is ever true. Not “does the conjunction work”, but “is each term, on its own, ever satisfied by any symbol on any bar”.

Items 4 and 5 need a tool, because the failing exploration cannot report on itself. That tool is the second formula in this challenge.

Work out, with evidence rather than inspection, why the exploration returns nothing — and then repair it so that it returns a sensible candidate list.

There is more than one fault. Two of them are in the formula, one is in the analysis settings, and one is in the interaction between the formula and the data. A repair that fixes only the first fault you find will still return nothing, which is itself a useful thing to experience.

Give yourself twenty minutes before reading the hints.

Read one at a time.

Hint 1: the code is the last place to look

Section titled “Hint 1: the code is the last place to look”

An empty exploration has four possible locations, and only one of them is the formula:

Where an empty result actually comes from

  1. The modeScan honours Buy/Sell; Explore honours Filter and AddColumn. Wrong button, no output, no error
  2. The universeApply to = Current symbol, an empty watch list, or a Filter Settings conjunction that matches nothing
  3. The rangeA stale From-To window, or a range so narrow that the reporting bar is not the one you meant
  4. The formulaA condition that can never be true, or a Null that silently swallows the conjunction
Check them in this order. The first three take thirty seconds each and eliminate most cases.

Hint 2: measure the universe before reading the code

Section titled “Hint 2: measure the universe before reading the code”

Replace the Filter line with Filter = 1;, set Range to 1 recent bar(s), and run Explore. Now you get exactly one row per symbol the run examined, unconditionally.

Count the rows. Is it 1,900? Is it 300? Is it one?

That single number decides whether you are debugging a formula or a settings problem, and it takes about fifteen seconds to obtain.

A conjunction that is false tells you nothing about which term was responsible. Break it apart: give every condition its own column, and add MIN and MAX summary rows.

If the maximum of a condition column across the entire universe is 0, that condition was false for every symbol on every bar reported — and no threshold anywhere else in the formula can be the cause.

Here is that instrument, ready to run:

Complete runnable AFL

exploration-doctor.afl
// exploration-doctor.afl
// Part 12 - Challenge: The Scan That Returns Nothing (diagnostic formula)
//
// Nothing here screens anything. It is an instrument: it reports every symbol
// the run examined and, for each one, the state of every input and every
// condition the broken screen depends on. An empty candidate list is a result
// with no information in it; this turns it into a table you can read.
//
// HOW TO RUN IT
// Apply to : the same setting the broken screen used - if you change it now
// you are diagnosing a different run
// Range : 1 recent bar(s)
// Button : Explore
//
// Filter = 1 with a one-bar range gives exactly one row per symbol, which is
// the other way of writing Status("lastbarinrange"). It is used here on
// purpose: if the row count does not match the size of your universe, the
// problem is upstream of the formula and no amount of reading the code will
// find it.
MinTurnover = 20000000;
TrendPeriod = 200;
BreakPeriod = 50;
VolPeriod = 50;
VolMultiple = 2;
RsiCeiling = 70;
AvgTurnover = MA( Close * Volume, 200 );
AvgVolume = MA( Volume, VolPeriod );
RelVolume = Volume / AvgVolume;
PriorHigh = HHV( High, BreakPeriod );
Rsi14 = RSI( 14 );
// Each condition is reported separately as 1, 0 or Null. A Null column is the
// single most useful thing on this table: it says the condition was never
// false, it was never evaluated, because one of its inputs did not exist.
CondLiquid = AvgTurnover > MinTurnover;
CondTrend = Close > MA( Close, TrendPeriod );
CondNewHigh = Close > PriorHigh;
CondVolume = RelVolume > VolMultiple;
CondRsi = Rsi14 < RsiCeiling;
Filter = 1;
AddColumn( Close, "Close", 1.2 ); // column 3
AddColumn( Volume, "Volume", 1.0 ); // column 4
AddColumn( AvgTurnover, "Avg turnover", 1.0 ); // column 5
AddColumn( IsNull( AvgTurnover ), "turnover Null?", 1.0 ); // column 6
AddColumn( IsNull( MA( Close, TrendPeriod ) ), "MA200 Null?", 1.0 );// column 7
AddColumn( PriorHigh, "HHV(High,50)", 1.2 ); // column 8
AddColumn( Close - PriorHigh, "Close - HHV", 1.2 ); // column 9
AddColumn( Nz( CondLiquid, -1 ), "c1 liquid", 1.0 ); // column 10
AddColumn( Nz( CondTrend, -1 ), "c2 trend", 1.0 ); // column 11
AddColumn( Nz( CondNewHigh, -1 ), "c3 new high", 1.0 ); // column 12
AddColumn( Nz( CondVolume, -1 ), "c4 volume", 1.0 ); // column 13
AddColumn( Nz( CondRsi, -1 ), "c5 rsi", 1.0 ); // column 14
// -1 in a condition column means Null: not evaluated, rather than rejected.
//
// COUNT (16), MIN (4) and MAX (8), and no TOTAL: summing a column of 1, 0 and
// -1 would mean nothing. The MAX row is the one that ends arguments. If the
// maximum of a condition column across the whole universe is 0, that condition
// was false for every symbol on every reported bar, and no threshold anywhere
// else in the formula can be the cause. If the maximum of "Close - HHV" is
// negative, the new-high test cannot fire by construction.
AddSummaryRows( 16 + 4 + 8, 1.0, 9, 10, 11, 12, 13, 14 );
SetSortColumns( -9 ); // the symbols that came closest to a new high first

Download exploration-doctor.afl68 lines

HHV( array, periods ) returns the highest value of the array over the last periods bars — and “the last periods bars” includes the current bar.

Now write down the relationship between Close and High on a single bar, and consider what that implies about Close > HHV( High, 50 ).

In AFL, an indicator that has not warmed up yields Null. Comparing Null with a number does not give false; it gives an unknown. And an unknown anywhere in a chain of ANDs makes the whole chain unknown, which Filter does not treat as true.

How many of the 1,900 symbols have at least 200 bars of history? What does MA( Close * Volume, 200 ) produce for the others?

Run exploration-doctor.afl with Apply to unchanged and Range set to 1 recent bar(s). Four things come out of that one table.

The COUNT summary row tells you how many symbols the run examined. If that number is 1, the diagnosis is complete before you have read a line of the formula: Apply to was left on Current symbol. This is the most common single cause of an empty screen and it is almost always a leftover from debugging a formula on one chart the previous evening.

If the number is a few hundred rather than 1,900, the Filter Settings window is narrowing the universe — remember that multiple categories there combine with logical AND, so ticking a market and a sector passes only the symbols in both.

Look at the automatic Date/Time column. Every row should carry the same date, and it should be the most recent trading day in your database.

If it shows a date from two years ago, Range is set to From-To dates left over from a backtest. The run succeeded, reported on the last bar inside that window, and found nothing — which is entirely correct behaviour for a question nobody meant to ask. This is the settings fault that produces “it worked yesterday”: nothing in the formula changed, but the range did, or the data moved past the end of the window.

Read the MAX row for the c3 new high column and for Close - HHV.

c3 has a maximum of 0. Close - HHV has a maximum that is negative or zero, never positive.

That is the proof: Close > HHV( High, 50 ) is false on every bar of every symbol in the database, because HHV( High, 50 ) includes the current bar’s high, and a bar’s close can never exceed its own high. The condition is not strict, or badly calibrated, or unlucky — it is impossible by construction, and it has been since the formula was written. Which means the claim that it returned forty rows yesterday cannot be true of this code.

The fix is to shift the window back one bar, so that “a new 50-day high” means “above the highest high of the previous fifty bars”:

Fragment — not a complete formula

PriorHigh = Ref( HHV( High, BreakPeriod ), -1 );
NewHigh = Close > PriorHigh;

Read the turnover Null? column. On a database where several hundred symbols were added in the last few months, a large block of rows shows 1: MA( Close * Volume, 200 ) has no value for a symbol with 90 bars of history.

Then read the condition columns for those same rows. In exploration-doctor.afl a -1 means the condition evaluated to Null — not false, but not evaluated. Those symbols were never rejected by any test. They fell out of the conjunction because one unknown makes the whole conjunction unknown.

Two things are wrong here at once. The liquidity average is 200 bars long for no reason — fifty measures the same thing and needs a quarter of the history. And the conjunction has no explicit decision about what to do with the unknowns. IsTrue() supplies one.

Finding 5: the threshold that belongs to another market

Section titled “Finding 5: the threshold that belongs to another market”

MinTurnover = 20000000 is a floor of twenty million currency units of daily turnover. On a very large market that removes perhaps 90% of symbols. On a smaller one it removes all of them. Sort the doctor’s Avg turnover column descending and read the top row: if your most heavily traded symbol trades four million a day, this threshold was never going to pass anything, and it was copied from material written about somewhere else.

Complete runnable AFL

empty-scan-fixed.afl
// empty-scan-fixed.afl
// Part 12 - Challenge: The Scan That Returns Nothing (worked solution)
//
// The same screen, with the four code defects repaired and the two settings
// defects written down where they cannot be forgotten. Every change is
// annotated with what it fixes.
//
// SETTINGS THIS FORMULA ASSUMES - these are half of the original fault and no
// amount of code can compensate for them:
// Apply to : Filter, with your universe watch list selected. "Current
// symbol" examines exactly one symbol, and a screen that examines
// one symbol is not a screen.
// Range : All quotations, or a recent-bars range large enough to warm the
// 200-bar average. A stale From-To range left over from an
// earlier backtest is the classic silent cause: the run succeeds,
// reports on a bar from three years ago, and finds nothing.
// Button : Explore. This formula assigns Filter and adds columns, so Scan
// would ignore all of it.
_SECTION_BEGIN( "Momentum screen - repaired" );
// FIX 1 (thresholds): a turnover floor is a property of a market, not a
// universal constant. This one was carried over from a different, much larger
// market. Derive it from your own universe with the stage audit, then write
// down which universe it belongs to.
MinTurnover = 2000000; // currency units per day, this market, this decade
TrendPeriod = 200;
BreakPeriod = 50;
VolPeriod = 50;
VolMultiple = 2;
RsiCeiling = 70;
// FIX 2 (warm-up): the liquidity average was 200 bars long for no reason,
// which meant every recently listed symbol produced Null and was silently
// dropped before any of the interesting conditions were reached. Fifty bars
// measures the same thing and needs a quarter of the history.
AvgTurnover = MA( Close * Volume, VolPeriod );
AvgVolume = MA( Volume, VolPeriod );
RelVolume = Volume / AvgVolume;
// FIX 3 (impossible condition): HHV( High, 50 ) includes today's high, and the
// close can never exceed the high of its own bar. The comparison was therefore
// false on every bar of every symbol in the database. What the author meant
// was "above the highest high of the previous 50 bars", which needs the window
// shifted back by one bar.
PriorHigh = Ref( HHV( High, BreakPeriod ), -1 );
Liquid = AvgTurnover > MinTurnover;
UpTrend = Close > MA( Close, TrendPeriod );
NewHigh = Close > PriorHigh;
HeavyVolume = RelVolume > VolMultiple;
NotExtended = RSI( 14 ) < RsiCeiling;
// FIX 4 (Null propagation): one Null anywhere in a chain of ANDs makes the
// whole chain Null, and Null is not true, so the symbol vanishes without
// explanation. IsTrue() converts "not known" into an explicit 0. The symbols
// that lack history are still rejected - but now they are rejected by a
// decision that is written in the formula rather than by an accident.
Setup = IsTrue( Liquid AND UpTrend AND NewHigh AND HeavyVolume AND NotExtended );
// FIX 5 (Filter logic): the original wrote Filter = Buy AND lastbarinrange,
// which reports a symbol only if the event happened on the very last bar of
// the range. That is a legitimate thing to want for an end-of-day screen, but
// it is not what "twenty or thirty a week" describes. Reporting every event
// bar inside a recent window is. Choose one deliberately:
//
// RecentOnly = True -> today's candidates only, one row per symbol at most
// RecentOnly = False -> every event bar in the range, several rows per symbol
RecentOnly = True;
if( RecentOnly )
Filter = Setup AND Status( "lastbarinrange" );
else
Filter = Setup;
AddColumn( Close, "Close", 1.2 );
AddColumn( RelVolume, "Rel volume", 1.2 );
AddColumn( AvgTurnover, "Turnover", 1.0 );
AddColumn( RSI( 14 ), "RSI(14)", 1.0 );
AddColumn( 100 * ( Close / PriorHigh - 1 ), "% above 50d high", 1.2 );
AddColumn( DateTime(), "Bar (ISO)", formatDateTimeISO );
AddSummaryRows( 16, 1.0, 3 ); // COUNT: how many rows this actually produced
_SECTION_END();

Download empty-scan-fixed.afl86 lines

The settings assumptions are written into the header comment, because half of the original fault was in settings that no formula can compensate for. The five code changes are annotated in place: the recalibrated threshold, the shortened liquidity window, the shifted HHV window, the explicit IsTrue() decision about unknowns, and the deliberate choice between “today’s candidates only” and “every event bar in the range”.

That last one deserves a note. The original wrote:

Fragment — not a complete formula

Filter = Buy AND Status( "lastbarinrange" );

which reports a symbol only if the event occurred on precisely the final bar of the range. That is a reasonable thing to want for an end-of-day screen — but it is not what “twenty or thirty a week” describes, and combining an event with a single-bar report is a reliable way to get a list that is empty on most days and looks broken. Making the choice explicit, with a constant at the top of the file, means it is a decision rather than an accident.

Five distinct faults, in four distinct categories. It is worth being able to name them, because each has a different detection method.

Filter logic. Two problems here. First, an event (Buy) was combined with a single-bar report (lastbarinrange), so the screen could only ever fire on the exact day. Second — and more general — a formula that assigns both Buy and Filter behaves completely differently depending on which button is pressed, and neither button complains. Detection: check the button first; then ask whether each half of the Filter expression is a state or an event.

Range settings. A From-To range left over from an earlier backtest reports on a bar from the past, correctly and silently. Detection: look at the Date/Time column of an unconditional exploration. It tells you which bar the run is actually reporting on.

Universe selection. Apply to on Current symbol, or a Filter Settings conjunction that matches far fewer symbols than intended. Detection: Filter = 1; with a one-bar range, and read the row count.

Null propagation. An indicator with insufficient history yields Null; Null compared with a number yields an unknown; an unknown anywhere in an AND chain makes the whole chain unknown; and Filter reports no row. The symbol is not rejected — it is never evaluated, and nothing says so. Detection: an explicit IsNull() column per input, and Nz( condition, -1 ) so that “unknown” is visually distinct from “false”.

A condition that cannot be true. Close > HHV( High, N ) is the classic, because the window includes the current bar. Others in the same family: Close > High, Low > Close, Volume > HHV( Volume, N ), and any comparison of a value against a range that contains it. Detection: MIN and MAX summary rows on each condition column. A maximum of 0 across an entire database is proof.

Keep this. It takes about three minutes end to end and it works for scans, explorations and backtests alike.

  1. Which button? Confirm the mode matches the formula. Filter and AddColumn need Explore; Buy and Sell need Scan.
  2. How many symbols? Filter = 1;, Range 1 recent bar(s), Explore. Read the row count. Compare with what you expected.
  3. Which bar? Read the Date/Time column of that same run. Is it the bar you meant?
  4. Which condition? Give each term its own column. Add AddSummaryRows( 4 + 8 + 16, 1.0 ); for MIN, MAX and COUNT. Any condition whose maximum is 0 is your culprit.
  5. Which unknowns? Add an IsNull() column for every input the conditions depend on. Use Nz( condition, -1 ) so that unknown and false do not look the same.
  6. Only now, read the code. By this point the fault is localised to one condition or one input, and reading is quick.

An empty result is not a result; it is an absence of information, and the job is to convert it into evidence before forming any theory. You have a procedure for that now, and five named root causes to test against. You have also seen the specific, permanent trap that HHV includes the current bar — and the more general one, that in AFL “false” and “not known” look identical in a result list unless you deliberately make them look different.

Check your understanding

Question 1. Why can Close > HHV( High, 50 ) never be true?
NewHigh = Close > HHV( High, 50 );
Show the answer and why

Answer: The 50-bar window includes the current bar, and a close can never exceed its own bar high

HHV looks back over the last N bars including the current one, so its value is at least today’s High, and Close is at most today’s High. Shifting the window with Ref( HHV( High, 50 ), -1 ) expresses what was actually meant.

Question 2. An exploration reports nothing. You set Filter = 1; and Range to 1 recent bar(s), and get exactly one row. What have you learned?
Show the answer and why

Answer: The run examined one symbol, so Apply to is on Current symbol or the universe is a one-symbol list

Filter = 1 with a one-bar range gives one row per symbol examined. One row means one symbol was examined. The database may well contain two thousand; the run did not see them, which is a settings problem and not a formula problem.

Question 3. Which of these would make "false" and "not known" visually distinguishable in an exploration column? Select all that apply.
Show the answer and why

Answer: AddColumn( Nz( Condition, -1 ), "cond", 1.0 ), AddColumn( IsNull( Input ), "input Null?", 1.0 )

Nz with a sentinel value maps unknown to -1, which is distinct from 0. A separate IsNull column reports the unknown directly. IsTrue deliberately collapses unknown into 0 — correct for the screen itself, wrong for a diagnostic. The last option leaves the two indistinguishable, which is the original problem.

Question 4. A screen worked last month and returns nothing today, with no code change. Which single check is most likely to explain it fastest?
Show the answer and why

Answer: Reading the Date/Time column of an unconditional exploration to see which bar the run is reporting on

Nothing in the formula changed, so look at what did: the settings and the data. The reported bar tells you at once whether a stale From-To range is pinning the run to a date in the past — the classic cause of "it worked last month".

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — How to create your own explorationamibroker.com/guide/h_exploration.html2026-08-31
  2. 02AmiBroker User's Guide — Using New Analysis window§ Apply to and Rangeamibroker.com/guide/h_newanalysis.html2026-08-31
  3. 03AFL Function Reference — Statusamibroker.com/guide/afl/status.html2026-08-31
  4. 04AFL Function Reference — HHVamibroker.com/guide/afl/hhv.html2026-08-31
  5. 05AFL Function Reference — IsTrueamibroker.com/guide/afl/istrue.html2026-08-31
  6. 06AFL Function Reference — Nzamibroker.com/guide/afl/nz.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.