Skip to content
Level 3 · AFL DeveloperLessonPart 09 · page 4 of 726 min
26Minutes
8AFL functions
5Sources
StandardRequires
AFL functions taught here8

ValueWhen(): Carrying a Value Forward

An event happens on one bar. Almost everything you want to do with it happens afterwards. What was the price when the breakout occurred? How far has it moved since? Where was the low of the bar that triggered the setup, so a stop can be placed relative to it? Each of these needs a value that was true on one specific bar to remain available on every bar that follows.

ValueWhen() is the function that does that. It samples an array at the moment a condition was true and holds the sample until the condition is true again — the electronics idea of sample-and-hold, applied to a price series. It is also the function whose argument order catches the largest number of experienced programmers, because the order that reads naturally in English is the wrong one.

Fragment — not a complete formula

ValueWhen( EXPRESSION, ARRAY, n = 1 )

The documented behaviour: it returns the value ARRAY had at the n-th most recent bar on which EXPRESSION was true. The default n is 1, meaning the most recent occurrence.

ValueWhen samples at the event, then holds

The output changes only on event bars. Between them it is flat, which is why these series draw as horizontal shelves on a chart. Cells before the required occurrence exists are marked unknown: the documentation does not state what is returned there.
Bar01234567
Event01001000
Close1011121314151617
ValueWhen( Event, Close, 1 )the most recent event?11111114141414
ValueWhen( Event, Close, 2 )the event before that????11111111
The output changes only on event bars. Between them it is flat, which is why these series draw as horizontal shelves on a chart. Cells before the required occurrence exists are marked unknown: the documentation does not state what is returned there. Prices in this diagram are invented for the illustration. They are not market data and nothing should be inferred from them.

Two things follow from the shape of that output, and both are worth internalising now.

The output is flat between events. On a chart, a ValueWhen() series draws as a set of horizontal shelves with sudden vertical jumps. Beginners frequently report this as a data error. It is the function working correctly.

The output is stale by design. If the condition has not fired for two years, the value being held is two years old. That is often exactly what you want — the last swing high is the last swing high, however long ago it formed — but it means a shelf stretching across half the chart is telling you something about the condition, not about the price.

The argument order that compiles either way

Section titled “The argument order that compiles either way”

Read this sentence: “the value of the close when the crossover happened”. Now write it as a function call. Almost everyone writes:

Fragment — not a complete formula

// WRONG - the arguments are the wrong way round
PriceAtSignal = ValueWhen( Close, Crossover );

The condition comes first. The array to sample comes second:

Fragment — not a complete formula

// Correct
PriceAtSignal = ValueWhen( Crossover, Close );

The wrong version compiles. It runs. It produces an array. What it computes is “the value of Crossover at the most recent bar on which Close was non-zero” — which, since the close is non-zero on every bar, is just Crossover shifted by nothing at all. No warning, no error, a plausible-looking array of ones and zeros where you expected prices.

A habit that removes the problem entirely: name the condition variable something that could not possibly be a price. SignalBar, Crossover, IsBreakout. When the first argument is called SignalBar and the second is called Close, a reversed call looks wrong on the page.

The third argument selects which occurrence, counting backwards from the current bar. n = 1 is the most recent, n = 2 the one before that, and so on.

This is how you compare an event with its predecessor without writing a loop:

Fragment — not a complete formula

SwingHigh = /* your swing definition */;
HighNow = ValueWhen( SwingHigh, High, 1 );
HighPrev = ValueWhen( SwingHigh, High, 2 );
HigherHigh = HighNow > HighPrev;

Part 4 used exactly this construction to classify market structure. It is worth noticing what it does not need: no loop, no index arithmetic, no memory of which bar was which. Two array expressions and a comparison.

The counting is in occurrences, not bars. n = 2 does not mean “two bars ago”; it means “one event before the latest one”, which might be two bars ago or two hundred. Confusing the two is the most common off-by-one error in this area, and it produces results that look almost right on a dense condition and wildly wrong on a sparse one.

The official page adds a note that deserves reading slowly:

this function allows also 0 and negative values for n - this enables referencing future

The sentence ends there in the shipped page on amibroker.com — the text is truncated at that point. What survives is enough to be a serious warning.

ValueWhen( Condition, Close, 0 ) reads forward to the next occurrence. Negative values reach further forward still. This is the same defect as a positive Ref() period, dressed in different clothes, and it is harder to spot because nothing about a zero looks dangerous.

The canonical use is remembering what the price was when a position was opened:

Fragment — not a complete formula

EntryPrice = ValueWhen( Buy, Close );
OpenProfit = 100 * ( Close - EntryPrice ) / EntryPrice;

That is correct as long as the entry really happened at the signal bar’s close. Very often it did not. If your rules signal on the close of one bar and enter at the open of the next, the price you would actually have paid is the open of the bar after the signal:

Fragment — not a complete formula

// The signal, delayed by one bar: true on the bar you would have traded.
TradeBar = Ref( Buy, -1 );
EntryPrice = ValueWhen( TradeBar, Open );

Ref( Buy, -1 ) shifts the signal forward in time by one bar — the bar after the signal now carries it — using a negative period, because that bar is reading the signal that occurred one bar ago. If that sentence made you pause, re-read the first lesson of this part; this construction is where the sign convention earns its keep.

ValueWhen captures a value at an event. Two related functions measure the extreme reached since an event:

Fragment — not a complete formula

HighestSince( EXPRESSION, ARRAY, Nth = 1 )
LowestSince( EXPRESSION, ARRAY, Nth = 1 )

Same condition-first ordering, same occurrence-counting third argument. The official example measures the highest close since MACD crossed above zero.

The difference between these and HHV/LLV is the difference between an anchor and a window, and it matters more than it first appears:

Fragment — not a complete formula

// Anchored at the entry: the peak stops moving when the trade is closed out.
PeakSinceEntry = HighestSince( Buy, High );
// A rolling window: the "peak" changes as the window slides forward,
// for reasons that have nothing to do with the trade.
PeakLast20 = HHV( High, 20 );

A trailing stop built on HHV( High, 20 ) loosens as the twenty-bar window drops the entry bar behind it. A trailing stop built on HighestSince( Buy, High ) is measured from the trade itself. They are different rules; only one of them is anchored to anything a trader cares about.

HighestSinceBars and LowestSinceBars are the matching age measures — how many bars since the extreme was reached, counting from the condition. As with HHVBars, they return counts, not prices.

The behaviour of all of these before the condition has ever been true, and when fewer than Nth occurrences exist, is not stated on the official pages. Guard them the way the previous lesson guarded BarsSince, with an explicit Cum( Condition ) > 0 test.

Put sample-and-hold on screen: the price recorded at the most recent signal, the price at the signal before it, and the highest and lowest prices reached since the most recent one — all as shelves that only move when the event fires.

Complete runnable AFL

event-anchored-levels.afl
// event-anchored-levels.afl
// Part 9 - ValueWhen(): Carrying a Value Forward
//
// Draws the price recorded at the most recent signal bar as a horizontal shelf,
// together with the extreme reached since that same bar. The shelf is exactly
// what ValueWhen() produces: a value sampled at an event and then held, bar
// after bar, until the event happens again.
//
// The contrast with HHV() is the point of the last two lines. HighestSince()
// measures from a fixed anchor - the signal bar - so the level stops moving once
// the signal has passed. HHV() drags a window along with the current bar, so its
// level keeps changing for reasons that have nothing to do with the trade.
//
// Assumptions:
// - The signal used here (close crossing above a moving average) is a
// placeholder. The lesson is the sample-and-hold mechanics, not this rule.
// - Before the first signal there is nothing to hold, so the shelves are empty
// at the left edge of the chart. That is correct, not a fault.
// - ValueWhen holds a stale value indefinitely. A shelf stretching over two
// years means the condition has not fired for two years.
// - No Buy or Sell variable is set. This formula describes; it does not trade.
_SECTION_BEGIN( "Event-anchored levels" );
MaPeriod = Param( "Average length (bars)", 50, 5, 250, 1 );
Average = MA( Close, MaPeriod );
SignalBar = Cross( Close, Average );
// ---------------------------------------------------------------------------
// Sample and hold
// ---------------------------------------------------------------------------
// ValueWhen takes the CONDITION first and the array to sample second. Swapping
// them compiles and returns silent nonsense, so read the argument order twice.
// n = 1 is the most recent occurrence, n = 2 the one before it.
PriceAtSignal = ValueWhen( SignalBar, Close, 1 );
PriceAtPrevSignal = ValueWhen( SignalBar, Close, 2 );
BarOfSignal = ValueWhen( SignalBar, BarIndex(), 1 );
// ---------------------------------------------------------------------------
// Excursion since the anchor
// ---------------------------------------------------------------------------
// HighestSince and LowestSince also take the condition first. They measure from
// the event, which is what an entry-anchored stop or excursion measure needs.
PeakSinceSignal = HighestSince( SignalBar, High, 1 );
TroughSinceSignal = LowestSince( SignalBar, Low, 1 );
BarsHeld = BarsSince( SignalBar );
MovePct = 100 * ( Close - PriceAtSignal ) / PriceAtSignal;
// ---------------------------------------------------------------------------
// Drawing
// ---------------------------------------------------------------------------
Plot( Close, "Close", colorDefault, styleCandle );
Plot( PriceAtSignal, "Close at signal", colorBlue, styleStaircase | styleThick );
Plot( PriceAtPrevSignal, "Close at previous", colorBlueGrey, styleStaircase | styleDashed );
Plot( PeakSinceSignal, "High since signal", colorGreen, styleStaircase );
Plot( TroughSinceSignal, "Low since signal", colorRed, styleStaircase );
PlotShapes( SignalBar * shapeUpArrow, colorBlue, 0, Low, -20 );
_N( Title =
Name() + " - " + Interval( 2 ) + " - event-anchored levels\n" +
StrFormat( "Last signal: %g bar(s) ago, at bar index %g\n",
BarsHeld, BarOfSignal ) +
StrFormat( "Close then %g, close now %g (%g%% since the signal)\n",
PriceAtSignal, Close, MovePct ) +
StrFormat( "Close at the previous signal: %g\n", PriceAtPrevSignal ) +
StrFormat( "Since the signal: highest high %g, lowest low %g",
PeakSinceSignal, TroughSinceSignal ) );
_SECTION_END();

Download event-anchored-levels.afl75 lines

The formula defines one event and then samples four different things from it. PriceAtSignal and PriceAtPrevSignal use n = 1 and n = 2 to capture the last two occurrences. BarOfSignal samples BarIndex() rather than a price, which is a useful trick: it tells you which bar the anchor sits on, and it makes the title readable when you are checking the formula against the chart.

PeakSinceSignal and TroughSinceSignal use the anchored-extreme functions, so their levels stop moving between events, exactly as the shelves do. The whole chart is therefore made of horizontal steps that change only on signal bars, which makes the mechanism visible at a glance.

styleStaircase is used deliberately. A ValueWhen() series drawn with the default line style joins its steps with sloping segments, which suggests the value changed gradually. It did not: it changed on one bar and was constant in between. The staircase style draws what actually happened.

  • ValueWhen( EXPRESSION, ARRAY, n ) — sample and hold, condition first.
  • HighestSince( EXPRESSION, ARRAY, Nth ) / LowestSince(...) — the extreme since the n-th most recent occurrence of the condition.
  • BarsSince( ARRAY ) — how long the current anchor has been in force.
  • Plot( array, name, color, style, ... ) with styleStaircase — draws the steps as steps.
  • StrFormat( formatstr, ... ) — assembles the title. Note that for arrays it uses the selected value, so moving the chart’s selection line moves the numbers in the title to that bar. That is a feature: it turns the title into a readout.

The check that would catch a wrong argument order or a wrong n: pick any signal arrow, note the close of that exact bar from the price scale, and compare it with the height of the blue shelf immediately to the right of it. They must be the same number. Then look one arrow further left: that close should match the grey dashed shelf over the same stretch.

If the blue shelf sits at a value that is not any close on the chart, the sample is coming from the wrong array. If it changes on non-signal bars, the condition is not event-shaped — which is the subject of the next lesson.

  • Reversed arguments. ValueWhen( Close, SignalBar ) compiles and yields ones and zeros. Check the units of the output first.
  • Using n as a bar count. n = 5 is the fifth most recent occurrence, not five bars ago.
  • Feeding it a state instead of an event. If the condition is true on long runs of bars, “the most recent occurrence” is the current bar on every one of those bars, and the output simply tracks the input array. The shelf never forms.
  • Reading the left edge. Before enough occurrences exist there is nothing to hold, and the official page does not say what is returned. Guard it.

Add a stop level anchored to the signal bar rather than to a rolling window: StopLevel = ValueWhen( SignalBar, Low ) - 0.5 * ValueWhen( SignalBar, ATR( 14 ) ); and plot it as a fourth staircase. Note what it does when the signal has not fired for a long time — the level is as old as the anchor, which may be a reason to add a time-based invalidation. Part 34 develops stop placement properly.

ValueWhen() samples an array at the bar a condition was true and holds the sample until the condition is true again. It is how a one-bar event supplies information to every bar that follows, and it is the backbone of entry prices, anchored levels and structure comparisons.

Three things decide whether it does what you meant. The condition comes first, and the reversed call compiles silently. The third argument counts occurrences, not bars, and values of zero or below are documented to read the future, which makes them unusable in any decision. And the condition needs to be event-shaped for the hold to mean anything at all — which is the distinction the next lesson is entirely about.

Check your understanding

Question 1. What does this line compute?
X = ValueWhen( Close, Cross( Close, MA( Close, 50 ) ) );
Show the answer and why

Answer: The value of the crossover array at the most recent bar where Close was non-zero

The arguments are reversed. The condition comes first, so this treats Close as the condition - true on every bar - and samples the crossover array. It compiles and returns ones and zeros where prices were expected.

Question 2. Which value of n makes ValueWhen() read information that was not available yet?
Show the answer and why

Answer: n = 0

The official page states that zero and negative values enable referencing the future. Any n of 1 or more counts backwards through occurrences that have already happened.

Question 3. A trailing stop must be measured from the entry bar, not from a sliding window. Which is right?
Show the answer and why

Answer: HighestSince( Buy, High ) - 2 * ATR( 14 )

HighestSince anchors at the condition, so the peak is measured from the trade. HHV drags a window forward regardless of when the trade started, Highest measures from the first delivered bar, and the last option has its arguments reversed.

Question 4. A ValueWhen() line on a chart is flat for eighteen months, then jumps. What does that mean?
Show the answer and why

Answer: The condition did not become true for eighteen months, so the held value did not change

Sample-and-hold means the output changes only on occurrences. A long shelf is a statement about the condition, not about the price - and a useful signal that the condition may be rarer than you assumed.

Question 5. True or false: ValueWhen( SignalBar, Close, 3 ) returns the close three bars before the signal.
Show the answer and why

Answer: False

False. The third argument counts occurrences of the condition, not bars. It returns the close at the third most recent bar on which SignalBar was true, which could be any distance away.

Sources for this lesson

5 verified · checked 2026-08-31

  1. 01AFL Function Reference — ValueWhenamibroker.com/guide/afl/valuewhen.html2026-08-31
  2. 02AFL Function Reference — HighestSinceamibroker.com/guide/afl/highestsince.html2026-08-31
  3. 03AFL Function Reference — LowestSinceamibroker.com/guide/afl/lowestsince.html2026-08-31
  4. 04AFL Function Reference — HighestSinceBarsamibroker.com/guide/afl/highestsincebars.html2026-08-31
  5. 05AFL Function Reference — SumSinceamibroker.com/guide/afl/sumsince.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.