Fixed position sizes ignore market conditions. Vol targeting adjusts how much you put on based on how volatile the market currently is.
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.
BlaveClaw implements vol targeting in two steps:
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.
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).
| Realized vol (ann.) | Target vol | Raw scale | After cap | Position 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×.
The defaults (lookback=720, periods_per_year=8760) are for a 1h strategy. For other intervals, both values must be recalculated:
| Interval | periods_per_year | lookback (30-day window) |
|---|---|---|
5min | 105,120 (12 × 24 × 365) | 8,640 bars |
1h (default) | 8,760 | 720 bars |
4h | 2,190 | 180 bars |
8h | 1,095 | 90 bars |
1d | 365 | 30 bars |
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 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.
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_vol | Implied typical MDD | Use 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.