Lección 1.7.3 · 70 min
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.
- Módulo
- 1.7 · Análisis técnico programado
- Duración
- 70 min
Lecciones del nivel 1
1.1 · Cómo funcionan los mercados
1.2 · Clases de activos
- 1.2.1 Acciones: splits, dividendos y eventos corporativos
- 1.2.2 ETFs: réplica, tracking error, apalancados y UCITS
- 1.2.3 Futuros: especificaciones, margen, roll y contango
- 1.2.4 Forex: pares, pips, lotes, rollover y CFD
- 1.2.5 Opciones: call, put, valor intrínseco y valor temporal
- 1.2.6 Cripto: spot, perpetuos, funding y custodia
- 1.2.7 Renta fija y tipos de interés; CFDs y productos apalancados
1.3 · Fuentes de datos de mercado
- 1.3.1 Tipos de datos de mercado: EOD, barras, ticks, libro y referencia
- 1.3.2 Fuentes de datos gratuitas
- 1.3.3 APIs de datos retail y prosumer
- 1.3.4 Proveedores de datos profesionales
- 1.3.5 Datos de cripto y on-chain
- 1.3.6 Brókeres como fuente de datos
- 1.3.7 Licencias y términos de uso de datos
- 1.3.8 Cómo elegir proveedor de datos
1.4 · Ingeniería de datos financieros
- 1.4.1 Descarga por REST y websockets: paginación, límites y reintentos
- 1.4.2 Limpieza de datos: faltantes, ticks erróneos, huecos y zonas horarias
- 1.4.3 Ajustes por splits y dividendos: precios ajustados y sin ajustar
- 1.4.4 Universos sin sesgo de supervivencia: deslistados, fusiones y cambios de símbolo
- 1.4.5 Construcción de barras: tiempo, ticks, volumen, dólar y desequilibrio
- 1.4.6 Series continuas de futuros: reglas de roll, ajuste por diferencia y ajuste por ratio
- 1.4.7 Almacenamiento: CSV, Parquet, particionado, DuckDB, TimescaleDB y ArcticDB
- 1.4.8 Pipeline reproducible: descarga, validación, actualización incremental y tests de calidad
1.5 · Datos fundamentales y macro
1.6 · Análisis exploratorio financiero
1.7 · Análisis técnico programado
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.
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:
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")2011 sesionesLas cuatro cantidades de cada vela son restas vectorizadas:
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) 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.25El 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
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")doji 301
martillo 24
envolvente alcista 46
envolvente bajista 43
Name: sesiones, dtype: int64Los 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):
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))
comparacionValores 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 43Las 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:
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%}")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 se conoce al cierre de ; la primera ejecución posible es la apertura de . La salida se fija antes de mirar los datos: aquí, la apertura de (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.
Fuente Mermaid
flowchart LR P[Patrón en t] --> E[Entrada t+1] E --> S[Salida t+6] S --> N[Neto de costes]
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)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.133Ningún estadístico llega a 2. El mejor, el martillo de TA-Lib, gana un 1,29 % neto en cinco sesiones con , 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 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:
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) 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.379Con el 20 %, el doji «funciona»: . Con el 5 %, . 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:
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) n neta_pct t
2018-2021 28 1.919 1.828
2022-2025 26 0.622 0.810El martillo gana un 1,92 % neto en la primera mitad () y un 0,62 % en la segunda (), 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 por azar, y la de que alguno lo dé sería .
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]]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.968Uno 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 en cada prueba, es decir, . 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 de los 33?
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}")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.17En la mediana, el mejor de 33 patrones inventados tiene un 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.
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 sesiones anteriores y las siguientes.
Las siguientes. Un máximo local de 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:
Fuente Mermaid
flowchart LR M[Máximo en t] --> K[5 sesiones] K --> F[Confirmado t+5] F --> U[Nivel usable]
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)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.13shift(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.
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) 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.6La 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 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:
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)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: int64Los 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 sesiones a cada lado se conoce 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
- Repite la medición del martillo de TA-Lib con horizontes de 1, 3, 10 y 20 sesiones (salida en la apertura de ). ¿Es estable el resultado?
Solución
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) 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.254El horizonte es otro parámetro libre, y el resultado cambia con él: 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 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.
- 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
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) 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.4El 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 (): la diferencia está dentro del ruido. Y, como las resistencias, el soporte se rompe más veces de las que aguanta.
- 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
- ¿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?
Respuesta plegada en cada pregunta
Autoevaluación
TA-Lib marca 409 doji en NOVA y la regla «cuerpo ≤ 10 % del rango del día» marca 301. ¿Por qué?
- ATA-Lib tiene un error
- BTA-Lib compara el cuerpo con el 10 % del rango medio de las 10 sesiones anteriores, no con el rango del propio día
- CLa regla propia usa precios sin ajustar
- DTA-Lib cuenta también los martillos
Ver respuesta
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.
Un patrón aparece al cierre del día t. ¿Cuál es la entrada realista para medir su rentabilidad?
- AEl cierre del día t
- BLa apertura del día t
- CLa apertura del día t + 1
- DEl mínimo del día t + 1
Ver respuesta
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).
Pruebas 33 patrones y el mejor tiene t = 1,97. ¿Qué concluyes?
- AQue funciona, porque t está cerca de 2
- BNada: 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
- CQue funciona solo en la segunda mitad
- DQue hay que probar más patrones
Ver respuesta
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.
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?
- AQue la resistencia funciona mejor a corto plazo
- BQue 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
- CQue el ATR cambia
- DNada: son dos muestras distintas
Ver respuesta
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.
¿Qué es un nivel placebo y para qué sirve?
- AUn soporte que no se ha tocado nunca
- BUn 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
- CEl precio medio de la sesión
- DUn nivel calculado con datos sintéticos
Ver respuesta
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.