Lección 0.2.6 · 40 min

Introducción a los asistentes de código con IA: prompts, revisión y tests

Un asistente escribe en segundos una señal de trading que parece correcta. Esta lección sigue un caso completo: la función que propone, el backtest espectacular que produce, el test que demuestra que usa datos del futuro y el prompt que lo habría evitado.

Fig. — La misma estrategia de pendiente de la media de 20 sesiones sobre SINT: con la media centrada que propuso el asistente y con la media corregida, frente a comprar y mantener
Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

Qué es un asistente de código

Un asistente de código es un modelo de lenguaje entrenado con enormes cantidades de código y texto que propone código a partir de una descripción y del contexto que le des. Hay tres formas de usarlo, que se diferencian en cuánto hace por su cuenta:

FormaQué haceEjemplosQué revisar
AutocompletadoSugiere la línea o la función que estás escribiendoCopilot en VS Code, CursorCada sugerencia antes de aceptarla
ChatResponde preguntas y escribe fragmentos que copias túChat del editor, Claude, ChatGPTEl fragmento entero y cómo encaja en tu código
AgenteLee el repositorio, edita ficheros y ejecuta comandos y testsClaude Code, Codex, el agente de CursorEl diff completo, como una pull request (lección 0.2.5)

Las tres comparten una propiedad: el código que producen es plausible. Tiene el aspecto del código que resolvería el problema, con nombres razonables y comentarios convincentes. Muchas veces es también correcto. Pero el modelo no conoce tus datos, no sabe a qué hora se opera tu señal y no ha ejecutado tu backtest. Lo que no le digas, lo decide él sin avisar.

En trading esto tiene un coste concreto. Un error de programación normal suele romper algo: una excepción, un gráfico vacío. Un error de anticipación (lección 0.1.1) no rompe nada: produce un backtest mejor. Es el error que más cuesta ver y el que más dinero hace perder, porque convence.

La lección 0.1.3 fijó tres reglas para usar asistentes. Siguen en pie y esta lección explica el porqué de la segunda:

  1. Nivel 0 sin asistente. El código de los ejercicios lo escribes tú. Puedes preguntar dudas conceptuales a un chat. Esta lección no cambia eso: enseña a juzgar código generado, que es justo lo que no se puede hacer sin saber escribirlo.
  2. Después, la IA propone y tú decides: primero la prueba, después el código, lectura completa, pruebas y commit.
  3. Nada sensible en el prompt: ni claves, ni datos con licencia restrictiva, ni datos personales.

El resto de la lección es un caso completo, de los que se repiten a diario.

Un caso: la señal que propone el asistente

Un usuario pide a un chat, sin más contexto:

Escribe una función de pandas que dé una señal de compra cuando la media móvil de 20 sesiones del precio de cierre tenga pendiente positiva. Quiero una media suavizada y sin retraso.

Los precios son los de SINT, el ETF ficticio del curso:

IN [01]python
import pandas as pd

precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
                      index_col="fecha")
cierre = precios["cierre"]
rent = cierre.pct_change().fillna(0)
print(len(cierre), "sesiones;", cierre.index[0].date(), "a",
      cierre.index[-1].date())
OUT [01]Precios de SINT
1303 sesiones; 2021-01-04 a 2025-12-31

La respuesta, con el aspecto típico de un asistente (docstring, nombres claros, un comentario que justifica la decisión):

IN [02]python
def senal_pendiente_ia(cierre: pd.Series,
                       ventana: int = 20) -> pd.Series:
    """1 si la media móvil sube, 0 si no.

    Se usa una media centrada para eliminar el retraso."""
    media = cierre.rolling(ventana, center=True).mean()
    return (media.diff() > 0).astype(int)

El usuario hace lo que aprendió en la lección 0.1.1: desplaza la posición un día para operar después de la señal, y mide el resultado frente a comprar y mantener:

IN [03]python
def backtest(senal: pd.Series) -> pd.Series:
    """Rentabilidad diaria; la señal del cierre se opera al día
    siguiente."""
    posicion = senal.shift(1).fillna(0)
    return posicion * rent

def resumen(r: pd.Series) -> str:
    total = (1 + r).prod() - 1
    sharpe = r.mean() / r.std() * 252 ** 0.5
    return f"total {total:+7.1%}   Sharpe {sharpe:5.2f}"

r_ia = backtest(senal_pendiente_ia(cierre))
print(f"{'Asistente':<20}", resumen(r_ia))
print(f"{'Comprar y mantener':<20}", resumen(rent))
OUT [03]Backtest de la señal del asistente
Asistente            total +324.1%   Sharpe  2.10
Comprar y mantener   total  +43.5%   Sharpe  0.48

Un Sharpe por encima de 2 con una media móvil, en un activo que en el mismo periodo se queda en 0,48. La curva de la cabecera (en ámbar) sube casi sin retrocesos. Es el momento de sospechar: una regla tan simple no debería batir así al mercado, y menos en un activo con una caída del 25 % en 2022.

La línea resaltada es el problema: center=True. Por defecto, rolling etiqueta cada ventana con su último día: la media del 3 de junio usa los 20 cierres que terminan el 3 de junio. Con center=True, la etiqueta es el centro de la ventana: la media del 3 de junio usa diez cierres anteriores, el del propio día y nueve posteriores. «Sin retraso», sí, porque mira al futuro. Se puede comprobar qué días entran en cada ventana sustituyendo los precios por su posición en la serie:

IN [04]python
pos = pd.Series(range(len(cierre)), index=cierre.index)
dia = pd.Timestamp("2024-06-03")
for centrada in (False, True):
    v = pos.rolling(20, center=centrada)
    primero = cierre.index[int(v.min()[dia])].date()
    ultimo = cierre.index[int(v.max()[dia])].date()
    print(f"center={centrada!s:<5}: de {primero} a {ultimo}")
OUT [04]Qué días usa la media del 3 de junio de 2024
center=False: de 2024-05-07 a 2024-06-03
center=True : de 2024-05-20 a 2024-06-14

Con la media centrada, la señal del 3 de junio sabe cómo cerró el mercado hasta el 14 de junio. Desplazar la posición un día no sirve de nada: el futuro ya está dentro de la señal, no en el desfase. Y nada en el código lo delata a simple vista; el comentario incluso lo presenta como una ventaja.

Un test que lo detecta

Leer con atención ayuda, pero no escala: hay decenas de formas de colar el futuro en una señal (center=True, shift(-1), rellenar huecos hacia atrás con bfill, normalizar con la media de toda la muestra…). Una prueba que no depende de la forma del error es mejor. La idea: la señal de un día no puede cambiar si se añaden datos posteriores. Si se calcula con los datos hasta una fecha y después con todos, las señales de esas fechas deben coincidir:

IN [05]python
def comprobar_sin_anticipacion(funcion, cierre: pd.Series,
                               cortes=(250, 500, 750, 1000)):
    completa = funcion(cierre)
    for n in cortes:
        parcial = funcion(cierre.iloc[:n])
        distintos = int((parcial != completa.iloc[:n]).sum())
        assert distintos == 0, (
            f"hasta {cierre.index[n - 1].date()}: {distintos} días "
            "de la señal cambian al añadir datos posteriores")

Es una función con assert sencillos: si la condición no se cumple, lanza un error con el mensaje. Con pytest (lección 0.4.5) se convierte en un test que se ejecuta solo antes de cada commit. Aplicada a la función del asistente:

IN [06]python
comprobar_sin_anticipacion(senal_pendiente_ia, cierre)
OUT [06]La señal del asistente no pasa
AssertionError: hasta 2021-12-17: 9 días de la señal cambian al añadir datos posteriores

Nueve señales de diciembre de 2021 cambian cuando se añaden los cierres posteriores al 17 de diciembre. El test no sabe nada de medias centradas: solo comprueba una propiedad que cualquier señal honesta cumple. Por eso sirve también para la próxima función que te proponga un asistente, sea cual sea su error.

La corrección es quitar center=True: la media de cada día usa ese día y los 19 anteriores. Tiene retraso, y ese retraso es el precio de no conocer el futuro. La versión corregida pasa el test:

IN [07]python
def senal_pendiente(cierre: pd.Series,
                    ventana: int = 20) -> pd.Series:
    """1 si la media móvil (solo datos pasados) sube; 0 si no."""
    media = cierre.rolling(ventana).mean()
    return (media.diff() > 0).astype(int)

comprobar_sin_anticipacion(senal_pendiente, cierre)
print("Sin anticipación")
r_ok = backtest(senal_pendiente(cierre))
print(f"{'Corregida':<20}", resumen(r_ok))
OUT [07]senales.py (corregida)
Sin anticipación
Corregida            total   -8.9%   Sharpe -0.07

De +324 % a −8,9 %. La estrategia real no bate a comprar y mantener; la del asistente no existía. Las tres curvas de la figura de la cabecera salen de estas celdas. Fíjate en que la versión corregida es peor: un test bien hecho a menudo empeora el resultado, porque elimina una ventaja falsa. Si después de corregir un error tu backtest mejora, revisa dos veces.

El prompt que lo habría evitado

El primer prompt dejaba al modelo decidir lo importante. Uno mejor da el contexto, el contrato de los datos, la regla temporal explícita y la prueba que tiene que pasar:

Contexto: backtest diario en pandas 3.0. cierre es una pd.Series de cierres diarios con índice de fechas ordenado y sin huecos.

Tarea: función senal_pendiente(cierre, ventana=20) -> pd.Series que devuelva 1 si la media móvil simple de ventana sesiones sube respecto al día anterior y 0 si no, con el mismo índice que cierre.

Regla temporal: la señal del día t solo puede usar cierres hasta t, incluido. Nada de center=True, shift negativo, bfill ni estadísticos de toda la muestra. La posición se desplaza fuera de la función.

Debe pasar este test: (pegar comprobar_sin_anticipacion).

Sin dependencias nuevas. Explica en una frase qué hace cada línea.

Esto es lo que la regla 2 del curso pide: la prueba la escribes tú, antes, y el asistente la recibe como parte de la tarea. Si la prueba la escribiera el mismo asistente que la función, podría heredar el mismo malentendido. La última línea, pedir que explique cada línea, no garantiza nada, pero facilita la lectura completa que tienes que hacer igual.

Con un agente: instrucciones y permisos

Un agente no recibe un prompt suelto: trabaja dentro del repositorio, lee ficheros, ejecuta comandos y cambia varios ficheros a la vez. Eso cambia dos cosas.

La primera, el contexto. Lo que en un chat repetirías en cada prompt se escribe una vez en un fichero de instrucciones del repositorio, que el agente lee al empezar: CLAUDE.md en Claude Code, AGENTS.md en Codex y en muchos otros1Nota 1 · Claude Code Docs, «Cómo Claude recuerda tu proyecto» (code.claude.com/docs/es/memory), y el formato abierto AGENTS.md (agents.md), que lista los agentes compatibles; consultados el 21 de octubre de 2026.. Para el proyecto del curso bastan unas líneas:

IN [08]AGENTS.mdmarkdown
# Reglas del proyecto ppt-curso
- Python 3.14 con uv. Dependencias solo con `uv add`, nunca pip.
- Señales: el valor del día t usa datos hasta el cierre de t.
  La posición es la señal desplazada un día. Prohibidos
  center=True, shift negativo, bfill y estadísticos de toda la muestra.
- Toda señal nueva pasa comprobar_sin_anticipacion (tests/).
- Antes de terminar: `uv run pytest`. No cambies un test para
  que pase.
- No leas ni muestres `.env`. Ninguna clave en el código.

La línea sobre los tests no es un adorno: un agente al que se le pide que «haga pasar los tests» puede conseguirlo cambiando el test en lugar del código. Por eso el diff se revisa entero, tests incluidos.

La segunda, los permisos. Un agente que ejecuta comandos puede leer cualquier fichero al que tú tengas acceso, incluido .env. Mientras aprendes: claves de papel o ninguna, trabajo en una rama y cambios que entran por pull request, como los tuyos (lección 0.2.5). Nunca un agente con acceso a una cuenta real del bróker.

Qué revisar en código generado

Al revisar código generado para trading, esta lista cubre los errores más comunes:

RevisaSeñales de alarma
Anticipacióncenter=True, shift(-n), bfill, medias o desviaciones de toda la muestra, modelos ajustados con todos los datos
DesfaseSeñal y rentabilidad del mismo día multiplicadas sin shift(1)
CostesNinguna comisión ni deslizamiento; operaciones diarias sin coste
Huecosfillna(0) en precios, dropna que desalinea series, festivos
DependenciasPaquetes que no conoces: compruébalos en PyPI antes de uv add
SecretosClaves escritas en el código o impresas en logs (lección 0.2.4)

Ejercicios

  1. Sin ejecutarla, di si esta señal tiene sesgo de anticipación y por qué; después compruébalo con comprobar_sin_anticipacion: (cierre > cierre.mean()).astype(int).
Solución
IN [09]python
def senal_sobre_media_total(cierre: pd.Series) -> pd.Series:
    return (cierre > cierre.mean()).astype(int)

try:
    comprobar_sin_anticipacion(senal_sobre_media_total, cierre)
except AssertionError as error:
    print("Anticipación:", error)
OUT [09]Comparar con la media de toda la muestra
Anticipación: hasta 2021-12-17: 115 días de la señal cambian al añadir datos posteriores

Sí: cierre.mean() es la media de los cinco años, así que la señal de 2021 depende de los precios de 2025. Es uno de los errores de la tabla (estadísticos de toda la muestra) y el test lo detecta sin saber nada de él.

  1. Un asistente rellena los huecos de una serie de precios con cierre.bfill(). ¿Por qué es anticipación? ¿Con qué lo sustituirías?
Solución

bfill rellena cada hueco con el siguiente valor conocido: el precio de un día festivo o sin datos sería el cierre de un día posterior. Si la señal usa ese valor, sabe algo del futuro. La alternativa es ffill (el último valor conocido, que sí existía en ese momento) o, mejor aún, no inventar precios: dejar el hueco y no operar ese día. La limpieza de huecos se trata en el nivel 1.

  1. Escribe, sin enviarlo, el prompt que darías a un asistente para una función que calcule la volatilidad móvil de 20 sesiones anualizada a partir de cierre. Incluye contexto, contrato, regla temporal y al menos una prueba.
Solución

Un ejemplo. Contexto: pandas 3.0, cierre es una pd.Series diaria con índice de fechas ordenado. Tarea: volatilidad_movil(cierre, ventana=20) -> pd.Series, desviación típica muestral (ddof=1, lección 0.2.2) de las rentabilidades simples de las últimas ventana sesiones, multiplicada por la raíz de 252, con el mismo índice. Regla temporal: el valor del día t solo usa cierres hasta t; las primeras sesiones sin ventana completa, NaN. Pruebas: el test de truncado; con una serie de precios constante, la volatilidad es 0; con una serie que alterna +1 % y −1 %, la volatilidad es positiva y estable. Sin dependencias nuevas.

Footnotes

  1. Claude Code Docs, «Cómo Claude recuerda tu proyecto» (code.claude.com/docs/es/memory), y el formato abierto AGENTS.md (agents.md), que lista los agentes compatibles; consultados el 21 de octubre de 2026. ↩

  2. J. Spracklen et al., «We Have a Package for You! A Comprehensive Analysis of Package Hallucinations by Code Generating LLMs» (arXiv:2406.10279), consultado el 21 de octubre de 2026. ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. ¿Por qué cierre.rolling(20, center=True).mean() introduce sesgo de anticipación en una señal?

    • APorque center=True es más lento
    • BPorque la media del día t incluye los precios de los días siguientes: el valor de hoy depende de cierres que todavía no existen
    • CPorque una ventana de 20 sesiones es demasiado corta
    • DNo lo introduce si la posición se desplaza un día
    Ver respuesta

    Respuesta B. Porque la media del día t incluye los precios de los días siguientes: el valor de hoy depende de cierres que todavía no existen

    Con center=True, la etiqueta de la ventana es su centro: la media del día t usa diez sesiones anteriores y nueve posteriores. Desplazar la posición un día no lo arregla, porque el futuro ya está dentro de la señal.

  2. ¿Qué comprueba el test de truncado de la lección?

    • AQue la función es rápida
    • BQue la señal calculada con datos hasta una fecha coincide con la calculada con todos los datos para esas mismas fechas
    • CQue la señal tiene tantos unos como ceros
    • DQue el backtest gana dinero
    Ver respuesta

    Respuesta B. Que la señal calculada con datos hasta una fecha coincide con la calculada con todos los datos para esas mismas fechas

    Si añadir datos futuros cambia la señal de días pasados, la señal usaba información que en su momento no existía. Es una propiedad que cualquier señal debe cumplir, sea cual sea la regla.

  3. Un asistente propone uv add pandas-ta-pro para calcular un indicador. ¿Qué haces antes de instalarlo?

    • AInstalarlo: si el asistente lo nombra, existe
    • BComprobar en PyPI y en la web del proyecto que el paquete existe, quién lo publica y si es el que buscas
    • CInstalarlo con pip en lugar de uv
    • DPedirle al asistente que confirme que existe
    Ver respuesta

    Respuesta B. Comprobar en PyPI y en la web del proyecto que el paquete existe, quién lo publica y si es el que buscas

    Los modelos inventan nombres de paquetes con frecuencia, y hay atacantes que publican paquetes maliciosos con esos nombres. El asistente no puede comprobarlo por ti: lo compruebas tú en la fuente.

  4. Según las reglas del curso, ¿cuál es el orden correcto a partir del nivel 1?

    • APedir el código, ejecutarlo, y si el backtest sale bien, commit
    • BEscribir la prueba, pedir el código, leerlo entero, ejecutar las pruebas y solo entonces commit
    • CPedir el código y las pruebas al asistente a la vez y hacer commit
    • DEscribir el código a mano y pedir al asistente que lo apruebe
    Ver respuesta

    Respuesta B. Escribir la prueba, pedir el código, leerlo entero, ejecutar las pruebas y solo entonces commit

    La prueba, escrita por ti antes, fija lo que el código tiene que cumplir. Si la escribe el mismo asistente que el código, puede heredar el mismo error. Un backtest que «sale bien» no es una prueba: el de la lección salía espectacular.

Ideas clave

  • Autocompletado, chat y agente: cuanto más actúa el asistente por su cuenta, más contexto necesita y más hay que revisar.
  • El código generado es plausible, no necesariamente correcto. En trading, el error típico es usar datos del futuro sin que se note.
  • Un backtest espectacular es un motivo para sospechar, no para hacer commit.
  • El test de truncado (la señal hasta t no cambia al añadir datos posteriores) detecta la anticipación en cualquier señal.
  • Un buen prompt da el contrato de datos, la regla temporal y los tests que el código debe pasar. En el nivel 0, el código lo escribes tú.

Recursos