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.
- Módulo
- 0.2 · Entorno de trabajo moderno
- Duración
- 35 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
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:
| Editor | Notebook | |
|---|---|---|
| Para qué | Código que se queda: src/, tests, scripts | Explorar datos, probar ideas, contar un análisis |
| Unidad | Fichero .py | Celda (código + salida) |
| Ejecución | De principio a fin, siempre igual | Celda a celda, en el orden que elijas |
| Ejemplo | src/ppt/senales.py con su test | notebooks/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):
| Editor | Quién y cómo | Notebooks | IA |
|---|---|---|---|
| VS Code | Microsoft; gratuito; extensiones para Python y Jupyter | .ipynb con la extensión de Jupyter | GitHub Copilot, con un plan gratuito limitado |
| Cursor | Basado en el código de VS Code; plan gratuito con IA limitada y planes de pago | Los de VS Code | Es el centro del producto: chat, autocompletado y agente |
| PyCharm | JetBrains; desde 2025.1, un único producto con las funciones básicas gratis y una suscripción Pro | .ipynb integrado, también en la parte gratuita | Agentes (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.tomly el.venval 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:
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 / JupyterLab | marimo | |
|---|---|---|
| Formato del fichero | .ipynb (JSON con código y salidas) | .py (Python normal) |
| Orden de ejecución | El que elijas, celda a celda | Reactivo: al cambiar una celda se ejecutan las que dependen de ella |
| En Git | Diferencias ruidosas; las salidas viajan dentro del fichero | Diferencias limpias, como cualquier .py |
| Como script | Hay que convertirlo | uv run notebook.py |
| Ecosistema | El 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 # marimouv 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:
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.
for orden in ([1, 2, 3, 4], [1, 2, 3, 4, 3, 4], [1, 2, 4]):
print(f"{str(orden):<20} {ejecutar(orden):+.1%}")[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"] / 2para 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:
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")[1, 2, 7, 4] fuera de ordenCon 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:
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:
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:
# 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%}")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:
%load_ext autoreload
%autoreload 2Ejercicios
- Instala un editor, ábrelo sobre la carpeta
ppt-cursode la lección 0.2.2 y selecciona el intérprete de.venv. Creanotebooks/explorar-sint.ipynb, cargasint_diario.csvy 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).
- Con la función
ejecutarde 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
print(f"{ejecutar([1, 2, 3, 3, 3, 4]):+.1%}")-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.
- 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
-
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
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
posicionya 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.
¿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.
¿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.Un
.ipynbtiene 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.venvdel 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.