Cómo crear un screener multiactivo en Python con RSI, ATR, ADX y medias móviles

0
7
Screener multiactivos para Python

¿Usas screeners de forma regular para analizar y seleccionar activos para invertir o especular a corto plazo? Si es así, en este artículo presentamos un screener desarrollado en el lenguaje de programación Python que permite analizar múltiples mercados de forma simultánea para determinar que activos están en tendencia o con una alta posibilidad de cambiar su dirección. Mostramos el código completo junto con una descripción de cada parte de tal forma que el lector tenga una comprensión de su funcionamiento.

Un screener puede ser una herramienta particularmente útil para seguir múltiples activos financieros en una sola interfaz. Dependiendo del diseño, podemos tener screeners que dan seguimiento a distintos valores y métricas, como el precio, volumen de negociación, máximos y mínimos de periodo e indicadores técnicos conocidos y no tan conocidos como medias móviles, el RSI, el oscilador estocástico o el ADX, entre muchos otros.

Los screeners pueden desarrollarse para analizar los mercados que más nos interesa con base en criterios y estrategias muy específicas. Por ejemplo, aquí en Tecnicasdetrading.com creamos un screener para Metatrader 4 que permite evaluar decenas de pares de divisas y otros activos usando las reglas de una estrategias de trading específica de tal forma que determina en cual de estos mercados se ha producido una señal de compra o venta.

Más información en: Screener del sistema de trading multitimeframe Cowabunga

En este sentido, un screener nos evita el trabajo de seguir múltiples activos de interés usando gran cantidad de gráficos de precios. En el screener podemos vigilar todos estos activos de forma simultánea.

Por eso, el screener que explicamos en este artículo puede convertirse en una herramienta útil si operas de forma regular en varios mercados usando herramientas de análisis técnico. Es un screener relativamente simple que usamos como ejemplo pero puede usarse como base para crear herramientas más complejas.

La siguiente imagen muestra la información que nos da el screener:

Screener multiactivos para Python

Screener multiactivos para Python

Principales características del screener

  • Como ya indicamos, es un screener desarrollado en Python, usando bibliotecas conocidas como yfinance y Pandas.
  • Para todos los activos indicados en el código (ver más adelante), el screener obtiene y calcula los siguientes datos:
    • Precio de cierre.
    • Si el precio ha roto el máximo diario
    • Si el precio ha roto el mínimo diario
    • Si el precio ha roto el máximo semanal
    • Si el precio ha roto el mínimo semanal
    • Si el precio ha roto el máximo mensual
    • Si el precio ha roto el mínimo mensual
    • Si el precio está arriba de las SMA de 20, 50 y 200 periodos.
    • Valor del RSI.
    • Estado del RSI (sobrecompra o sobreventa)
    • Valor del ATR
    • Valor del ADX
    • Estado de la tendencia (Fuerte o lateral)
  • El screener NO se actualiza en tiempo real, ya que usa la biblioteca yFinance para obtener los datos financieros y como esta biblioteca no es oficial de Yahoo Finance, si el script hace un exceso de llamadas para la obtención de datos la página de Yahoo puede banear nuestra IP e impedir que se obtenga más información financiera desde nuestra computadora. Una solución para esto puede ser programar el script para que haga una llamada a Yahoo Finance cada 15 minutos por ejemplo, para así calcular nuevamente los indicadores y rehacer el screener. Pero dados los parámetros e indicadores que usa el screener, no creemos que sea necesario ya que la información que presenta es más aplicable para trades a mediano y largo plazo (máximo/mínimo diario o semanal, por ejemplo). Por lo tanto, el screener puede ser usado una vez al día, al inicio de la sección del mercado, para tener un panorama de los activos que parecen más interesantes.
  • El código del screener se presenta completo y puede modificarse para agregar o eliminar activos, agregar o quitar indicadores o agregar nuevas funciones, como por ejemplo indicadores menos conocidos.

Ahora vamos a explicar detalladamente cada parte del código.

Código del screener del mercado – Descripción del código sección a sección

Tecnicasdetrading.com no es un sitio web enfocado en programación y por lo tanto no vamos a hacer una descripción profunda de cada línea de código. Para eso hay muy buenos tutoriales. Pero si vamos a detallar para que sirve el código en las distintas funciones.

Cabecera del código

"""
Screener Técnico Simple - Python + Matplotlib
------------------------------------------------
Para una lista de activos, muestra:
- Si el precio superó el máximo/mínimo diario, semanal o mensual
- Si está por encima/debajo de 3 medias móviles (SMA 20, 50, 200)
- Si el RSI indica sobrecompra / sobreventa
- ATR (volatilidad) y ADX (fuerza de la tendencia)

Requisitos (instalar antes de correr):
    pip install yfinance pandas numpy matplotlib
"""

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

Esta primera parte del código sirve para importar las bibliotecas que usa el script para generar el screener. Son bibliotecas conocidas, útiles y con bastante documentación en Internet.

  • yfinance: Es una biblioteca que obtiene datos financieros actualizados de Yahoo Finance. No es una biblioteca oficial de este sitio y debe usarse con cuidado.
  • Pandas: Es una biblioteca de código abierto para la manipulación y análisis de datos en Python. Se usa para ordenar los datos obtenidos a través de yfinance, generar dataframes más fáciles de manejar e incluso para calcular algunos indicadores con menos código.
  • Numpy: NumPy es una biblioteca de código abierto para Python especializada en computación numérica y ciencia de datos. Se usa para el manejo de datos y para facilitar los cálculos usados para la obtención de indicadores.
  • matplotlib.pyplot: Esta biblioteca sirve para trazar gráficos, cuadros y otros arreglos visuales similares. Es una excelente opción para presentar datos y resultados de forma clara y comprensible.

Variables de configuración (líneas 19-29)

# ----------------------------------------------------
# 1. CONFIGURACIÓN — lo único que normalmente cambiarás
# ----------------------------------------------------
TICKERS = ["AAPL", "MSFT", "GOOGL", "TSLA", "AMZN", "NVDA","EURUSD=X","BTC-USD","ETH-USD"]
SMA_PERIODS = [20, 50, 200]
RSI_PERIOD = 14
RSI_OVERBOUGHT = 70
RSI_OVERSOLD = 30
ATR_PERIOD = 14
ADX_PERIOD = 14
ADX_TENDENCIA_FUERTE = 25  # umbral clásico de Wilder

Estos son los valores y variables que podemos modificar para cambiar lo que muestra el screener. Por ejemplo, en TICKERS podemos agregar más activos para hacer un screener más amplio, como IBM, GBPUSD, LMT, AMD y otros.

Las otras variables corresponden a los periodos de cálculo de indicadores como las medias móviles (SMA), RSI y ADX.

Funciones de cálculo de indicadores (líneas 32-78)

def calcular_rsi(precios: pd.Series, periodo: int = 14) -> pd.Series:
    """RSI clásico de Wilder, calculado con medias móviles simples."""
    delta = precios.diff()
    ganancia = delta.clip(lower=0)
    perdida = -delta.clip(upper=0)
    media_ganancia = ganancia.rolling(periodo).mean()
    media_perdida = perdida.rolling(periodo).mean()
    rs = media_ganancia / media_perdida
    return 100 - (100 / (1 + rs))


def calcular_true_range(data: pd.DataFrame) -> pd.Series:
    """True Range: la mayor distancia entre High/Low/Cierre anterior.
    Es la base tanto del ATR como del ADX."""
    alto_bajo = data["High"] - data["Low"]
    alto_cierre_prev = (data["High"] - data["Close"].shift(1)).abs()
    bajo_cierre_prev = (data["Low"] - data["Close"].shift(1)).abs()
    return pd.concat([alto_bajo, alto_cierre_prev, bajo_cierre_prev], axis=1).max(axis=1)


def calcular_atr(data: pd.DataFrame, periodo: int = 14) -> pd.Series:
    """ATR = promedio móvil del True Range. Mide VOLATILIDAD (rango de movimiento),
    no dirección. Útil para saber si un breakout es "grande" o solo ruido normal."""
    tr = calcular_true_range(data)
    return tr.rolling(periodo).mean()


def calcular_adx(data: pd.DataFrame, periodo: int = 14) -> pd.Series:
    """ADX = mide la FUERZA de la tendencia (no la dirección).
    Por convención: ADX > 25 -> tendencia fuerte, ADX < 20 -> mercado lateral."""
    up_move = data["High"].diff()
    down_move = -data["Low"].diff()

    plus_dm = np.where((up_move > down_move) & (up_move > 0), up_move, 0.0)
    minus_dm = np.where((down_move > up_move) & (down_move > 0), down_move, 0.0)

    tr = calcular_true_range(data)
    atr = tr.rolling(periodo).mean()

    plus_di = 100 * pd.Series(plus_dm, index=data.index).rolling(periodo).mean() / atr
    minus_di = 100 * pd.Series(minus_dm, index=data.index).rolling(periodo).mean() / atr

    dx = 100 * (plus_di - minus_di).abs() / (plus_di + minus_di)
    return dx.rolling(periodo).mean()  # esto es el ADX

Aquí no se descarga nada todavía — son funciones “puras”: reciben datos y devuelven un cálculo. Eso las hace fáciles de probar y de explicar por separado.

-calcular_rsi — El RSI mide si las subidas recientes pesan más que las bajadas:

  • delta = diferencia entre el precio de hoy y el de ayer.
  • clip(lower=0) se queda solo con los días que subieron (las bajadas quedan en 0) → esa es la “ganancia”.
  • clip(upper=0) y el signo negativo hacen lo inverso → la “pérdida”.
  • Se promedian ambas con rolling(14) y se aplica la fórmula clásica de Wilder.

-calcular_true_range — El “rango real” de una vela, considerando también el cierre anterior (por si hubo un gap). Se calculan las 3 distancias posibles y se toma la mayor con .max(axis=1). Esta función es la base de las otras dos.

-calcular_atr — Literalmente el promedio móvil del True Range. Por eso es tan corta: solo reutiliza calcular_true_range y le aplica .rolling(periodo).mean().

-calcular_adx — La más densa. La idea paso a paso:

  • up_move/down_move: cuánto subió el máximo y cuánto bajó el mínimo respecto al día anterior.
  • plus_dm/minus_dm (Directional Movement): con np.where nos quedamos solo con el movimiento “dominante” del día — si subió más de lo que bajó, cuenta como movimiento alcista, y viceversa.
  • plus_di/minus_di: se normalizan esos movimientos dividiendo entre el ATR, para expresarlos como porcentaje de la volatilidad total.
  • dx: mide qué tan separados están +DI y -DI (mucha separación = tendencia clara, uno domina al otro).
  • El ADX final es un promedio móvil del DX, para suavizarlo.

Función de análisis de los activos (líneas 81-146)

def analizar_activo(ticker: str) -> dict | None:
    """Descarga datos diarios y devuelve un diccionario con el estado del activo."""
    # Usamos yf.Ticker().history() en vez de yf.download() porque
    # siempre devuelve columnas planas (evita el MultiIndex que rompe el resample)
    data = yf.Ticker(ticker).history(period="2y", interval="1d", auto_adjust=True)
    if data.empty:
        print(f"⚠️  Sin datos para {ticker}")
        return None
    data.index = data.index.tz_localize(None)  # quitamos timezone para evitar problemas con resample

    # Indicadores
    data["RSI"] = calcular_rsi(data["Close"], RSI_PERIOD)
    data["ATR"] = calcular_atr(data, ATR_PERIOD)
    data["ADX"] = calcular_adx(data, ADX_PERIOD)
    for p in SMA_PERIODS:
        data[f"SMA{p}"] = data["Close"].rolling(p).mean()

    precio_actual = data["Close"].iloc[-1].item()

    # Máximo/mínimo de la vela ANTERIOR (para saber si hoy los "rompe")
    max_diario = data["High"].iloc[-2].item()
    min_diario = data["Low"].iloc[-2].item()

    # Resample a semanal y mensual para obtener sus máximos/mínimos
    semanal = data.resample("W").agg({"High": "max", "Low": "min"})
    mensual = data.resample("ME").agg({"High": "max", "Low": "min"})
    max_semanal = semanal["High"].iloc[-2].item()
    min_semanal = semanal["Low"].iloc[-2].item()
    max_mensual = mensual["High"].iloc[-2].item()
    min_mensual = mensual["Low"].iloc[-2].item()

    rsi_actual = data["RSI"].iloc[-1].item()
    if rsi_actual >= RSI_OVERBOUGHT:
        estado_rsi = "Sobrecompra"
    elif rsi_actual <= RSI_OVERSOLD: estado_rsi = "Sobreventa" else: estado_rsi = "Neutral" resultado = { "Ticker": ticker, "Precio": round(precio_actual, 2), "Rompe Max D": precio_actual > max_diario,
        "Rompe Min D": precio_actual < min_diario, "Rompe Max S": precio_actual > max_semanal,
        "Rompe Min S": precio_actual < min_semanal, "Rompe Max M": precio_actual > max_mensual,
        "Rompe Min M": precio_actual < min_mensual, } for p in SMA_PERIODS: sma_val = data[f"SMA{p}"].iloc[-1].item() resultado[f"> SMA{p}"] = precio_actual > sma_val

    resultado["RSI"] = round(rsi_actual, 1)
    resultado["Estado RSI"] = estado_rsi

    atr_actual = data["ATR"].iloc[-1].item()
    adx_actual = data["ADX"].iloc[-1].item()

    resultado["ATR"] = round(atr_actual, 2)
    resultado["ATR %"] = round(100 * atr_actual / precio_actual, 2)  # volatilidad relativa al precio
    resultado["ADX"] = round(adx_actual, 1)
    resultado["Tendencia"] = "Fuerte" if adx_actual >= ADX_TENDENCIA_FUERTE else "Débil / Lateral"

    return resultado

Es una función que recibe un ticker y devuelve un diccionario con todo su diagnóstico. Ese patrón (una función por activo) es clave: hace que construir_tabla después sea trivial.

  • Líneas 85-89: descarga el histórico y limpia el índice de fechas (quitamos zona horaria porque estorba al hacer resample).
  • Líneas 92-96: aquí se calculan todos los indicadores y se guardan como columnas nuevas dentro del propio DataFrame data. Es más fácil trabajar así que tener variables sueltas.
  • Líneas 100-110: sacamos el máximo/mínimo del día, semana y mes anteriores — usamos .iloc[-2] (la penúltima vela) para no comparar el precio contra sí mismo. resample(“W”) y resample(“ME”) agrupan las velas diarias en semanas/meses y sacan el máximo/mínimo de cada grupo.
  • Líneas 112-118: clasificamos el RSI actual en una de 3 categorías de texto, usando los umbrales que definiste en la configuración.
  • Líneas 120-129: acá arranca el diccionario resultado — cada clave será una columna de la tabla final. Los “Rompe Max/Min” son simples comparaciones booleanas (True/False).
  • Líneas 131-133: un bucle que agrega una columna por cada SMA (> SMA20, > SMA50, > SMA200) sin tener que repetir código tres veces.
  • Líneas 138-144: sacamos el último valor de ATR y ADX, y agregamos las columnas finales — incluyendo ATR % (volatilidad relativa al precio) y la etiqueta Tendencia.

Función de construcción de tablas (líneas 149-151)

def construir_tabla(tickers: list[str]) -> pd.DataFrame:
    filas = [r for t in tickers if (r := analizar_activo(t)) is not None]
    return pd.DataFrame(filas)

Esto es una list comprehension con walrus operator (:=): por cada ticker, llama a analizar_activo, guarda el resultado en r, y solo lo incluye si no fue None (es decir, si el ticker sí tenía datos). Es una forma compacta de hacer “map + filtro” en una sola línea. Si te parece muy densa para el tutorial, se puede escribir igual con un for normal — el resultado es el mismo.

Función de graficación del screener con Matploblib (líneas 157-198)

# ----------------------------------------------------
# 3. VISUALIZACIÓN CON MATPLOTLIB (tabla tipo semáforo)
# ----------------------------------------------------
def graficar_screener(df: pd.DataFrame, archivo_salida: str = "screener.png"):
    columnas_bool = [c for c in df.columns if c.startswith(("Rompe", ">"))]

    # Convertimos True/False a check para que se vea limpio
    tabla_texto = df.copy()
    for c in columnas_bool:
        tabla_texto[c] = tabla_texto[c].map({True: "✔", False: ""})

    fig, ax = plt.subplots(figsize=(15, 0.55 * len(df) + 1.5))
    ax.axis("off")

    tabla = ax.table(
        cellText=tabla_texto.values,
        colLabels=tabla_texto.columns,
        cellLoc="center",
        loc="center",
    )
    tabla.auto_set_font_size(False)
    tabla.set_fontsize(9)
    tabla.scale(1, 1.7)

    verde = "#c6efce"
    rojo = "#ffc7ce"

    # Coloreamos cada celda según su valor (fila 0 = encabezado)
    for fila_idx, (_, fila) in enumerate(df.iterrows()):
        for col_idx, col in enumerate(df.columns):
            celda = tabla[fila_idx + 1, col_idx]
            if col in columnas_bool and fila[col]:
                celda.set_facecolor(verde)
            elif col == "Estado RSI":
                if fila[col] == "Sobrecompra":
                    celda.set_facecolor(rojo)
                elif fila[col] == "Sobreventa":
                    celda.set_facecolor(verde)
            elif col == "Tendencia" and fila[col] == "Fuerte":
                celda.set_facecolor(verde)

    plt.title("Screener Técnico Multiactivos - Tecnicasdetrading.com", fontsize=15, weight="bold", pad=20)
    plt.tight_layout()
    plt.savefig(archivo_salida, dpi=150, bbox_inches="tight")
    plt.show()
  • Línea 158: identifica automáticamente cuáles columnas son booleanas (empiezan con “Rompe” o “>”), para no tener que listarlas a mano.
  • Líneas 161-163: convierte True/False en ✔/vacío — Matplotlib va a dibujar texto, no íconos, así que esto es solo estético.
  • Líneas 168-176: ax.table() es la función clave — dibuja una tabla usando los valores del DataFrame como celdas y las columnas como encabezado.
  • Líneas 182-193: aquí está el “semáforo”. Recorremos cada celda (fila × columna) y le cambiamos el color de fondo según su contenido: verde si es una condición cumplida, rojo si es sobrecompra, etc. El +1 en fila_idx + 1 es porque la fila 0 de la tabla la ocupa el encabezado.
  • Línea 197: guarda la imagen como PNG además de mostrarla, útil para incluir en el tutorial o compartir el resultado.

Bloque de ejecución (líneas 204-207)

# ----------------------------------------------------
# 4. EJECUCIÓN
# ----------------------------------------------------
if __name__ == "__main__":
    df = construir_tabla(TICKERS)
    print(df.to_string(index=False))
    graficar_screener(df)

El clásico if __name__ == “__main__”: — asegura que el código solo se ejecute si corres el archivo directamente (no si alguien lo importa como módulo en otro script). Buena práctica para que tu tutorial también sirva de referencia de cómo estructurar proyectos.

Recomendaciones

Este indicador fue desarrollado como ejemplo para traders y programadores de Python con el fin de mostrar lo que podemos crear con este lenguaje de programación. Es relativamente simple, sobre todo en comparación con herramientas equivalentes que podemos encontrar en plataformas como TradingView. Sin embargo, podemos aplicar varias mejoras que pueden aumentar la funcionalidad de la herramienta, tales como:

  • Usar fuentes de datos financieros más completas que permitan actualizar el screener en tiempo real o al menos en intervalos de tiempo más cortos.
  • Agregar indicadores técnicos adicionales más complejos que analicen aspectos distintos del mercado, como el volumen, volatilidad, etc.
  • Combinar los análisis de la acción del precio e indicadores con análisis de sentimiento del mercado.
  • Incorporar análisis de patrones de precios que pueden servir como confirmación de tendencias o cambios de tendencia.
  • Agregar filtros que permitan ordenar los activos de acuerdo a distintos criterios.
  • Usar screeners que sean específicos para mercados distintos y que incorporen indicadores propios de estos mercados, como la relación P/E en el caso de las acciones.
  • Incorporar pronósticos basados en alguna estrategia de trading técnica o similar.

 

TagsPython

Leave a reply