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.
The unit problem
Section titled “The unit problem”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.
What the normalisation actually equalises
Section titled “What the normalisation actually equalises”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.
Choosing the multiple
Section titled “Choosing the multiple”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.
Writing it
Section titled “Writing it”What we are building
Section titled “What we are building”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.
The complete formula
Section titled “The complete formula”Complete runnable 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 reachedAtrPeriod = 20; // lookback for Average True RangeStopAtrMult = 3.0; // stop sits this many ATRs below the entry priceMaxSizePercent = 20; // cap: no position larger than this share of equityMinAtrPct = 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 );How it works
Section titled “How it works”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.
Functions worth a closer look
Section titled “Functions worth a closer look”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, … )— withstopModePoint, theamountis 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 )andMin( 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()andLowest()are the functions that do that.
What you should see
Section titled “What you should see”Test it
Section titled “Test it”Pick one symbol and one entry from the Detailed log, then reconstruct the size by hand:
- On the chart, add
ATR(20)as an indicator and read its value on the bar before the entry. - Multiply by 3. That is the stop distance the trade should have used.
- Divide 1.0 by (stop distance ÷ that bar’s close) to get the intended weight as a percentage.
- 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.
Common errors
Section titled “Common errors”- No cap. Symptom: one position dwarfs the others, and
Max. system % drawdownis driven almost entirely by it. Cause: a low-ATR symbol asked for more than the account andAllowPositionShrinkingobligingly 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.
stopModePercentandstopModePointare 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.
Take it further
Section titled “Take it further”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.
When volatility changes after you are in
Section titled “When volatility changes after you are 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
Sources for this lesson
5 verified · checked 2026-08-31
- 01AFL Function Reference — ATRamibroker.com/guide/afl/atr.html2026-08-31
- 02AFL Function Reference — ApplyStop§ volatile parameter, Chandelier exitamibroker.com/guide/afl/applystop.html2026-08-31
- 03AFL Function Reference — SetPositionSizeamibroker.com/guide/afl/setpositionsize.html2026-08-31
- 04AmiBroker User's Guide — Portfolio-level backtesting§ Setting up position sizeamibroker.com/guide/h_portfolio.html2026-08-31
- 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.