---
title: "1.7.1 Indicadores técnicos en Python con pandas"
description: "Medias, MACD, RSI, estocástico, Bollinger, ATR, ADX, Donchian, Keltner, OBV, VWAP y perfil de volumen, programados con pandas y sin mirar al futuro."
url: "https://pythonparatrading.com/curso-python-trading/nivel-1/indicadores-tecnicos-en-python/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-1/indicadores-tecnicos-en-python.png"
---

# 1.7.1 · Indicadores técnicos en Python

> Un indicador técnico es una transformación de precios y volúmenes pasados en un número. Esta lección programa los más usados con pandas, uno por familia, sobre la serie ajustada de NOVA y sobre una sesión operación a operación, y enseña a comprobar que ninguno usa información del futuro.

- Curso: [Nivel 1: Datos financieros, mercados y análisis](https://pythonparatrading.com/curso-python-trading/nivel-1/)
- Módulo: 1.7 · Análisis técnico programado
- Duración: 75 min
- Requisitos: [0.5.3 · Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles](https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-groupby-merge-resample-y-rolling/), [1.1.1 · Participantes y centros de negociación](https://pythonparatrading.com/curso-python-trading/nivel-1/participantes-y-centros-de-negociacion/), [1.4.3 · Ajustes por splits y dividendos: precios ajustados y sin ajustar](https://pythonparatrading.com/curso-python-trading/nivel-1/ajustes-por-splits-y-dividendos/)

## Al terminar esta lección

- Programar con pandas, vectorizados, SMA, EMA, KAMA, MACD, RSI, estocástico, Bollinger, ATR, ADX, Donchian, Keltner y OBV.
- Calcular el VWAP de una sesión y un perfil de volumen con su POC y su área de valor, con y sin subastas y bloques.
- Clasificar los indicadores por familia (tendencia, momento, volatilidad, volumen) y saber qué mide cada uno.
- Detectar la anticipación en un indicador con la prueba de truncado y evitar los errores típicos: precios sin ajustar, ventanas centradas y canales sin `shift(1)`.

## Qué es un indicador y qué no promete

Un indicador técnico es una función que toma precios y volúmenes **pasados** y devuelve un número por sesión: una
media, un cociente, un rango. No predice nada por sí mismo. Resume una propiedad de la serie —hacia dónde va, con qué
fuerza, cuánto se mueve, cuánto volumen lo acompaña— de una forma que se puede comparar entre días y entre activos.
Que esa propiedad sirva para ganar dinero es otra pregunta, y se responde con un backtest honesto (módulos 2 y 3), no
con un gráfico.

Programarlos uno mismo tiene dos ventajas. La primera, saber exactamente qué calcula cada uno: el «RSI de 14» de dos
plataformas puede dar valores distintos, y la lección 1.7.2 enseña por qué. La segunda, controlar el tiempo: cada
valor tiene que poder calcularse con lo que se sabía al cierre de su sesión, y la señal que produzca solo puede
ejecutarse en la sesión siguiente.

```mermaid
flowchart LR
  D[OHLCV ajustado] --> I[Indicador en t]
  I --> S[Señal en t]
  S --> E[Orden en t+1]
```

Los indicadores clásicos se agrupan en cuatro familias. Combinar dos de la misma familia (RSI y estocástico, por
ejemplo) añade poco: miden casi lo mismo.

| Familia | Indicadores | Qué mide | Entrada | Uso típico |
|---|---|---|---|---|
| Tendencia | SMA, EMA, KAMA, MACD, ADX | Dirección y fuerza del movimiento | Cierre (ADX: máximo, mínimo, cierre) | Filtros de régimen, cruces, seguimiento de tendencia |
| Momento | RSI, estocástico, histograma del MACD | Velocidad reciente frente a la habitual | Cierre (estocástico: máximo y mínimo) | Sobrecompra y sobreventa, divergencias |
| Volatilidad | ATR, Bollinger, Keltner, Donchian | Amplitud del movimiento | Máximo, mínimo, cierre | Tamaño de posición, stops, rupturas |
| Volumen | OBV, VWAP, perfil de volumen | Cuánto se negocia y a qué precio | Precio y volumen (VWAP y perfil: operaciones) | Confirmación, referencia de ejecución, zonas de precio |

## Los datos: NOVA ajustada

La serie diaria de NOVA (`nova_diario_sin_ajustar.csv`) tiene los precios que se negociaron, de 2018 a 2025. En ese
periodo NOVA pagó 33 dividendos e hizo un split de 4 por 1 el 23 de agosto de 2021 (`nova_eventos.csv`). Para
calcular indicadores hay que ajustar la serie hacia atrás, con el método multiplicativo de la lección 1.4.3: cada
dividendo multiplica los precios anteriores por $1 - D / C_{t-1}$ y el split los divide por 4. El volumen solo se
ajusta por el split (se multiplica por 4 antes de la fecha), porque un dividendo no cambia el número de acciones.

```python
import numpy as np
import pandas as pd

bruto = pd.read_csv("nova_diario_sin_ajustar.csv",
                    parse_dates=["fecha"], index_col="fecha")
eventos = pd.read_csv("nova_eventos.csv", parse_dates=["fecha_ex"])

def factores(bruto, eventos):
    """Factor de precio (splits y dividendos) y de volumen (splits)."""
    cierre_ant = bruto["cierre"].shift(1)
    f_precio = pd.Series(1.0, index=bruto.index)
    f_volumen = pd.Series(1.0, index=bruto.index)
    for e in eventos.itertuples():
        antes = bruto.index < e.fecha_ex
        if e.tipo == "split":
            f_precio[antes] /= e.ratio
            f_volumen[antes] *= e.ratio
        else:
            f_precio[antes] *= 1 - e.importe / cierre_ant[e.fecha_ex]
    return f_precio, f_volumen

f_precio, f_volumen = factores(bruto, eventos)
px = bruto[["apertura", "maximo", "minimo", "cierre"]].mul(
    f_precio, axis=0)
px.columns = ["o", "h", "l", "c"]
px["v"] = (bruto["volumen"] * f_volumen).astype("int64")
o, h, l, c, v = (px[k] for k in ["o", "h", "l", "c", "v"])
print(f"{len(px)} sesiones; factor del 2018-01-02: "
      f"{f_precio.iloc[0]:.4f}")
px.round(2).loc["2021-08-19":"2021-08-24"]
```
```output
2011 sesiones; factor del 2018-01-02: 0.2196
                o      h      l      c         v
fecha
2021-08-19  54.72  57.51  54.40  55.05   9886444
2021-08-20  54.53  54.94  52.04  54.81   8813464
2021-08-23  55.07  55.19  53.08  54.40  13427377
2021-08-24  54.47  54.91  52.37  54.40  13974110
```

El factor del primer día, 0,2196, junta el split (÷ 4) y ocho años de dividendos, que lo reducen otro 12 %. Alrededor del split,
la serie ajustada es continua: el 20 de agosto cerró a 240,00 USD sin ajustar y a 54,81 ajustado; el 23, primer día
con acciones nuevas, abrió a 60,28 sin ajustar y 55,07 ajustado. Sin el ajuste, cualquier indicador de rango o de
nivel se rompe:

```python
def rango_verdadero(h, l, c):
    c_ant = c.shift(1)
    tramos = pd.concat([h - l, (h - c_ant).abs(),
                        (l - c_ant).abs()], axis=1)
    return tramos.max(axis=1, skipna=False)

tr = pd.DataFrame({
    "sin_ajustar": rango_verdadero(bruto["maximo"], bruto["minimo"],
                                   bruto["cierre"]),
    "ajustado": rango_verdadero(h, l, c),
})
tr.loc["2021-08-19":"2021-08-25"].round(2)
```
```output
            sin_ajustar  ajustado
fecha
2021-08-19        13.65      3.12
2021-08-20        13.20      3.01
2021-08-23       181.89      2.10
2021-08-24         2.78      2.54
2021-08-25         3.69      3.37
```

El 23 de agosto de 2021 NOVA se movió 2,30 USD entre su máximo y su mínimo (60,41 − 58,11), pero el rango verdadero
sin ajustar es de 181,89: la distancia entre el cierre anterior, 240,00, y el mínimo del día. Un ATR de 14 sesiones
sin ajustar vale 28,50 USD ese día y arrastra el salto durante semanas: un stop a «dos ATR» quedaría a 57 USD de un
precio de 60. Con la serie ajustada, el rango verdadero de ese día es 2,10, como el de cualquier otro. Toda la
lección usa `px`, la serie ajustada.

## Medias: SMA, EMA y KAMA

La **media simple** (SMA) de $n$ sesiones pondera igual los últimos $n$ cierres:

$$
\mathrm{SMA}_t = \frac{1}{n} \sum_{i=0}^{n-1} C_{t-i}
$$

La **media exponencial** (EMA) es recursiva: mezcla el cierre de hoy con la EMA de ayer, con $\alpha = 2/(n+1)$. Su
memoria es infinita, pero el peso de un cierre de hace $k$ sesiones decrece como $(1-\alpha)^k$:

$$
\mathrm{EMA}_t = \alpha\, C_t + (1 - \alpha)\, \mathrm{EMA}_{t-1}
$$

La **media adaptativa de Kaufman** (KAMA) cambia su $\alpha$ cada día según el **cociente de eficiencia** $ER_t$: el
desplazamiento neto de $n$ sesiones dividido por la suma de los movimientos de cada sesión. Con una tendencia limpia
$ER$ vale 1 y la media se acerca a una EMA rápida (2 sesiones); con ruido lateral $ER$ tiende a 0 y la media casi no se
mueve (EMA de 30):

$$
ER_t = \frac{|C_t - C_{t-n}|}{\sum_{i=0}^{n-1} |C_{t-i} - C_{t-i-1}|}
\qquad
s_t = \left(ER_t\,(\alpha_r - \alpha_l) + \alpha_l\right)^2
\qquad
\mathrm{KAMA}_t = \mathrm{KAMA}_{t-1} + s_t\,(C_t - \mathrm{KAMA}_{t-1})
$$

En pandas, la SMA es `rolling(n).mean()` y la EMA, `ewm(span=n, adjust=False)`. `adjust=False` es la recursión de
arriba; con `adjust=True` (el valor por defecto) pandas normaliza los pesos de la ventana finita que ve, y los
primeros valores cambian. `min_periods=n` deja vacíos los $n-1$ primeros, en los que la media aún no tiene datos
suficientes. KAMA no se puede vectorizar del todo: el coeficiente cambia cada día y la recursión necesita el valor
anterior. El cociente de eficiencia sí se vectoriza, y el bucle que queda recorre arrays de NumPy, no filas de
pandas.

```python
def sma(x, n):
    return x.rolling(n).mean()

def ema(x, n):
    return x.ewm(span=n, adjust=False, min_periods=n).mean()

def kama(x, n=10, rapida=2, lenta=30):
    cambio = (x - x.shift(n)).abs()
    ruido = x.diff().abs().rolling(n).sum()
    er = cambio / ruido
    a_r, a_l = 2 / (rapida + 1), 2 / (lenta + 1)
    s = ((er * (a_r - a_l) + a_l) ** 2).to_numpy()
    p = x.to_numpy()
    out = np.full(len(p), np.nan)
    out[n] = p[n]
    for i in range(n + 1, len(p)):
        out[i] = out[i - 1] + s[i] * (p[i] - out[i - 1])
    return pd.Series(out, index=x.index), er

k, er = kama(c)
medias = pd.DataFrame({"c": c, "sma20": sma(c, 20),
                       "ema20": ema(c, 20), "kama": k, "er": er})
medias.loc["2025-12-24":].round(2)
```
```output
                c  sma20  ema20   kama    er
fecha
2025-12-24  69.67  67.93  68.37  68.34  0.64
2025-12-26  68.43  67.84  68.37  68.34  0.08
2025-12-29  68.22  67.83  68.36  68.34  0.14
2025-12-30  68.17  67.82  68.34  68.33  0.27
2025-12-31  68.19  67.92  68.32  68.33  0.04
```

En las últimas sesiones de 2025 NOVA se mueve de lado y el cociente de eficiencia es bajo: KAMA apenas cambia (68,34,
68,34, 68,34, 68,33, 68,33) mientras el cierre baja de 69,67 a 68,19. La SMA y la EMA siguen al precio con más
nervio.

¿Cuánto retraso tiene cada media? En una tendencia perfectamente lineal se puede medir exacto: la media queda un
número fijo de sesiones por detrás del precio.

```python
rampa = pd.Series(np.arange(200.0))
for nombre, media in [("SMA 20", sma(rampa, 20)),
                      ("EMA 20", ema(rampa, 20)),
                      ("KAMA 10", kama(rampa)[0])]:
    print(f"{nombre}: {rampa.iloc[-1] - media.iloc[-1]:.2f} sesiones")
```
```output
SMA 20: 9.50 sesiones
EMA 20: 9.50 sesiones
KAMA 10: 1.25 sesiones
```

La SMA y la EMA de 20 sesiones tienen el mismo retardo, $(n-1)/2 = 9{,}5$ sesiones: la EMA no es «más rápida» por
ser exponencial, sino porque reacciona antes a un cambio brusco (pesa más el último dato). KAMA, con la tendencia
limpia de la rampa ($ER = 1$), se queda a 1,25 sesiones: $s = (2/3)^2$ y el retardo de una EMA con ese coeficiente es
$(1-s)/s$. Pagar menos retardo en tendencia a cambio de quedarse quieta en el ruido es todo su diseño.

## Momento: RSI, MACD y estocástico

El **RSI** de Wilder compara la media de las subidas con la media de las bajadas de las últimas sesiones. Wilder
suaviza con su propia media, que es una EMA con $\alpha = 1/n$ (no $2/(n+1)$):

$$
RS_t = \frac{\overline{U}_t}{\overline{D}_t}
\qquad
\mathrm{RSI}_t = 100 - \frac{100}{1 + RS_t}
\qquad
\overline{U}_t = \overline{U}_{t-1} + \frac{U_t - \overline{U}_{t-1}}{n}
$$

donde $U_t = \max(C_t - C_{t-1}, 0)$ y $D_t = \max(C_{t-1} - C_t, 0)$. El **MACD** de Appel es la diferencia entre
dos EMA (12 y 26 sesiones), con una línea de señal (EMA de 9 del propio MACD) y un histograma (la diferencia entre
las dos). El **estocástico** de Lane sitúa el cierre dentro del rango de las últimas 14 sesiones, de 0 (en el mínimo)
a 100 (en el máximo), suavizado dos veces con medias de 3:

$$
\%K_t = 100 \cdot \frac{C_t - \min(L_{t-13..t})}{\max(H_{t-13..t}) - \min(L_{t-13..t})}
$$

```python
def wilder(x, n):
    return x.ewm(alpha=1 / n, adjust=False, min_periods=n).mean()

def rsi(x, n=14):
    d = x.diff()
    sube = wilder(d.clip(lower=0), n)
    baja = wilder((-d).clip(lower=0), n)
    return 100 - 100 / (1 + sube / baja)

def macd(x, rapida=12, lenta=26, senal=9):
    linea = ema(x, rapida) - ema(x, lenta)
    s = ema(linea, senal)
    return pd.DataFrame({"macd": linea, "senal": s,
                         "hist": linea - s})

def estocastico(c, h, l, n=14, k=3, d=3):
    minimo, maximo = l.rolling(n).min(), h.rolling(n).max()
    k_rapido = 100 * (c - minimo) / (maximo - minimo)
    k_lento = k_rapido.rolling(k).mean()
    return pd.DataFrame({"k": k_lento, "d": k_lento.rolling(d).mean()})

momento = pd.concat([rsi(c).rename("rsi"), macd(c),
                     estocastico(c, h, l)], axis=1)
momento.tail(4).round(2)
```
```output
              rsi  macd  senal  hist      k      d
fecha
2025-12-26  51.50  0.49   0.37  0.12  83.25  86.86
2025-12-29  50.37  0.42   0.38  0.04  73.88  82.26
2025-12-30  50.09  0.35   0.37 -0.02  64.89  74.01
2025-12-31  50.21  0.30   0.36 -0.06  58.13  65.64
```

Al cerrar 2025, NOVA está en tierra de nadie: RSI en torno a 50, MACD positivo pero por debajo de su señal
(histograma negativo desde el 30 de diciembre) y el estocástico bajando desde la zona alta. Tres indicadores de
momento, tres lecturas compatibles: se mueven juntos porque miden casi lo mismo.

La figura de la cabecera pone el RSI debajo del precio en el primer semestre de 2025. NOVA sube de 53 a 70 USD hasta
el 25 de febrero, cae hasta 44 el 10 de abril y se recupera a medias. El RSI no pasa de 70 en todo el semestre (el
pico de febrero se queda por debajo), y pasa 11 sesiones por debajo de 30, con un mínimo de 22,5 el 8 de abril, dos
sesiones antes del mínimo del precio. «Sobreventa» no quiere decir «va a subir»: el RSI estuvo por debajo de 30 desde
el 26 de marzo mientras el precio seguía cayendo. Una regla que compra cuando el RSI cruza 30 hacia arriba es una
hipótesis que hay que probar, no una ley.

```python
r = rsi(c)
print(f"RSI > 70: {(r > 70).mean():.1%} de las sesiones")
print(f"RSI < 30: {(r < 30).mean():.1%} de las sesiones")
```
```output
RSI > 70: 7.3% de las sesiones
RSI < 30: 8.0% de las sesiones
```

En toda la serie, el RSI pasa un 7,3 % de las sesiones por encima de 70 y un 8,0 % por debajo de 30: los extremos
son raros por construcción, y una señal que solo se activa en ellos tendrá pocas operaciones (y pocas operaciones son
poca evidencia, lección 0.1.2).

## Volatilidad: ATR, Bollinger, Keltner y Donchian

El **rango verdadero** $TR_t = \max(H_t - L_t,\ |H_t - C_{t-1}|,\ |L_t - C_{t-1}|)$ amplía el rango de la sesión con
el hueco desde el cierre anterior, y el **ATR** es su media de Wilder. Las **bandas de Bollinger** son la SMA de 20
más y menos dos desviaciones típicas de los 20 últimos cierres; el **canal de Keltner** (en su versión moderna) es una
EMA de 20 más y menos dos ATR; el **canal de Donchian** es el máximo de los máximos y el mínimo de los mínimos de las
últimas 20 sesiones. Los tres son «el precio más y menos una medida de su amplitud»; cambia cómo se mide.

```python
def atr(h, l, c, n=14):
    return wilder(rango_verdadero(h, l, c), n)

def bollinger(x, n=20, k=2):
    media = sma(x, n)
    sd = x.rolling(n).std(ddof=0)
    inf, sup = media - k * sd, media + k * sd
    return pd.DataFrame({"media": media, "sup": sup, "inf": inf,
                         "pct_b": (x - inf) / (sup - inf)})

def keltner(h, l, c, n=20, n_atr=10, k=2):
    centro, a = ema(c, n), atr(h, l, c, n_atr)
    return pd.DataFrame({"centro": centro, "sup": centro + k * a,
                         "inf": centro - k * a})

def donchian(h, l, n=20):
    return pd.DataFrame({"sup": h.rolling(n).max(),
                         "inf": l.rolling(n).min()})

bb = bollinger(c)[["sup", "inf", "pct_b"]]
kc = keltner(h, l, c)[["sup", "inf"]]
dc = donchian(h, l)
vol = pd.concat([atr(h, l, c).rename("atr"), bb.add_prefix("bb_"),
                 kc.add_prefix("kc_"), dc.add_prefix("dc_")], axis=1)
vol.tail(3).round(2)
```
```output
             atr  bb_sup  bb_inf  bb_pct_b  kc_sup  kc_inf  dc_sup  dc_inf
fecha
2025-12-29  1.39   70.71   64.96      0.57   71.08   65.64   70.27   64.54
2025-12-30  1.34   70.69   64.95      0.56   70.93   65.75   70.27   64.54
2025-12-31  1.30   70.70   65.13      0.55   70.82   65.83   70.27   64.54
```

La desviación típica de Bollinger usa `ddof=0` (la de la población), que es la convención de Bollinger y de TA-Lib;
la de pandas por defecto es `ddof=1`, y con 20 datos la banda saldría un 2,6 % más ancha (la lección 1.7.2 mide esa
diferencia). `pct_b` sitúa el cierre dentro de la banda: 0 en la inferior, 1 en la superior; el 31 de diciembre de
2025 vale 0,55, casi en el centro.

El ATR está en dólares por acción y es la medida de volatilidad más usada para dimensionar posiciones y colocar
stops (módulo 2). La figura muestra el canal de Keltner y, debajo, el ATR, en un tramo en el que la
volatilidad de NOVA se dobla:

*Fig. — NOVA (ajustada), noviembre de 2024 a abril de 2025: canal de Keltner alrededor del cierre y, debajo, el ATR de 14 sesiones en dólares.* Dos paneles con eje de tiempo común, de noviembre de 2024 a abril de 2025. Arriba, el cierre ajustado de NOVA y el canal de Keltner (EMA de 20 sesiones más y menos dos ATR de 10): el precio oscila entre 53 y 62 hasta enero, salta por encima del canal en febrero hasta 70 y cae por debajo en marzo y abril hasta 44. Abajo, el ATR de 14 sesiones, que sube de 1,61 a 3,11 USD; el máximo, destacado, es el último día, el 29 de abril.

De noviembre de 2024 a abril de 2025 el ATR de 14 sesiones pasa de 1,61 a 3,11 USD, y en proporción del precio, del
2,8 % al 6,6 %. El canal de Keltner se ensancha con él. En febrero, una subida del 15 % en una sola sesión (el día
4) lleva el cierre por encima del canal; en marzo y abril, la caída lo lleva por debajo durante semanas: en esos seis meses NOVA cierra 8
veces por encima y 19 por debajo.

## ADX: fuerza de la tendencia

El **ADX** de Wilder no dice hacia dónde va el precio, sino si va a algún sitio. Compara el movimiento direccional
positivo ($+DM$: lo que sube el máximo respecto a ayer, si es mayor que lo que baja el mínimo) con el negativo
($-DM$), los divide por el ATR para obtener $+DI$ y $-DI$, y suaviza la distancia normalizada entre los dos:

$$
DX_t = 100 \cdot \frac{|{+DI_t} - {-DI_t}|}{{+DI_t} + {-DI_t}}
\qquad
\mathrm{ADX}_t = \mathrm{Wilder}_{14}(DX)_t
$$

```python
def adx(h, l, c, n=14):
    sube, baja = h.diff(), -l.diff()
    dm_mas = sube.where((sube > baja) & (sube > 0), 0.0)
    dm_menos = baja.where((baja > sube) & (baja > 0), 0.0)
    a = atr(h, l, c, n)
    di_mas = 100 * wilder(dm_mas, n) / a
    di_menos = 100 * wilder(dm_menos, n) / a
    dx = 100 * (di_mas - di_menos).abs() / (di_mas + di_menos)
    return pd.DataFrame({"di_mas": di_mas, "di_menos": di_menos,
                         "adx": wilder(dx, n)})

fuerza = adx(h, l, c)
print(fuerza.tail(3).round(1))
print(f"Sesiones con ADX > 25: {(fuerza['adx'] > 25).mean():.1%}")
```
```output
            di_mas  di_menos   adx
fecha
2025-12-29    22.8      22.7  12.0
2025-12-30    21.9      21.8  11.2
2025-12-31    22.0      20.8  10.6
Sesiones con ADX > 25: 47.3%
```

Al cerrar 2025 el ADX de NOVA está en 10,6, sin tendencia; $+DI$ y $-DI$ casi empatados. En toda la serie, el ADX
supera 25 (el umbral habitual de «hay tendencia») en el 47,3 % de las sesiones: un filtro tan permisivo deja pasar
casi la mitad de los días. El ADX se usa como filtro de régimen (operar rupturas solo cuando hay tendencia), no como
señal.

## Donchian y el desplazamiento que no puede faltar

Una regla de ruptura clásica (la de las tortugas de Dennis) compra cuando el precio supera el máximo de las últimas
20 sesiones. Programada sin cuidado no da ninguna señal:

```python
dc = donchian(h, l)
print("Sin desplazar:", (c > dc["sup"]).sum(), "cierres por encima")
print("Desplazado una sesión:",
      (c > dc["sup"].shift(1)).sum(), "por encima,",
      (c < dc["inf"].shift(1)).sum(), "por debajo")
```
```output
Sin desplazar: 0 cierres por encima
Desplazado una sesión: 148 por encima, 125 por debajo
```

El canal de hoy incluye el máximo de hoy, y el cierre nunca supera al máximo de su propia sesión: cero rupturas. La
regla correcta compara el cierre de hoy con el canal de las 20 sesiones **anteriores** (`shift(1)`): 148 cierres por
encima y 125 por debajo en ocho años. El error contrario es peor: comparar el cierre de ayer con un canal que ya
incluye el máximo de hoy da rupturas «perfectas» que no se podían ver.

*Fig. — NOVA (ajustada), segundo semestre de 2025: cierre y canal de Donchian de las 20 sesiones anteriores; en azul, cierres por encima del máximo previo; en naranja, por debajo del mínimo previo.* Cierre ajustado de NOVA de agosto a diciembre de 2025 con el canal de Donchian de las 20 sesiones anteriores, en escalones. Entre el 13 de agosto, primera ruptura destacada a 52,68 USD, y el 3 de octubre el precio sube hasta 69,40 y marca 15 cierres por encima del máximo previo, en azul. En octubre hay dos cierres por debajo del mínimo previo, en naranja, y a mediados de noviembre dos nuevos máximos; después el precio se queda dentro del canal, entre 65 y 70.

En el segundo semestre de 2025, la subida de NOVA de 52,68 a 69,40 USD entre el 13 de agosto y el 3 de octubre
produce una ruptura tras otra (15 cierres por encima del máximo previo); la primera, el 13 de agosto. Luego el precio se queda dentro del canal, con dos cierres por debajo
del mínimo previo en octubre y dos nuevos máximos a mediados de noviembre. Un sistema de rupturas vive de pocos
tramos así y sufre en los laterales.

## Volumen: OBV, VWAP y perfil

El **OBV** (*on-balance volume*) de Granville acumula el volumen con el signo del cambio del cierre: suma el de los
días que sube y resta el de los que baja. Su nivel no significa nada (depende del día en que empieza la suma); se
mira su pendiente, o si diverge del precio:

```python
def obv(c, v):
    return (np.sign(c.diff()).fillna(0) * v).cumsum()

ob = obv(c, v)
print(f"OBV a 31-12-2025: {ob.iloc[-1] / 1e6:,.1f} M de acciones")
cambio = ob.iloc[-1] - ob.loc[:"2024"].iloc[-1]
print(f"Cambio en 2025: {cambio / 1e6:,.1f} M")
```
```output
OBV a 31-12-2025: -375.2 M de acciones
Cambio en 2025: -0.8 M
```

El nivel, −375 millones de acciones, es un artefacto de dónde empieza la suma. Lo informativo es el cambio: en 2025
el OBV de NOVA apenas se mueve (−0,8 millones) mientras el precio ajustado sube de 56,28 a 68,19, un 21 %. Es lo que
el análisis técnico llama una divergencia. Si significa algo se mide, como cualquier otra señal, con rentabilidades
posteriores y muchos casos, no con uno.

El **VWAP** (*volume-weighted average price*) es el precio medio de una sesión ponderado por volumen:

$$
\mathrm{VWAP}_t = \frac{\sum_{i \le t} p_i\, q_i}{\sum_{i \le t} q_i}
$$

Es la referencia con la que se mide la ejecución institucional: un algoritmo que compra «al VWAP» intenta pagar ese
precio medio. Se calcula sobre operaciones, no sobre barras, y la lección 1.1.1 ya avisaba: **qué operaciones** entran
cambia el resultado. Con la sesión de NOVA del 2 de junio de 2025, todas las operaciones frente a solo la negociación
continua (sin subastas ni bloques):

```python
op = pd.read_parquet("nova_operaciones.parquet")

def vwap(df):
    pq = (df["precio"] * df["tamano"]).cumsum()
    return pq / df["tamano"].cumsum()

op["vwap_todo"] = vwap(op)
continua = op[op["condicion"] == "regular"]
op["vwap_continua"] = vwap(continua).reindex(op.index).ffill()

antes_cierre = op.iloc[-2]
final = op.iloc[-1]
print(f"Antes de la subasta de cierre: {antes_cierre['vwap_todo']:.3f}"
      f" frente a {antes_cierre['vwap_continua']:.3f}")
print(f"Al final del día: {final['vwap_todo']:.3f}"
      f" frente a {final['vwap_continua']:.3f}")

barras = pd.read_parquet("nova_barras_5min.parquet")
dia = barras[barras["ts"].dt.date == pd.Timestamp("2025-06-02").date()]
tipico = (dia["maximo"] + dia["minimo"] + dia["cierre"]) / 3
aprox = (tipico * dia["volumen"]).sum() / dia["volumen"].sum()
print(f"Aproximación con barras de 5 min: {aprox:.3f}")
```
```output
Antes de la subasta de cierre: 53.341 frente a 53.350
Al final del día: 53.319 frente a 53.350
Aproximación con barras de 5 min: 53.311
```

*Fig. — NOVA, sesión del 2 de junio de 2025 (hora de Nueva York): VWAP acumulado con todas las operaciones y, debajo, su diferencia con el VWAP solo de negociación continua (sin subastas ni bloques).* Dos paneles de la sesión de NOVA del 2 de junio de 2025, de 9:30 a 16:00 de Nueva York. Arriba, el último precio cada 5 minutos, que sube de 53,35 a cerca de 53,93 a media mañana y cae hasta cerca de 52,55 hacia las 15:10 antes de cerrar a 53,12, y el VWAP acumulado, mucho más suave, entre 53,32 y 53,56; seis líneas de puntos marcan los bloques. Abajo, la diferencia entre el VWAP con todas las operaciones y el de negociación continua: se mueve entre +0,3 y −1 céntimo, con escalones en cada bloque, y la subasta de cierre la lleva a −3,1 céntimos, destacado.

Durante el día, las dos versiones van casi juntas: los bloques del centro oscuro mueven el VWAP con todas las
operaciones unas décimas de céntimo cada vez (los escalones de la figura). Justo antes de la subasta de cierre la
diferencia es de un céntimo. La subasta, 885.500 acciones a 53,12, muy por debajo del VWAP del día, la lleva a 3,1
céntimos. Para un algoritmo que ha comprado 200.000 acciones, son 6.200 USD de diferencia según con qué VWAP se le
compare. La aproximación con barras de 5 minutos y el precio típico, $(H + L + C)/3$, da 53,311: cerca, pero ni
siquiera es ninguno de los dos.

<aside class="callout" data-kind="aviso">

Un VWAP de proveedor puede incluir o no la subasta de cierre, los bloques y las operaciones fuera de hora, y casi
nunca lo dice en el nombre del campo. Antes de comparar una ejecución con «el VWAP», hay que saber cuál es
(lección 1.3.1).

</aside>

El **perfil de volumen** reparte el volumen del día por niveles de precio en vez de por tiempo: cuántas acciones se
negociaron en cada tramo de 5 céntimos. El tramo con más volumen es el **POC** (*point of control*) y el **área de
valor** es el intervalo contiguo alrededor del POC que reúne el 70 % del volumen, construido añadiendo cada vez el
tramo vecino con más volumen:

```python
def perfil(df, paso=0.05):
    tramo = (np.floor(df["precio"] / paso + 1e-9) * paso).round(2)
    return df.groupby(tramo)["tamano"].sum()

def area_valor(p, frac=0.70):
    i = lo = hi = int(np.argmax(p.to_numpy()))
    acumulado = p.iloc[i]
    while acumulado / p.sum() < frac:
        arriba = p.iloc[hi + 1] if hi + 1 < len(p) else -1
        abajo = p.iloc[lo - 1] if lo > 0 else -1
        if arriba >= abajo:
            hi, acumulado = hi + 1, acumulado + arriba
        else:
            lo, acumulado = lo - 1, acumulado + abajo
    return p.index[lo], p.index[hi] + 0.05, acumulado / p.sum()

p_cont = perfil(continua)
lo_va, hi_va, cubre = area_valor(p_cont)
print(f"Continua: POC {p_cont.idxmax():.2f}, área de valor "
      f"{lo_va:.2f}-{hi_va:.2f} ({cubre:.1%} del volumen)")
p_todo = perfil(op)
print(f"Todas: POC {p_todo.idxmax():.2f} con "
      f"{p_todo.max():,} acciones")
```
```output
Continua: POC 53.45, área de valor 53.15-53.90 (72.2% del volumen)
Todas: POC 53.10 con 967,099 acciones
```

*Fig. — Perfil de volumen de NOVA el 2 de junio de 2025, negociación continua: acciones negociadas en cada tramo de 5 céntimos; en gris claro, el área de valor (70 % del volumen alrededor del POC).* Barras horizontales con las acciones negociadas en continua por NOVA el 2 de junio de 2025 en cada tramo de 5 céntimos, de 52,55 a 53,95 USD. El tramo de más volumen, el POC, destacado, es el de 53,45, con unas 659.000 acciones; el área de valor, en gris claro, va de 53,15 a 53,90. Le siguen 53,35, con 566.000, y 53,40, 53,70 y 53,75, con algo más de 500.000 cada uno; en el mínimo del día, 52,55, hay otro tramo de 250.000.

En negociación continua, el POC de NOVA ese día es el tramo de 53,45 a 53,50, y el área de valor va de 53,15 a
53,90: el 72,2 % del volumen (el algoritmo se pasa del 70 % al añadir el último tramo). Con todas las operaciones, el
POC salta a 53,10: la subasta de cierre, 885.500 acciones en un solo precio, pesa más que cualquier tramo de la
negociación continua. Un perfil «con todo» señala como zona de interés un precio que nadie eligió en el libro: lo fijó
el cruce de la subasta. El perfil de volumen se usa como mapa de soportes y resistencias; la lección 1.7.3 explica por
qué hay que desconfiar de esos mapas.

## La prueba de truncado: ningún indicador debe ver el futuro

Todos los indicadores de esta lección cumplen la regla del principio: su valor en $t$ solo usa datos hasta $t$. La
forma de **comprobarlo**, en lugar de suponerlo, es la prueba de truncado: calcular el indicador con la serie completa
y con la serie cortada en una fecha, y comparar los valores hasta esa fecha. Si el indicador usa el futuro, los dos
cálculos no coinciden.

```mermaid
flowchart TB
  A[Serie completa] --> B[Indicador]
  A --> C[Serie hasta t]
  C --> D[Indicador hasta t]
  B --> E{¿Iguales hasta t?}
  D --> E
  E -- sí --> F[Sin anticipación]
  E -- no --> G[Usa el futuro]
```

La celda aplica la prueba a cada indicador y a dos «indicadores» con anticipación que aparecen en código real: una
media centrada (suaviza muy bien en un gráfico) y una normalización con la media y la desviación de toda la serie
(el típico *z-score* calculado antes de partir los datos):

```python
def sin_anticipacion(f, datos, corte):
    completo = f(datos).loc[:corte]
    truncado = f(datos.loc[:corte])
    return np.allclose(completo, truncado, equal_nan=True)

pruebas = {
    "SMA 20": lambda d: sma(d["c"], 20),
    "EMA 20": lambda d: ema(d["c"], 20),
    "KAMA": lambda d: kama(d["c"])[0],
    "RSI 14": lambda d: rsi(d["c"]),
    "MACD": lambda d: macd(d["c"])["hist"],
    "ATR 14": lambda d: atr(d["h"], d["l"], d["c"]),
    "ADX 14": lambda d: adx(d["h"], d["l"], d["c"])["adx"],
    "Donchian": lambda d: donchian(d["h"], d["l"])["sup"],
    "Media centrada": lambda d: d["c"].rolling(21, center=True).mean(),
    "z-score global": lambda d: (d["c"] - d["c"].mean()) / d["c"].std(),
}
for nombre, f in pruebas.items():
    print(f"{nombre:15} {sin_anticipacion(f, px, '2023-06-30')}")
```
```output
SMA 20          True
EMA 20          True
KAMA            True
RSI 14          True
MACD            True
ATR 14          True
ADX 14          True
Donchian        True
Media centrada  False
z-score global  False
```

Los ocho indicadores pasan; los dos impostores, no. La prueba es barata y conviene dejarla en los tests del
proyecto (lección 0.4.5) para cada indicador nuevo, con varias fechas de corte. Hay una trampa que no detecta: la
propia serie ajustada. Los factores de ajuste se calculan con los dividendos posteriores, así que el precio ajustado
de 2023 en un fichero de 2025 no es el que se veía en 2023:

```python
hasta = bruto.loc[:"2023-06-30"]
conocidos = eventos[eventos["fecha_ex"] <= "2023-06-30"]
f_2023, _ = factores(hasta, conocidos)
c_2023 = hasta["cierre"] * f_2023
cociente = c.loc[:"2023-06-30"] / c_2023
print(f"Precio ajustado hoy / ajustado en 2023: {cociente.min():.4f}"
      f" a {cociente.max():.4f}")
print("RSI igual:", np.allclose(rsi(c).loc[:"2023-06-30"], rsi(c_2023),
                              equal_nan=True))
print("SMA igual:", np.allclose(sma(c, 20).loc[:"2023-06-30"],
                              sma(c_2023, 20), equal_nan=True))
```
```output
Precio ajustado hoy / ajustado en 2023: 0.9328 a 0.9328
RSI igual: True
SMA igual: False
```

Los dividendos pagados después de junio de 2023 escalan todos los precios anteriores por un mismo factor, 0,9328.
Un indicador que solo depende de cocientes entre precios (RSI, rentabilidades, %b) no cambia; uno que está en
dólares (SMA, ATR, niveles de Donchian) sí. Para señales relativas no importa. Para una regla con un nivel absoluto
(«comprar si el precio pasa de 50 USD», un stop a un precio fijo, un soporte), sí: hay que usar los precios como
eran en cada fecha, o reajustar en cada fecha del backtest (lecciones 1.4.3 y 1.4.4).

## Errores típicos

- **Precios sin ajustar.** El split de NOVA convierte un día normal en un rango verdadero de 181,89 USD. Ajustar antes
  de calcular.
- **Ventanas centradas o normalizaciones globales.** `center=True`, `mean()` de toda la serie, `MinMaxScaler`
  ajustado con todos los datos: anticipación. La prueba de truncado los caza.
- **Canales y extremos sin `shift(1)`.** Comparar con un máximo que incluye la sesión actual da cero rupturas o, con
  el desplazamiento al revés, rupturas imposibles.
- **El periodo de calentamiento.** Los primeros valores de una EMA o de un RSI dependen de cómo se inicializa la
  recursión. Con la serie de NOVA desde 2018 no importa en 2025, pero en un histórico corto sí (lección 1.7.2).
- **Convenciones que no se dicen.** `span` frente a `alpha`, `ddof=0` frente a `ddof=1`, Keltner con EMA y ATR o con
  SMA y rango medio: dos «RSI de 14» pueden no coincidir. Documentar la convención junto a la función.
- **Optimizar los parámetros.** Probar 50 longitudes de media y quedarse con la mejor es un problema de pruebas
  múltiples (lección 1.7.3), no un descubrimiento.

## Ejercicios

1. El ancho de las bandas de Bollinger, $(B_{sup} - B_{inf}) / \mathrm{SMA}_{20}$, mide la volatilidad relativa.
   Calcula su mínimo de 2025 en NOVA (el momento de máxima «compresión») y di qué pasó con el precio en las 20
   sesiones siguientes.

**Solución.**

```python
bb = bollinger(c)
ancho = ((bb["sup"] - bb["inf"]) / bb["media"]).loc["2025"]
fecha = ancho.idxmin()
despues = c.loc[fecha:].iloc[:21]
ini, fin = despues.iloc[0], despues.iloc[-1]
print(f"Ancho mínimo: {ancho.min():.1%} el {fecha:%Y-%m-%d}")
print(f"Cierre: {ini:.2f}; 20 sesiones después: {fin:.2f}"
      f" ({fin / ini - 1:+.1%})")
print(f"Rango: {despues.min():.2f}-{despues.max():.2f}")
```
```output
Ancho mínimo: 3.2% el 2025-08-04
Cierre: 51.76; 20 sesiones después: 52.26 (+1.0%)
Rango: 50.36-57.00
```

La compresión mínima de 2025 es del 3,2 %, el 4 de agosto. En las 20 sesiones siguientes NOVA rompe hacia arriba
(la primera ruptura de Donchian, el 13 de agosto), toca 57,00 y vuelve: 20 sesiones después cierra a 52,26, un 1,0 %
por encima. La compresión no dice hacia dónde saldrá el precio, solo que la volatilidad reciente es baja comparada
con su media.
Una sola observación no prueba nada: para saber si la compresión anticipa movimientos grandes habría que medir, sobre
todas las compresiones, la volatilidad de las sesiones siguientes frente a la de sesiones elegidas al azar.

2. Calcula el VWAP de la sesión del 2 de junio de 2025 excluyendo solo la subasta de cierre (con la de apertura y los
   bloques). ¿Cuánto de la diferencia de 3,1 céntimos explica la subasta de cierre por sí sola?

**Solución.**

```python
sin_cierre = op[op["condicion"] != "subasta_cierre"]
v_sin_cierre = vwap(sin_cierre).iloc[-1]
print(f"Todas: {op['vwap_todo'].iloc[-1]:.4f}")
print(f"Sin subasta de cierre: {v_sin_cierre:.4f}")
print(f"Solo continua: {op['vwap_continua'].iloc[-1]:.4f}")
```
```output
Todas: 53.3191
Sin subasta de cierre: 53.3412
Solo continua: 53.3503
```

Sin la subasta de cierre, el VWAP pasa de 53,3191 a 53,3412: la subasta explica 2,2 de los 3,1 céntimos. Los 0,9
restantes hasta la versión continua (53,3503) son de los bloques y de la subasta de apertura. Los tres VWAP son legítimos; lo que no
es legítimo es comparar una ejecución con uno de ellos sin decir cuál.

3. Un compañero calcula el RSI de NOVA con los precios **sin ajustar** y dice que no importa porque el RSI «es un
   cociente». ¿Tiene razón? Piensa en qué pasa la sesión del split.

**Solución.**

Tiene razón a medias. El RSI no cambia si toda la serie se multiplica por un mismo número (por eso un ajuste por
dividendos posterior no lo altera), pero el split no escala toda la serie: corta un salto de −180 USD en una sola
sesión. Esa «bajada» entra en la media de Wilder de las bajadas con un peso de 1/14 y la domina durante semanas: el
RSI sin ajustar pasa de 63,2 el 18 de agosto de 2021 a 21,8 el 24 y se queda entre 22 y 26 hasta finales de
septiembre, un mes en el que la serie ajustada baja un 3 % y su RSI se mueve entre 38 y 66. Con la serie ajustada, ese día es una
sesión normal. Los cocientes son inmunes a escalar toda la serie, no a un escalón en mitad de ella.

## Autoevaluación

1. Con un canal de Donchian de 20 sesiones calculado con `rolling(20).max()` sobre los máximos, ¿cuántas veces cierra NOVA por encima del canal en 2018-2025?

   - A. Unas 150, una por cada ruptura
   - B. Ninguna: el canal incluye el máximo de la propia sesión, y el cierre nunca supera al máximo
   - C. Depende del ajuste por dividendos
   - D. Todas las sesiones de tendencia alcista

   *Respuesta B: Ninguna: el canal incluye el máximo de la propia sesión, y el cierre nunca supera al máximo.* El máximo de las 20 últimas sesiones incluye la de hoy, y el cierre de hoy es menor o igual que su máximo. La regla de ruptura compara con el canal de las sesiones anteriores: `rolling(20).max().shift(1)`. Con ese desplazamiento salen 148 cierres por encima en NOVA.

2. ¿Qué diferencia una EMA de 20 sesiones de una SMA de 20 sesiones?

   - A. La EMA usa solo las 20 últimas sesiones y la SMA toda la historia
   - B. La EMA pondera todas las sesiones pasadas con pesos que decrecen geométricamente; la SMA pondera igual las 20 últimas y nada las anteriores
   - C. Ninguna: son dos nombres de la misma media
   - D. La EMA usa precios futuros para reducir el retardo

   *Respuesta B: La EMA pondera todas las sesiones pasadas con pesos que decrecen geométricamente; la SMA pondera igual las 20 últimas y nada las anteriores.* La EMA es recursiva: cada valor mezcla el precio de hoy (peso α = 2/21) con la EMA de ayer. Su memoria es infinita pero se desvanece; con el mismo número de sesiones, su retardo ante una tendencia lineal es el mismo que el de la SMA, (n − 1)/2 sesiones, y reacciona antes a un cambio brusco.

3. El RSI de Wilder suaviza las subidas y las bajadas con una media exponencial. ¿Qué α usa en pandas un RSI de 14 sesiones?

   - A. `span=14`, es decir, α = 2/15
   - B. `alpha=1/14`
   - C. `com=14`
   - D. `halflife=14`

   *Respuesta B: `alpha=1/14`.* Wilder suaviza con α = 1/n (lo que en pandas es `alpha=1/14`, equivalente a `com=13`). Con `span=14` el RSI responde casi el doble de rápido y no coincide con el de ninguna plataforma.

4. El VWAP de NOVA del 2 de junio de 2025 es 53,319 con todas las operaciones y 53,350 solo con negociación continua. ¿Qué explica casi toda la diferencia?

   - A. Un error de redondeo
   - B. La subasta de cierre: 885.500 acciones a 53,12, por debajo del VWAP del día, que tiran de la media hacia abajo
   - C. Las operaciones del internalizador
   - D. La subasta de apertura

   *Respuesta B: La subasta de cierre: 885.500 acciones a 53,12, por debajo del VWAP del día, que tiran de la media hacia abajo.* Justo antes de las 16:00 la diferencia es de un céntimo; la subasta de cierre, casi el 10 % del volumen del día en un único precio, la lleva a 3,1 céntimos. Quien compara su ejecución con «el VWAP» tiene que decir cuál.

5. Una media móvil centrada (`rolling(21, center=True)`) suaviza mejor que una normal. ¿Por qué no sirve para una señal?

   - A. Porque es más lenta de calcular
   - B. Porque el valor de cada día usa los diez días siguientes: falla la prueba de truncado
   - C. Porque pandas no la admite en series con fechas
   - D. Porque solo funciona con ventanas impares

   *Respuesta B: Porque el valor de cada día usa los diez días siguientes: falla la prueba de truncado.* Centrar la ventana mete en el valor de hoy diez precios que todavía no se conocen. En un gráfico queda bonita; en un backtest es anticipación pura. Calculada con la serie cortada en una fecha, sus últimos diez valores salen vacíos o distintos.

## Ideas clave

- Un indicador en la sesión t solo puede usar datos hasta t; la orden que provoca se ejecuta en t + 1. Esa regla, y no el indicador, es lo que separa un backtest útil de uno engañoso.
- Casi todos los indicadores clásicos son medias móviles (simples, exponenciales o de Wilder) de precios, rangos o volúmenes: pandas los calcula con `rolling` y `ewm` sin bucles. KAMA, recursiva con un coeficiente que cambia cada día, necesita uno.
- Las familias miden cosas distintas: tendencia (medias, MACD, ADX), momento (RSI, estocástico), volatilidad (ATR, Bollinger, Keltner, Donchian) y volumen (OBV, VWAP, perfil). Combinar dos de la misma familia apenas añade información.
- Los indicadores se calculan sobre precios ajustados: sin ajustar, el split de NOVA de 2021 da un rango verdadero de 181,89 USD en una sesión que se movió 2,30.
- La prueba de truncado detecta la anticipación: el indicador calculado con la serie cortada en una fecha debe coincidir con el calculado con toda la serie hasta esa fecha.
- El VWAP y el perfil de volumen dependen de qué operaciones se incluyan: la subasta de cierre mueve el VWAP de NOVA 3,1 céntimos y lleva el POC de 53,45 a 53,10.

## Recursos

### Research

- [Tu primer sistema en Python: cruce de medias móviles sobre SPY](https://pythonparatrading.com/primer-sistema-cruce-de-medias/) · Research-0018 · 7 min
- [Estimadores de Volatilidad](https://pythonparatrading.com/estimadores-de-volatilidad-para-trading/) · Research-0002 · 4 min

### Documentación

- [pandas: DataFrame.ewm (span, alpha, adjust y min_periods)](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html) · pandas.pydata.org
- [pandas: DataFrame.rolling (ventanas, center y min_periods)](https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.rolling.html) · pandas.pydata.org
- [TA-Lib: índice de funciones por grupo (definiciones de referencia)](https://ta-lib.org/functions/) · ta-lib.org

### Datos

- [Datos del nivel 1: NOVA diario sin ajustar, 2018-2025 (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/nova_diario_sin_ajustar.csv) · Descarga · nova_diario_sin_ajustar.csv
- [Datos del nivel 1: eventos corporativos de NOVA (CSV)](https://pythonparatrading.com/curso/datos/nivel-1/nova_eventos.csv) · Descarga · nova_eventos.csv

Lección anterior: [1.6.3 · Estacionalidad: día de la semana, mes, cambio de mes y sesiones intradía](https://pythonparatrading.com/curso-python-trading/nivel-1/estacionalidad-y-efectos-calendario/)

Lección siguiente: [1.7.2 · Implementación propia frente a TA-Lib: validar resultados](https://pythonparatrading.com/curso-python-trading/nivel-1/validar-indicadores-frente-a-ta-lib/)
