Lección 0.2.3 · 35 min

Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)

El editor es donde escribes el sistema; el notebook, donde lo exploras. Esta lección elige uno de cada, los conecta al entorno del proyecto y enseña el error más caro de los notebooks: un resultado que depende del orden en que ejecutaste las celdas.

Fig. — El mismo notebook de cuatro celdas sobre SINT, ejecutado en tres órdenes distintos: tres rentabilidades finales
03/06
Módulo
0.2 · Entorno de trabajo moderno
Duración
35 min
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

Dos herramientas para dos trabajos

Un proyecto de trading tiene dos tipos de código. El exploratorio: cargar unos datos, mirarlos, probar una idea, dibujar una curva de capital. Y el que se queda: las funciones que calculan señales, posiciones y costes, que se reutilizan, se prueban y algún día operarán en real. Cada tipo tiene su herramienta:

EditorNotebook
Para quéCódigo que se queda: src/, tests, scriptsExplorar datos, probar ideas, contar un análisis
UnidadFichero .pyCelda (código + salida)
EjecuciónDe principio a fin, siempre igualCelda a celda, en el orden que elijas
Ejemplosrc/ppt/senales.py con su testnotebooks/explorar-sint.ipynb

En la práctica se usan los dos a la vez, y los editores modernos abren también notebooks. Lo que importa es que los dos usen el mismo entorno del proyecto (.venv, lección 0.2.2): si el notebook importa pandas del Python del sistema y tus tests del entorno del proyecto, no estás probando lo mismo que exploras.

El editor: VS Code, Cursor o PyCharm

Cualquiera de los tres sirve para todo el curso. Las diferencias están en el modelo de licencia y en cómo integran la IA (lección 0.2.6):

EditorQuién y cómoNotebooksIA
VS CodeMicrosoft; gratuito; extensiones para Python y Jupyter.ipynb con la extensión de JupyterGitHub Copilot, con un plan gratuito limitado
CursorBasado en el código de VS Code; plan gratuito con IA limitada y planes de pagoLos de VS CodeEs el centro del producto: chat, autocompletado y agente
PyCharmJetBrains; desde 2025.1, un único producto con las funciones básicas gratis y una suscripción Pro.ipynb integrado, también en la parte gratuitaAgentes (Junie, Claude, Codex) con suscripción de JetBrains o con tus claves

Si no sabes cuál elegir, empieza por VS Code: es gratuito, lo usa la mayoría de tutoriales y lo que aprendas te sirve en Cursor, que comparte su interfaz1Nota 1 · Fuentes de la tabla, consultadas el 21 de octubre de 2026: Cursor Docs, «VS Code Migration» («Cursor is based upon the VS Code codebase») y su página de precios (plan gratuito con uso limitado de IA); GitHub Docs, planes de Copilot (Copilot Free); JetBrains, «Unified PyCharm overview» (funciones básicas gratuitas, Jupyter incluido, desde 2025.1).. PyCharm trae de fábrica más herramientas de refactorización y depuración, a cambio de una interfaz más cargada.

Sea cual sea, hay que decirle qué intérprete usar. Con el proyecto creado en la lección 0.2.2, abre la carpeta ppt-curso (no un fichero suelto) y elige el Python de .venv:

  • VS Code y Cursor: paleta de comandos (Ctrl+Mayús+P o Cmd+Mayús+P), «Python: Select Interpreter» y la entrada que apunta a .venv. Suele aparecer la primera, marcada como recomendada.
  • PyCharm: suele detectar el pyproject.toml y el .venv al abrir la carpeta; si no, en la configuración del proyecto, «Python Interpreter», añade el intérprete existente de .venv.

Para comprobarlo, crea un fichero con estas dos líneas y ejecútalo desde el editor. La salida debe mencionar .venv:

IN [01]comprobar_entorno.pypython
import sys
print(sys.prefix)

Jupyter, JupyterLab o marimo

Un notebook mezcla código, salidas, gráficos y texto en un mismo documento, ejecutado celda a celda contra un proceso de Python que se mantiene vivo (el kernel). Es la forma natural de explorar: cargas cinco años de precios una vez y pruebas veinte variantes sin volver a leer el fichero.

Jupyter Notebook / JupyterLabmarimo
Formato del fichero.ipynb (JSON con código y salidas).py (Python normal)
Orden de ejecuciónEl que elijas, celda a celdaReactivo: al cambiar una celda se ejecutan las que dependen de ella
En GitDiferencias ruidosas; las salidas viajan dentro del ficheroDiferencias limpias, como cualquier .py
Como scriptHay que convertirlouv run notebook.py
EcosistemaEl estándar: lo abren VS Code, PyCharm, Colab…Más joven; editor propio en el navegador

JupyterLab es la interfaz moderna de Jupyter (pestañas, explorador de ficheros, terminal); el Jupyter Notebook clásico es la versión mínima. Los dos usan el mismo formato. Para arrancarlos con el entorno del proyecto:

uv add --dev ipykernel                   # para notebooks en VS Code o PyCharm
uv run --with jupyter jupyter lab        # JupyterLab en el navegador
uv add --dev marimo
uv run marimo edit notebooks/explorar.py # marimo

uv run --with jupyter añade Jupyter solo para esa ejecución, sin meterlo en las dependencias del proyecto. Así el notebook ve exactamente las librerías del lock.

El estado oculto

En un notebook clásico, el kernel recuerda todo lo que has ejecutado, en el orden en que lo ejecutaste. El documento muestra otra cosa: el código en el orden en que está escrito. Cuando las dos cosas no coinciden, el notebook miente sin que lo parezca.

Un ejemplo con el error más caro del trading sistemático, el desfase de la señal (lección 0.1.1). Este notebook de cuatro celdas calcula una estrategia que está comprada cuando el cierre de SINT supera su media de 50 sesiones. La celda 3 desplaza la posición un día, para operar al día siguiente de la señal. Para simular el kernel, cada celda es un texto que se ejecuta con exec sobre un diccionario compartido; no hace falta entender exec ahora, solo que hace lo mismo que pulsar «ejecutar» en una celda:

IN [02]python
import pandas as pd

celdas = {
    1: """precios = pd.read_csv("sint_diario.csv",
                      parse_dates=["fecha"],
                      index_col="fecha")["cierre"]""",
    2: """media = precios.rolling(50).mean()
posicion = (precios > media).astype(int)""",
    3: "posicion = posicion.shift(1).fillna(0)",
    4: """rent = precios.pct_change().fillna(0)
resultado = (1 + posicion * rent).prod() - 1""",
}

def ejecutar(orden: list[int]) -> float:
    kernel = {"pd": pd}          # memoria del kernel, vacía
    for n in orden:
        exec(celdas[n], kernel)
    return kernel["resultado"]

Ahora, el mismo notebook ejecutado en tres órdenes que en la vida real pasan sin que nadie se dé cuenta: de arriba abajo; de arriba abajo y, después, otra vez las dos últimas celdas (para «actualizar el resultado»); y saltándose la celda 3 porque se ejecutó la 4 directamente tras retocar la 2.

IN [03]python
for orden in ([1, 2, 3, 4], [1, 2, 3, 4, 3, 4], [1, 2, 4]):
    print(f"{str(orden):<20} {ejecutar(orden):+.1%}")
OUT [03]Tres órdenes, tres resultados
[1, 2, 3, 4]         -2.3%
[1, 2, 3, 4, 3, 4]   -6.9%
[1, 2, 4]            +280.8%

El código es idéntico en los tres casos; la figura de la cabecera dibuja los tres resultados. Al repetir la celda 3, la posición queda desplazada dos días y el resultado empeora. Al saltársela, la estrategia opera con la información del mismo cierre que pretende anticipar: sesgo de anticipación, y una rentabilidad que no existe. En un notebook real, la salida de la celda 4 mostraría cualquiera de los tres números, y nada en la pantalla diría cuál.

Otras formas de estado oculto, igual de comunes:

  • Variables huérfanas. Borras la celda que definía coste = 0.001, pero la variable sigue en memoria y todo funciona... hasta que otra persona abre el notebook.
  • Datos modificados en el sitio. Una celda hace df["cierre"] = df["cierre"] / 2 para ajustar un split. Cada ejecución vuelve a dividir.
  • Imports que cambian. Editas una función en src/ y el kernel sigue usando la versión vieja hasta que lo reinicias.

La defensa es una regla: antes de creerte un número de un notebook, reinicia el kernel y ejecuta todo («Restart Kernel and Run All Cells» en JupyterLab, «Restart» y «Run All» en VS Code). Es exactamente lo que hace ejecutar con [1, 2, 3, 4]: memoria vacía y celdas en orden. Si el número cambia, el que tenías era falso.

Leer el orden en un .ipynb

Un .ipynb es un JSON. Cada celda de código guarda su execution_count: el número que ves como In [7]. Si los contadores no crecen de arriba abajo, las celdas no se ejecutaron en una sola pasada. Este es un notebook mínimo, recortado a lo esencial, con un orden sospechoso:

IN [04]python
import json

nb = json.loads("""{"cells": [
  {"cell_type": "markdown", "source": ["# SINT"]},
  {"cell_type": "code", "execution_count": 1, "source": ["..."]},
  {"cell_type": "code", "execution_count": 2, "source": ["..."]},
  {"cell_type": "code", "execution_count": 7, "source": ["..."]},
  {"cell_type": "code", "execution_count": 4, "source": ["..."]}
]}""")

contadores = [c["execution_count"] for c in nb["cells"]
              if c["cell_type"] == "code"]
en_orden = contadores == list(range(1, len(contadores) + 1))
print(contadores, "en orden" if en_orden else "fuera de orden")
OUT [04]Contadores de ejecución de un .ipynb
[1, 2, 7, 4] fuera de orden

Con json.load(open("explorar.ipynb")) harías lo mismo con un notebook real. Es una comprobación barata antes de compartir un resultado o de hacer commit de un notebook.

marimo resuelve el problema de raíz: analiza qué variables define y usa cada celda y construye un grafo. Cuando cambias una celda, ejecuta (o marca como desactualizadas) todas las que dependen de ella. Para el ejemplo anterior, el grafo es una cadena:

preciosmedia, posicionshift de posicionresultado
Fuente Mermaid
flowchart TB
  C1[precios] --> C2[media, posicion]
  C2 --> C3[shift de posicion]
  C3 --> C4[resultado]

Ese grafo exige una cosa: cada variable se define en una sola celda. La celda 3 (posicion = posicion.shift(1)) no es válida en marimo, porque redefine una variable de otra celda; hay que escribir posicion_ejecutada = posicion.shift(1). Es una restricción, y es justo la que impide el error de la figura.

Limpiar las salidas tiene una segunda ventaja: las diferencias que muestra Git (lección 0.2.5) se reducen al código. En JupyterLab, «Edit», «Clear Outputs of All Cells» antes de guardar; en VS Code, el botón «Clear All Outputs» de la barra del notebook. Un notebook que se va a compartir se guarda, además, recién ejecutado de principio a fin: así sus contadores van en orden y sus salidas corresponden al código que se ve.

Cuándo pasar el código a src/

El notebook es el borrador. Cuando una función cumple alguna de estas condiciones, sale del notebook a un fichero de src/ppt/:

  • Se reutiliza: la usas en un segundo notebook. Copiarla es crear dos versiones que divergirán.
  • Necesita un test: cualquier cálculo del que dependa una decisión de dinero (señales, posiciones, costes, tamaño de la posición). Los tests se escriben contra src/, no contra un notebook (lección 0.4.5).
  • Va a producción: el sistema que opera en real no ejecuta notebooks.

En el ejemplo, la lógica de las celdas 2 a 4 merece salir. Como función, el desfase ya no depende de cuántas veces ejecutes algo:

IN [05]python
def rentabilidad_media_movil(precios: pd.Series,
                             ventana: int = 50) -> float:
    """Rentabilidad total de estar comprado sobre la media móvil.

    La señal del cierre se opera en la sesión siguiente.
    """
    media = precios.rolling(ventana).mean()
    senal = (precios > media).astype(int)
    posicion = senal.shift(1).fillna(0)
    rent = precios.pct_change().fillna(0)
    return float((1 + posicion * rent).prod() - 1)

Desde el notebook se importa en lugar de copiarse. Aquí la función ya está definida en la celda anterior; en tu proyecto, la primera línea sería el import:

IN [06]python
# en tu notebook: from ppt.backtest import rentabilidad_media_movil
precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
                      index_col="fecha")["cierre"]
for ventana in (20, 50, 100):
    r = rentabilidad_media_movil(precios, ventana)
    print(f"Media de {ventana:>3} sesiones: {r:+.1%}")
OUT [06]El notebook usa la función
Media de  20 sesiones: +34.7%
Media de  50 sesiones: -2.3%
Media de 100 sesiones: -17.0%

Con 50 sesiones sale el −2,3 % del notebook ejecutado en orden: la función y el notebook correcto dicen lo mismo. Que la media de 20 dé un +34,7 % no la convierte en la buena: elegir el parámetro que mejor salió en el pasado es sobreajuste, y el nivel 2 explica cómo evitarlo.

Para que el kernel recoja los cambios que hagas en src/ sin reiniciarlo, Jupyter tiene una extensión de IPython que recarga los módulos antes de cada celda. Se activa en la primera celda del notebook:

IN [07]python
%load_ext autoreload
%autoreload 2

Ejercicios

  1. Instala un editor, ábrelo sobre la carpeta ppt-curso de la lección 0.2.2 y selecciona el intérprete de .venv. Crea notebooks/explorar-sint.ipynb, carga sint_diario.csv y dibuja el cierre. ¿Cómo compruebas, desde el propio notebook, que el kernel es el del proyecto?
Solución

Con una celda que imprima sys.prefix: debe terminar en ppt-curso/.venv. Otra forma: import pandas; pandas.__version__ debe dar la versión de uv.lock. Si el notebook dice que no encuentra pandas, el kernel es otro Python: vuelve a elegir el intérprete (en VS Code, el selector de kernel arriba a la derecha del notebook).

  1. Con la función ejecutar de la lección, calcula el resultado del orden [1, 2, 3, 3, 3, 4]. Antes de ejecutarlo, predice si será mejor o peor que el de [1, 2, 3, 4] y por qué.
Solución
IN [08]python
print(f"{ejecutar([1, 2, 3, 3, 3, 4]):+.1%}")
OUT [08]Tres desfases
-17.5%

La posición queda desplazada tres días: la estrategia entra y sale tres sesiones tarde de cada movimiento. Aquí empeora (−17,5 % frente a −2,3 %), pero no hay una regla general: con otros datos podría mejorar. Lo importante es que no es la estrategia que creías estar probando.

  1. Reescribe las cuatro celdas del ejemplo para que sean válidas en marimo (cada variable definida en una sola celda). ¿Qué ha cambiado en la celda 3?
Solución

Celda 1: precios = …. Celda 2: media = precios.rolling(50).mean() y senal = (precios > media).astype(int). Celda 3: posicion = senal.shift(1).fillna(0), que ahora define una variable nueva a partir de otra en lugar de modificar la existente. Celda 4: rent = … y resultado = …. Al no redefinir nada, ejecutar una celda dos veces da siempre lo mismo: el notebook es idempotente, igual que la función de src/.

Footnotes

  1. Fuentes de la tabla, consultadas el 21 de octubre de 2026: Cursor Docs, «VS Code Migration» («Cursor is based upon the VS Code codebase») y su página de precios (plan gratuito con uso limitado de IA); GitHub Docs, planes de Copilot (Copilot Free); JetBrains, «Unified PyCharm overview» (funciones básicas gratuitas, Jupyter incluido, desde 2025.1). ↩

Respuesta plegada en cada pregunta

Autoevaluación

  1. Ejecutas por segunda vez una celda que hace posicion = posicion.shift(1). ¿Qué le pasa al backtest?

    • ANada: ejecutar una celda dos veces da lo mismo que una
    • BLa posición queda desplazada dos días y el resultado cambia, aunque el código del notebook sea el mismo
    • CPython da un error porque posicion ya existe
    • DEl notebook se reinicia solo
    Ver respuesta

    Respuesta B. La posición queda desplazada dos días y el resultado cambia, aunque el código del notebook sea el mismo

    La celda modifica una variable que ya existe en memoria: cada ejecución la desplaza un día más. El código que ves no ha cambiado, pero el estado sí. Es el estado oculto.

  2. ¿Qué hace marimo cuando cambias una celda de la que dependen otras?

    • ANada; hay que volver a ejecutar a mano las celdas siguientes
    • BVuelve a ejecutar las celdas que dependen de ella (o las marca como desactualizadas), así que el estado siempre coincide con el código
    • CBorra todas las variables
    • DGuarda el notebook en formato .ipynb
    Ver respuesta

    Respuesta B. Vuelve a ejecutar las celdas que dependen de ella (o las marca como desactualizadas), así que el estado siempre coincide con el código

    marimo analiza qué variables define y usa cada celda y construye un grafo de dependencias. Por eso no permite definir la misma variable en dos celdas: rompería el grafo.

  3. ¿Cuál de estos motivos NO es una buena razón para mover una función del notebook a src/?

    • ALa usas en dos notebooks distintos
    • BNecesita tests
    • CVa a ejecutarse en el sistema que opera en real
    • DOcupa más de tres líneas
    Ver respuesta

    Respuesta D. Ocupa más de tres líneas

    El tamaño no es el criterio. Lo es la reutilización, la necesidad de probarla y que vaya a producción. Una función de tres líneas que calcula el tamaño de la posición debe estar en src/ con su test.

  4. Un .ipynb tiene celdas con contadores de ejecución 1, 2, 7, 4. ¿Qué indica?

    • AQue el notebook tiene cuatro celdas con errores
    • BQue las celdas no se ejecutaron de arriba abajo en una sola pasada: las salidas pueden no corresponder al código tal como está
    • CQue se ejecutó con cuatro núcleos
    • DNada: los contadores son aleatorios
    Ver respuesta

    Respuesta B. Que las celdas no se ejecutaron de arriba abajo en una sola pasada: las salidas pueden no corresponder al código tal como está

    El contador indica el orden en que se ejecutó cada celda en la sesión. Si no crece de arriba abajo, alguien ejecutó celdas fuera de orden o repitió alguna. Reinicia y ejecuta todo antes de creerte el resultado.

Ideas clave

  • Editor para el código que se queda (src/, tests); notebook para explorar. Los dos, con el intérprete de .venv del proyecto.
  • Un notebook clásico guarda estado en memoria: el resultado depende del orden en que ejecutaste las celdas, no del orden en que están escritas.
  • Antes de creerte un número de un notebook: reiniciar el kernel y ejecutar todo de arriba abajo.
  • marimo evita el estado oculto con un grafo de dependencias y guarda los notebooks como .py, legibles en Git.
  • Lo que se reutiliza, se prueba o va a producción sale del notebook a src/, con su test.

Recursos