Skip to content
Level 3 · AFL DeveloperProjectPart 10 · page 6 of 850 min
50Minutes
14AFL functions
8Sources
StandardRequires
AFL functions taught here14

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.

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.

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.

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

  1. MA slopeover SlopeBars bars
  2. ÷ ATRmakes it comparable
  3. Threshold + sideraw up / raw down
  4. Confirmationall of the last N bars
  5. LabelUP, DOWN or RANGE
Every step uses only bars that have already closed. That is what makes the label reproducible - and what makes it late.

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.

Complete runnable AFL

trend-regime.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();

Download trend-regime.afl109 lines

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.

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.

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.

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.

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.

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.

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

Price
fallingrising
Raw condition
true
Confirmed label
UP

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.

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.

  1. 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.
  2. 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.
  3. Perturb the parameters. Change ConfirmBars from 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 with TrendPeriod from 100 to 110.
  4. 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.
  5. 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 + SlopeBars and typically much more. If it is ever less, something is reading forward and you have a bug — check that no Ref() uses a positive shift.
  6. 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.
  • The whole chart is one colour. The slope threshold is too low, so almost every bar qualifies. Raise it, or check that ATR is 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 ColorBlend factor 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, 100 for 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.
  • BarsSince reports 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.

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.

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

Question 1. Why is the moving-average slope divided by ATR before it is compared with a threshold?
Show the answer and why

Answer: To express it in volatility units so one threshold means the same thing on instruments with different price levels

A raw slope is in price units per bar, so the same number is steep on a low-priced share and negligible on a high-priced index. Dividing by ATR makes the measurement comparable across instruments.

Question 2. What does this expression require?
ConfirmedUp = Sum( RawUp, ConfirmBars ) == ConfirmBars;
Show the answer and why

Answer: That RawUp was true on every bar of the trailing window

Sum counts the true bars in the trailing window. Requiring the count to equal the window length is the same as requiring every bar in it to be true - a single false bar resets it.

Question 3. With ConfirmBars = 5 and SlopeBars = 20, what is the minimum number of bars between a genuine turn and an UP label?
Show the answer and why

Answer: 25

The confirmation window and the slope window add: at least 25 bars. The averaging window adds a further, unquantified delay on top, which is why the figure is reported as a minimum.

Question 4. Sticky mode carries the last directional label through range periods. What does that cost?
Show the answer and why

Answer: It hides the classifier’s uncertainty, so a bar can display UP long after the up conditions stopped holding

ValueWhen carries the last non-zero label forward. The chart is calmer, but a displayed label may be weeks stale. It is a presentation choice, not a measurement, which is why it defaults to off.

Question 5. Which of these would indicate a bug rather than a design trade-off? Select all that apply.
Show the answer and why

Answer: The label appears before the price low that started the move

A label that precedes the event it describes means something is reading forward - check for a positive Ref shift. A moving label on the unfinished bar and a long lag are both expected; extreme parameter sensitivity is a warning about fitting, not a coding bug.

Sources for this lesson

8 verified · checked 2026-08-31

  1. 01AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
  2. 02AFL Function Reference — MAamibroker.com/guide/afl/ma.html2026-08-31
  3. 03AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
  4. 04AFL Function Reference — BarsSinceamibroker.com/guide/afl/barssince.html2026-08-31
  5. 05AFL Function Reference — Nzamibroker.com/guide/afl/nz.html2026-08-31
  6. 06AFL Function Reference — Plotamibroker.com/guide/afl/plot.html2026-08-31
  7. 07AFL Function Reference — SetChartBkColoramibroker.com/guide/afl/setchartbkcolor.html2026-08-31
  8. 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.