---
title: "0.2.3 Editores y notebooks: VS Code, Jupyter y marimo"
description: "Qué editor elegir (VS Code, Cursor o PyCharm), cuándo usar Jupyter, JupyterLab o marimo, el estado oculto de los notebooks y cuándo pasar el código a src/."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/editores-y-notebooks-jupyter-marimo/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/editores-y-notebooks-jupyter-marimo.png"
---

# 0.2.3 · 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.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.2 · Entorno de trabajo moderno
- Duración: 35 min
- Requisitos: [0.2.2 · Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles](https://pythonparatrading.com/curso-python-trading/nivel-0/entornos-virtuales-y-dependencias-con-uv/)

## Al terminar esta lección

- Elegir un editor y un tipo de notebook según el trabajo, y conectarlos al entorno virtual del proyecto creado con uv.
- Explicar qué es el estado oculto de un notebook y reproducir cómo el orden de ejecución cambia el resultado de un backtest.
- Comprobar si un notebook se ejecutó en orden y volver a ejecutarlo de principio a fin antes de fiarse de un número.
- Decidir cuándo una función debe salir del notebook y pasar a `src/` con sus tests.

## 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 interfaz[^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`:

```python skip title="comprobar_entorno.py"
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:

```bash
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:

```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.

```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%}")
```
```output
[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:

```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")
```
```output
[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:

```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.

<aside class="callout" data-kind="aviso">

Un `.ipynb` guarda las salidas dentro del fichero: tablas, gráficos y cualquier cosa que imprimiste. Si una celda
mostró una clave de API, un saldo de cuenta o datos con licencia, eso viaja con el notebook a Git. Limpia las salidas
antes del commit o no imprimas nunca nada sensible (lección 0.2.4).

</aside>

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:

```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:

```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%}")
```
```output
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:

```python skip
%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).

2. 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.**

```python
print(f"{ejecutar([1, 2, 3, 3, 3, 4]):+.1%}")
```
```output
-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.

3. 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/`.

[^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).

## Autoevaluación

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

   - A. Nada: ejecutar una celda dos veces da lo mismo que una
   - B. La posición queda desplazada dos días y el resultado cambia, aunque el código del notebook sea el mismo
   - C. Python da un error porque `posicion` ya existe
   - D. El notebook se reinicia solo

   *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?

   - A. Nada; hay que volver a ejecutar a mano las celdas siguientes
   - 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
   - C. Borra todas las variables
   - D. Guarda el notebook en formato `.ipynb`

   *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/`?

   - A. La usas en dos notebooks distintos
   - B. Necesita tests
   - C. Va a ejecutarse en el sistema que opera en real
   - D. Ocupa más de tres líneas

   *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?

   - A. Que el notebook tiene cuatro celdas con errores
   - 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á
   - C. Que se ejecutó con cuatro núcleos
   - D. Nada: los contadores son aleatorios

   *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

### Research

- [El entorno de trabajo - 1](https://pythonparatrading.com/docker-para-trading/) · Research-0013 · 4 min

### Documentación

- [VS Code: Python en Visual Studio Code](https://code.visualstudio.com/docs/python/python-tutorial) · code.visualstudio.com
- [VS Code: notebooks de Jupyter](https://code.visualstudio.com/docs/datascience/jupyter-notebooks) · code.visualstudio.com
- [PyCharm unificado: qué es gratuito y qué es Pro](https://www.jetbrains.com/help/pycharm/unified-pycharm.html) · jetbrains.com
- [uv: usar Jupyter en un proyecto](https://docs.astral.sh/uv/guides/integration/jupyter/) · docs.astral.sh
- [uv: usar marimo](https://docs.astral.sh/uv/guides/integration/marimo/) · docs.astral.sh

Lección anterior: [0.2.2 · Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles](https://pythonparatrading.com/curso-python-trading/nivel-0/entornos-virtuales-y-dependencias-con-uv/)

Lección siguiente: [0.2.4 · Terminal básica, rutas, variables de entorno y ficheros .env](https://pythonparatrading.com/curso-python-trading/nivel-0/terminal-rutas-y-variables-de-entorno/)
