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.
The symptoms
Section titled “The symptoms”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// 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();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”.
Evidence to collect
Section titled “Evidence to collect”Before touching the formula, collect five pieces of evidence. Each one is cheap and each one eliminates a whole family of causes.
- Which button was pressed. Scan or Explore. This formula assigns both
BuyandFilter, so the two buttons honour different halves of it and produce different — possibly both empty — outputs. - 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?
- What Range is set to. In particular, whether it is a From-To range and what those dates are.
- 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.
- 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.
The task
Section titled “The task”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.
Hints, in increasing order
Section titled “Hints, in increasing order”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
- The modeScan honours Buy/Sell; Explore honours Filter and AddColumn. Wrong button, no output, no error
- The universeApply to = Current symbol, an empty watch list, or a Filter Settings conjunction that matches nothing
- The rangeA stale From-To window, or a range so narrow that the reporting bar is not the one you meant
- The formulaA condition that can never be true, or a Null that silently swallows the conjunction
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.
Hint 3: test every condition separately
Section titled “Hint 3: test every condition separately”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// 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 3AddColumn( Volume, "Volume", 1.0 ); // column 4AddColumn( AvgTurnover, "Avg turnover", 1.0 ); // column 5AddColumn( IsNull( AvgTurnover ), "turnover Null?", 1.0 ); // column 6AddColumn( IsNull( MA( Close, TrendPeriod ) ), "MA200 Null?", 1.0 );// column 7AddColumn( PriorHigh, "HHV(High,50)", 1.2 ); // column 8AddColumn( Close - PriorHigh, "Close - HHV", 1.2 ); // column 9AddColumn( Nz( CondLiquid, -1 ), "c1 liquid", 1.0 ); // column 10AddColumn( Nz( CondTrend, -1 ), "c2 trend", 1.0 ); // column 11AddColumn( Nz( CondNewHigh, -1 ), "c3 new high", 1.0 ); // column 12AddColumn( Nz( CondVolume, -1 ), "c4 volume", 1.0 ); // column 13AddColumn( 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 firstHint 4: look at what HHV includes
Section titled “Hint 4: look at what HHV includes”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 ).
Hint 5: one unknown poisons a conjunction
Section titled “Hint 5: one unknown poisons a conjunction”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?
Solution
Section titled “Solution”Run exploration-doctor.afl with Apply to unchanged and Range set to 1 recent bar(s). Four
things come out of that one table.
Finding 1: the universe
Section titled “Finding 1: the universe”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.
Finding 2: the range
Section titled “Finding 2: the range”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.
Finding 3: the impossible condition
Section titled “Finding 3: the impossible condition”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;Finding 4: the silent Null
Section titled “Finding 4: the silent Null”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.
The repaired formula
Section titled “The repaired formula”Complete runnable 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 symbolRecentOnly = 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();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.
Root causes, named
Section titled “Root causes, named”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.
A debugging procedure for empty results
Section titled “A debugging procedure for empty results”Keep this. It takes about three minutes end to end and it works for scans, explorations and backtests alike.
- Which button? Confirm the mode matches the formula.
FilterandAddColumnneed Explore;BuyandSellneed Scan. - How many symbols?
Filter = 1;, Range 1 recent bar(s), Explore. Read the row count. Compare with what you expected. - Which bar? Read the Date/Time column of that same run. Is it the bar you meant?
- 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. - Which unknowns? Add an
IsNull()column for every input the conditions depend on. UseNz( condition, -1 )so that unknown and false do not look the same. - Only now, read the code. By this point the fault is localised to one condition or one input, and reading is quick.
What changed in your understanding
Section titled “What changed in your understanding”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
Sources for this lesson
6 verified · checked 2026-08-31
- 01AmiBroker User's Guide — How to create your own explorationamibroker.com/guide/h_exploration.html2026-08-31
- 02AmiBroker User's Guide — Using New Analysis window§ Apply to and Rangeamibroker.com/guide/h_newanalysis.html2026-08-31
- 03AFL Function Reference — Statusamibroker.com/guide/afl/status.html2026-08-31
- 04AFL Function Reference — HHVamibroker.com/guide/afl/hhv.html2026-08-31
- 05AFL Function Reference — IsTrueamibroker.com/guide/afl/istrue.html2026-08-31
- 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.