5. Un caso completo: Bandas de Bollinger con RSI
Nivel 12: Backtesting y Métricas de Riesgo Reales
Vamos a construir una estrategia algo más compleja combinando Bandas de Bollinger y el Índice de Fuerza Relativa (RSI) con Backtrader. La lógica es de reversión a la media con confirmación:
- Comprar cuando el precio cierra por debajo de la banda inferior de Bollinger y el RSI está por debajo de 30, lo que indica una condición de sobreventa.
- Vender cuando el precio cierra por encima de la banda superior y el RSI está por encima de 70, condición de sobrecompra.
La idea de exigir dos condiciones simultáneas es reducir falsos positivos: el precio puede perforar la banda inferior en plena tendencia bajista sin que haya nada que revertir, y el RSI actúa de filtro.
import backtrader as bt
import yfinance as yf
import datetime
class BollingerRSIStrategy(bt.Strategy):
params = (
('bband_period', 20),
('rsi_period', 14),
('overbought', 70),
('oversold', 30),
('stddev', 2),
)
def __init__(self):
self.bband = bt.indicators.BollingerBands(
self.data.close,
period=self.params.bband_period,
devfactor=self.params.stddev
)
self.rsi = bt.indicators.RelativeStrengthIndex(
period=self.params.rsi_period
)
def next(self):
if self.data.close < self.bband.lines.bot and self.rsi < self.params.oversold:
self.buy()
elif self.data.close > self.bband.lines.top and self.rsi > self.params.overbought:
self.sell()
data_df = yf.download('AAPL', start='2020-01-01', end='2022-01-01')
data = bt.feeds.PandasData(dataname=data_df)
cerebro = bt.Cerebro(stdstats=False)
cerebro.addstrategy(BollingerRSIStrategy)
cerebro.adddata(data)
cerebro.broker.set_cash(100000.0)
cerebro.addsizer(bt.sizers.FixedSize, stake=1000)
cerebro.broker.setcommission(commission=0.001)
print('Valor inicial de la cartera: %.2f' % cerebro.broker.getvalue())
cerebro.run()
print('Valor final de la cartera: %.2f' % cerebro.broker.getvalue())
cerebro.plot(iplot=True)Qué hace cada pieza
BollingerBands construye tres líneas: mid (la media móvil simple del periodo indicado), top y bot, situadas a devfactor desviaciones típicas por encima y por debajo de la media. Con devfactor=2 y retornos aproximadamente normales, alrededor del 5% de los cierres quedarían fuera de las bandas. Los retornos financieros no son normales y tienen colas gruesas, así que en la práctica se sale de las bandas bastante más a menudo, sobre todo en episodios de estrés.
RelativeStrengthIndex con periodo 14 es la configuración estándar de Wilder. Ojo con una trampa habitual: en un activo con tendencia alcista fuerte y sostenida, el RSI de 14 puede pasar meses sin bajar de 40. La estrategia simplemente no comprará, y el backtest devolverá un resultado plano que se puede confundir con una estrategia prudente cuando en realidad es una estrategia inactiva.
stdstats=False en el constructor de Cerebro desactiva los observadores estándar (valor de cartera, operaciones y compraventas) del gráfico. Sirve para que el dibujo quede limpio cuando ya tienes varios paneles de indicadores, pero significa que pierdes de vista la curva de capital, que es justamente lo que más deberías mirar. Úsalo con conocimiento de causa.
Advertencias del autor, que conviene tomarse en serio
- Valida la estrategia con datos distintos y bajo escenarios variados para asegurar su robustez.
- Ten presente que el rendimiento pasado no es indicativo de resultados futuros, y que el backtesting debe ser una herramienta entre varias al desarrollar una estrategia.
- Aplica siempre principios sólidos de gestión del riesgo: fija niveles de stop-loss y arriesga solo un pequeño porcentaje de la cartera en cada operación.
Sobre este código concreto hay dos deficiencias que ya deberías detectar tú solo. La primera: no hay stop-loss ni objetivo de beneficio, así que una posición perdedora puede quedarse abierta indefinidamente. La segunda: como no se comprueba self.position, varias señales de compra consecutivas acumulan posición sin control. Añadir if not self.position antes de comprar y usar self.close() para salir es el mínimo exigible antes de tomarse en serio cualquier número que salga de aquí.