---
title: "1.7.3 Patrones de velas y soportes: codificar y medir"
description: "Doji, martillo y envolvente con umbrales explícitos y con TA-Lib, soportes y resistencias por máximos locales, y cómo medir si alguno funciona."
url: "https://pythonparatrading.com/curso-python-trading/nivel-1/patrones-de-velas-y-soportes/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-1/patrones-de-velas-y-soportes.png"
---

# 1.7.3 · Patrones de velas y soportes y resistencias

> Un patrón de velas es una regla sobre cuatro precios; un soporte, una regla sobre máximos y mínimos pasados. Las dos cosas se pueden programar y medir. Esta lección las codifica, las prueba con entradas realistas, costes y pruebas múltiples, y enseña por qué la mayoría de los resultados «buenos» desaparecen al hacerlo bien.

- 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: 70 min
- Requisitos: [1.7.1 · Indicadores técnicos en Python](https://pythonparatrading.com/curso-python-trading/nivel-1/indicadores-tecnicos-en-python/), [0.7.2 · Probabilidad y distribuciones, correlación frente a causalidad y regresión simple](https://pythonparatrading.com/curso-python-trading/nivel-0/probabilidad-distribuciones-y-regresion/)

## Al terminar esta lección

- Codificar doji, martillo y envolventes con umbrales explícitos y compararlos con las funciones `CDL` de TA-Lib.
- Medir la rentabilidad posterior a un patrón sin anticipación: entrada en la apertura siguiente, salida a horizonte fijo y costes.
- Reconocer las tres formas de engañarse con patrones: umbrales elegidos a posteriori, muchas pruebas a la vez y pocos casos.
- Detectar soportes y resistencias por máximos y mínimos locales, saber cuándo se conocen de verdad y contrastarlos con niveles placebo.

## Una vela son cuatro números

Una vela japonesa resume una sesión con cuatro precios: apertura, máximo, mínimo y cierre. El **cuerpo** es la
distancia entre apertura y cierre (azul si el cierre es mayor, naranja si es menor); las **sombras**, lo que el
precio recorrió por encima y por debajo del cuerpo. Los patrones de velas son reglas sobre esas cantidades, de una,
dos o tres sesiones, con nombres de la tradición japonesa del arroz: doji, martillo, envolvente, estrella de la
mañana.

*Fig. — Anatomía de tres patrones de velas y la regla numérica con la que los codifica la lección: doji, martillo y envolvente alcista (esquema).* Cuatro velas dibujadas a mano. Un doji: cuerpo casi nulo con sombras largas arriba y abajo; regla: cuerpo menor o igual que el 10 % del rango. Un martillo alcista con el cuerpo arriba, una sombra superior mínima y una sombra inferior larga, destacada; regla: sombra inferior de al menos dos cuerpos y sombra superior de como mucho el 10 % del rango. Una envolvente alcista: una vela bajista pequeña seguida de una alcista cuyo cuerpo cubre el de la anterior.

La literatura de análisis técnico los describe con palabras: «cuerpo pequeño», «sombra inferior larga», «tras una
tendencia bajista». Para programarlos hay que convertir cada palabra en un número, y ahí empieza el problema: cada
número es una decisión, y cada decisión cambia qué sesiones cuentan como patrón.

| Patrón | Sesiones | Descripción habitual | Regla de esta lección |
|---|---|---|---|
| Doji | 1 | Apertura y cierre casi iguales: indecisión | Cuerpo ≤ 10 % del rango del día |
| Martillo | 1 | Cuerpo pequeño arriba, sombra inferior larga, tras caídas | Sombra inferior ≥ 2 cuerpos, superior ≤ 10 % del rango, cuerpo > 5 % del rango, cierre menor que hace 5 sesiones |
| Envolvente alcista | 2 | Vela alcista cuyo cuerpo cubre el de una bajista anterior | Ayer bajista, hoy alcista, apertura ≤ cierre de ayer y cierre ≥ apertura de ayer |
| Envolvente bajista | 2 | La simétrica | Ayer alcista, hoy bajista, apertura ≥ cierre de ayer y cierre ≤ apertura de ayer |

## Los datos y las cantidades de cada vela

La lección usa NOVA ajustada por splits y dividendos, como las dos anteriores, de 2018 a 2025:

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

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):
    cierre_ant = bruto["cierre"].shift(1)
    f = pd.Series(1.0, index=bruto.index)
    for e in eventos.itertuples():
        antes = bruto.index < e.fecha_ex
        if e.tipo == "split":
            f[antes] /= e.ratio
        else:
            f[antes] *= 1 - e.importe / cierre_ant[e.fecha_ex]
    return f

px = bruto[["apertura", "maximo", "minimo", "cierre"]].mul(
    factores(bruto, eventos), axis=0)
px.columns = ["o", "h", "l", "c"]
o, h, l, c = (px[k] for k in "ohlc")
O, H, L, C = (px[k].to_numpy() for k in "ohlc")
atr = pd.Series(talib.ATR(H, L, C, 14), index=px.index)
print(len(px), "sesiones")
```
```output
2011 sesiones
```

Las cuatro cantidades de cada vela son restas vectorizadas:

```python
cuerpo = (c - o).abs()
rango = h - l
sombra_sup = h - np.maximum(o, c)
sombra_inf = np.minimum(o, c) - l
velas = pd.DataFrame({"o": o, "c": c, "cuerpo": cuerpo,
                      "rango": rango, "sombra_sup": sombra_sup,
                      "sombra_inf": sombra_inf})
velas.loc["2025-12-22":"2025-12-26"].round(2)
```
```output
                o      c  cuerpo  rango  sombra_sup  sombra_inf
fecha
2025-12-22  69.38  69.53    0.15   1.16        0.74        0.27
2025-12-23  69.51  69.83    0.32   0.96        0.12        0.52
2025-12-24  69.51  69.67    0.16   1.30        0.56        0.58
2025-12-26  69.48  68.43    1.05   1.36        0.06        0.25
```

El 22 y el 24 de diciembre de 2025 son días de cuerpo pequeño (0,15 y 0,16 USD, un 13 % y un 12 % de su rango):
casi doji, pero no con un umbral del 10 %. Con un umbral un poco mayor entrarían. La sección «Tres formas de fabricar
un patrón» mide cuánto cambia el resultado con ese umbral.

## Doji, martillo y envolvente con umbrales explícitos

```python
doji = cuerpo <= 0.10 * rango
cayendo = c < c.shift(5)
martillo = ((sombra_inf >= 2 * cuerpo) & (sombra_sup <= 0.10 * rango)
            & (cuerpo > 0.05 * rango) & cayendo)
ayer_baja, ayer_sube = c.shift(1) < o.shift(1), c.shift(1) > o.shift(1)
env_alcista = (ayer_baja & (c > o) & (o <= c.shift(1))
               & (c >= o.shift(1)))
env_bajista = (ayer_sube & (c < o) & (o >= c.shift(1))
               & (c <= o.shift(1)))
propios = {"doji": doji, "martillo": martillo,
           "envolvente alcista": env_alcista,
           "envolvente bajista": env_bajista}
pd.Series({k: int(m.sum()) for k, m in propios.items()},
          name="sesiones")
```
```output
doji                  301
martillo               24
envolvente alcista     46
envolvente bajista     43
Name: sesiones, dtype: int64
```

Los patrones de dos sesiones usan `shift(1)`: la vela de ayer, que a la hora de evaluar la de hoy ya se conoce. Ninguno
mira hacia delante. TA-Lib trae 61 funciones de patrones (`CDL…`) que devuelven, por sesión, 0 si no hay patrón y
±100 si lo hay (+100 alcista, −100 bajista; en algunos, ±80 para una versión menos marcada):

```python
ta_doji = talib.CDLDOJI(O, H, L, C)
ta_martillo = talib.CDLHAMMER(O, H, L, C)
ta_env = talib.CDLENGULFING(O, H, L, C)
print("Valores de CDLENGULFING:", sorted(set(ta_env)))

comparacion = pd.DataFrame({
    "propio": [doji.sum(), martillo.sum(), env_alcista.sum(),
               env_bajista.sum()],
    "talib": [(ta_doji != 0).sum(), (ta_martillo > 0).sum(),
              (ta_env > 0).sum(), (ta_env < 0).sum()],
    "ambos": [(doji & (ta_doji != 0)).sum(),
              (martillo & (ta_martillo > 0)).sum(),
              (env_alcista & (ta_env > 0)).sum(),
              (env_bajista & (ta_env < 0)).sum()],
}, index=list(propios))
comparacion
```
```output
Valores de CDLENGULFING: [np.int32(-100), np.int32(-80), np.int32(0), np.int32(80), np.int32(100)]
                    propio  talib  ambos
doji                   301    409    272
martillo                24     54     13
envolvente alcista      46     46     46
envolvente bajista      43     45     43
```

Las envolventes casi coinciden: la regla es geométrica y deja poco margen. El doji y el martillo, no. TA-Lib no
compara el cuerpo con el rango del propio día, sino con el **rango medio de las 10 sesiones anteriores**. Con esa
regla, el doji propio reproduce exactamente el de TA-Lib:

```python
rango_medio = rango.rolling(10).mean().shift(1)
doji_ta = cuerpo <= 0.10 * rango_medio
print("Sesiones:", doji_ta.sum(), "| iguales a CDLDOJI:",
      f"{(doji_ta == (ta_doji != 0)).mean():.1%}")
```
```output
Sesiones: 409 | iguales a CDLDOJI: 100.0%
```

Las dos definiciones son razonables. La de TA-Lib llama doji a un día de cuerpo pequeño **para lo que suele moverse
NOVA**, aunque ese día el rango fuera estrecho; la propia, a un día cuyo cuerpo es pequeño **para ese día**. El
martillo de TA-Lib añade más condiciones de ese tipo (sombras comparadas con medias de varias sesiones, posición
respecto a la vela anterior). Ninguna es «la correcta». Lo que importa es que el resultado de un estudio de patrones
depende de la definición, y que hay que publicarla con el resultado.

## Medir sin engañarse

La medición tiene que respetar el tiempo. El patrón de la sesión $t$ se conoce al cierre de $t$; la primera
ejecución posible es la **apertura de $t+1$**. La salida se fija antes de mirar los datos: aquí, la apertura de
$t+6$ (cinco sesiones en posición). Los costes, un 0,10 % por operación completa (comisión y media horquilla a cada
lado, lección 1.1.5). Un patrón bajista se mide como una venta.

```mermaid
flowchart LR
  P[Patrón en t] --> E[Entrada t+1]
  E --> S[Salida t+6]
  S --> N[Neto de costes]
```

```python
COSTE = 0.001
fwd = o.shift(-6) / o.shift(-1) - 1  # apertura t+1 a apertura t+6

def evaluar(mascara, signo=1):
    r = (signo * fwd[mascara]).dropna() - COSTE
    t = r.mean() / r.std() * np.sqrt(len(r))
    return {"n": len(r), "neta_pct": 100 * r.mean(), "t": t}

casos = {
    "doji (propio)": (doji, 1), "martillo (propio)": (martillo, 1),
    "env. alcista (propio)": (env_alcista, 1),
    "env. bajista (propio)": (env_bajista, -1),
    "CDLDOJI": (ta_doji != 0, 1), "CDLHAMMER": (ta_martillo > 0, 1),
    "CDLENGULFING +": (ta_env > 0, 1),
    "CDLENGULFING -": (ta_env < 0, -1),
}
res = pd.DataFrame({k: evaluar(*v) for k, v in casos.items()}).T
print(f"Cualquier sesión, comprando: {100 * (fwd.mean() - COSTE):.3f} %"
      f" neto en 5 sesiones")
res.astype({"n": int}).round(3)
```
```output
Cualquier sesión, comprando: 0.172 % neto en 5 sesiones
                         n  neta_pct      t
doji (propio)          299     0.475  1.362
martillo (propio)       24     1.641  1.648
env. alcista (propio)   46    -0.657 -0.829
env. bajista (propio)   43    -0.111 -0.160
CDLDOJI                407     0.471  1.712
CDLHAMMER               54     1.294  1.968
CDLENGULFING +          46    -0.657 -0.829
CDLENGULFING -          45    -0.090 -0.133
```

Ningún estadístico $t$ llega a 2. El mejor, el martillo de TA-Lib, gana un 1,29 % neto en cinco sesiones con
$t = 1{,}97$, sobre 54 casos. Antes de celebrarlo, tres comprobaciones.

La primera: comprar **cualquier** sesión ya daba un 0,17 % neto, porque NOVA sube en el periodo. Parte de lo que
ganan los patrones alcistas es la deriva de la acción, no el patrón. La segunda: los rendimientos de cinco sesiones
de patrones cercanos se solapan (dos doji en días seguidos comparten cuatro de sus cinco días), así que los
estadísticos $t$ están inflados. La tercera, la más importante, viene a continuación.

## Tres formas de fabricar un patrón que funciona

**Elegir el umbral después de mirar.** «Cuerpo pequeño» puede ser un 3 %, un 10 % o un 30 % del rango. Cada umbral
da otro conjunto de sesiones:

```python
umbrales = [0.03, 0.05, 0.10, 0.20, 0.30]
pd.DataFrame({f"{u:.0%}": evaluar(cuerpo <= u * rango)
              for u in umbrales}).T.astype({"n": int}).round(3)
```
```output
       n  neta_pct      t
3%    80     0.894  1.341
5%   145     0.152  0.309
10%  299     0.475  1.362
20%  596     0.499  2.103
30%  901     0.252  1.379
```

Con el 20 %, el doji «funciona»: $t = 2{,}10$. Con el 5 %, $t = 0{,}31$. Si se publica solo el 20 %, el lector ve un
patrón significativo; si se publican los cinco, ve ruido. Un efecto real no debería depender tanto de un umbral
arbitrario.

**Mirar un solo periodo.** Partiendo la muestra en dos:

```python
mitad = "2021-12-31"
m = ta_martillo > 0
pd.DataFrame({"2018-2021": evaluar(m & (px.index <= mitad)),
              "2022-2025": evaluar(m & (px.index > mitad))}
             ).T.astype({"n": int}).round(3)
```
```output
            n  neta_pct      t
2018-2021  28     1.919  1.828
2022-2025  26     0.622  0.810
```

El martillo gana un 1,92 % neto en la primera mitad ($t = 1{,}83$) y un 0,62 % en la segunda ($t = 0{,}81$), con
unos 27 casos en cada una. Con tan pocos casos, la diferencia entre las mitades es tan grande como el efecto.

**Probar muchos y quedarse con el mejor.** Es la trampa grande. TA-Lib tiene 61 patrones; en NOVA, 33 aparecen al
menos 10 veces. Si ninguno tuviera efecto, cada uno tendría un 5 % de probabilidad de dar $|t| > 1{,}96$ por azar, y
la de que **alguno** lo dé sería $1 - 0{,}95^{33} = 82\ \%$.

```python
patrones = talib.get_function_groups()["Pattern Recognition"]
filas = {}
for nombre in patrones:
    senal = np.sign(getattr(talib, nombre)(O, H, L, C))
    senal = pd.Series(senal, index=px.index)
    hay = senal != 0
    if hay.sum() >= 10:
        r = (senal[hay] * fwd[hay]).dropna() - COSTE
        filas[nombre] = {"n": len(r), "neta_pct": 100 * r.mean(),
                         "t": r.mean() / r.std() * np.sqrt(len(r))}
todos = pd.DataFrame(filas).T.astype({"n": int})
print(len(patrones), "patrones;", len(todos), "con 10 casos o más;",
      (todos["t"].abs() > 1.96).sum(), "con |t| > 1,96")
todos.sort_values("t").round(3).iloc[[0, 1, 2, -3, -2, -1]]
```
```output
61 patrones; 33 con 10 casos o más; 1 con |t| > 1,96
                       n  neta_pct      t
CDLSHOOTINGSTAR       27    -1.728 -1.940
CDLGAPSIDESIDEWHITE   13    -2.374 -1.785
CDLADVANCEBLOCK       12    -1.248 -1.469
CDLDOJI              407     0.471  1.712
CDLLONGLEGGEDDOJI    405     0.486  1.756
CDLHAMMER             54     1.294  1.968
```

*Fig. — Estadístico t de la rentabilidad neta a 5 sesiones tras cada uno de los 33 patrones de TA-Lib que aparecen al menos 10 veces en NOVA, con los umbrales de una prueba, del mejor de 33 placebos y de Bonferroni.* 33 puntos ordenados de menor a mayor, uno por patrón de TA-Lib con al menos 10 casos en NOVA, con su estadístico t entre −1,94 y 1,97. Líneas horizontales en ±1,96 (una sola prueba al 5 %), ±2,68 (mediana del mejor de 33 placebos) y ±3,17 (Bonferroni para 33 pruebas). El peor es la estrella fugaz, −1,94; el mejor, destacado, el martillo, con 1,97, apenas por encima de 1,96 y muy por debajo de las otras dos líneas.

Uno solo de los 33 pasa de 1,96: el martillo, con 1,968, por ocho milésimas. El peor es la estrella fugaz, con −1,94:
un patrón bajista cuya «venta» pierde. Con 33 pruebas y ningún efecto real, lo esperable era que alguno pasara. Para saber si 1,97 es mucho **con 33 pruebas**, hay
dos herramientas. La corrección de Bonferroni exige $p < 0{,}05/33$ en cada prueba, es decir, $|t| > 3{,}17$. Y la
simulación placebo responde directamente: si se eligen fechas al azar con el mismo número de casos que cada patrón
real, ¿cuánto vale el mejor $|t|$ de los 33?

```python
from scipy.stats import norm

ns = todos["n"].to_numpy()
rentas = fwd.dropna().to_numpy()
rng = np.random.default_rng(2027)
maximos = []
for _ in range(1000):
    ts = []
    for n in ns:
        signo = rng.choice([-1, 1])
        r = signo * rng.choice(rentas, n, replace=False) - COSTE
        ts.append(r.mean() / r.std() * np.sqrt(n))
    maximos.append(np.max(np.abs(ts)))
maximos = np.array(maximos)
print(f"Mejor |t| de 33 placebos: mediana {np.median(maximos):.2f},"
      f" percentil 95 {np.percentile(maximos, 95):.2f}")
print(f"Placebos con un |t| máximo >= 1,97: "
      f"{(maximos >= 1.97).mean():.1%}")
print(f"Umbral de Bonferroni (33 pruebas, 5 %): "
      f"{norm.ppf(1 - 0.05 / 66):.2f}")
```
```output
Mejor |t| de 33 placebos: mediana 2.68, percentil 95 3.58
Placebos con un |t| máximo >= 1,97: 96.0%
Umbral de Bonferroni (33 pruebas, 5 %): 3.17
```

En la mediana, el mejor de 33 patrones inventados tiene un $|t|$ de 2,68; el mejor patrón real, 1,97. Los patrones
de velas de TA-Lib se comportan en NOVA **peor que fechas elegidas al azar**: en el 96,0 % de las simulaciones el
placebo encuentra un «patrón» mejor que el martillo, y el percentil 95 del placebo, 3,58, está por encima incluso del
umbral de Bonferroni.

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

Los datos de NOVA son sintéticos: salen de un modelo sin patrones de velas, así que el resultado era el esperable.
Lo que la lección enseña es el procedimiento, no la conclusión. En datos reales la evidencia publicada es mixta: Lo,
Mamaysky y Wang (2000) encuentran que algunas figuras clásicas aportan información incremental en acciones de EE. UU.
de 1962 a 1996, y Osler (2000) que los soportes y resistencias que publicaban seis firmas de divisas ayudaban a
predecir interrupciones de tendencia intradía. La única forma de saber si algo funciona en un mercado concreto es
medirlo así: entrada realista, costes, placebo y todas las pruebas a la vista.

</aside>

## Soportes y resistencias: máximos y mínimos locales

Un **soporte** es un precio en el que la caída se ha detenido antes; una **resistencia**, uno en el que se ha
detenido la subida. La forma más común de programarlos es con **máximos y mínimos locales** (*swing highs* y *swing
lows*): un máximo es local si es el mayor de las $k$ sesiones anteriores y las $k$ siguientes.

Las $k$ **siguientes**. Un máximo local de $k = 5$ solo se sabe que lo es cinco sesiones después: hasta entonces,
cualquier sesión siguiente podría superarlo. La versión directa con pandas, una ventana centrada, lo marca el mismo
día en que se forma:

```mermaid
flowchart LR
  M[Máximo en t] --> K[5 sesiones]
  K --> F[Confirmado t+5]
  F --> U[Nivel usable]
```

```python
K = 5
es_max = h == h.rolling(2 * K + 1, center=True).max()
es_min = l == l.rolling(2 * K + 1, center=True).min()
print("Máximos locales:", es_max.sum(), "| mínimos:", es_min.sum())

# el último nivel CONFIRMADO: el máximo de t-K se conoce en t
resistencia = h.where(es_max).shift(K).ffill()
soporte = l.where(es_min).shift(K).ffill()
# lo mismo SIN esperar la confirmación (anticipación)
resistencia_mal = h.where(es_max).ffill()
niveles = pd.DataFrame({"h": h, "resistencia": resistencia,
                        "resistencia_mal": resistencia_mal,
                        "soporte": soporte})
niveles.loc["2025-12-01":"2025-12-05"].round(2)
```
```output
Máximos locales: 114 | mínimos: 112
                h  resistencia  resistencia_mal  soporte
fecha
2025-12-01  68.92        68.30            71.34    64.13
2025-12-02  67.79        71.34            71.34    64.13
2025-12-03  67.68        71.34            71.34    64.13
2025-12-04  67.10        71.34            71.34    64.13
2025-12-05  66.43        71.34            71.34    64.13
```

`shift(K)` es lo que separa un nivel conocido de uno adivinado. El máximo del 24 de noviembre de 2025, 71,34, es
resistencia en la versión sin esperar desde ese mismo día; la confirmada no lo incorpora hasta el 2 de diciembre,
cinco sesiones después (con el festivo de Acción de Gracias por medio). Por eso el 1 de diciembre una usa 71,34 y la
otra todavía 68,30. La diferencia parece
pequeña en una tabla. En una regla de trading es enorme.

La figura de la cabecera dibuja las 46 últimas sesiones de 2025 con los patrones detectados (10 doji, una envolvente
alcista el 31 de octubre y una bajista el 9 de diciembre) y los niveles confirmados a trazos. El máximo local del 31
de octubre, 68,30, solo se puede usar como resistencia desde el 7 de noviembre, cinco sesiones después; ese día NOVA
cierra a 65,94, más de dos dólares por debajo.

## ¿Se respetan los niveles? Contraste con placebos

La prueba: cada vez que el máximo del día alcanza la última resistencia confirmada viniendo desde abajo (el cierre
de ayer estaba por debajo), ¿cierra el precio por debajo del nivel cinco sesiones después? Si la resistencia es
real, debería ocurrir más que con un nivel cualquiera. Los niveles cualquiera son **placebos**: el mismo nivel
desplazado medio o un ATR arriba o abajo, que nadie ha visto como resistencia.

```python
def toques(nivel):
    toque = (h >= nivel) & (c.shift(1) < nivel)
    respeta = (c.shift(-5) < nivel)[toque & c.shift(-5).notna()]
    return {"toques": int(len(respeta)),
            "respeta_pct": 100 * respeta.mean()}

pruebas = {f"real {d:+.1f} ATR": resistencia + d * atr
           for d in [-1.0, -0.5, 0.0, 0.5, 1.0]}
pruebas["con anticipación"] = resistencia_mal
pruebas["máximo de 20 previas"] = h.rolling(20).max().shift(1)
pd.DataFrame({k: toques(n) for k, n in pruebas.items()}).T.round(1)
```
```output
                      toques  respeta_pct
real -1.0 ATR          192.0         44.8
real -0.5 ATR          192.0         43.8
real +0.0 ATR          146.0         47.3
real +0.5 ATR          113.0         60.2
real +1.0 ATR           85.0         56.5
con anticipación       211.0         67.3
máximo de 20 previas   288.0         47.6
```

*Fig. — Cuántas veces se «respeta» una resistencia en NOVA (entre paréntesis, número de toques): la real confirmada a tiempo, cuatro placebos desplazados, el máximo de las 20 sesiones previas y la versión con anticipación.* Barras horizontales con el porcentaje de toques de una resistencia tras los que el cierre de cinco sesiones después queda por debajo del nivel, con una línea en el 50 %. Resistencia real confirmada: 47,3 % de 146 toques. Placebos un ATR y medio ATR por debajo: 44,8 % y 43,8 %, con 192 toques cada uno; medio ATR y un ATR por encima: 60,2 % y 56,5 %. Máximo de las 20 sesiones previas: 47,6 % de 288. Con anticipación, destacada: 67,3 % de 211 toques.

La resistencia real, confirmada a tiempo, se «respeta» en el 47,3 % de los 146 toques: se rompe más veces de las que
aguanta. Los placebos desplazados hacia abajo dan casi lo mismo (44,8 % y 43,8 %), y los desplazados hacia arriba,
**más** (60,2 % y 56,5 %): un nivel por encima de todos los máximos recientes solo se toca tras una subida fuerte, y
tras una subida fuerte el precio suele devolver algo. Nada de eso tiene que ver con que el nivel sea especial.

Y la versión con anticipación da un 67,3 %: una resistencia que funciona dos de cada tres veces. Es un espejismo de
manual. El máximo local del día $t$ es, por construcción, mayor que los cinco máximos siguientes; si se usa como
nivel el mismo día, el precio **tiene** que quedar por debajo. Un vendedor de indicadores que enseña soportes y
resistencias «perfectos» sobre el gráfico suele estar enseñando esto.

El máximo de las 20 sesiones anteriores (el canal de Donchian de la lección 1.7.1, ya desplazado) es la otra forma
habitual de definir una resistencia. Se toca 288 veces y se respeta en el 47,6 %: lo mismo que la resistencia por
máximos locales y menos que una moneda al aire.

## Agrupamiento de precios

Otra definición de nivel: los precios donde se han concentrado muchos máximos y mínimos, como en un perfil de
volumen (lección 1.7.1) pero con extremos de sesión. Con los extremos de 2025 en tramos de 0,50 USD:

```python
a2025 = px.loc["2025"]
extremos = pd.concat([a2025["h"], a2025["l"]])
tramo = (np.floor(extremos / 0.5) * 0.5).round(2)
cuenta = tramo.value_counts().sort_index()
print("Tramos con extremos:", len(cuenta), "| media:",
      round(cuenta.mean(), 1), "extremos por tramo")
cuenta.sort_values(ascending=False).head(5)
```
```output
Tramos con extremos: 55 | media: 9.1 extremos por tramo
67.5    25
51.5    24
52.0    24
50.5    21
51.0    20
Name: count, dtype: int64
```

Los tramos con más extremos son simplemente los precios donde NOVA pasó más tiempo en 2025: de 50,50 a 52,50 (el
lateral de mayo a agosto) y 67,50 (el de octubre a diciembre). Un agrupamiento así describe dónde estuvo el precio; que anticipe dónde se
detendrá es otra hipótesis, y se contrasta igual que la de los máximos locales: con toques, placebos y costes.

## Lo que queda

- **Codificar es fácil; medir bien es el trabajo.** Entrada en la apertura siguiente, salida fijada de antemano,
  costes, comparación con operar cualquier día.
- **Publicar todas las pruebas.** Umbrales, horizontes y patrones probados. El número de pruebas es parte del
  resultado (lección 1.6.2 y módulo 3).
- **Placebos siempre.** Fechas al azar para los patrones, niveles desplazados para los soportes. Si el placebo
  funciona igual, no hay nada.
- **Los extremos locales se confirman tarde.** Un máximo de $k$ sesiones a cada lado se conoce $k$ sesiones después.
- **Los precios ajustados mueven los niveles.** Un soporte en la serie ajustada hacia atrás no es el precio que se vio
  en pantalla (lección 1.7.1); para niveles absolutos, usar la serie sin ajustar del periodo o reajustar en cada
  fecha.

## Ejercicios

1. Repite la medición del martillo de TA-Lib con horizontes de 1, 3, 10 y 20 sesiones (salida en la apertura de
   $t+1+k$). ¿Es estable el resultado?

**Solución.**

```python
def evaluar_h(mascara, k, signo=1):
    r = (signo * (o.shift(-1 - k) / o.shift(-1) - 1)[mascara]).dropna()
    r = r - COSTE
    return {"n": len(r), "neta_pct": 100 * r.mean(),
            "t": r.mean() / r.std() * np.sqrt(len(r))}

pd.DataFrame({f"{k} sesiones": evaluar_h(ta_martillo > 0, k)
              for k in [1, 3, 5, 10, 20]}).T.astype({"n": int}).round(3)
```
```output
              n  neta_pct      t
1 sesiones   54    -0.185 -0.852
3 sesiones   54     0.246  0.533
5 sesiones   54     1.294  1.968
10 sesiones  54     1.399  1.246
20 sesiones  54     2.224  1.254
```

El horizonte es otro parámetro libre, y el resultado cambia con él: $t = -0{,}85$ a una sesión, 0,53 a tres, 1,97 a
cinco y 1,25 a diez y a veinte. Si el martillo tuviera un efecto, se esperaría
un patrón ordenado (por ejemplo, ganancias que crecen y se estabilizan); un $t$ que sube y baja según el horizonte,
sobre 54 casos, es lo que produce el ruido. Probar cinco horizontes multiplica también el número de pruebas.

2. Cuenta cuántas veces el precio toca la última **soporte** confirmado desde arriba (el mínimo del día baja hasta
   el nivel y el cierre de ayer estaba por encima) y en qué porcentaje cierra por encima cinco sesiones después.
   Compáralo con los placebos de medio ATR.

**Solución.**

```python
def toques_soporte(nivel):
    toque = (l <= nivel) & (c.shift(1) > nivel)
    aguanta = (c.shift(-5) > nivel)[toque & c.shift(-5).notna()]
    return {"toques": int(len(aguanta)),
            "aguanta_pct": 100 * aguanta.mean()}

pd.DataFrame({f"soporte {d:+.1f} ATR": toques_soporte(soporte + d * atr)
              for d in [-0.5, 0.0, 0.5]}).T.round(1)
```
```output
                  toques  aguanta_pct
soporte -0.5 ATR    93.0         37.6
soporte +0.0 ATR   100.0         42.0
soporte +0.5 ATR   129.0         36.4
```

El soporte confirmado aguanta en el 42,0 % de sus 100 toques, algo más que los placebos (37,6 % y 36,4 %). Pero con
100 toques el error típico de un porcentaje así es de unos 5 puntos ($\sqrt{0{,}42 \cdot 0{,}58 / 100}$): la
diferencia está dentro del ruido. Y, como las resistencias, el soporte se rompe más veces de las que aguanta.

3. Un proveedor anuncia un escáner de patrones de velas «con un 72 % de acierto». ¿Qué cinco preguntas le harías
   antes de creértelo?

**Solución.**

1. ¿Acierto medido cómo? ¿Precio de entrada al cierre del patrón o a la apertura siguiente, y salida a qué horizonte,
   fijado antes o elegido después? 2. ¿Neto de costes y comparado con qué? Un 72 % de días alcistas en una acción que
   sube puede ser solo la deriva. 3. ¿Cuántos patrones, umbrales, horizontes y activos se probaron para llegar a ese
   72 %, y cuántos casos hay detrás? 4. ¿Los niveles y patrones se calculan con información disponible en cada
   momento (máximos locales confirmados, ningún `center=True`)? 5. ¿Funciona fuera de la muestra en la que se
   diseñó, en un periodo y unos activos que el proveedor no haya visto?

## Autoevaluación

1. TA-Lib marca 409 doji en NOVA y la regla «cuerpo ≤ 10 % del rango del día» marca 301. ¿Por qué?

   - A. TA-Lib tiene un error
   - B. TA-Lib compara el cuerpo con el 10 % del rango medio de las 10 sesiones anteriores, no con el rango del propio día
   - C. La regla propia usa precios sin ajustar
   - D. TA-Lib cuenta también los martillos

   *Respuesta B: TA-Lib compara el cuerpo con el 10 % del rango medio de las 10 sesiones anteriores, no con el rango del propio día.* Con la definición de TA-Lib (rango medio de las 10 velas anteriores), la regla propia da exactamente las mismas 409 sesiones. Dos definiciones razonables del mismo patrón dan conjuntos distintos, y por tanto resultados distintos.

2. Un patrón aparece al cierre del día t. ¿Cuál es la entrada realista para medir su rentabilidad?

   - A. El cierre del día t
   - B. La apertura del día t
   - C. La apertura del día t + 1
   - D. El mínimo del día t + 1

   *Respuesta C: La apertura del día t + 1.* El patrón solo existe cuando la vela de t ha cerrado. Comprar a ese mismo cierre supone actuar antes de saber que había patrón; la primera ejecución posible es la apertura siguiente (o la subasta de cierre si se calcula unos minutos antes, que es otra regla).

3. Pruebas 33 patrones y el mejor tiene t = 1,97. ¿Qué concluyes?

   - A. Que funciona, porque t está cerca de 2
   - B. Nada: con 33 pruebas, el mejor de 33 patrones al azar tiene |t| de 2,68 en la mediana; el umbral de Bonferroni al 5 % es 3,17
   - C. Que funciona solo en la segunda mitad
   - D. Que hay que probar más patrones

   *Respuesta B: Nada: con 33 pruebas, el mejor de 33 patrones al azar tiene |t| de 2,68 en la mediana; el umbral de Bonferroni al 5 % es 3,17.* Con muchas pruebas, el máximo de los estadísticos crece aunque no haya nada. La pregunta correcta no es si el mejor pasa de 2, sino si pasa de lo que daría el azar con el mismo número de pruebas.

4. Una «resistencia» definida por máximos locales de 5 sesiones a cada lado se «respeta» en el 67 % de los toques. Con el nivel confirmado cinco sesiones después, solo en el 47 %. ¿Qué ha pasado?

   - A. Que la resistencia funciona mejor a corto plazo
   - B. Que el primer cálculo usa el máximo local el mismo día en que se forma, y para serlo el precio tiene que bajar en las cinco sesiones siguientes: anticipación
   - C. Que el ATR cambia
   - D. Nada: son dos muestras distintas

   *Respuesta B: Que el primer cálculo usa el máximo local el mismo día en que se forma, y para serlo el precio tiene que bajar en las cinco sesiones siguientes: anticipación.* Un máximo local solo se sabe que lo es cuando han pasado las sesiones de la derecha de la ventana. Usarlo antes mete en la regla la caída que lo define. Confirmado a tiempo, el nivel se rompe más veces de las que se respeta.

5. ¿Qué es un nivel placebo y para qué sirve?

   - A. Un soporte que no se ha tocado nunca
   - B. Un nivel desplazado a propósito (por ejemplo, el real más o menos un ATR) para comprobar si el real se comporta distinto que uno cualquiera
   - C. El precio medio de la sesión
   - D. Un nivel calculado con datos sintéticos

   *Respuesta B: Un nivel desplazado a propósito (por ejemplo, el real más o menos un ATR) para comprobar si el real se comporta distinto que uno cualquiera.* Si los toques de un soporte inventado se comportan igual que los del real, el soporte no aporta nada. En NOVA, los niveles desplazados medio ATR o uno hacia arriba se «respetan» más que el real.

## Ideas clave

- Un patrón de velas es una regla con umbrales: cuánto es «cuerpo pequeño» o «sombra larga» hay que decirlo con números. Definiciones razonables distintas dan conjuntos de sesiones distintos.
- La medición honesta entra en la apertura siguiente al patrón, sale a un horizonte fijado antes de mirar, descuenta costes y compara con lo que habría dado operar cualquier día.
- Probar muchos patrones, umbrales u horizontes y quedarse con el mejor fabrica resultados: el umbral de doji del 20 % da t = 2,10 en NOVA; el de 5 %, 0,31.
- En los datos de NOVA ninguno de los 33 patrones con al menos 10 casos supera |t| = 2, y el mejor queda por debajo del máximo típico de 33 patrones al azar. Los datos son sintéticos y no tienen patrones dentro; en datos reales la evidencia publicada es mixta.
- Un máximo o mínimo local de k sesiones a cada lado solo se conoce k sesiones después. Usarlo antes convierte cualquier resistencia en una resistencia perfecta.
- Los soportes y resistencias se contrastan con niveles placebo: si un nivel desplazado funciona igual o mejor, el nivel no es especial.

## Recursos

### Research

- [Alpha Factors y Alpha Trading en Python](https://pythonparatrading.com/alpha-factors-y-alpha-trading-en-python/) · Research-0007 · 5 min
- [Procesos Estocásticos - Paseos Aleatorios](https://pythonparatrading.com/procesos-estocasticos/) · Research-0005 · 12 min

### Documentación

- [TA-Lib para Python: funciones de reconocimiento de patrones (CDL)](https://ta-lib.github.io/ta-lib-python/func_groups/pattern_recognition.html) · ta-lib.github.io

### Libros

- [Andrew W. Lo, Harry Mamaysky y Jiang Wang, «Foundations of Technical Analysis», NBER Working Paper 7613 (2000)](https://www.nber.org/papers/w7613) · nber.org
- [Carol Osler, «Support for Resistance: Technical Analysis and Intraday Exchange Rates», FRBNY Economic Policy Review 6(2), 2000](https://www.newyorkfed.org/research/epr/00v06n2/0007osle) · newyorkfed.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

Lección anterior: [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/)

Lección siguiente: [1.7.4 · Multi-timeframe sin anticipación](https://pythonparatrading.com/curso-python-trading/nivel-1/multi-timeframe-sin-look-ahead/)
