Skip to content
Level 5 · Real-Time AmiBroker UserLessonPart 25 · page 2 of 330 min
30Minutes
9AFL functions
8Sources
StandardRequires
AFL functions taught here9

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.

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

Five bars of state, three events, one alert. The reset condition here is a confirmed break below the average by a margin, which never happens in this window.
Bar12345678
Close9.8010.5010.3510.5510.4011.2011.0011.30
Average10.3010.3010.4010.4010.4510.5010.6010.70
Close > Averagethe state01010111
Cross(Close, Average)the event01010100
Reset conditionno confirmed break below00000000
ExRem(event, reset)one signal01000000
Five bars of state, three events, one alert. The reset condition here is a confirmed break below the average by a margin, which never happens in this window. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

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.

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.

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 ), where NewSession is 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 );

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
// 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();

Download alert-fire-once-audit.afl66 lines

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.

Proving an alert fires exactly once, with no live feed

  1. Fix the inputsOne symbol, one interval, one Analysis range, parameters written down. Change nothing after this point
  2. Count the ground truthRun the audit Exploration. Note the clean-event count and the exact bar timestamps
  3. Check the windowIf a completed-bar guard is in use, lookback must be at least 2 or the guard leaves the window empty
  4. Prove the alert path worksSet flags to 1+2, run the Scan twice. Two identical lines means the plumbing is live
  5. Prove the suppression worksRestore the default flags, run the Scan twice more. One line, not two
  6. Walk the historyBar Replay from before the first signal, stepping forward. The alert for bar T must appear on the step to T+1
  7. ReconcileAlert Output line count and timestamps must match the Exploration list exactly. Any difference is a defect, not noise
Every step runs on stored data. Nothing here needs a subscription or an open market.

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

Question 1. A five-minute chart runs `AlertIf( Cross( Close, Level ), "", "Break", 1 );` with no completed-bar guard, on a live feed with the default three-second refresh. The price ticks above Level at 10:31 and back below at 10:33, within one bar. What is the most likely outcome?
AlertIf( Cross( Close, Level ), "", "Break", 1 );
Show the answer and why

Answer: An alert at 10:31 for a breakout that did not survive the bar

Cross() evaluates against the forming bar, whose Close moves in both directions. It becomes true on the refresh after 10:31 and the alert fires there. The bar then closes below Level, so the signal you were told about does not exist in the finished data. Bit 8 of flags prevents a repeat for the same bar stamp, which is why the answer is one bad alert rather than forty.

Question 2. Which of these are true of AmiBroker built-in repeat suppression? Select all that apply.
Show the answer and why

Answer: It is keyed on the alert type, so two alerts sharing a type can silence each other, It stores the type of the last alert per symbol, It can be switched off by passing flags of 1+2

The first three are documented. The fourth is not: nothing in the AFL reference reads or resets the alert state machine, which is precisely why the verification procedure counts signals in an Exploration rather than interrogating the alert system.

Question 3. You want an alert on a breakout, but no more than one per trading day even if the level is broken, lost and broken again. Which reset argument to ExRem() expresses that?
Show the answer and why

Answer: A change of calendar day

ExRem blocks signals until its second argument is true, so a day-change array re-arms the latch once per session and nothing else does. The opposite crossover would re-arm mid-session; a fixed bar count would re-arm at an arbitrary point unrelated to the session; passing the condition itself would defeat the latch entirely.

Question 4. Under Bar Replay, with a completed-bar guard in place, a signal occurs on bar T. On which step does the alert appear?
Show the answer and why

Answer: The step onto bar T+1

While bar T is the newest bar, the guard excludes it. Stepping to T+1 makes bar T complete, and the alert fires then. This one-step offset is the most useful single check in the whole procedure: if the alert arrives on the step onto T, the guard is missing or wrong.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AmiBroker User's Guide — Using formula-based alerts§ Internal logic; Notesamibroker.com/guide/h_alerts.html2026-08-31
  2. 02AFL Function Reference — AlertIfamibroker.com/guide/afl/alertif.html2026-08-31
  3. 03AFL Function Reference — ExRemamibroker.com/guide/afl/exrem.html2026-08-31
  4. 04AFL Function Reference — ExRemSpanamibroker.com/guide/afl/exremspan.html2026-08-31
  5. 05AFL Function Reference — Flipamibroker.com/guide/afl/flip.html2026-08-31
  6. 06AmiBroker User's Guide — Bar Replayamibroker.com/guide/w_barreplay.html2026-08-31
  7. 07AmiBroker User's Guide — Preferences§ Intraday tabamibroker.com/guide/w_preferences.html2026-08-31
  8. 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.