Project: Trend Regime Indicator
“The market is in an uptrend” is the kind of sentence that sounds like information and usually is not, because the speaker rarely says what would make it false. This project turns it into a measurement: a classifier that labels every bar UP, DOWN or RANGE according to rules written down in advance, and that reports its own lag on the chart so nobody can mistake the label for a forecast.
Objective
Section titled “Objective”Build a chart formula that:
- defines three regimes from two objective measurements, both computed only from closed bars;
- requires a condition to persist before it accepts a label, so the classification does not flicker;
- colours the chart background by the current regime and repeats the label as a ribbon;
- marks the bar on which the classification changed;
- reports the regime, how long it has held, and the minimum structural lag of the classifier, in the title;
- offers a “sticky” mode and is honest about what that mode costs.
Prerequisites
Section titled “Prerequisites”The four lessons of this part, plus Ref(), Sum(), IIf() and Boolean arrays from Part 9.
Daily data on a liquid instrument with at least five years of history is ideal, because you
want several genuine regime changes in view.
Defining a regime objectively
Section titled “Defining a regime objectively”A definition is only objective if two people applying it to the same data get the same answer. That rules out “it looks like a trend” and it rules out anything involving a line you drew.
This classifier uses two measurements.
Direction and steepness of a long average. The slope of MA( Close, TrendPeriod ) measured
over SlopeBars bars. A raw slope is in price units per bar, which is useless across
instruments — a slope of 0.4 is steep on a £12 share and invisible on a 7,000-point index. So
the slope is divided by ATR( AtrPeriod ), giving ATR units per bar. Now a threshold of
0.05 means the same thing everywhere: the average is climbing at five per cent of a typical
bar’s range, per bar.
Which side of the average price is on. Close > TrendLine or Close < TrendLine. This is
crude and it is deliberate: it stops the classifier calling an uptrend while price is beneath
its own long average, which is the sort of result that makes a reader distrust everything else
the tool says.
Both conditions must hold together, and both must hold on every one of the last ConfirmBars
bars. Sum() over a Boolean array counts how many bars in the window were true, so requiring
the count to equal the window length is the same as requiring all of them.
From price to label
- MA slopeover SlopeBars bars
- ÷ ATRmakes it comparable
- Threshold + sideraw up / raw down
- Confirmationall of the last N bars
- LabelUP, DOWN or RANGE
Anything the raw conditions do not confirm is RANGE. That is not a third measurement; it is the absence of the first two, which is the honest way to define it. A classifier with a positive definition of “range” — low ADX, narrow Bollinger width, whatever — is making an additional claim that then needs its own justification.
The complete formula
Section titled “The complete formula”Complete runnable AFL
/* * Trend regime classifier - Part 10 project. * * What it does * Labels every bar as UP, DOWN or RANGE from two objective measurements: * the slope of a long average, expressed in ATR units per bar so that it is * comparable across instruments, and the side of that average price is on. * A label is only issued once the condition has held for a set number of * consecutive bars. * * What it is not * - It is not a forecast. Every input is a function of bars that have * already closed, so the label describes the recent past. * - It has irreducible lag. The average needs TrendPeriod bars, the slope is * measured over SlopeBars bars, and the confirmation needs ConfirmBars * bars, so a change of regime can only be announced well after the market * began to change. The title reports that lag on every bar. * - The label on the current, unfinished bar can change before the bar * closes. Only completed bars carry a settled label. * * Assumptions * - Chart pane formula, applied to the price pane. * - Any interval. On very short intervals the ATR normalisation still works, * but the confirmation window should be widened. */
_SECTION_BEGIN( "Trend regime" );
TrendPeriod = Param( "Trend average periods", 100, 20, 400, 5 );SlopeBars = Param( "Slope measured over (bars)", 20, 2, 100, 1 );SlopeThreshold = Param( "Slope threshold (ATR per bar x 100)", 5, 0, 50, 1 ) / 100;AtrPeriod = Param( "ATR periods", 20, 2, 100, 1 );ConfirmBars = Param( "Confirmation bars", 5, 1, 30, 1 );Sticky = ParamToggle( "Carry last direction through ranges", "No|Yes", 0 );ShowBackground = ParamToggle( "Regime background", "No|Yes", 1 );ShowChanges = ParamToggle( "Mark regime changes", "No|Yes", 1 );
UpTint = ParamColor( "Up colour", colorPaleGreen );DownTint = ParamColor( "Down colour", colorRose );RangeTint = ParamColor( "Range colour", colorLightGrey );
TrendLine = MA( Close, TrendPeriod );Volatility = ATR( AtrPeriod );
// Slope in ATR units per bar. Dividing by ATR is what makes the same threshold// mean the same thing on a 12-dollar share and a 4,000-point index.Slope = ( TrendLine - Ref( TrendLine, -SlopeBars ) ) / ( SlopeBars * Volatility );
RawUp = Slope > SlopeThreshold AND Close > TrendLine;RawDown = Slope < -SlopeThreshold AND Close < TrendLine;
// Confirmation: the raw condition must have been true on every one of the last// ConfirmBars bars. Sum() of a Boolean array counts the true bars in the window.ConfirmedUp = Sum( RawUp, ConfirmBars ) == ConfirmBars;ConfirmedDown = Sum( RawDown, ConfirmBars ) == ConfirmBars;
// One array holds the whole classification: 1 up, -1 down, 0 range.Direction = IIf( ConfirmedUp, 1, IIf( ConfirmedDown, -1, 0 ) );
// Optional stickiness: carry the last confirmed direction forward instead of// falling back to RANGE. This makes the chart calmer and the classifier less// honest, because a stale label is still displayed as a current one.if( Sticky ) Regime = Nz( ValueWhen( Direction != 0, Direction ), 0 );else Regime = Direction;
RegimeTint = IIf( Regime == 1, UpTint, IIf( Regime == -1, DownTint, RangeTint ) );
// AmiBroker has no per-bar pane background. The documented way to colour bars// behind everything else is a full-height own-scale area plot: the constant 1// against a 0..1 scale fills the pane, and ZOrder -2 puts it behind the grid,// which sits at ZOrder 0.if( ShowBackground ) Plot( 1, "Regime background", ColorBlend( RegimeTint, GetChartBkColor(), 0.75 ), styleArea | styleOwnScale | styleNoLabel | styleNoTitle, 0, 1, 0, -2 );
Plot( Close, "Price", colorDefault, styleCandle | styleNoTitle | GetPriceStyle() );Plot( TrendLine, "Trend " + NumToStr( TrendPeriod, 1.0 ), colorBlueGrey, styleLine | styleThick );
// A ribbon repeats the label at the foot of the pane, so the regime is still// readable when the background is switched off or the theme is dark.Plot( 2, "Regime", RegimeTint, styleArea | styleOwnScale | styleNoLabel | styleNoTitle, -0.5, 100 );
Changed = Regime != Ref( Regime, -1 );
if( ShowChanges ){ GraphXSpace = 8; PlotShapes( IIf( Changed, shapeSmallSquare, shapeNone ), colorDarkGrey, 0, High, 14 );}
// The structural lag, stated in the tool itself rather than in a footnote.// The confirmation window and the slope window are both explicit; the averaging// window adds more lag on top of these two, which is why this is a minimum.MinimumLag = ConfirmBars + SlopeBars;
RegimeWord = WriteIf( Regime == 1, "UP", WriteIf( Regime == -1, "DOWN", "RANGE" ) );
_N( Title = StrFormat( "%s regime: %s for %g bars slope %g ATR/bar minimum structural lag %g bars\n" + "This is a description of bars that have already closed, not a forecast.", Name(), RegimeWord, SelectedValue( BarsSince( Changed ) ), SelectedValue( Slope ), MinimumLag ) );
_SECTION_END();How it works
Section titled “How it works”The measurements
Section titled “The measurements”Fragment — not a complete formula
Slope = ( TrendLine - Ref( TrendLine, -SlopeBars ) ) / ( SlopeBars * Volatility );The numerator is the average’s change over the window. Dividing by SlopeBars converts it to
change per bar; dividing by ATR( AtrPeriod ) converts price units to volatility units. The
result is a small number, typically well under 0.2, which is why the parameter is expressed as
“ATR per bar × 100” — sliders are much easier to use in whole numbers.
ATR is passed an explicit period because, unlike almost every other indicator in AFL, it has
no documented default. ATR() with no argument is not documented behaviour, and the course
never writes it.
Confirmation
Section titled “Confirmation”Fragment — not a complete formula
ConfirmedUp = Sum( RawUp, ConfirmBars ) == ConfirmBars;RawUp is a Boolean array, so Sum() counts its true bars over the trailing window. Requiring
the count to equal the window length means every bar in it was true. There is no smoothing
here and no averaging of a condition: a single false bar resets the requirement, which is what
makes the classification stable rather than merely slow.
One array holds the classification
Section titled “One array holds the classification”Fragment — not a complete formula
Direction = IIf( ConfirmedUp, 1, IIf( ConfirmedDown, -1, 0 ) );Nested IIf() collapses the three cases into one numeric array, which is much easier to work
with than three Boolean arrays. It also makes the mutual exclusion structural: a bar cannot be
both UP and DOWN, because the outer IIf decides first.
The two conditions cannot both be true anyway — one needs Close > TrendLine and the other
Close < TrendLine — but relying on that would be relying on a coincidence of the current
definition. The nested form survives a future change to the rules.
Sticky mode, and why it is off by default
Section titled “Sticky mode, and why it is off by default”Fragment — not a complete formula
if( Sticky ) Regime = Nz( ValueWhen( Direction != 0, Direction ), 0 );else Regime = Direction;ValueWhen( condition, array ) returns the value the array had on the most recent bar where
the condition was true, carried forward. So sticky mode replaces every RANGE bar with the last
directional label. Nz() handles the left edge, where there has not yet been a directional
label to carry, by substituting 0.
The chart looks much calmer with it on. It is also less honest: a bar labelled UP under sticky mode may be a bar where the up conditions stopped holding weeks ago. The tool offers the mode because people want it, defaults it off, and says so in the code comment.
Background colouring
Section titled “Background colouring”AmiBroker has no per-bar pane background. SetChartBkColor() sets one colour for the whole
pane, which cannot vary across bars. The documented way to get a per-bar background is a plot:
Fragment — not a complete formula
Plot( 1, "Regime background", ColorBlend( RegimeTint, GetChartBkColor(), 0.75 ), styleArea | styleOwnScale | styleNoLabel | styleNoTitle, 0, 1, 0, -2 );A constant of 1 against an own scale running 0 to 1 fills the full height of the pane; the
colour argument is an array, so the fill changes colour bar by bar; and ZOrder -2 puts it
behind the grid, which sits at ZOrder 0. ColorBlend at a factor of 0.75 mixes the regime
colour three-quarters of the way towards the pane’s own background, which keeps it as a tint
rather than a wall of colour — and, because it blends towards GetChartBkColor(), it stays
sensible in both light and dark themes.
The ribbon at the foot of the pane repeats the same information at two per cent of pane height. That redundancy is deliberate: colour alone should never be the only way to read a chart, and the ribbon plus the title’s regime word give two more channels.
Transitions
Section titled “Transitions”Fragment — not a complete formula
Changed = Regime != Ref( Regime, -1 );A change of label is a change in the array from one bar to the next. BarsSince( Changed )
then reports how long the current label has held, which is the single most useful number the
tool produces — a regime that has held for three bars deserves a different amount of confidence
from one that has held for ninety.
On the very first bar Ref( Regime, -1 ) has no previous value, so the comparison there is not
meaningful. That affects one bar at the left edge of the loaded range and is not worth
special-casing, but it is worth knowing when you are counting transitions.
The lag you cannot remove
Section titled “The lag you cannot remove”This is the part of the project that matters most, and it is why the formula prints its own lag on every bar.
Three separate delays are stacked in this classifier.
The averaging window. MA( Close, 100 ) responds to a change in price gradually. A step
change in price takes 100 bars to be fully reflected in the average and roughly half that to
move it appreciably.
The slope window. The slope compares the average now with the average SlopeBars bars ago,
so it cannot register a turn that happened fewer than SlopeBars bars back.
The confirmation window. The label is only issued once the raw condition has held for
ConfirmBars consecutive bars, which by construction means at least ConfirmBars bars after
the condition first became true.
When the market turned, and when the label said so
Add the last two together and you get the number the title reports as the minimum structural
lag: ConfirmBars + SlopeBars. It is a minimum, not an estimate, because the averaging
window adds an unquantified amount on top of it and because the raw condition itself may not
become true immediately after a turn.
There is one further honesty point about the right-hand edge. The label on the current,
unfinished bar can change before that bar closes, because Close is still moving. Only
completed bars carry a settled label. When you use this tool, read the second-to-last bar.
What you should see
Section titled “What you should see”Applied to a daily chart of a liquid instrument over several years: broad tinted stretches of green during sustained advances, pink during sustained declines, and grey during the periods where the classifier declines to commit. Small grey squares above the bars where the label changed. A ribbon along the foot of the pane repeating the colour. A title reading something like:
regime: UP for 63 bars slope 0.081 ATR/bar minimum structural lag 25 bars
The grey stretches should be uncomfortable at first. A classifier that is never uncertain is one that has been tuned to hide its uncertainty.
Test it
Section titled “Test it”- Count the transitions. Over ten years of daily data with the default settings, count the label changes. If there are hundreds, the confirmation window is too short for the instrument; if there are two, it is too long to be informative. Neither is “wrong” — but you should know which regime of the classifier’s own behaviour you are in.
- Compare against your own reading. Print or screenshot a five-year stretch with the background off, mark where you think the trends were, then turn the background on. The interesting cases are the disagreements, especially where the tool says RANGE and you said trend.
- Perturb the parameters. Change
ConfirmBarsfrom 5 to 6 and see how much of the classification moves. A classifier whose output changes substantially for a one-bar change in a parameter is fitted to the sample, not measuring the market. Do the same withTrendPeriodfrom 100 to 110. - Cross-instrument sanity. Apply it unchanged to three instruments with very different price levels — a large-cap share, an index, a currency pair if you have one. Because the slope is in ATR units, the same threshold should behave comparably on all three. If one of them is permanently RANGE, the ATR normalisation is not doing its job on that data, and the most likely cause is a data-quality problem such as gaps or a long flat stretch.
- Verify the lag empirically. Find a clear turn. Note the bar where price bottomed and the
bar where the label became UP. The gap should be at least
ConfirmBars + SlopeBarsand typically much more. If it is ever less, something is reading forward and you have a bug — check that noRef()uses a positive shift. - Sticky comparison. Turn sticky mode on and off over the same range. Count how many bars change from RANGE to a directional label. That number is the amount of uncertainty the mode is concealing.
Common errors
Section titled “Common errors”- The whole chart is one colour. The slope threshold is too low, so almost every bar
qualifies. Raise it, or check that
ATRis receiving a sensible period — an ATR near zero on a flat or badly padded series makes the normalised slope enormous. - The background hides the candles. Lower the
ColorBlendfactor towards the background, or check the ZOrder: at 0 or above, the fill is drawn in front of the grid and can be drawn in front of price. - The ribbon is invisible. Its own scale must be
-0.5, 100for a value of 2 to appear as a band at the foot; if those two numbers landed in the wrong argument slots you get nothing. BarsSincereports a huge number at the left edge. There has been no transition yet within the loaded data. Not a bug.- Labels differ between the chart and an Exploration of the same formula. Parameter values are stored per Chart ID, and Analysis runs under Chart ID 0. Set the parameters in the Analysis window too.
- The current bar’s label keeps changing. Correct behaviour. The bar has not closed.
Extensions
Section titled “Extensions”A second opinion. Add ADX() as an optional additional requirement for a directional
label — for example, require ADX( 14 ) above a threshold. ADX measures trend strength
without direction, so it is a genuinely different measurement rather than another function of
the same average. Then measure how many labels it removes and how many bars of extra lag it
costs. Note that the official page does not document ADX’s internal smoothing or its warm-up
length, so treat the early bars with suspicion.
Regime duration statistics. Instead of reporting only how long the current regime has lasted, accumulate the durations of past regimes and report the median. That gives the reader a base rate: “this regime has lasted 12 bars; the median historically was 40” is a far more useful sentence than either number alone.
A higher-timeframe gate. Part 14 introduces the timeframe functions properly. Once you have them, compute the regime on weekly bars and display it alongside the daily one. Two timeframes disagreeing is information; two timeframes agreeing mostly means the weekly one is a smoothed copy of the daily one.
Export it for testing. The chart tells you the labels look plausible. Only an exploration across a universe, and eventually a backtest, can tell you whether conditioning on the label changes anything. Part 12 builds the exploration; Part 27 onwards builds the test.
What changed
Section titled “What changed”You have a regime classifier whose rules are written down, whose output is reproducible, and whose lag is displayed rather than hidden. More importantly, you have the habit that makes such a tool safe to own: stating the delay between the world changing and the label changing, and treating the label as a description of a window of the past.
Check your understanding
Sources for this lesson
8 verified · checked 2026-08-31
- 01AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
- 02AFL Function Reference — MAamibroker.com/guide/afl/ma.html2026-08-31
- 03AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
- 04AFL Function Reference — BarsSinceamibroker.com/guide/afl/barssince.html2026-08-31
- 05AFL Function Reference — Nzamibroker.com/guide/afl/nz.html2026-08-31
- 06AFL Function Reference — Plotamibroker.com/guide/afl/plot.html2026-08-31
- 07AFL Function Reference — SetChartBkColoramibroker.com/guide/afl/setchartbkcolor.html2026-08-31
- 08AFL Function Reference — WriteIfamibroker.com/guide/afl/writeif.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.