Skip to content
Level 3 · AFL DeveloperLessonPart 10 · page 3 of 828 min
28Minutes
8AFL functions
6Sources
StandardRequires
AFL functions taught here8

PlotShapes() and Visual Signals

Lines describe states. Arrows describe events. PlotShapes() is how an event that happened on one particular bar gets marked on that bar, and it is the function most likely to put your arrows somewhere baffling the first few times you use it — because two of its arguments do not mean what their names suggest.

Fragment — not a complete formula

PlotShapes( shape, color, layer = 0, yposition = graph0,
offset = -12, XShift = 0 );
Argument Meaning
shape A number or an array. Zero draws nothing; each non-zero value selects a shape.
color A colour, or an array of colours, one per bar.
layer Layer number. Default 0.
yposition Where to anchor the shape, in price units. Default graph0 — the first plotted line.
offset A nudge, in screen pixels. Default −12.
XShift Shifts shapes sideways by N bars. Added in AmiBroker 5.66.

PlotShapes() returns nothing, so you cannot use its result in an expression.

yposition is a price. offset is pixels. They are two different coordinate systems in one call. Learners routinely write offset = ATR( 10 ) hoping for “one ATR above the bar” and get shapes flung off the top of the pane, because an ATR of 1.8 was interpreted as a one-and-a-bit pixel nudge — or, on a high-priced instrument, as a nudge of several hundred pixels. To offset in price terms, put the arithmetic into yposition:

Fragment — not a complete formula

// Wrong: ATR is a price, offset is pixels.
PlotShapes( MyShape, colorGreen, 0, Low, ATR( 10 ) );
// Right: do the price arithmetic in the position argument.
PlotShapes( MyShape, colorGreen, 0, Low - ATR( 10 ), 0 );

The sign of offset is inverted. Negative moves the shape down, positive moves it up. The default of −12 pushes a shape twelve pixels below its anchor, which is why arrows appear under the price line if you never touch that argument.

The complete documented list, in the order the guide gives it:

shapeNone, shapeUpArrow, shapeDownArrow, shapeHollowUpArrow, shapeHollowDownArrow, shapeSmallUpTriangle, shapeSmallDownTriangle, shapeHollowSmallUpTriangle, shapeHollowSmallDownTriangle, shapeUpTriangle, shapeDownTriangle, shapeHollowUpTriangle, shapeHollowDownTriangle, shapeSmallSquare, shapeHollowSmallSquare, shapeSquare, shapeHollowSquare, shapeSmallCircle, shapeHollowSmallCircle, shapeCircle, shapeHollowCircle, shapeStar, shapeHollowStar, shapeDigit0shapeDigit9, and the modifier shapePositionAbove.

Two official rules govern where a shape lands relative to its anchor:

  • Odd shape values are drawn below the indicator; even values are drawn above it.
  • Consequently, every shape whose name contains “Up” is already positioned below its anchor, and every shape whose name contains “Down” is already positioned above it.

That second rule is the reason for the one genuine prohibition in this function. shapePositionAbove is an additive modifier that lifts an otherwise unpositioned shape above its anchor, and AmiBroker’s author has stated plainly that it must not be added to any shape whose name contains “Up” or “Down”. It is valid only with the squares, circles, stars and digits.

An up arrow marking a buy therefore sits below the bar by default, which is the convention every charting package uses, and is why it looks right without you doing anything.

shape is an array, and zero means “draw nothing”. Two idioms follow from that.

The arithmetic idiom appears throughout AmiBroker’s own examples:

Fragment — not a complete formula

MyShape = Buy * shapeUpArrow + Sell * shapeDownArrow;
PlotShapes( MyShape, IIf( Buy, colorGreen, colorRed ) );

It works because a false condition contributes zero. It also breaks quietly: if Buy and Sell are both true on one bar, the two constants add, and the sum is some third shape you never asked for. On a bar where two conditions overlap you get a random-looking glyph.

The conditional idiom does not have that failure mode:

Fragment — not a complete formula

PlotShapes( IIf( Buy, shapeUpArrow, shapeNone ), colorGreen, 0, Low, -12 );
PlotShapes( IIf( Sell, shapeDownArrow, shapeNone ), colorRed, 0, High, 12 );

Two calls, two independent decisions, no arithmetic on undocumented values. Prefer it.

Colour follows the same pattern. The color argument accepts an array, so one call can mark the same shape in different colours according to a second condition — strength of the move, whether a filter agreed, whatever you are testing.

The most common way to ruin a chart is to mark a state as though it were an event. Close > MA( Close, 50 ) is true on hundreds of consecutive bars; plotting a shape wherever it is true gives you hundreds of arrows and no information.

A state, the event that starts it, and what each one plots

Cross() reduces a state to the single bar on which it began. That is what a marker should show.
Bar123456
Close > MA011101
Cross( Close, MA )010001
Shapes from the state
Shapes from the event
Cross() reduces a state to the single bar on which it began. That is what a marker should show.

Cross() already gives you a one-bar event. ExRem( array1, array2 ) removes the repeats that survive anyway — the case where two averages sit on top of each other and cross back and forth for several bars. Flip( array1, array2 ) is the inverse: it turns a pair of events back into a state that stays on until the opposite event arrives, which is exactly what you want for a ribbon.

Three further habits keep charts readable:

  • One idea per marker shape. If entries and exits share a glyph, the chart is a puzzle.
  • Anchor to the bar, not to the indicator. Passing Low or High as yposition puts the marker against the price bar it refers to. The default anchors to the first plotted line, which is rarely what you mean on a price chart.
  • Use a ribbon for the state and shapes for the events. A two-per-cent-tall coloured band at the foot of the pane tells you which side of the line you are on for every bar without adding a glyph to every bar.

PlotText( "text", x, y, color, bkcolor = colorDefault, yoffset = 0 ) writes a string at an arbitrary position. Its coordinate system is a third variation on the theme: x is a bar index, y is a price, and yoffset is pixels. Mixing those up is the number one PlotText() bug.

PlotText() also takes numbers rather than arrays, so the documented way to label many bars is a for loop indexing the price arrays — which is expensive on a long chart and should always be guarded by a condition. PlotTextSetFont() does the same job with a chosen typeface and size, and carries a side effect worth knowing: it sets the font for every subsequent PlotText() call in the same formula.

For marking signals, shapes are almost always the better tool. Reach for text when the label carries information a glyph cannot — a price level, a count, a name.

A price pane that marks the bars where a fast average crosses a slow one, with arrows held clear of the bar in either pixels or ATR units, plus a ribbon at the foot of the pane showing which side of the cross the market is currently on.

The moving-average cross is used here because everybody recognises it, not because the course is recommending it. Its behaviour is examined properly in the backtesting parts.

Complete runnable AFL

signal-markers.afl
/*
* Marking events without burying the chart - worked example for Part 10.
*
* Assumptions
* - Chart pane formula, applied to the price pane.
* - The moving-average cross used here is a demonstration of event marking,
* not a recommendation. It is one of the most heavily reused rules in
* circulation and is chosen only because every reader recognises it.
* - Markers are drawn on the bar where the event occurred. Acting on that
* bar's close is a separate decision with its own execution assumptions.
*/
_SECTION_BEGIN( "Signal markers" );
// Arrows drawn with a pixel offset are clipped against the pane edge unless
// there is head and foot room to draw into.
GraphXSpace = 8;
FastPeriod = Param( "Fast periods", 20, 2, 200, 1 );
SlowPeriod = Param( "Slow periods", 50, 2, 400, 1 );
MarkerOffset = Param( "Marker offset (screen pixels)", 12, 0, 60, 1 );
PriceAnchor = ParamToggle( "Anchor in ATR units instead", "No|Yes", 0 );
AtrMultiple = Param( "ATR clearance", 0.75, 0.1, 4, 0.05 );
ShowRibbon = ParamToggle( "State ribbon", "No|Yes", 1 );
FastLine = MA( Close, FastPeriod );
SlowLine = MA( Close, SlowPeriod );
Plot( Close, "Price", colorDefault, styleBar );
Plot( FastLine, "Fast " + NumToStr( FastPeriod, 1.0 ), colorBlue, styleLine );
Plot( SlowLine, "Slow " + NumToStr( SlowPeriod, 1.0 ), colorOrange, styleLine );
// Events, not states. Cross() is already one bar wide; ExRem removes the
// repeated crossings that happen when two averages sit on top of each other in
// a flat market, so each change of side is marked exactly once.
CrossUp = ExRem( Cross( FastLine, SlowLine ), Cross( SlowLine, FastLine ) );
CrossDown = ExRem( Cross( SlowLine, FastLine ), Cross( FastLine, SlowLine ) );
// Two ways to hold a marker clear of the bar it belongs to.
// Pixel clearance keeps the gap constant on screen at every zoom level.
// ATR clearance keeps the gap constant in the instrument's own units, so it
// stays honest when the pane is rescaled or the symbol changes.
Clearance = AtrMultiple * ATR( 14 );
if( PriceAnchor )
{
LowAnchor = Low - Clearance;
HighAnchor = High + Clearance;
PixelNudge = 0;
}
else
{
LowAnchor = Low;
HighAnchor = High;
PixelNudge = MarkerOffset;
}
// PlotShapes( shape, color, layer, yposition, offset, XShift )
// yposition is in PRICE units.
// offset is in SCREEN PIXELS, and its sign is inverted: negative moves DOWN.
// shapeUpArrow is already positioned below its anchor and shapeDownArrow
// above it, so the offsets only push them further clear of the bar.
PlotShapes( IIf( CrossUp, shapeUpArrow, shapeNone ),
colorBrightGreen, 0, LowAnchor, -PixelNudge );
PlotShapes( IIf( CrossDown, shapeDownArrow, shapeNone ),
colorRed, 0, HighAnchor, PixelNudge );
// A ribbon answers "which side are we on" for every bar without adding a marker
// to every bar. The plotted constant is the ribbon height as a percentage of
// pane height, because the own-scale range is set to -0.5 .. 100.
if( ShowRibbon )
{
OnFastSide = Flip( CrossUp, CrossDown );
Plot( 2, "Side",
IIf( OnFastSide, colorPaleGreen, colorRose ),
styleOwnScale | styleArea | styleNoLabel | styleNoTitle, -0.5, 100 );
}
_N( Title = StrFormat( "%s fast %g / slow %g crossings in the loaded range: %g",
Name(), FastPeriod, SlowPeriod,
LastValue( Cum( CrossUp OR CrossDown ) ) ) );
_SECTION_END();

Download signal-markers.afl84 lines

After the parameters, the two averages are plotted so that the reader can see what the arrows refer to.

CrossUp and CrossDown reduce the relationship between the two lines to single bars. Cross() supplies the event; ExRem() removes any repeat before the opposite event has occurred, so the chart shows an alternating sequence of up and down markers rather than clusters.

The PriceAnchor toggle switches between the two ways of holding a marker clear of the bar. With it off, the anchor is the bar’s own low or high and the clearance is a pixel offset — constant on screen at any zoom level. With it on, the clearance is a multiple of ATR and the pixel nudge is zero — constant in the instrument’s own units, so it survives a change of symbol or a rescale. Building both into one formula makes the distinction concrete, and there is no universally right answer: pixels are better for reading, price units are better for screenshots and comparisons.

The two PlotShapes() calls use the conditional idiom. shapeUpArrow is anchored to the low and pushed further down; shapeDownArrow is anchored to the high and pushed up. Neither carries shapePositionAbove, because both already carry a position.

Finally the ribbon. Flip( CrossUp, CrossDown ) converts the two event arrays back into a state that is true from an up-cross until the next down-cross. Plotting the constant 2 against an own scale of −0.5 to 100 produces a band two per cent of the pane’s height at its foot, which is the official ribbon idiom.

  • PlotShapes( shape, color, layer, yposition, offset, XShift ) — draws glyphs. yposition is a price, offset is pixels, negative offsets move down.
  • ExRem( array1, array2 ) — removes excess signals: keeps the first true in array1 until a true appears in array2.
  • Flip( array1, array2 ) — a latch: on at the first true in array1, off at the next true in array2.
  • GraphXSpace — a reserved variable; percentage of extra room above and below the graph.

Green up arrows below the bar where the fast average crossed above the slow one, red down arrows above the bar where it crossed back, exactly one marker per change of side, and a green-or-pink band along the bottom of the pane. The title reports how many crossings exist in the loaded data.

  1. Count the arrows in one screen and compare with the crossing count in the title over the same range. Every crossing should have exactly one marker, alternating in direction.
  2. Set Marker offset to 0. The arrows should sit exactly on the bar’s low and high.
  3. Switch Anchor in ATR units on and zoom in and out. The pixel-anchored arrows keep a constant screen gap; the ATR-anchored ones keep a constant price gap and therefore change their screen gap as you zoom.
  4. Remove ExRem from CrossUp on a choppy sideways symbol. Clusters of arrows should appear where the averages are entangled. That is the behaviour ExRem exists to remove.
  • Arrows are off the top or bottom of the pane. A price expression ended up in offset. Move it to yposition.
  • Arrows are on the wrong side of the bar. Remember negative offset moves down; and an up arrow is already positioned below its anchor.
  • A strange, unrequested glyph appears occasionally. Two shape constants were added together on a bar where both conditions were true. Use the IIf( cond, shape, shapeNone ) form instead.
  • Arrows are clipped at the pane edge. Raise GraphXSpace.
  • shapePositionAbove seems to do nothing. It was combined with a shape whose name contains “Up” or “Down”. Those already carry a position.

Add a third marker: a hollow square on any bar where a cross occurred and the bar’s range was larger than its recent average. Then look at how often that condition coincides with the cross. The interesting result is usually how little the extra filter changes, which is a useful thing to discover on a chart before discovering it in a backtest.

You can now put a mark on the exact bar where something happened, control its position in either pixels or price, colour it conditionally, and keep the chart legible by separating the event from the state it starts. You also know the two documented rules that decide whether a shape lands above or below its anchor, and why arithmetic on shape constants is a bad idea.

Next: putting the settings of all this on the Parameters dialog, so you stop editing code to change a number.

Check your understanding

Question 1. What does the offset argument of PlotShapes() measure, and which direction is positive?
Show the answer and why

Answer: Screen pixels; positive moves up

offset is in screen pixels and its sign is inverted relative to most expectations: negative moves the shape down, positive moves it up. The default of -12 places shapes below the anchor.

Question 2. What is wrong with this line?
MyShape = Buy * shapeUpArrow + Sell * shapeDownArrow;
Show the answer and why

Answer: If Buy and Sell are both true on one bar the two constants add, producing an unintended third shape

The idiom relies on false contributing zero. When both conditions are true on the same bar their values sum, and the sum selects some other shape. IIf( cond, shape, shapeNone ) has no such failure mode.

Question 3. Which shapes may shapePositionAbove legitimately be combined with? Select all that apply.
Show the answer and why

Answer: shapeSmallSquare, shapeStar, shapeDigit5

Shapes whose names contain "Up" or "Down" already carry a position, so the modifier is meaningless with them. It is valid with the squares, circles, stars and digits.

Question 4. You want one marker on the bar where a state begins, not on every bar the state is true. Which function reduces the state to that single bar?
Show the answer and why

Answer: Cross()

Cross() is true only on the bar where one series moves above the other. Flip() does the opposite - it converts a pair of events back into a continuous state.

Sources for this lesson

6 verified · checked 2026-08-31

  1. 01AFL Function Reference — PlotShapesamibroker.com/guide/afl/plotshapes.html2026-08-31
  2. 02AFL Function Reference — PlotTextamibroker.com/guide/afl/plottext.html2026-08-31
  3. 03AFL Function Reference — PlotTextSetFontamibroker.com/guide/afl/plottextsetfont.html2026-08-31
  4. 04AmiBroker User's Guide — Creating your own indicators, part 2amibroker.com/guide/h_indbuilder2.html2026-08-31
  5. 05AFL Function Reference — ExRemamibroker.com/guide/afl/exrem.html2026-08-31
  6. 06AFL Function Reference — Flipamibroker.com/guide/afl/flip.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.