Contenido educativo de programación. No es asesoramiento financiero. El trading cripto puede hacerte perder todo tu capital; empieza siempre en modo papel y usa solo dinero que puedas permitirte perder.
Casi todo el mundo que empieza a programar un bot de trading vive el mismo espejismo: escribe un backtest, ve una curva de beneficio que sube como un cohete y piensa que ha encontrado la gallina de los huevos de oro. Luego lo pone en real y pierde dinero desde el primer día. La diferencia entre ese espejismo y la realidad tiene nombre: backtesting honesto. En Python es facilísimo mentirse a uno mismo sin darse cuenta, y en esta guía vamos a desmontar los 5 errores clásicos que hacen que tu estrategia parezca rentable cuando no lo es.
Usaremos como hilo conductor una estrategia de juguete —un cruce de medias móviles sobre el precio de cierre— con ejemplos de código puramente docentes. El objetivo no es que copies esta estrategia (no la uses para operar), sino que aprendas a auditar cualquier backtest que hagas con ojo crítico.
El punto de partida: un cruce de medias móviles
Partimos de un DataFrame de pandas con una columna close (el cierre de cada vela). La idea básica: compramos cuando la media rápida está por encima de la lenta y nos salimos cuando cruza por debajo.
import pandas as pd
# df tiene índice temporal y una columna 'close'
df['sma_rapida'] = df['close'].rolling(20).mean()
df['sma_lenta'] = df['close'].rolling(50).mean()
# Señal: 1 = en mercado, 0 = fuera
df['senal'] = (df['sma_rapida'] > df['sma_lenta']).astype(int)
df['retorno'] = df['close'].pct_change()
Hasta aquí, inofensivo. El problema empieza cuando calculamos cuánto habríamos ganado. Ahí es donde se cuelan los cinco errores.
Error 1: look-ahead bias (usar datos del futuro)
Es el error más venenoso porque es invisible en la curva de resultados: parece que ganas mucho porque, literalmente, estás haciendo trampa con información que en real no tendrías. La versión más habitual es decidir y operar con la misma vela:
# ❌ ERROR: aplicas la señal de HOY al retorno de HOY.
# Estás usando el cierre de la vela para decidir y, a la vez,
# cobrando el movimiento de esa misma vela. Cuando conoces el
# cierre, la vela YA ha terminado: no puedes operar en su interior.
df['retorno_estrategia'] = df['senal'] * df['retorno']
La corrección es una sola línea, pero lo cambia todo: solo puedes operar la vela siguiente a la que generó la señal, porque es la única información que de verdad conocías a tiempo.
# ✅ Operas con la señal de la vela ANTERIOR (la que ya conocías)
df['retorno_estrategia'] = df['senal'].shift(1) * df['retorno']
Ese .shift(1) suele recortar una parte enorme del beneficio ficticio. Si tu estrategia solo gana sin el shift, no tienes una estrategia: tienes una máquina del tiempo. El mismo error aparece de forma más sutil al normalizar con la media/desviación de todo el histórico, al rellenar huecos con datos posteriores o al usar el máximo/mínimo de la sesión antes de que la sesión acabe.
Error 2: sobreajuste / overfitting (demasiados grados de libertad)
Cuantas más reglas, filtros y excepciones le añades a una estrategia, más fácil es que encaje a la perfección en el pasado... y que se estrelle en cuanto ve datos nuevos. El overfitting es ajustar el modelo al ruido del histórico en lugar de a una señal real.
La trampa clásica: probar cientos de combinaciones de parámetros sobre los mismos datos y quedarte con la mejor.
import itertools
def backtest(df, rapida, lenta, comision=0.0):
sma_r = df['close'].rolling(rapida).mean()
sma_l = df['close'].rolling(lenta).mean()
senal = (sma_r > sma_l).astype(int).shift(1).fillna(0)
ret = df['close'].pct_change()
ops = senal.diff().abs().fillna(0)
neto = senal * ret - ops * comision
return (1 + neto.fillna(0)).prod() - 1 # retorno total
mejor = None
for rapida, lenta in itertools.product(range(5, 60, 5), range(20, 200, 10)):
if rapida >= lenta:
continue
r = backtest(df, rapida, lenta)
if mejor is None or r > mejor[0]:
mejor = (r, rapida, lenta)
print(mejor) # p.ej. (3.12, 15, 90) -> ¡+312 %! (mentira)
Has probado casi 200 combinaciones. Con tantos intentos, alguna gana por puro azar, igual que si lanzas 200 monedas al aire alguna sacará muchas caras seguidas. Ese "+312 %" no es una estrategia ganadora: es la combinación que mejor memorizó el pasado. La señal de alarma es tener más parámetros libres que lógica de mercado detrás.
Error 3: ignorar comisiones y slippage
En el backtest ideal compras y vendes al precio exacto de cierre y gratis. En el exchange real pagas comisión en cada operación (típicamente ~0,1 % por lado) y sufres slippage: el precio al que te ejecutan no es el que viste. Una estrategia que opera mucho puede ser rentable en el papel y ruinosa en real solo por los costes.
COMISION = 0.001 # 0,1 % por operación
SLIPPAGE = 0.0005 # 0,05 % de deslizamiento estimado
# Nº de cambios de posición = número de operaciones
df['operaciones'] = df['senal'].shift(1).diff().abs().fillna(0)
coste = df['operaciones'] * (COMISION + SLIPPAGE)
df['retorno_neto'] = df['senal'].shift(1) * df['retorno'] - coste
bruto = (1 + (df['senal'].shift(1) * df['retorno']).fillna(0)).prod() - 1
neto = (1 + df['retorno_neto'].fillna(0)).prod() - 1
print(f"Bruto: {bruto:.1%} | Neto: {neto:.1%}")
No es raro ver un bruto de +80 % convertirse en un neto negativo cuando la estrategia entra y sale mil veces. Regla de oro: ninguna curva de equity es creíble si no descuenta comisiones y slippage. Y si quieres ser exigente, modela también el spread y el impacto de mercado en órdenes grandes.
Error 4: datos de mala calidad
Un backtest es tan bueno como los datos que lo alimentan. En cripto es habitual arrastrar huecos, velas duplicadas, ceros, precios rellenados hacia delante o —el más traicionero— sesgo de supervivencia: probar solo con las monedas que hoy siguen vivas, ignorando las decenas que fueron a cero. Antes de fiarte de un solo número, audita el dataset:
print("Huecos (NaN): ", df['close'].isna().sum())
print("Ceros/negativos: ", (df['close'] <= 0).sum())
print("Velas duplicadas: ", df.index.duplicated().sum())
# ¿El intervalo entre velas es constante? (¿faltan periodos?)
print(df.index.to_series().diff().value_counts().head())
Si el value_counts muestra varios intervalos distintos, tienes saltos temporales que tu estrategia interpretará como movimientos de precio inexistentes. Y ojo con rellenar con ffill() a lo loco: arrastrar el último precio conocido puede colarte un look-ahead sutil o inventar liquidez que nunca existió. Usa datos de una fuente fiable, del par que realmente vas a operar y del mismo periodo horario.
Error 5: curve-fitting de parámetros (optimizar contra la historia)
Es el primo hermano del overfitting, pero merece punto aparte porque es el que más gente sofistica sin darse cuenta. Consiste en ajustar los parámetros a la curva histórica y presentar ese resultado como si fuera el rendimiento esperado. Si eliges las medias 15/90 porque son las que mejor habrían funcionado, estás describiendo el pasado, no prediciendo el futuro.
El antídoto mínimo es separar los datos: optimizas en un tramo (in-sample) y validas en otro que el proceso nunca ha visto (out-of-sample).
n = len(df)
train = df.iloc[:int(n * 0.7)] # aquí BUSCAS los parámetros
test = df.iloc[int(n * 0.7):] # aquí solo VALIDAS, sin tocar nada
# 1) Optimizas únicamente con 'train'
mejor = None
for rapida, lenta in itertools.product(range(5, 60, 5), range(20, 200, 10)):
if rapida >= lenta:
continue
r = backtest(train, rapida, lenta, comision=0.001)
if mejor is None or r > mejor[0]:
mejor = (r, rapida, lenta)
_, rapida, lenta = mejor
# 2) Aplicas ESOS parámetros a datos nunca vistos
r_train = backtest(train, rapida, lenta, comision=0.001)
r_test = backtest(test, rapida, lenta, comision=0.001)
print(f"In-sample: {r_train:.1%} | Out-of-sample: {r_test:.1%}")
Si el resultado out-of-sample se desploma (o se vuelve negativo) frente al in-sample, tus parámetros estaban curve-fitteados: memorizaban el pasado. Una estrategia con fundamento aguanta razonablemente bien fuera de muestra. Para ir más allá, el estándar es la validación walk-forward: reoptimizar en ventanas móviles y encadenar solo los tramos out-of-sample para simular cómo habrías operado de verdad, sin conocer el futuro en ningún momento.
Una checklist rápida de backtest honesto
- ¿Operas con
shift(1)? Nunca decidas y ejecutes en la misma vela. - ¿Descuentas comisiones y slippage? Sin costes, la curva miente.
- ¿Cuántos parámetros libres tienes? Menos reglas, menos ruido memorizado.
- ¿Validaste fuera de muestra? Train/test o walk-forward, siempre.
- ¿Auditaste los datos? Huecos, duplicados, sesgo de supervivencia.
- ¿Empezaste en modo papel? Antes del dinero real, siempre paper trading.
El backtesting honesto en Python no consiste en encontrar la curva más bonita, sino en intentar tumbar tu propia estrategia por todos los medios antes de que lo haga el mercado con tu dinero. Un backtest cuyo único mérito es que sube mucho suele esconder al menos uno de estos cinco errores.
Estos cinco fallos son exactamente lo que trabajamos, con código real y paso a paso, en el módulo de backtesting honesto del curso Bot de Trading de Criptomonedas en Python: Ingeniería para No Perder. Allí montamos un motor de backtest con shift, costes, validación walk-forward y modo papel, para que sepas distinguir una estrategia con fundamento de un espejismo antes de arriesgar un solo euro.
Aviso importante: este es contenido educativo de programación, no asesoramiento financiero ni de inversión. El ejemplo del cruce de medias es puramente docente y no es una recomendación para operar. El trading de criptomonedas conlleva un riesgo real de perder tu capital, total o parcialmente. Antes de usar cualquier bot con dinero real, pruébalo siempre en modo papel (paper trading) y no inviertas más de lo que puedas permitirte perder. Para cualquier decisión de inversión o fiscal, consulta a un profesional cualificado.