Los tamaños de posición fijos ignoran las condiciones del mercado. El objetivo de volatilidad ajusta cuánto inviertes según la volatilidad actual del mercado.
La misma estrategia resulta tan aburrida durante los meses tranquilos que te dan ganas de apagarla; y luego, en la semana en que el mercado empieza a moverse con violencia, sufre un drawdown mucho más profundo de lo que sugería la media del backtest. La señal no cambió, cambió el mercado, y el tamaño de cada apuesta que pones siguió siendo el mismo.
El objetivo de volatilidad consiste en mover el tamaño de posición de forma inversa a la volatilidad realizada del mercado: si la volatilidad está por encima de tu objetivo inviertes menos, si está por debajo inviertes más, hasta un límite.
Una estrategia básica de Tipo A genera una señal de 1.0 (largo) o 0.0 (plano). El runner despliega todo el capital asignado cuando la señal es 1.0. Esto funciona, pero tiene un problema oculto: la estrategia realiza una apuesta del mismo tamaño sin importar si el mercado está tranquilo o caótico.
En un período de baja volatilidad, un movimiento diario del 1% es grande. En un período de alta volatilidad, un movimiento del 1% es ruido. Si tu estrategia entra con el mismo tamaño de posición en ambos entornos, tu riesgo real en dólares oscila drásticamente, y tus drawdowns en períodos volátiles serán mucho mayores de lo que sugiere el promedio de tu backtest.
Blave Agent implementa el objetivo de volatilidad en dos pasos:
log_return = log(Close / Close.shift(1)) realized_vol = log_return.rolling(lookback).std() * sqrt(periods_per_year)
Esto calcula la desviación estándar móvil de los retornos logarítmicos y luego la anualiza multiplicándola por sqrt(periods_per_year). El resultado es la volatilidad realizada anualizada, la misma unidad que la volatilidad del ratio de Sharpe.
Valores por defecto: lookback=720 barras, periods_per_year=8760. Están calibrados para una estrategia de intervalo de 1h: 720 barras = 30 días × 24 horas; 8760 = 24 × 365.
scale = (target_vol / realized_vol).clip(upper=vol_cap) scaled_signal = signal * scale
La señal (1.0 = largo completo) se multiplica por el ratio de volatilidad, limitado a vol_cap. El resultado es un tamaño de posición fraccional.
Valores por defecto: target_vol=0.30 (30% anualizado), vol_cap=2.0 (apalancamiento máximo de 2×).
| Volatilidad realizada (anual.) | Volatilidad objetivo | Escala bruta | Tras el límite | Tamaño de posición |
|---|---|---|---|---|
| 10% | 30% | 3.0× | 2.0× (limitado) | 200% de la asignación |
| 20% | 30% | 1.5× | 1.5× | 150% de la asignación |
| 30% | 30% | 1.0× | 1.0× | 100% — base |
| 60% | 30% | 0.5× | 0.5× | 50% de la asignación |
| 90% | 30% | 0.33× | 0.33× | 33% de la asignación |
Con un 30% de volatilidad realizada, la estrategia opera a tamaño completo. Durante una crisis, cuando la volatilidad realizada se dispara al 90%, la estrategia recorta automáticamente a un tercio de la posición. En un período tranquilo con un 10% de volatilidad, escala hasta el límite de 2×.
Los valores por defecto (lookback=720, periods_per_year=8760) son para una estrategia de 1h. Para otros intervalos, ambos valores deben recalcularse:
| Intervalo | periods_per_year | lookback (ventana de 30 días) |
|---|---|---|
5min | 105,120 (12 × 24 × 365) | 8,640 barras |
1h (por defecto) | 8,760 | 720 barras |
4h | 2,190 | 180 barras |
8h | 1,095 | 90 barras |
1d | 365 | 30 barras |
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
| Parámetro | Por defecto | Qué significa | Cuándo cambiarlo |
|---|---|---|---|
TARGET_VOL | 0.30 | Volatilidad objetivo anualizada del 30% | Según el MDD que puedas aceptar ÷ 2 |
VOL_CAP | 2.0 | Apalancamiento máximo de 2× | Si no quieres apalancamiento alguno, pon 1.0 |
VOL_LOOKBACK | 720 | Longitud de la ventana de estimación de volatilidad (en barras) | Recalcular al cambiar de intervalo, véase la tabla de arriba |
PERIODS_PER_YEAR | 8760 | Periodos por año usados para anualizar (intervalo de 1h) | Recalcular al cambiar de intervalo, véase la tabla de arriba |
Ten en cuenta que add_realized_vol debe llamarse en fetch_data (antes de calcular las señales), porque apply_vol_scaling lee df['realized_vol'], que la primera función establece in situ.
El límite existe porque el objetivo de volatilidad puede generar un apalancamiento extremo en entornos de volatilidad persistentemente baja. Si la volatilidad realizada cae al 5%, la escala sin límite sería de 6×, tres veces la asignación. Ante un movimiento adverso de un día, esto puede provocar un drawdown catastrófico incluso con una señal fundamentalmente sólida.
El límite por defecto de 2× restringe a la estrategia a desplegar como máximo el doble del capital asignado. Para estrategias en las que quieras evitar cualquier apalancamiento, configura vol_cap=1.0. Esto convierte el objetivo de volatilidad en un dimensionamiento de posición puramente ajustado por volatilidad y sin apalancamiento: el tamaño de la posición solo se reduce en alta volatilidad, nunca se expande más allá del tamaño completo.
Una regla práctica útil: target_vol ≈ MDD aceptable ÷ 2. Esto proviene de la relación empírica entre la volatilidad anualizada y el drawdown máximo en estrategias de seguimiento de tendencia.
| target_vol | MDD típico implícito | Caso de uso |
|---|---|---|
| 10% | ~−20% | Conservador, prioridad en la preservación del capital |
| 20% | ~−40% | Moderado |
| 30% (por defecto) | ~−60% | Agresivo — presupuesto de riesgo completo |
¿Tu periods_per_year encaja con el intervalo? El valor por defecto 8760 es para 1h. Es 105,120 para 5min, 2,190 para 4h, 1,095 para 8h y 365 para 1d. Si te equivocas aquí, el paso de anualización sale mal, y con él todas las celdas posteriores.
¿Tu lookback sigue siendo de 30 días? 720 barras = 1h × 30 días. Hay que cambiarlo con el intervalo: 8,640 barras para 5min, 180 para 4h, 90 para 8h, 30 para 1d. En velas diarias, 30 se queda corto: puedes plantearte 252 (un año) para una estimación más estable.
¿Tu target_vol encaja con el MDD que puedes aceptar? La regla práctica es target_vol ≈ MDD aceptable ÷ 2: 10% para alrededor de −20%, 20% para alrededor de −40%, y el 30% por defecto para alrededor de −60%. (Cómo leer el MDD, en Cómo leer un backtest.)
¿Te da vol_cap el apalancamiento que quieres? El valor por defecto 2.0 significa como mucho el doble del capital asignado. Si lo pones en 1.0, solo reducirá la posición cuando la volatilidad sea alta, y nunca la ampliará por encima de la asignación completa cuando sea baja.
¿El TARGET_VOL de la estrategia individual es coherente con el target_vol_pct del Manager de cartera? El Manager decide el apalancamiento global con esa misma lógica (apalancamiento = target_vol ÷ volatilidad realizada de la cartera). Si ambos no coinciden, combinar varias estrategias cuenta el riesgo por duplicado.
lookback barras: cuanto más larga es la ventana, más estable es la estimación y más lenta su reacción; cuanto más corta, más se pasa de frenada ante un pico puntual. Ninguna de las dos te avisa por adelantado de que la volatilidad va a saltar.