Duplicate Alerts and How to Stop Them
A reader on a support forum once described leaving an alert running overnight on a
five-minute chart and finding four hundred and eleven lines in the Alert Output window the
next morning, every one of them saying the same thing about the same symbol. The condition
was correct. The formula had no bug in the ordinary sense. It said Close > MA( Close, 50 ),
and for most of that night, that was true.
This is the single most common failure in alerting, and Part 9 already gave you the tool to avoid it. What that part called the difference between a state and an event was, at the time, a point about how formulas behave. Here it has a cost attached: an alert wired to a state will interrupt you until you stop believing it.
Why a state alerts on every bar
Section titled “Why a state alerts on every bar”Two separate multiplications are at work, and it is worth keeping them apart.
The first is the array. A state condition is true on every bar of a run. As bars accumulate,
each of those bars takes its turn as “the most recent bar”, which is the only bar
AlertIf() examines by default. Twelve bars above the average means twelve opportunities to
alert, one per bar, all of them reporting the same thing.
The second is re-execution. A chart pane with a real-time feed re-runs its formula on the Preferences refresh interval — three seconds by default. An Analysis window with “Auto repeat Scan/Explore” enabled re-runs on whatever interval you set. So even within a single bar the formula executes many times, and each execution presents the same still-true state to the same alert call.
State, event, and event after ExRem
| Bar | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
|---|---|---|---|---|---|---|---|---|
Close | 9.80 | 10.50 | 10.35 | 10.55 | 10.40 | 11.20 | 11.00 | 11.30 |
Average | 10.30 | 10.30 | 10.40 | 10.40 | 10.45 | 10.50 | 10.60 | 10.70 |
Close > Averagethe state | 0 | 1 | 0 | 1 | 0 | 1 | 1 | 1 |
Cross(Close, Average)the event | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 0 |
Reset conditionno confirmed break below | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
ExRem(event, reset)one signal | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
Read the rows downwards. The state is true on five bars. Cross() reduces that to three —
better, but a symbol oscillating around its average will still produce a cluster. ExRem()
reduces the cluster to one, because it blocks every event after the first until a reset
occurs. Which reset you choose turns out to matter more than which trigger you choose.
Layer one: alert on events, not states
Section titled “Layer one: alert on events, not states”This is the fix that works in every context, on every interval, in both editions, and depends on nothing undocumented. Convert the condition into the bar on which something became true.
| Instead of this state | Alert on this event |
|---|---|
Close > MA( Close, 50 ) |
Cross( Close, MA( Close, 50 ) ) |
RSI( 14 ) < 30 |
Cross( 30, RSI( 14 ) ) |
High > Level |
Cross( High, Level ) |
ADX( 14 ) > 25 AND Close > Open |
Cross( ADX( 14 ), 25 ) AND Close > Open |
The fourth row is the interesting one. When a condition combines several tests, only one of them should be an event; the rest are context that must hold at the moment the event occurs. Making all of them events almost never fires, because the odds of two things crossing on the same bar are poor. Making none of them events fires constantly. Picking which single test carries the event is a design decision, and it is usually the one that is most nearly instantaneous.
Layer two: ExRem, applied to alerts
Section titled “Layer two: ExRem, applied to alerts”ExRem( ARRAY1, ARRAY2 ) returns 1 on the first true in ARRAY1, then 0 until ARRAY2 is
true, even where ARRAY1 is true again. You met it in Part 9 for cleaning up Buy and
Sell signals. Used on an alert, the second argument stops being “the opposite signal” and
becomes an explicit answer to the question when am I willing to be told again?
Fragment — not a complete formula
// One alert per breakout, re-armed only by a confirmed close back below the level.RawBreak = Cross( High, Level );Rearm = Cross( Level - ReArmBuffer, Close );Trigger = ExRem( RawBreak, Rearm );Three reset choices come up constantly, and they encode genuinely different intentions:
- The opposite event.
ExRem( GoLong, GoFlat )— tell me on each swing. This is the Part 9 pattern, unchanged. - A time boundary.
ExRem( Trigger, NewSession ), whereNewSessionis a change of calendar day — tell me once per session, no matter what the price does in between. This is what the project at the end of this part uses. - A price re-arm. As in the fragment above — tell me again only if the market genuinely gave the level back. This is the strictest, and the right choice when the alert is expensive to act on.
Two related functions are worth knowing about, with a caveat on each. ExRemSpan( ARRAY, numbars ) passes the first signal and blocks the rest for numbars bars — a plain
“not more than once every N bars” throttle. The official reference marks it as obsolete
and directs you to ApplyStop() for its original purpose of N-bar exits; that redirection is
about backtesting rather than alerting, but you should know the function carries that label
before you build on it. Flip( ARRAY1, ARRAY2 ) is the latch itself: it returns 1 from the
first true in ARRAY1 until a true in ARRAY2 resets it. Where ExRem gives you the single
bar, Flip gives you the whole span, which is what you want when the alert text needs to say
how long the condition has been in force.
Layer three: the built-in repeat suppression
Section titled “Layer three: the built-in repeat suppression”AmiBroker has its own de-duplication, and it is on by default. Two of the four bits in
flags do it: bit 4 suppresses repeated alerts of the same type, bit 8 suppresses
repeated alerts with the same date/time. Behind them sits the finite state machine the
function reference warns about — it stores the type of the last alert per symbol, so
every symbol carries its own memory.
The most visible consequence is one that reads as a bug the first time you meet it: run the same scan twice and the second run reports nothing. That is bits 4 and 8 doing exactly what they are for. The tutorial’s own remedy, for when you are experimenting and want the repetition, is to drop them:
Fragment — not a complete formula
// Suppression off. Every run reports the signal again - use while testing only.AlertIf( Condition, "", "Text", 1, 1+2 );Layer four: intrabar re-triggering
Section titled “Layer four: intrabar re-triggering”Everything so far has assumed a bar is a finished thing. On a live feed, or under Bar Replay, the newest bar is not: it is being assembled from ticks as they arrive, and your formula sees whatever it has become so far.
Consider a level at 11.00 and a bar forming between 10:30 and 10:35.
| Moment | Close so far | High so far | Close > 11.00 |
High > 11.00 |
|---|---|---|---|---|
| 10:30:20 | 10.96 | 10.97 | 0 | 0 |
| 10:31:40 | 11.03 | 11.04 | 1 | 1 |
| 10:33:05 | 10.98 | 11.04 | 0 | 1 |
| 10:34:50 | 11.01 | 11.06 | 1 | 1 |
| 10:34:59 (final) | 10.99 | 11.06 | 0 | 1 |
Those numbers are illustrative, not a recording of a real instrument. What they show is
structural and always true: within a forming bar, Close moves in both directions, while
High and Low only extend. So a close-based condition flickers on and off, and a
high-based condition latches true the instant the first tick pokes through — including on a
bar that ends up closing back below.
The documented fix is to alert only on a bar that can no longer change:
Fragment — not a complete formula
barcomplete = BarIndex() < LastValue( BarIndex() );AlertIf( barcomplete AND condition, "", "Text", 1 );The cost is honest and unavoidable: you learn one bar late. On five-minute bars that is up to five minutes. Whether that is acceptable is a question about your rule, not about AmiBroker — and if it is not acceptable, the answer is a shorter interval, not an alert on a bar that is still changing its mind. A rule that needs to fire inside the bar should be written on bars short enough that “inside the bar” barely exists.
The guard and the lookback cancel each other out
Section titled “The guard and the lookback cancel each other out”There is a trap in that recipe as the User’s Guide prints it, and it is the single most useful thing on this page.
Both official pages state that AlertIf() considers only the lookback most recent bars, and
both give lookback a default of 1. So by default the function examines exactly one bar:
the newest one. The barcomplete guard, meanwhile, is false on exactly one bar: the newest
one. Put the two documented defaults together and the value AlertIf() reads is false by
construction, on every execution, forever. The guard and the lookback window have no bar in
common.
The fix is one argument. Pass a lookback of at least 2, so the window reaches back to the
most recent bar that has actually finished:
Fragment — not a complete formula
barcomplete = BarIndex() < LastValue( BarIndex() );// The sixth argument is the lookback. Left at its default of 1 this alert// examines only the bar that barcomplete has just excluded, and never fires.AlertIf( barcomplete AND condition, "", "Text", 1, 1+2+4+8, 2 );Widening the lookback brings back the repetition problem in a controlled form: for the whole duration of the bar following a signal, that signal is still inside the examined window. This is where flag bit 8 stops being a backstop and starts being the mechanism, because “same date/time” is precisely what a re-read of the same completed bar is. Keep it on, and if you would rather own that behaviour in code, store the last alerted bar stamp in a static variable and compare against it.
Two related settings belong here. The chart refresh interval lives in
Tools -> Preferences -> Intraday and defaults to three seconds, so an unguarded alert on a
live chart gets roughly twenty chances per minute to fire. Entering zero in that field
requests a refresh on every trade; the manual states plainly that the Standard edition will
not allow it, so that one is Professional-only.
Verifying that an alert fires exactly once
Section titled “Verifying that an alert fires exactly once”AlertIf() returns nothing, so you cannot ask it what it did. The way round that is to
establish, from the data, how many times it should have fired, then check that the Alert
Output window agrees.
Goal. Turn “I think this alerts once” into a number you counted.
Complete runnable AFL
// alert-fire-once-audit.afl// Part 25 - Duplicate Alerts and How to Stop Them//// Answers the question you must answer BEFORE you let AlertIf() near a rule:// how many times should this alert have fired over this history?//// Run it in Analysis -> Explore over the same symbol, range and interval you// intend to alert on. Every row is one bar. The running counts on the final// row are the numbers your Alert Output window has to reproduce.//// Assumptions declared up front:// - Static audit of stored bars. No real-time feed, no Professional edition,// no market open.// - The rule under test is deliberately trivial - price against one moving// average - so the arithmetic stays checkable by eye. Replace the three// lines under "The rule under test" with your own and the audit still// works unchanged.// - Counts are cumulative from the first bar the Analysis range covers, so// changing the range changes every count. Fix the range before comparing.
_SECTION_BEGIN( "Fire-once audit" );
AvgPeriod = Param( "Average period", 50, 2, 400, 1 );ShowAll = ParamToggle( "Rows", "Interesting bars only|Every bar", 0 );
// --- The rule under test -------------------------------------------------Average = MA( Close, AvgPeriod );
// STATE: true on every bar of the run. This is what people alert on by// accident, and it is the whole reason this page exists.StateCond = Close > Average;
// EVENT: true only on the bar where the state began.EventCond = Cross( Close, Average );
// The opposite event, which is what resets the latch below.ResetCond = Cross( Average, Close );
// EVENT, de-bounced: ExRem passes the first event through and blocks every// later one until a reset occurs, so a rule that flickers around the average// still produces one signal per genuine swing.CleanCond = ExRem( EventCond, ResetCond );
// --- The counts that matter ----------------------------------------------StateCount = Cum( StateCond );EventCount = Cum( EventCond );CleanCount = Cum( CleanCond );
// The newest bar of the array is still forming on a live or replayed chart.// 1 = the bar can no longer change, 0 = it still can.BarComplete = BarIndex() < LastValue( BarIndex() );
Filter = ShowAll OR StateCond OR EventCond OR ResetCond;
AddColumn( Close, "Close", 1.4 );AddColumn( Average, "Average", 1.4 );AddColumn( StateCond, "State", 1.0 );AddColumn( EventCond, "Event", 1.0 );AddColumn( ResetCond, "Reset", 1.0 );AddColumn( CleanCond, "Clean event", 1.0 );AddColumn( StateCount, "State bars", 1.0 );AddColumn( EventCount, "Events", 1.0 );AddColumn( CleanCount, "Clean events", 1.0 );AddColumn( BarComplete, "Complete", 1.0 );
_SECTION_END();How it works. The Exploration computes the same rule three ways and puts them side by
side. StateCond is the naive condition. EventCond is the crossover. CleanCond is the
crossover after ExRem() with the opposite crossover as its reset. Each gets a running total
via Cum(), so the final row of the output carries the three counts. The last column reports
whether the bar was complete.
Key functions. Cum( ARRAY ) returns the running sum, which on a Boolean array is a
running count. ExRem( ARRAY1, ARRAY2 ) is the de-bouncer described above.
BarIndex() < LastValue( BarIndex() ) is the completed-bar guard.
Expected result. One row per interesting bar, with three counts that get further apart as you scroll down. On a liquid daily symbol over five years with a 50-bar average, expect the state count in the high hundreds, the event count in the tens, and the clean count equal to or slightly below the event count. If the clean count is not well below the state count, you have not yet fixed anything.
Test it. Run the same Exploration twice without changing a thing; the counts must be identical. Then narrow the Analysis range by a year and confirm every count drops. If a count is unchanged after shortening the range, you have a range setting that is not what you think it is — a much more common problem than a broken formula.
Common errors. Comparing this Exploration’s counts against alert lines produced with a different parameter value, or over a different Analysis range, will disagree for reasons that have nothing to do with the alert. Fix the parameters and the range first, and write them down.
Extension. Add a fourth condition using ExRemSpan() and a fifth using Flip(), and
compare their counts against ExRem() on the same data. Seeing three de-bouncers disagree on
the same series is the fastest way to understand what each one actually promises.
The procedure
Section titled “The procedure”Proving an alert fires exactly once, with no live feed
- Fix the inputsOne symbol, one interval, one Analysis range, parameters written down. Change nothing after this point
- Count the ground truthRun the audit Exploration. Note the clean-event count and the exact bar timestamps
- Check the windowIf a completed-bar guard is in use, lookback must be at least 2 or the guard leaves the window empty
- Prove the alert path worksSet flags to 1+2, run the Scan twice. Two identical lines means the plumbing is live
- Prove the suppression worksRestore the default flags, run the Scan twice more. One line, not two
- Walk the historyBar Replay from before the first signal, stepping forward. The alert for bar T must appear on the step to T+1
- ReconcileAlert Output line count and timestamps must match the Exploration list exactly. Any difference is a defect, not noise
Two details make the replay step work. Bar Replay is global — it truncates the data at the playback position for charts and for the Analysis window alike — so stepping it forward one bar is exactly equivalent to one new bar arriving. And because the alert formula excludes the newest bar, the alert for a signal on bar T appears when you step onto bar T+1, not when you step onto T. That one-step offset is a prediction you can check, and if the alert appears on the same step as the signal, your completed-bar guard is not doing its job.
Two outcomes are diagnostic rather than fatal. No lines at all points at the lookback window before it points at anything else — check that argument before you touch the logic. More lines than the Exploration counted points at the forming bar, and the test in the next section will show you it happening.
A state condition is true on every bar of a run, and an alert examines the newest bar every
time the formula executes — so a correct condition produces incorrect behaviour. Four
independent layers fix it, and they are not alternatives. Alert on events rather than states.
De-bounce with ExRem(), choosing the reset that expresses when you want to hear about it
again. Exclude the forming bar with BarIndex() < LastValue( BarIndex() ), accepting one bar
of delay in exchange for a signal that cannot change its mind — and widen lookback to 2 in
the same edit, because the guard and the default lookback have no bar in common. Leave
AmiBroker’s own per-type, per-symbol suppression on, for the job it is genuinely good at,
which is saying one permitted signal once rather than deciding which signals are permitted.
Then prove it. The number of times an alert should fire is countable from stored data, and Bar Replay will walk your formula through history one bar at a time so you can watch the lines appear. An alert you have not counted is an alert you are trusting on the strength of having written it.
Check your understanding
Sources for this lesson
8 verified · checked 2026-08-31
- 01AmiBroker User's Guide — Using formula-based alerts§ Internal logic; Notesamibroker.com/guide/h_alerts.html2026-08-31
- 02AFL Function Reference — AlertIfamibroker.com/guide/afl/alertif.html2026-08-31
- 03AFL Function Reference — ExRemamibroker.com/guide/afl/exrem.html2026-08-31
- 04AFL Function Reference — ExRemSpanamibroker.com/guide/afl/exremspan.html2026-08-31
- 05AFL Function Reference — Flipamibroker.com/guide/afl/flip.html2026-08-31
- 06AmiBroker User's Guide — Bar Replayamibroker.com/guide/w_barreplay.html2026-08-31
- 07AmiBroker User's Guide — Preferences§ Intraday tabamibroker.com/guide/w_preferences.html2026-08-31
- 08AmiBroker User's Guide — The New Analysis window§ Auto repeat Scan/Exploreamibroker.com/guide/h_newanalysis.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.