feedback
← Back to Learn

Volatility Targeting Explained

Fixed position sizes ignore market conditions. Vol targeting adjusts how much you put on based on how volatile the market currently is.

The problem with fixed position sizes

A basic Type A strategy outputs a signal of 1.0 (long) or 0.0 (flat). The runner deploys the full allocated capital when the signal is 1.0. This works, but it has a hidden problem: the strategy takes the same-sized bet regardless of whether the market is calm or chaotic.

In a low-volatility period, a 1% daily move is large. In a high-volatility period, a 1% move is noise. If your strategy enters with the same position size in both environments, your actual dollar risk swings dramatically — and your drawdowns in volatile periods will be far larger than what your backtest average suggests.

Vol targeting solves this by scaling position size inversely with realized volatility: when the market is more volatile than your target, you put on less; when it's calmer, you put on more — up to a cap.

The formula

BlaveClaw implements vol targeting in two steps:

Step 1 — Measure realized volatility

log_return = log(Close / Close.shift(1))
realized_vol = log_return.rolling(lookback).std() * sqrt(periods_per_year)

This computes the rolling standard deviation of log returns, then annualizes it by multiplying by sqrt(periods_per_year). The result is annualized realized volatility — the same unit as Sharpe ratio volatility.

Defaults: lookback=720 bars, periods_per_year=8760. These are calibrated for a 1h interval strategy: 720 bars = 30 days × 24 hours; 8760 = 24 × 365.

Step 2 — Scale the signal

scale = (target_vol / realized_vol).clip(upper=vol_cap)
scaled_signal = signal * scale

The signal (1.0 = full long) is multiplied by the volatility ratio, capped at vol_cap. The result is a fractional position size.

Defaults: target_vol=0.30 (30% annualized), vol_cap=2.0 (maximum 2× leverage).

Concrete examples

Realized vol (ann.)Target volRaw scaleAfter capPosition size
10%30%3.0×2.0× (capped)200% of allocation
20%30%1.5×1.5×150% of allocation
30%30%1.0×1.0×100% — baseline
60%30%0.5×0.5×50% of allocation
90%30%0.33×0.33×33% of allocation

At 30% realized vol the strategy runs at full size. During a crisis when realized vol spikes to 90%, the strategy automatically trims to one-third position. In a calm period at 10% vol, it scales up to the cap of 2×.

Adjusting for different intervals

The defaults (lookback=720, periods_per_year=8760) are for a 1h strategy. For other intervals, both values must be recalculated:

Intervalperiods_per_yearlookback (30-day window)
5min105,120 (12 × 24 × 365)8,640 bars
1h (default)8,760720 bars
4h2,190180 bars
8h1,09590 bars
1d36530 bars
For daily strategies, a 30-bar lookback is short. Consider using a longer lookback like 252 bars (one year) to capture a more stable volatility estimate and avoid over-reacting to short-term spikes.

How to add it to a strategy

VOL_TARGETING    = True
TARGET_VOL       = 0.30   # 30% annualized target
VOL_CAP          = 2.0    # max 2× leverage
VOL_LOOKBACK     = 720    # bars in lookback window
PERIODS_PER_YEAR = 8760   # for 1h interval

def fetch_data(hdrs):
    from lib.data import fetch_kline
    from lib.strategy import add_realized_vol
    df = fetch_kline(SYMBOL, INTERVAL, START, END, hdrs)
    if VOL_TARGETING:
        add_realized_vol(df, VOL_LOOKBACK, PERIODS_PER_YEAR)
    return df

def compute_signals(df):
    from lib.strategy import apply_vol_scaling
    signal = pd.Series(np.nan, index=df.index)
    # ... your signal logic here ...
    if VOL_TARGETING:
        return apply_vol_scaling(signal, df, TARGET_VOL, VOL_CAP)
    return signal

Note that add_realized_vol must be called in fetch_data (before signals are computed), because apply_vol_scaling reads df['realized_vol'] which is set in-place by the first function.

The vol_cap: why 2× and not higher

The cap exists because vol targeting can produce extreme leverage in persistently low-volatility environments. If realized vol drops to 5%, the uncapped scale would be 6× — three times the allocation. On a 1-day adverse move, this can cause a catastrophic drawdown even with a fundamentally sound signal.

The default cap of 2× limits the strategy to deploying at most double the allocated capital. For strategies where you want to avoid any leverage at all, set vol_cap=1.0. This turns vol targeting into pure vol-adjusted position sizing with no leverage — position size only shrinks in high-vol, never expands past full size.

Target vol and implied MDD

A useful rule of thumb: target_vol ≈ acceptable MDD ÷ 2. This comes from the empirical relationship between annualized volatility and maximum drawdown in trend-following strategies.

target_volImplied typical MDDUse case
10%~−20%Conservative, capital preservation priority
20%~−40%Moderate
30% (default)~−60%Aggressive — full risk budget

This same relationship is used in the portfolio manager when sizing the overall portfolio: leverage = target_vol / portfolio_realized_vol. Setting your individual strategy's TARGET_VOL to be consistent with the manager's target_vol_pct avoids double-counting risk when strategies are combined.

← Back to Learn