3. Tu primera señal de trading real: el cruce MACD
Nivel 3: Datos de Mercado y APIs
Hasta ahora has descargado y visualizado precios. En este módulo das el paso que convierte un gráfico en una estrategia: generar automáticamente señales de compra y venta a partir de un indicador técnico.
El indicador que vas a usar es el MACD (Moving Average Convergence/Divergence), uno de los más usados en análisis técnico porque combina tendencia y momentum en un solo número. Se construye con dos medias móviles exponenciales (EMA) de distinta longitud: una rápida y una lenta.
ema_rapida = datos["Close"].ewm(span=12, adjust=False).mean()
ema_lenta = datos["Close"].ewm(span=26, adjust=False).mean()
macd = ema_rapida - ema_lenta
señal = macd.ewm(span=9, adjust=False).mean()Una media exponencial (EMA) da más peso a los precios recientes que a los antiguos, a diferencia de la media móvil simple del módulo anterior, que pesa todos los días de la ventana por igual. Eso la hace reaccionar más rápido a cambios recientes de precio. El MACD es, literalmente, la diferencia entre la EMA rápida (12 periodos) y la EMA lenta (26 periodos); la línea de señal es, a su vez, una EMA de 9 periodos calculada sobre el propio MACD.
La interpretación clásica es: cuando el MACD cruza por encima de la línea de señal, es una señal de compra; cuando cruza por debajo, es una señal de venta. Convertir esa idea en código significa recorrer la serie día a día y comparar las dos líneas en cada punto:
def señales_macd(df):
precio_compra = []
precio_venta = []
posicion_abierta = False
for i in range(len(df)):
if df["MACD"].iloc[i] > df["Señal"].iloc[i]:
if not posicion_abierta:
precio_compra.append(df["Open"].iloc[i + 1] if i + 1 < len(df) else float("nan")) # la señal nace con el cierre de i; se ejecuta a la apertura de i+1
precio_venta.append(float("nan"))
posicion_abierta = True
else:
precio_compra.append(float("nan"))
precio_venta.append(float("nan"))
elif df["MACD"].iloc[i] < df["Señal"].iloc[i]:
if posicion_abierta:
precio_venta.append(df["Open"].iloc[i + 1] if i + 1 < len(df) else float("nan")) # se sale igual que se entra: a la apertura del dia siguiente
precio_compra.append(float("nan"))
posicion_abierta = False
else:
precio_compra.append(float("nan"))
precio_venta.append(float("nan"))
else:
precio_compra.append(float("nan"))
precio_venta.append(float("nan"))
return precio_compra, precio_ventaEl detalle importante de esta función es la variable posicion_abierta: sin ella, el código generaría una señal de compra en cada día que el MACD esté por encima de la señal, no solo en el momento exacto del cruce — lo que produciría docenas de señales de compra seguidas en vez de una sola. posicion_abierta actúa como memoria: solo compra si no tenías ya una posición abierta, y solo vende si la tenías. Este patrón — una variable de estado que recuerda en qué situación está la estrategia — es la base de prácticamente cualquier generador de señales, por sofisticado que sea el indicador de turno.
Con las dos listas resultantes, puedes marcarlas directamente sobre el precio para ver, visualmente, si la lógica tiene sentido antes de fiarte de ella:
datos["Compra"], datos["Venta"] = señales_macd(datos)
plt.scatter(datos.index, datos["Compra"], color="green", marker="^", label="Compra")
plt.scatter(datos.index, datos["Venta"], color="red", marker="v", label="Venta")
plt.plot(datos["Close"], alpha=0.35, label="Precio")
plt.legend()
plt.show()Este ejemplo tiene un límite importante que conviene tener claro desde ya: genera señales, no una estrategia completa. No calcula cuánto capital arriesgar, no tiene stop-loss, y no ha pasado por ningún backtest riguroso — eso llega en niveles posteriores. Por ahora, el objetivo es entender el patrón: indicador, comparación, señal, estado.