State versus Event: The Distinction That Breaks Formulas
Here are two lines of AFL. They differ by one function call.
Fragment — not a complete formula
Buy = Close > MA( Close, 50 );Buy = Cross( Close, MA( Close, 50 ) );Both compile. Both produce a Boolean array of the same length. Both can be handed to a scan, an alert, an exploration or the backtester without complaint. On a ten-year daily chart the first might be true on fourteen hundred bars and the second on forty-six.
They answer different questions, and the difference is not a detail of AFL. It is the difference between “price is above the average” and “price has just moved above the average” — between a condition and a moment. Nearly every formula that looks correct and behaves strangely has this at its root, which is why this lesson is longer than its neighbours and why the challenge at the end of this part is built entirely from it.
Two kinds of Boolean array
Section titled “Two kinds of Boolean array”The course’s terminology page defines both, and it is worth restating precisely:
- A state is a condition that is true over a span of bars.
Close > MA(Close, 50)is a state. So isRSI(14) < 30,Volume > MA(Volume, 50), and every “is it the case that…” question. - An event is the bar on which something became true.
Cross(Close, MA(Close, 50))is an event. So is the first bar of a run, the bar a stop was hit, the bar a new high was set.
The distinction is not about what the arrays contain — both hold ones and zeros — but about their shape over time. A state comes in runs. An event comes in isolated bars.
One state, two events, over the same ten bars
| Bar | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
Fast | 9 | 10 | 12 | 13 | 12 | 10 | 9 | 8 | 10 | 12 |
Slow | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 | 11 |
STATE Fast > Slowtrue on 4 bars, in two runs | 0 | 0 | 1 | 1 | 1 | 0 | 0 | 0 | 0 | 1 |
EVENT Cross( Fast, Slow )true on 2 bars: the first of each run | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
EVENT Cross( Slow, Fast )true on the bar the run ended | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
Notice what the three rows have in common: nothing about their type or shape tells
AmiBroker which one you meant. Assigning any of them to Buy is legal. The
formula that results is not wrong in the sense of being broken; it is wrong in the
sense of answering a different question.
Cross() is an edge detector
Section titled “Cross() is an edge detector”Fragment — not a complete formula
Cross( ARRAY1, ARRAY2 )The documentation is short and exact: it gives 1 on the bar ARRAY1 crosses
above ARRAY2, and 0 otherwise. There is no direction argument and no third
argument. To detect a crossing in the other direction, the page prescribes
swapping the arguments: Cross( ARRAY2, ARRAY1 ).
That means the two calls below are opposite questions and are trivially easy to mistype:
Fragment — not a complete formula
GoingUp = Cross( Close, Average ); // Close crossing ABOVE AverageGoingDown = Cross( Average, Close ); // Average crossing above Close, // i.e. Close crossing BELOW AverageCross( a, b ) and Cross( b, a ) are different arrays
| Bar | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
Cross( Fast, Slow )up-crossings only | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Cross( Slow, Fast )down-crossings only | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
Both, on the same barnever - they are mutually exclusive | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Three further properties are worth knowing.
A crossing needs a previous bar. Cross compares the current relationship
with the previous one, so it cannot be true on the first bar of the array. The
same applies at the end of a warm-up run: until both inputs have real values there
is no previous relationship to compare with.
Crossing against a constant is normal. The official page’s own example crosses
an array with a moving average, and crossing an oscillator with a level —
Cross( RSI( 14 ), 30 ) — is idiomatic and common.
Ties are not documented. The official page does not define what happens when
the two arrays are exactly equal on a bar, or when they touch and separate without
strictly crossing. There is a widely circulated identity claiming that Cross(a,b)
is exactly a > b AND Ref(a,-1) <= Ref(b,-1). This course does not assert it,
because it is folklore rather than documentation, and because exact equality is
common in practice — round numbers, flat periods, low-priced instruments, and any
comparison against an integer level.
Failure one: a state used where an event was wanted
Section titled “Failure one: a state used where an event was wanted”This is the more common of the two, and its symptoms differ wildly depending on where the array ends up.
Fragment — not a complete formula
// Intended: "buy when price moves above the average."// Written: "buy on every bar price is above the average."Buy = Close > MA( Close, 50 );Sell = Close < MA( Close, 50 );In a Scan or Exploration you get a flood. The User’s Guide is explicit that an Exploration produces one report line per bar that passes the filter when the range covers multiple quotations, so a state that is true on fourteen hundred bars produces fourteen hundred rows — per symbol. People commonly conclude that AmiBroker is misconfigured. It is doing exactly what it was told.
On a chart you get a wall. PlotShapes() draws an arrow on every true bar,
so instead of a handful of markers you get a solid green band under a third of the
chart, which conveys nothing at all.
In an alert you get one notification per bar, for as long as the state lasts. On intraday data that is a notification every few minutes for days.
In the backtester — and this is the part that surprises people — you may get a
perfectly sensible result. AmiBroker’s default backtest mode removes redundant
entry signals: the User’s Guide’s portfolio-backtesting chapter states that after
an initial entry, subsequent entry signals are ignored until a matching exit, and
that this process is the same as ExRem() provides. So the same defective array
produces an unusable scan, an unreadable chart, a torrent of alerts — and a
backtest report that looks fine.
Failure two: an event used where a state was wanted
Section titled “Failure two: an event used where a state was wanted”The opposite mistake is rarer and much easier to diagnose, because it usually produces nothing at all.
Fragment — not a complete formula
// Intended: "buy on a crossover, but only while the trend is up."// Written: two events that would both have to happen on the same bar.Buy = Cross( Close, MA( Close, 20 ) ) AND Cross( MA( Close, 50 ), MA( Close, 200 ) );Two events ANDed together
| Bar | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
|---|---|---|---|---|---|---|---|---|---|---|
Event A | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
Event B | 0 | 1 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
Event A AND Event Bno bars at all | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
The second condition was meant as a trend filter — a state describing the market backdrop — and was written as an event. A fifty-over-two-hundred crossing happens perhaps twice a decade. Requiring it to coincide with an entry trigger on the same bar reduces the system to a handful of trades in a lifetime, or none.
The corrected version keeps the trigger as an event and the filter as a state:
Fragment — not a complete formula
TrendUp = MA( Close, 50 ) > MA( Close, 200 ); // STATE: the backdropTrigger = Cross( Close, MA( Close, 20 ) ); // EVENT: the moment
Buy = Trigger AND TrendUp;Read that as a sentence and the grammar is obvious: at the moment of the trigger, while the trend is up. Triggers are events. Filters, regimes, setups and context are states. Part 27 formalises the vocabulary — setup, trigger, regime — around exactly this split.
Converting deliberately
Section titled “Converting deliberately”Both conversions are easy. What matters is doing them on purpose and knowing what each one discards.
| I have | I want | Write |
|---|---|---|
| State | The bar it began | Cross( a, b ), or the rising edge of the state |
| State | The bar it ended | Cross( b, a ), or the falling edge |
| Two events | The state between them | Flip( startEvent, endEvent ) |
| Event | A window of eligibility | BarsSince( event ) < N |
| Event | The value captured at it | ValueWhen( event, array ) |
| Event | The extreme reached since it | HighestSince( event, High ) |
| Repeated signals | One per run | ExRem( signal, opposite ) |
State to event
Section titled “State to event”If the state came from comparing two arrays, Cross() is the direct expression.
If it did not — a state assembled from several conditions, say — you can take its
rising edge explicitly:
Fragment — not a complete formula
State = TrendUp AND LiquidityOK AND NOT EarningsWeek;RisingEdge = IsTrue( State ) AND NOT IsTrue( Ref( State, -1 ) );IsTrue() is doing real work in that line. It maps Null to 0 and any non-zero
value to 1, so the warm-up bars at the left edge cannot poison the result the way
a bare AND on Null operands would. Without it, both operands are Null at the
left edge and the whole expression evaluates to Null rather than to false.
This is the rising edge of State, by definition. It is not being claimed as
identical to Cross(): Cross compares two arrays and its tie handling is
undocumented, whereas this expression is about one array and has no ties to
resolve.
Event to state
Section titled “Event to state”Fragment — not a complete formula
Started = Cross( Close, Average );Ended = Cross( Average, Close );
InPosition = Flip( Started, Ended );Flip( ARRAY1, ARRAY2 ) is documented as a latch: it returns 1 from the first
true in ARRAY1 and keeps returning 1 until a true occurs in ARRAY2, which
resets it to 0 until the next true in ARRAY1. That is precisely “the state
between two events”, and it is the tool for shading a chart, computing exposure,
or gating other rules on whether a position is notionally open.
Two cautions come with it. Flip is sticky: if the reset event never fires,
the state stays on for the rest of the chart. That is correct behaviour, and it is
the reason a fallback reset — a stop, a time limit — belongs in the second
argument. And what happens when both arrays are true on the same bar is not
documented; if your two conditions can coincide, verify which one wins before you
depend on it.
Event to a window
Section titled “Event to a window”The middle ground is often what you actually want: not the single bar, and not an unbounded state, but a few bars of eligibility after something happened.
Fragment — not a complete formula
Setup = BarsSince( Cross( Close, MA( Close, 50 ) ) ) < 5;Trigger = High > Ref( High, -1 );
Buy = Setup AND Trigger;That reads as “within five bars of the crossover, on the first bar that takes out the previous bar’s high” — a setup and a trigger, in the vocabulary Part 27 uses.
Seeing the difference
Section titled “Seeing the difference”Put a state, both of its boundary events, and a state rebuilt from those events on the same chart, with counters that report how many bars each one is true. The counters turn a conceptual distinction into two numbers that are obviously different.
The complete formula
Section titled “The complete formula”Complete runnable AFL
// state-event-visualiser.afl// Part 9 - State versus Event//// Puts a state and the events at its two boundaries on the same chart, so the// difference can be seen rather than argued about://// STATE Close > MA( Close, N ) true on every bar the condition holds// EVENT Cross( Close, MA( Close, N ) ) true on the first of those bars only// EVENT Cross( MA( Close, N ), Close ) true on the bar the state ends//// The counters in the title report how many bars each array is true over the// delivered range. An array that is true on 1,400 bars and an array that is true// on 46 bars are not interchangeable inputs to anything.//// Assumptions:// - The moving-average rule is a vehicle for the distinction, not a// recommendation. Any two arrays would make the same point.// - The counts depend on how much history the chart delivered, so they will// change if you zoom or change symbol.// - The rebuilt state is expected to differ from the original over the warm-up// bars, where the average is still empty and no crossing has occurred yet.// - No Buy or Sell variable is set: this formula describes, it does not trade.
_SECTION_BEGIN( "State versus event" );
MaPeriod = Param( "Average length (bars)", 50, 5, 250, 1 );Average = MA( Close, MaPeriod );
// ---------------------------------------------------------------------------// One state, two events// ---------------------------------------------------------------------------
AboveState = Close > Average; // STATE: a condition that persistsCrossUp = Cross( Close, Average ); // EVENT: the bar the state beginsCrossDown = Cross( Average, Close ); // EVENT: the bar the state ends
// Flip() latches on the first array and releases on the second, so this rebuilds// the state from its two boundary events. Away from the warm-up bars it should// track AboveState bar for bar.RebuiltState = Flip( CrossUp, CrossDown );
// Where the two disagree. On a clean run this is non-zero only near the left// edge, before the first crossing has happened.Disagreement = IsTrue( AboveState ) != IsTrue( RebuiltState );
// ---------------------------------------------------------------------------// Counting// ---------------------------------------------------------------------------
TotalBars = LastValue( Cum( 1 ) );StateBars = LastValue( Cum( IsTrue( AboveState ) ) );UpEvents = LastValue( Cum( CrossUp ) );DownEvents = LastValue( Cum( CrossDown ) );DisagreeBars = LastValue( Cum( Disagreement ) );
// ---------------------------------------------------------------------------// Drawing// ---------------------------------------------------------------------------
Plot( Close, "Close", colorDefault, styleCandle );Plot( Average, StrFormat( "MA(%g)", MaPeriod ), colorBlue, styleLine | styleThick );
// The ribbon shows the state. The title states it in words as well, so the chart// never depends on colour alone to carry its meaning.Plot( 1, "State ribbon", IIf( AboveState, colorPaleGreen, colorLightGrey ), styleOwnScale | styleArea | styleNoLabel | styleNoTitle, 0, 10 );
PlotShapes( CrossUp * shapeUpArrow, colorGreen, 0, Low, -20 );PlotShapes( CrossDown * shapeDownArrow, colorRed, 0, High, 20 );
_N( Title = Name() + " - " + Interval( 2 ) + " - state versus event\n" + StrFormat( "Bars delivered to this run: %g\n", TotalBars ) + StrFormat( "STATE Close > MA(%g) true on %g bar(s)\n", MaPeriod, StateBars ) + StrFormat( "EVENT Cross( Close, MA ) true on %g bar(s)\n", UpEvents ) + StrFormat( "EVENT Cross( MA, Close ) true on %g bar(s)\n", DownEvents ) + StrFormat( "Rebuilt-state disagreements: %g bar(s)\n", DisagreeBars ) + WriteIf( AboveState, "Now: state ON", "Now: state OFF" ) + " / " + WriteIf( RebuiltState, "rebuild ON", "rebuild OFF" ) );
_SECTION_END();How it works
Section titled “How it works”The three arrays at the heart of the formula come from the same two inputs, so nothing about the market explains the difference between them — only the operator used. The ribbon renders the state as a continuous band; the arrows render the two events as isolated marks.
RebuiltState = Flip( CrossUp, CrossDown ) closes the loop by reconstructing the
state from its edges, and Disagreement counts the bars where the rebuild and the
original differ. Away from the warm-up region that count should be small, and
looking at where it is not is instructive: the rebuilt state cannot know it is “on”
until the first up-crossing has happened, whereas the original state is true from
the first bar the average exists and price happens to be above it.
The counting block wraps each array in LastValue( Cum( ... ) ), which is the
running total evaluated at the final bar — the number of true bars over the
delivered range. IsTrue() guards the state before counting, so warm-up Nulls
count as false rather than propagating.
Key functions
Section titled “Key functions”Cross( ARRAY1, ARRAY2 )— the edge detector, used in both directions.Flip( ARRAY1, ARRAY2 )— the latch that turns two events back into a state.IsTrue( ARRAY )— mapsNullto 0 and non-zero to 1, so counts are clean.Cum( ARRAY )andLastValue( ARRAY )— running total, read at the last bar. Note thatLastValue()reaches the end of the data, which is exactly why it is confined here to the title and never to a rule.WriteIf( EXPRESSION, "TRUE TEXT", "FALSE TEXT" )— puts the current status into words, so the chart never depends on the ribbon’s colour alone.
Expected result
Section titled “Expected result”Test it
Section titled “Test it”Count the arrows in one screenful and compare with the number of separate bands. There should be one up arrow per band, at its first bar, and one down arrow per completed band, at the bar after its last. If arrows appear inside a band, the “event” array is not event-shaped and something is wrong.
Then change the average length in the Parameters dialog from 50 to 20. Both counts should rise, and the ratio between them should fall — a shorter average produces more, shorter runs. If the state count changes and the event counts do not, the two are not being computed from the same inputs.
Common errors
Section titled “Common errors”- Reading the ribbon as a signal. The ribbon is the state. It is on for long stretches, and its being on is not a reason to enter anything.
- Expecting the rebuild to match perfectly. It cannot match before the first crossing. A non-zero disagreement count at the left edge is correct.
- Comparing the counts across symbols with different history lengths. The title reports the bar count for exactly this reason.
Extension
Section titled “Extension”Add a third event: the bar on which the state has been continuously true for
twenty bars, written as BarsSince( CrossUp ) == 20. Count it too. It is neither
a state nor a boundary event but a derived event — the kind that lets a rule act
on the maturity of a condition rather than on its beginning — and it is worth
noticing that the same array toolkit produced it.
The diagnostic habit
Section titled “The diagnostic habit”You can determine which kind of array you have without reading the formula. Count the true bars, and measure the longest unbroken run:
Fragment — not a complete formula
TrueBars = LastValue( Cum( IsTrue( Signal ) ) );RunLength = BarsSince( NOT IsTrue( Signal ) );LongestRun = LastValue( Highest( RunLength ) );A longest run of 1 means the array is event-shaped. A longest run of 60 means it is a state. A true-bar count in the thousands on a ten-year daily chart means a state; a count in the tens means events. Neither number requires you to understand the formula that produced the array, which is what makes this the first thing to run on inherited code.
The challenge at the end of this part is six formulas that look right and are not. Four of them are identified by those two numbers alone.
A state is true over a span of bars; an event is the bar something became true. AFL cannot tell them apart, because both are ordinary Boolean arrays of the same length, and no error message distinguishes them.
Cross( a, b ) converts a state into the event at its beginning, and swapping its
arguments gives the event at its end — a completely different array rather than
the negation of the first. Flip( start, end ) converts a pair of events back into
a state. BarsSince( event ) < N produces the middle ground of a bounded window.
ValueWhen and HighestSince carry information from an event forward.
A state used as a signal floods scans, charts and alerts while often leaving the backtest looking respectable, because the default backtest mode already discards redundant entries. An event used as a filter produces silence. The next lesson is about the tools people reach for when they hit the first of those symptoms, and about when reaching for them is the wrong response.
Check your understanding
Sources for this lesson
6 verified · checked 2026-08-31
- 01AFL Function Reference — Crossamibroker.com/guide/afl/cross.html2026-08-31
- 02AFL Function Reference — Flipamibroker.com/guide/afl/flip.html2026-08-31
- 03AFL Function Reference — ExRemamibroker.com/guide/afl/exrem.html2026-08-31
- 04AFL Function Reference — IsTrueamibroker.com/guide/afl/istrue.html2026-08-31
- 05AmiBroker User's Guide — Common Coding Mistakes in AFLamibroker.com/guide/a_mistakes.html2026-08-31
- 06AmiBroker User's Guide — How to create your own explorationamibroker.com/guide/h_exploration.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.