Skip to content
Level 4 · Trading System ResearcherLessonPart 34 · page 2 of 530 min
30Minutes
8AFL functions
5Sources
StandardRequires
AFL functions taught here8

Volatility-Based Position Sizing

A stop three points below entry is a wide stop on a share that moves twenty cents a day and a tight one on a share that moves four dollars a day. Everything in the previous lesson — the risk budget, the division, the share count — depends on a stop distance, and picking one number for a whole universe means picking a different rule for every symbol in it without noticing. Volatility-based sizing is the standard answer, and the standard answer has more holes in it than its popularity suggests.

By the end of this lesson you should be able to set a stop distance from Average True Range, derive the position weight that results, write the whole thing in AFL, say exactly what the normalisation equalises and what it leaves untouched, and describe what happens to your stated risk when volatility changes after the position is open.

Price points are not a unit you can compare across instruments. Neither are percentages: two shares can both be at 50.00, one drifting in a fifty-cent band and one covering four dollars a day, and an eight per cent stop means something entirely different to each. What you need is a yardstick made from the instrument’s own recent behaviour.

Average True Range is that yardstick. Part 6 covers its construction; the two facts that matter here are that true range includes the overnight gap, so ATR does not pretend the market is continuous, and that AmiBroker’s ATR() uses Wilder’s smoothing rather than a simple average, which is documented on the function’s own page. ATR(1) gives the plain true range if you want to build something else from it.

The stop distance then becomes a multiple:

D = k × ATR(n)

with k typically between two and four, and n usually between ten and twenty-five bars. Both are parameters, both are choices, and both will be tempting to optimise — which is a temptation this part will come back to.

Substitute D = k × ATR into the sizing identity from the previous lesson. Position weight, as a percentage of equity, is the risk fraction divided by the stop distance expressed as a fraction of price:

weight% = r / (k × ATR / Price)

The denominator is the instrument’s ATR expressed as a percentage of its own price — its normal daily movement in comparable units. Everything follows from that single ratio.

Take an account of 100,000, a one per cent risk fraction, k = 3, and three shares that all happen to trade at 50.00 but behave completely differently. A twenty per cent cap on any one position is in force.

Share ATR ATR as % of price Stop distance Weight wanted After the cap Shares Dollar risk
Quiet 0.60 1.2% 1.80 27.8% 20.0% 400 720
Ordinary 1.25 2.5% 3.75 13.3% 13.3% 266 998
Volatile 3.00 6.0% 9.00 5.6% 5.6% 111 999

Three positions, one price, weights differing by a factor of five, and — for the two the cap does not touch — the same loss if the stop is reached. These are illustrative figures, chosen to make the arithmetic visible; they are not measurements of any real instrument.

Three observations are worth extracting.

  • The volatile share gets the small position. People often expect the opposite, on the reasoning that a volatile instrument offers more opportunity. The rule is not making a judgement about opportunity; it is holding the loss constant.
  • The quiet share hits the cap. A low-ATR symbol asks for a very large position, and quiet is not the same as harmless. Whenever the cap binds, the trade is no longer running the risk policy you wrote down — its realised risk here is 0.72% rather than 1%.
  • Nothing here says anything about the probability of being stopped. Equal loss is not equal likelihood of taking that loss.

k is the only parameter in this scheme that has a clean interpretation, and it is worth being precise about what changing it does. Doubling k doubles the stop distance and halves the position weight. The dollar risk is unchanged, by construction. What changes is everything else:

  • the chance that ordinary noise reaches the stop, which falls;
  • the number of shares you hold when the position works, which falls with it;
  • the fraction of the account committed, and therefore Exposure % and every metric AmiBroker divides by it;
  • the number of round trips, and therefore the total commission and spread paid.

A wide stop is not more cautious than a narrow one under this rule — the loss is the same either way. It is a different bet about how much room the idea needs.

A portfolio backtest identical to the previous lesson’s in every respect except one: the stop distance comes from ATR() rather than from a structural low. Keeping everything else constant is deliberate — it makes the two reports comparable, and comparing them is the point of running both.

Complete runnable AFL

atr-position-size.afl
// atr-position-size.afl
// Part 34 - Volatility-Based Position Sizing
//
// Identical account, execution and signal rules to risk-based-position-size.afl.
// The one thing that changes is where the stop distance comes from: a multiple
// of Average True Range instead of a structural low. The claim being tested is
// that scaling the stop with each instrument's own recent range makes a 1%
// risk mean roughly the same thing in a quiet share and a violent one.
//
// ASSUMPTIONS - change any of these and every number the report shows changes:
// - Daily bars, split- and dividend-adjusted end-of-day data.
// - Signals are read on the close of the signal bar; orders fill at the NEXT
// bar's open. SetTradeDelays(1,1,1,1) enforces that.
// - Commission 0.1% of trade value, each way. Slippage is NOT modelled.
// - ExitAtStop = 1: stops are checked against High-Low and filled at the stop
// level. Optimistic. A gap through the level fills worse, and nothing in
// this formula knows that.
// - ATR uses Wilder's smoothing, which is what AmiBroker's ATR() implements.
// ATR(1) is the plain true range.
// - The stop distance is sampled once, on the signal bar, and held for the
// life of the trade (ApplyStop volatile parameter left at its default
// False). The position is never resized after entry.
// - Long only.
//
// How to run it: Formula Editor -> Send to Analysis -> Apply to: a watch list
// you fixed in advance -> Range: a date range you fixed in advance -> Backtest.
// ---- Account and execution ------------------------------------------------
MaxPositions = 10;
SetOption( "InitialEquity", 100000 );
SetOption( "MaxOpenPositions", MaxPositions );
SetOption( "AllowPositionShrinking", True );
SetOption( "CommissionMode", 1 );
SetOption( "CommissionAmount", 0.1 );
SetOption( "ActivateStopsImmediately", True );
SetTradeDelays( 1, 1, 1, 1 );
BuyPrice = Open;
SellPrice = Open;
RoundLotSize = 1;
// ---- Risk policy ----------------------------------------------------------
RiskPercent = 1.0; // percent of equity lost if the stop is reached
AtrPeriod = 20; // lookback for Average True Range
StopAtrMult = 3.0; // stop sits this many ATRs below the entry price
MaxSizePercent = 20; // cap: no position larger than this share of equity
MinAtrPct = 0.5; // floor on ATR, in percent of price, to stop the
// size calculation exploding in a becalmed symbol
// ---- Universe filter ------------------------------------------------------
MinTurnover = 2000000;
Liquid = MA( Close * Volume, 50 ) > MinTurnover;
// ---- Signals --------------------------------------------------------------
TrendPeriod = 200;
EntryPeriod = 50;
Trend = Close > MA( Close, TrendPeriod );
Buy = Cross( Close, MA( Close, EntryPeriod ) ) AND Trend AND Liquid;
Sell = Cross( MA( Close, EntryPeriod ), Close );
PositionScore = 100 - RSI( 14 );
// ---- Volatility and stop distance -----------------------------------------
RawAtr = ATR( AtrPeriod );
AtrFloor = Close * MinAtrPct / 100;
// A symbol that has barely moved for twenty bars produces a tiny ATR, and a
// tiny stop distance asks for an enormous position. The floor is not cosmetic:
// without it one dormant symbol can absorb the whole account.
UsableAtr = Max( RawAtr, AtrFloor );
StopDistance = StopAtrMult * UsableAtr;
// ---- Volatility-normalised size -------------------------------------------
// Position value / equity = RiskPercent * Price / StopDistance, so that a move
// of StopDistance against the position costs RiskPercent of equity.
SizePercent = RiskPercent * Close / StopDistance;
SizePercent = Min( SizePercent, MaxSizePercent );
// Both arrays are shifted by the buy delay so the values the backtester reads
// on the entry bar are the ones computed on the signal bar. SetTradeDelays
// shifts only Buy/Sell/Short/Cover; it never shifts PositionSize or the stop.
SizeAtEntry = Nz( Ref( SizePercent, -1 ), 0 );
StopAtEntry = Nz( Ref( StopDistance, -1 ), 1 );
SetPositionSize( SizeAtEntry, spsPercentOfEquity );
ApplyStop( stopTypeLoss, stopModePoint, StopAtEntry, 1 );
// Optional, and a genuinely different system: a trailing stop that follows
// current volatility instead of entry volatility. The fifth argument True is
// the documented "volatile" flag, which lets the distance change during the
// trade - the single-line Chandelier exit from the ApplyStop page. Enable it
// and the initial-risk arithmetic above no longer describes the whole trade.
// ApplyStop( stopTypeTrailing, stopModePoint, 3 * ATR( 14 ), True, True );

Download atr-position-size.afl95 lines

The account, execution and signal sections are unchanged, so the differences are confined to two blocks.

The volatility block computes ATR( AtrPeriod ) and then clamps it from below:

Fragment — not a complete formula

RawAtr = ATR( AtrPeriod );
AtrFloor = Close * MinAtrPct / 100;
UsableAtr = Max( RawAtr, AtrFloor );
StopDistance = StopAtrMult * UsableAtr;

The floor is expressed as a percentage of price rather than as a fixed number of points, so it means the same thing at 5.00 and at 500.00. Without it, a symbol that has been pinned in a narrow range — a stock in a takeover offer, a fund tracking a stable index, a bar series with repeated closes because the data source filled a hole — produces an ATR near zero, a stop distance near zero, and a position request several times the size of the account.

The sizing block is the same conversion as before, capped, and shifted by one bar to match the trade delay. The shift is not a nicety. With SetTradeDelays( 1, 1, 1, 1 ) the backtester enters on the bar after the signal and reads PositionSize there, so an unshifted formula sizes the trade using an ATR that includes the entry bar’s own high and low — data that did not exist when the decision was made. That is a look-ahead leak in the sizing rule, and it is invisible in the report because the report has no reason to mention it.

  • ATR( period ) — average true range, Wilder-smoothed. ATR(1) is the raw true range. Note it is the average of a range, not a standard deviation of returns: it is measured in price points and is comparable to a stop distance without conversion.
  • ApplyStop( type, mode, amount, exitatstop, volatile, … ) — with stopModePoint, the amount is a distance in points and may be an array, which is what allows one stop rule to produce a different distance for every symbol and every bar. The fifth argument decides whether that distance is frozen at entry or allowed to move; more on that immediately below.
  • Max( array1, array2 ) and Min( array1, array2 ) — element-by-element, so both the ATR floor and the position cap are applied per bar rather than once. Neither is a reduction over time; Highest() and Lowest() are the functions that do that.

Pick one symbol and one entry from the Detailed log, then reconstruct the size by hand:

  1. On the chart, add ATR(20) as an indicator and read its value on the bar before the entry.
  2. Multiply by 3. That is the stop distance the trade should have used.
  3. Divide 1.0 by (stop distance ÷ that bar’s close) to get the intended weight as a percentage.
  4. Compare with the position value in the log, divided by the portfolio equity in the same row.

If step 4 comes out at the entry bar’s ATR rather than the previous bar’s, the shift is missing. If it comes out at exactly twenty per cent, the cap bound and the trade is not running your risk policy.

  • No cap. Symptom: one position dwarfs the others, and Max. system % drawdown is driven almost entirely by it. Cause: a low-ATR symbol asked for more than the account and AllowPositionShrinking obligingly gave it everything available.
  • ATR period much shorter than the holding period. Symptom: stop distances that look arbitrary relative to the swings the system is trying to capture. A five-bar ATR describes this week; a system holding for two months is not sized by this week.
  • Mixing the units. stopModePercent and stopModePoint are different modes, and passing an ATR in points to the percent mode produces a stop of 3% when you meant three points, or the reverse. The report will not object.

Replace the fixed StopAtrMult with Optimize( "ATR multiple", 3, 1.5, 5, 0.5 ) and run an optimization for the surface, not for the winner. What you are looking for is whether the metric you care about changes smoothly across the range or jumps around. A smooth response means the choice is not critical and any value from the plateau is defensible. A spiky one means your result depends on a parameter value you have no reason to believe in.

The size is set once, at entry, and never revisited. The stop distance, by default, is also set once. Both facts have consequences.

ApplyStop()’s fifth parameter is documented as deciding whether the third parameter — the amount — “is sampled at the trade entry and remains fixed during the trade” or “can vary during the trade”. Left at its default of False, ApplyStop( stopTypeLoss, stopModePoint, 3 * ATR( 20 ), 1 ) uses the ATR of the entry bar for the whole trade, however long it lasts. That is usually what you want for an initial stop: the risk you accepted was the risk you accepted.

Set it to True and the distance follows current volatility. The documented single-line Chandelier exit is exactly that:

Fragment — not a complete formula

ApplyStop( stopTypeTrailing, stopModePoint, 3 * ATR( 14 ), True, True );

That is a genuinely different system, and it is worth being clear about what it does to your risk arithmetic. A volatile stop widens as the market becomes more violent, so the distance between price and stop grows after you have already bought a share count based on the old, narrower distance. Your realised risk on that position is now larger than one per cent, and nothing tells you so. The trailing behaviour usually compensates by having already locked in some gain, but “usually” is doing real work in that sentence.

Three more things do not adjust themselves:

  • The share count. If ATR triples while you hold, the position’s exposure to a normal day’s move has tripled. Under a fixed stop this does not change the loss at the stop; under a volatile stop it does.
  • The portfolio. A volatility expansion is rarely confined to one symbol. Every open position becoming more volatile at once is a portfolio event, not a position event.
  • Your other positions’ sizes. They were set at their own entries, under their own volatility regimes, and there is no mechanism in this scheme that revisits them.

AmiBroker does provide the machinery for adjusting an open position: assigning sigScaleIn or sigScaleOut to Buy scales an existing position rather than opening a new one, with the size given by SetPositionSize( pct, spsPercentOfPosition ). Two documented caveats matter before you reach for it: scaling works in the portfolio backtester only — the old backtester and Equity() ignore the signals entirely — and a scaled trade is reported as a single row with an average entry price, so per-leg detail exists only in the Detailed log.

What volatility normalisation does not fix

Section titled “What volatility normalisation does not fix”

It is a good technique with a well-defined job, and most of the trouble comes from expecting it to do jobs it never claimed.

ATR is a measurement of the past. Volatility clusters, so a recent reading is not a bad estimate of the near future — but it is late at exactly the turns that matter. Volatility regimes change abruptly, and every position sized during the calm was sized on the calm.

Quiet is not the same as safe. A symbol with a low ATR gets the largest position this rule will grant. If it is quiet because it is illiquid, or because it is in a pending takeover, or because the data source is repeating the previous close, the rule has just concentrated your account into the least tradable name in the universe. The cap is the only thing standing between you and that outcome, which is why the cap is not optional.

ATR does not anticipate gaps. True range includes the gap, so ATR rises after a gappy period — retrospectively. It cannot tell you that tomorrow morning’s announcement exists. Everything the previous lesson said about gap risk applies unchanged here.

Normalising volatility is not normalising risk across asset classes. A futures contract’s loss per point is its PointValue, and the capital it ties up is its MarginDeposit, neither of which is honoured unless SetOption( "FuturesMode", True ) is set — the User’s Guide is explicit that PointValue defaults to 1 regardless of the Information window until futures mode is on. A stock formula transplanted to futures without that will size everything wrongly and report the result with a straight face.

It says nothing about how many positions you hold or how alike they are. Ten positions each risking one per cent are ten one per cent risks only if they can fail independently. They rarely can. That is the subject of the next lesson.

Volatility-based sizing replaces an arbitrary stop distance with one drawn from the instrument’s own recent range, and the position weight that results is the risk fraction divided by the ATR-based stop expressed as a fraction of price. Quiet instruments get large positions and volatile ones get small positions, which is the intended behaviour and also the reason a hard cap on position size is part of the rule rather than an accessory to it.

In AFL the whole thing is two clamps and a division, with one correction that most examples omit: the sizing and stop arrays must be shifted to match the trade delay, or the size is computed from a bar the signal never saw.

And the technique’s boundary is sharp. It equalises the loss on one position if the stop behaves. It does not equalise the chance of that loss, it does not survive a gap, it does not know about your other nine positions, and it was computed from a window that has already closed.

Check your understanding

Question 1. Two shares both trade at 80.00. One has ATR(20) = 0.80, the other ATR(20) = 3.20. Under a 1% risk rule with a stop of 3 × ATR, what are the two position weights?
Show the answer and why

Answer: 33.3% and 8.3%

Weight = r / (k × ATR / Price). For the quiet share: 1 / (3 × 0.80 / 80) = 1/0.03 = 33.3%. For the volatile one: 1 / (3 × 3.20 / 80) = 1/0.12 = 8.3%. The account size cancels out, which is exactly why the rule can be written as a percentage of equity in AFL.

Question 2. You double the ATR multiple from 3 to 6, leaving the risk fraction unchanged. What happens to the dollar loss if the stop is reached?
Show the answer and why

Answer: It is unchanged, because the position shrank in exactly the same proportion

Dollar risk is shares × stop distance, and shares is the budget divided by that same distance. The distance cancels. What does change is the chance of reaching the stop, the number of shares held when the trade works, and the capital committed — so exposure, commissions and turnover all move even though the risk per trade does not.

Question 3. Which of these are true of ApplyStop with the volatile parameter left at its default? Select all that apply.
Show the answer and why

Answer: The amount is sampled at trade entry and held for the life of the trade, Passing 3 * ATR(20) therefore uses the entry bar’s ATR throughout, Setting it to True is what makes a single-line Chandelier exit possible

The documented default (False) samples the amount at entry and holds it. Only volatile = True lets it vary, which is what the official page gives as the Chandelier exit example. The third option describes volatile = True, not the default.

Question 4. A symbol in your universe has been suspended pending a takeover and its last twenty bars are repeated closes. What does ATR-based sizing do with it, and what stops that?
Show the answer and why

Answer: It requests an enormous position, and only an ATR floor and a position cap prevent it

Repeated closes give a true range near zero, so the stop distance approaches zero and the requested weight approaches infinity. AmiBroker will not invent money, but with position size shrinking enabled it will happily put every available currency unit into that one name. The ATR floor and the MaxSizePercent cap are what make the rule survive contact with defective data.

Sources for this lesson

5 verified · checked 2026-08-31

  1. 01AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
  2. 02AFL Function Reference — ApplyStop§ volatile parameter, Chandelier exitamibroker.com/guide/afl/applystop.html2026-08-31
  3. 03AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
  4. 04AmiBroker User's Guide — Portfolio-level backtesting§ Setting up position sizeamibroker.com/guide/h_portfolio.html2026-08-31
  5. 05AmiBroker User's Guide — Scaling in and out (pyramiding)amibroker.com/guide/h_pyramid.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.