Volatility Targeting for Position Sizing
Volatility targeting sizes a position to a fixed risk budget instead of an edge estimate. The formula, the lookback, the leverage cap, and the failure mode.
Volatility targeting is a position sizing rule that divides a fixed risk budget by how much an asset is currently moving: weight equals target volatility divided by realised volatility. It needs no view on returns at all, which is the practical reason desks reach for it ahead of Kelly criterion position sizing, a rule that cannot produce a number until you first hand it an edge estimate. Size falls when the market gets noisy and rises when it settles.
What is volatility targeting?
Two definitions before the formula. Realised volatility is how much an asset has actually moved over a recent window, measured as the standard deviation of its daily log returns. A log return is the natural logarithm of one close divided by the previous close, the version of a percentage change that adds up cleanly across days.
A standard deviation of daily returns is a daily number, and volatility is quoted per year, so the daily figure gets multiplied by the square root of 252, the rough count of trading sessions in a year. Skip that annualisation step and every weight you compute is off by a factor of about sixteen. The rule itself is one line:
weight = target volatility / realised volatility
Point a 10% annual risk budget at an asset running 20% realised volatility and the rule holds half the account in it. At 40% it holds a quarter. At 4% it asks for 250% of the account, which is where the cap comes in. Implementations clip the weight at a stated maximum, often 1.5x or 2x, so a quiet fortnight cannot talk the arithmetic into a position the account has no business carrying.
Volatility targeting vs the Kelly criterion
Full Kelly is expected excess return divided by variance. Split that into its pieces and it becomes the Sharpe ratio divided by volatility. Volatility targeting is a target divided by volatility. Same shape, different numerator: Kelly puts your estimate of the Sharpe ratio up there, and volatility targeting puts a number you chose.
That swap is the whole trade. An edge estimate is the least stable input in the sizing problem, and a full Kelly position sitting on a mis-measured edge is violent. A risk budget is a policy decision that never needs re-estimating. What you give up is any claim that the size is right for the opportunity. Volatility targeting will size a coin flip and a genuine edge identically, as long as the two move the same amount.
How much does realised volatility move?
The denominator has to move for any of this to matter, so start there. The panel below measures SPY's annualised realised volatility month by month over the past six complete years, with the 10% target drawn flat across it.
The exact SQL behind every number
WITH
px AS
(
SELECT
date AS d,
toFloat64(any(close)) AS c
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= toStartOfMonth(subtractYears(today(), 6))
AND date < toStartOfMonth(today())
GROUP BY d
),
px_sorted AS
(
SELECT arraySort(p -> p.1, groupArray((d, c))) AS pts
FROM px
),
rets AS
(
SELECT arrayJoin(arrayFilter(x -> abs(x.2) < 0.4,
arrayMap((a, b) -> (b.1, log(b.2 / a.2)),
arraySlice(pts, 1, length(pts) - 1),
arraySlice(pts, 2)))) AS r
FROM px_sorted
)
SELECT
toStartOfMonth(tupleElement(r, 1)) AS month,
formatDateTime(toStartOfMonth(tupleElement(r, 1)), '%b %Y') AS month_label,
round(stddevSamp(tupleElement(r, 2)) * sqrt(252) * 100, 1) AS realised_vol_pct,
10 AS target_vol_pct
FROM rets
GROUP BY month, month_label
ORDER BY monthRead the distance between the two lines as position size. In every month where the volatility line sits below the target, the rule holds more than the full account; in every month above it, less. The panel opens in Aug 2020 at 8.3%, and its most recent complete month, Jul 2026, measured 12.1%. The quiet months and the loud ones are separated by a factor of several, and the rule converts that spread straight into leverage.
One target, six very different positions
The same risk budget applied across names produces sizes that look nothing alike.
The exact SQL behind every number
WITH
px AS
(
SELECT
ticker,
date AS d,
toFloat64(any(close)) AS c
FROM global_markets.stocks_daily_aggs
WHERE ticker IN ('KO', 'SPY', 'MSFT', 'AAPL', 'NVDA', 'TSLA')
AND date >= subtractYears(today(), 1)
AND date < today()
GROUP BY ticker, d
),
px_sorted AS
(
SELECT
ticker,
arraySort(p -> p.1, groupArray((d, c))) AS pts
FROM px
GROUP BY ticker
),
vols AS
(
SELECT
ticker AS symbol,
arrayReduce('stddevSamp',
arrayFilter(x -> abs(x) < 0.4,
arrayMap((a, b) -> log(b.2 / a.2),
arraySlice(pts, 1, length(pts) - 1),
arraySlice(pts, 2)))) * sqrt(252) AS ann_vol
FROM px_sorted
)
SELECT
symbol,
round(ann_vol * 100, 1) AS realised_vol_pct,
round(0.10 / ann_vol, 2) AS raw_weight,
round(least(0.10 / ann_vol, 2.0), 2) AS capped_weight
FROM vols
ORDER BY realised_vol_pctOver the trailing year SPY was the calmest of the six at 12.9% annualised, and a 10% budget sizes it at 0.78 times the account. TSLA measured 46.6%, and the identical budget sizes it at 0.21 times. No name here reaches the 2x cap. Caps bind on short lookbacks inside calm stretches, rarely on a full year of data.
The panel also shows what the rule does not claim. It equalises risk contribution across positions and says nothing about which position is worth holding. Keep that separate from the low volatility anomaly, which is an argument about the returns of calm stocks. This is arithmetic about size.
Choosing the lookback
The lookback is the one free parameter with a bill attached. A short window notices a regime change within days and rewrites the position constantly. A long window is steady and slow, and it will still be reporting last quarter's calm two weeks into a selloff. The panel prices that trade: for each window it builds the daily weight, then measures how far the weight travels from one session to the next.
The exact SQL behind every number
WITH
px AS
(
SELECT
date AS d,
toFloat64(any(close)) AS c
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= subtractYears(today(), 5)
AND date < today()
GROUP BY d
),
px_sorted AS
(
SELECT arraySort(p -> p.1, groupArray((d, c))) AS pts
FROM px
),
rets AS
(
SELECT arrayFilter(x -> abs(x) < 0.4,
arrayMap((a, b) -> log(b.2 / a.2),
arraySlice(pts, 1, length(pts) - 1),
arraySlice(pts, 2))) AS r
FROM px_sorted
),
grid AS
(
SELECT
r,
arrayJoin([10, 21, 63, 126]) AS lb
FROM rets
),
weights AS
(
SELECT
lb,
arrayMap(i -> least(2.0,
0.10 / (arrayReduce('stddevSamp', arraySlice(r, i - lb + 1, lb)) * sqrt(252))),
range(lb, length(r) + 1)) AS w
FROM grid
)
SELECT
concat(toString(lb), '-session') AS lookback_window,
round(arrayAvg(w), 2) AS avg_weight,
round(100 * arrayAvg(arrayMap((a, b) -> abs(b - a),
arraySlice(w, 1, length(w) - 1),
arraySlice(w, 2))), 2) AS avg_daily_turnover_pct,
round(100.0 * arrayCount(x -> x > 1.999, w) / length(w), 1) AS days_at_cap_pct
FROM weights
ORDER BY lbAt the 10-session window the position moved an average of 6.89 percentage points of the account per session. At 126-session it moved 0.42. Average exposure over the same stretch came to 0.81x at the short window and 0.66x at the long one. The trading bill is where the two part company: every percentage point of turnover crosses a spread and pays a commission, whether the adjustment was worth making or not. The final column counts how often each window pushed the raw weight past the cap, which at the shortest window happened on 0.7% of sessions.
Where the rule breaks
Realised volatility is backward-looking, and that is not a footnote. The estimate you size on today summarises the last month of trading. When a calm regime ends, the rule is carrying its largest position on the first day of the break and steps down only after the damage has entered the window.
This panel pins the sequence to real tape: weekly readings of SPY's 21-session realised volatility from November 2019 through April 2020, next to the weight a 10% target and a 2x cap would have set.
The exact SQL behind every number
WITH
px AS
(
SELECT
date AS d,
toFloat64(any(close)) AS c
FROM global_markets.stocks_daily_aggs
WHERE ticker = 'SPY'
AND date >= '2019-09-01'
AND date < '2020-05-01'
GROUP BY d
),
px_sorted AS
(
SELECT arraySort(p -> p.1, groupArray((d, c))) AS pts
FROM px
),
rets AS
(
SELECT
arrayMap(p -> p.1, arraySlice(pts, 2)) AS ds,
arrayMap((a, b) -> log(b.2 / a.2),
arraySlice(pts, 1, length(pts) - 1),
arraySlice(pts, 2)) AS rs
FROM px_sorted
),
rolling AS
(
SELECT arrayJoin(arrayMap(i -> (ds[i],
arrayReduce('stddevSamp', arraySlice(rs, i - 20, 21)) * sqrt(252)),
range(21, length(rs) + 1))) AS pt
FROM rets
)
SELECT
tupleElement(pt, 1) AS date,
formatDateTime(tupleElement(pt, 1), '%b %e, %Y') AS week_label,
round(tupleElement(pt, 2) * 100, 1) AS realised_vol_pct,
round(100 * least(0.10 / tupleElement(pt, 2), 2.0), 1) AS weight_pct
FROM rolling
WHERE date >= '2019-11-01'
AND toDayOfWeek(date) = 5
ORDER BY dateIn the week of Nov 1, 2019 the trailing estimate stood at 10.6% and the rule would have carried 94% of the account. The estimate climbed through late February and March. By Apr 24, 2020 it read 46.2%, with the weight down to 21.6%. Every step down on that chart lands after the sessions it measures. The de-risking is real, and it is late.
The cap, and the guardrail beside it
Two things keep the arithmetic from writing checks the account cannot cover. The first is the leverage cap. The second is a rule that watches the account instead of the market: a drawdown limit that trims exposure or halts trading once equity falls a set distance below its high water mark. Volatility targeting has no idea whether you are winning. A run of losses inside an ordinary volatility regime will not move it at all, which is the job maximum drawdown tracking and circuit breakers for trading bots exist to do.
That pairing shows up in open code. riskguard, an MIT licensed risk service, lists dynamic position sizing and automated circuit breakers that halt trading on a threshold breach inside the same short feature set. Sizing rule and kill switch ship together, and neither substitutes for the other.
Run it on your own numbers
The whole method is about a dozen lines of arithmetic. This runs on a stock Python 3 install with no packages and no network. The price series is invented, a quiet stretch followed by a noisy one, so the weight has something to work with.
python3 - <<'PY'
import math
# An invented series: 16 quiet sessions, then 14 noisy ones.
closes = [
100.00, 100.20, 100.05, 100.30, 100.20, 100.38, 100.16, 100.28,
100.48, 100.30, 100.45, 100.33, 100.55, 100.35, 100.45, 100.61,
98.60, 101.06, 98.03, 99.99, 97.19, 99.52, 97.33, 100.25,
98.25, 100.71, 98.20, 100.16, 97.35, 99.78,
]
TARGET_VOL = 0.10 # annual risk budget
LOOKBACK = 10 # sessions in the volatility estimate
MAX_WEIGHT = 2.00 # leverage cap
rets = [math.log(b / a) for a, b in zip(closes, closes[1:])]
def stdev(xs):
mean = sum(xs) / len(xs)
return math.sqrt(sum((x - mean) ** 2 for x in xs) / (len(xs) - 1))
print('session ann_vol raw_w capped_w')
for i in range(LOOKBACK, len(rets) + 1):
ann = stdev(rets[i - LOOKBACK:i]) * math.sqrt(252)
raw = TARGET_VOL / ann
print(f'{i + 1:>7}{ann:>9.1%}{raw:>7.2f}{min(raw, MAX_WEIGHT):>10.2f}')
PY
The printout starts pinned at the 2.00 cap while the calm sessions fill the window, and finishes under 0.30 once the wide sessions have replaced them. Set LOOKBACK to 5 and the column turns twitchy. Set it to 20 and the step down arrives several sessions later.
FAQ
What is volatility targeting in trading?
It is a position sizing rule that holds a fixed amount of risk rather than a fixed amount of capital. You pick an annual volatility you are willing to run, measure what the asset is actually doing, and divide the first by the second to get the weight.
How do you calculate realised volatility?
Take daily log returns over a lookback window, compute their sample standard deviation, then multiply by the square root of 252 to put the figure on an annual footing. A 21-session window is the common choice for a one-month estimate.
Is volatility targeting better than the Kelly criterion?
They answer different questions. Kelly maximises long-run growth given an edge estimate and degrades badly when that estimate is wrong, while volatility targeting ignores edge and controls only the size of the swings.
What target volatility do people use?
Published volatility targeted funds and indices commonly quote annual targets somewhere between 5% and 15%, with the leverage cap stated alongside. The target is a policy choice rather than an output of the data.
Does volatility targeting improve returns?
Nothing in the formula contains an expected return, so nothing in it can manufacture an edge. What it changes is the shape of the risk carried over time, and it adds trading costs in exchange.
How these numbers are computed
- Returns are daily log returns from closing prices, and volatility is their sample standard deviation annualised by the square root of 252.
- The panels discard any single session whose log return exceeds 0.4 in absolute size, which stops a share split from entering an estimate as though it were a real move. Our split adjusted price history guide covers that hazard.
- Weights use a 10% annual target with a 2x cap throughout, and the cap is applied after the raw weight is computed.
- The 2019 to 2020 panel is pinned to fixed dates and never moves. The other three roll forward with the data.
Every panel here carries the exact SQL that produced it. Change the target, the lookback, the cap, or the ticker, and the same arithmetic runs against any name on the Strasmore terminal.