Lesson 0.2.3 · 35 min

Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)

The editor is where you write the system; the notebook, where you explore it. This lesson picks one of each, connects them to the project environment and shows the most expensive notebook mistake: a result that depends on the order in which you ran the cells.

Fig. — The same four-cell notebook on SINT, run in three different orders: three final returns
03/06
Module
0.2 · Modern working environment
Duration
35 min
Updated
Level 0 lessons
  1. 0.1 · Welcome and expectations

    1. 0.1.1 Systematic, algorithmic and quantitative trading versus discretionary trading
    2. 0.1.2 Realistic expectations: costs, time and risk of loss
    3. 0.1.3 Course structure, entry tests, personal repository and learning with AI
  2. 0.2 · Modern working environment

    1. 0.2.1 Installing Python with uv: versions and support cycle
    2. 0.2.2 Virtual environments and dependencies: uv init, uv add, pyproject.toml and lockfiles
    3. 0.2.3 Editors (VS Code, Cursor, PyCharm) and notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Basic terminal, paths, environment variables and .env files
    5. 0.2.5 Git and GitHub: commits, branches, pull requests and .gitignore
    6. 0.2.6 Introduction to AI coding assistants: prompts, review and tests
  3. 0.3 · Language fundamentals

    1. 0.3.1 Variables, types and operators; floating point and decimal for prices and ticks
    2. 0.3.2 Strings, f-strings and formatting numbers and dates
    3. 0.3.3 Lists, tuples, dictionaries, sets and comprehensions
    4. 0.3.4 Control flow: if, elif, else, for, while and match-case
    5. 0.3.5 Functions: arguments, *args and **kwargs, lambdas and scope
    6. 0.3.6 Exceptions; modules and packages, with Python 3.15 lazy imports
  4. 0.4 · Intermediate Python

    1. 0.4.1 Object-oriented programming: classes, inheritance, composition and dataclasses
    2. 0.4.2 Typing: type hints, mypy or ty, and Pydantic for configurations
    3. 0.4.3 Iterators, generators, decorators and context managers
    4. 0.4.4 Files (CSV, JSON, YAML, TOML) and dates with time zones
    5. 0.4.5 Professional logging and testing with pytest
    6. 0.4.6 Concurrency: threading, multiprocessing, asyncio and free-threading
  5. 0.5 · Numerical computing and data

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorization, linear algebra and random generators
    2. 0.5.2 pandas 3.0: Series, DataFrame, time indexes, Copy-on-Write, the str type and pd.col()
    3. 0.5.3 Selection, filtering, groupby, merge, pivot, resample and moving windows
    4. 0.5.4 Polars: lazy versus eager API, expressions, performance and when to pick it over pandas
    5. 0.5.5 DuckDB and essential SQL: SELECT, JOIN, GROUP BY and window functions on Parquet
  6. 0.6 · Visualisation

    1. 0.6.1 Object-oriented Matplotlib, Seaborn and Plotly: price, drawdown and candlestick charts
    2. 0.6.2 Quick dashboards with Streamlit: a monitoring dashboard in a single script
  7. 0.7 · Minimum mathematics and statistics

    1. 0.7.1 Simple and log returns, compound interest and descriptive statistics
    2. 0.7.2 Probability and distributions, correlation versus causation and simple regression
    3. 0.7.3 Intuitive linear algebra: vectors and matrices as groundwork for portfolios

Two tools for two jobs

A trading project has two kinds of code. The exploratory kind: load some data, look at it, try an idea, plot an equity curve. And the kind that stays: the functions that compute signals, positions and costs, which are reused, tested and will one day trade live. Each kind has its tool:

EditorNotebook
What forCode that stays: src/, tests, scriptsExploring data, trying ideas, telling an analysis
Unit.py fileCell (code + output)
ExecutionFrom start to finish, always the sameCell by cell, in the order you choose
Examplesrc/ppt/senales.py with its testnotebooks/explorar-sint.ipynb

In practice both are used at the same time, and modern editors also open notebooks. What matters is that both use the same environment of the project (.venv, lesson 0.2.2): if the notebook imports pandas from the system Python and your tests from the project environment, you are not testing the same thing you are exploring.

The editor: VS Code, Cursor or PyCharm

Any of the three works for the whole course. The differences are in the licensing model and in how they integrate AI (lesson 0.2.6):

EditorWho and howNotebooksAI
VS CodeMicrosoft; free; extensions for Python and Jupyter.ipynb with the Jupyter extensionGitHub Copilot, with a limited free plan
CursorBased on the VS Code codebase; free plan with limited AI and paid plansThose of VS CodeIt is the core of the product: chat, autocomplete and agent
PyCharmJetBrains; since 2025.1, a single product with the basic features free and a Pro subscriptionBuilt-in .ipynb, also in the free partAgents (Junie, Claude, Codex) with a JetBrains subscription or with your own keys

If you do not know which one to choose, start with VS Code: it is free, most tutorials use it and what you learn carries over to Cursor, which shares its interface1Note 1 · Sources for the table, accessed on October 21, 2026: Cursor Docs, "VS Code Migration" ("Cursor is based upon the VS Code codebase") and its pricing page (free plan with limited AI usage); GitHub Docs, Copilot plans (Copilot Free); JetBrains, "Unified PyCharm overview" (basic features free, Jupyter included, since 2025.1).. PyCharm ships with more refactoring and debugging tools out of the box, in exchange for a busier interface.

Whichever it is, you have to tell it which interpreter to use. With the project created in lesson 0.2.2, open the ppt-curso folder (not a single file) and choose the Python in .venv:

  • VS Code and Cursor: command palette (Ctrl+Shift+P or Cmd+Shift+P), "Python: Select Interpreter" and the entry that points to .venv. It usually appears first, marked as recommended.
  • PyCharm: it usually detects the pyproject.toml and the .venv when you open the folder; if not, in the project settings, "Python Interpreter", add the existing interpreter from .venv.

To check it, create a file with these two lines and run it from the editor. The output should mention .venv:

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

Jupyter, JupyterLab or marimo

A notebook mixes code, outputs, charts and text in a single document, run cell by cell against a Python process that stays alive (the kernel). It is the natural way to explore: you load five years of prices once and try twenty variants without reading the file again.

Jupyter Notebook / JupyterLabmarimo
File format.ipynb (JSON with code and outputs).py (plain Python)
Execution orderWhatever you choose, cell by cellReactive: changing a cell runs the ones that depend on it
In GitNoisy diffs; outputs travel inside the fileClean diffs, like any .py
As a scriptIt has to be converteduv run notebook.py
EcosystemThe standard: VS Code, PyCharm, Colab… open itYounger; its own editor in the browser

JupyterLab is the modern Jupyter interface (tabs, file browser, terminal); the classic Jupyter Notebook is the minimal version. Both use the same format. To start them with the project environment:

uv add --dev ipykernel                   # for notebooks in VS Code or PyCharm
uv run --with jupyter jupyter lab        # JupyterLab in the browser
uv add --dev marimo
uv run marimo edit notebooks/explorar.py # marimo

uv run --with jupyter adds Jupyter for that run only, without putting it in the project dependencies. That way the notebook sees exactly the libraries in the lock.

Hidden state

In a classic notebook, the kernel remembers everything you have run, in the order you ran it. The document shows something else: the code in the order it is written. When the two do not match, the notebook lies without looking like it.

An example with the most expensive mistake in systematic trading, the signal lag (lesson 0.1.1). This four-cell notebook computes a strategy that is long when SINT's close is above its 50-session average. Cell 3 shifts the position one day, to trade the day after the signal. To simulate the kernel, each cell is a string run with exec on a shared dictionary; there is no need to understand exec now, only that it does the same as pressing "run" on a cell:

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}          # kernel memory, empty
    for n in orden:
        exec(celdas[n], kernel)
    return kernel["resultado"]

Now, the same notebook run in three orders that in real life go unnoticed: top to bottom; top to bottom and then the last two cells again (to "refresh the result"); and skipping cell 3 because cell 4 was run directly after tweaking cell 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]Three orders, three results
[1, 2, 3, 4]         -2.3%
[1, 2, 3, 4, 3, 4]   -6.9%
[1, 2, 4]            +280.8%

The code is identical in all three cases; the figure in the header plots the three results. Repeating cell 3 leaves the position shifted two days and the result gets worse. Skipping it, the strategy trades with information from the same close it claims to anticipate: look-ahead bias, and a return that does not exist. In a real notebook, the output of cell 4 would show any of the three numbers, and nothing on screen would tell you which.

Other forms of hidden state, just as common:

  • Orphan variables. You delete the cell that defined coste = 0.001, but the variable is still in memory and everything works... until someone else opens the notebook.
  • Data modified in place. A cell does df["cierre"] = df["cierre"] / 2 to adjust for a split. Every run divides again.
  • Imports that change. You edit a function in src/ and the kernel keeps using the old version until you restart it.

The defense is a rule: before believing a number from a notebook, restart the kernel and run everything ("Restart Kernel and Run All Cells" in JupyterLab, "Restart" and "Run All" in VS Code). It is exactly what ejecutar does with [1, 2, 3, 4]: empty memory and cells in order. If the number changes, the one you had was false.

Reading the order in an .ipynb

An .ipynb is a JSON file. Each code cell stores its execution_count: the number you see as In [7]. If the counters do not increase from top to bottom, the cells were not run in a single pass. This is a minimal notebook, trimmed to the essentials, with a suspicious order:

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, "in order" if en_orden else "out of order")
OUT [04]Execution counts of an .ipynb
[1, 2, 7, 4] out of order

With json.load(open("explorar.ipynb")) you would do the same with a real notebook. It is a cheap check before sharing a result or committing a notebook.

marimo solves the problem at the root: it analyzes which variables each cell defines and uses and builds a graph. When you change a cell, it runs (or marks as stale) all the ones that depend on it. For the previous example, the graph is a chain:

preciosmedia, posicionposicion shiftresultado
Mermaid source
flowchart TB
  C1[precios] --> C2[media, posicion]
  C2 --> C3[posicion shift]
  C3 --> C4[resultado]

That graph demands one thing: each variable is defined in a single cell. Cell 3 (posicion = posicion.shift(1)) is not valid in marimo, because it redefines a variable from another cell; you have to write posicion_ejecutada = posicion.shift(1). It is a restriction, and it is precisely the one that prevents the mistake in the figure.

Clearing outputs has a second advantage: the diffs Git shows (lesson 0.2.5) are reduced to the code. In JupyterLab, "Edit", "Clear Outputs of All Cells" before saving; in VS Code, the "Clear All Outputs" button in the notebook toolbar. A notebook that is going to be shared is also saved freshly run from start to finish: that way its counters are in order and its outputs correspond to the code you see.

When to move code to src/

The notebook is the draft. When a function meets any of these conditions, it leaves the notebook for a file in src/ppt/:

  • It is reused: you use it in a second notebook. Copying it creates two versions that will diverge.
  • It needs a test: any calculation a money decision depends on (signals, positions, costs, position sizing). Tests are written against src/, not against a notebook (lesson 0.4.5).
  • It goes to production: the system that trades live does not run notebooks.

In the example, the logic of cells 2 to 4 deserves to leave. As a function, the lag no longer depends on how many times you run something:

IN [05]python
def rentabilidad_media_movil(precios: pd.Series,
                             ventana: int = 50) -> float:
    """Total return of being long above the moving average.

    The signal from the close is traded in the next session.
    """
    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)

From the notebook it is imported instead of copied. Here the function is already defined in the previous cell; in your project, the first line would be the import:

IN [06]python
# in your 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"{ventana:>3}-session average: {r:+.1%}")
OUT [06]The notebook uses the function
 20-session average: +34.7%
 50-session average: -2.3%
100-session average: -17.0%

With 50 sessions you get the −2.3% of the notebook run in order: the function and the correct notebook say the same thing. That the 20-session average gives +34.7% does not make it the right one: choosing the parameter that did best in the past is overfitting, and level 2 explains how to avoid it.

So that the kernel picks up the changes you make in src/ without restarting it, Jupyter has an IPython extension that reloads modules before each cell. It is enabled in the first cell of the notebook:

IN [07]python
%load_ext autoreload
%autoreload 2

Exercises

  1. Install an editor, open it on the ppt-curso folder from lesson 0.2.2 and select the .venv interpreter. Create notebooks/explorar-sint.ipynb, load sint_diario.csv and plot the close. How do you check, from the notebook itself, that the kernel is the project's?
Solution

With a cell that prints sys.prefix: it should end in ppt-curso/.venv. Another way: import pandas; pandas.__version__ should give the version in uv.lock. If the notebook says it cannot find pandas, the kernel is another Python: choose the interpreter again (in VS Code, the kernel picker at the top right of the notebook).

  1. With the ejecutar function from the lesson, compute the result of the order [1, 2, 3, 3, 3, 4]. Before running it, predict whether it will be better or worse than that of [1, 2, 3, 4] and why.
Solution
IN [08]python
print(f"{ejecutar([1, 2, 3, 3, 3, 4]):+.1%}")
OUT [08]Three lags
-17.5%

The position ends up shifted three days: the strategy enters and exits every move three sessions late. Here it gets worse (−17.5% versus −2.3%), but there is no general rule: with other data it could improve. What matters is that it is not the strategy you thought you were testing.

  1. Rewrite the four cells of the example so that they are valid in marimo (each variable defined in a single cell). What has changed in cell 3?
Solution

Cell 1: precios = …. Cell 2: media = precios.rolling(50).mean() and senal = (precios > media).astype(int). Cell 3: posicion = senal.shift(1).fillna(0), which now defines a new variable from another one instead of modifying the existing one. Cell 4: rent = … and resultado = …. Since nothing is redefined, running a cell twice always gives the same result: the notebook is idempotent, just like the function in src/.

Footnotes

  1. Sources for the table, accessed on October 21, 2026: Cursor Docs, "VS Code Migration" ("Cursor is based upon the VS Code codebase") and its pricing page (free plan with limited AI usage); GitHub Docs, Copilot plans (Copilot Free); JetBrains, "Unified PyCharm overview" (basic features free, Jupyter included, since 2025.1). ↩

Each answer is folded under its question

Self-assessment

  1. You run a second time a cell that does posicion = posicion.shift(1). What happens to the backtest?

    • ANothing: running a cell twice gives the same as running it once
    • BPython raises an error because posicion already exists
    • CThe notebook restarts on its own
    • DThe position ends up shifted two days and the result changes, even though the notebook's code is the same
    Show answer

    Answer D. The position ends up shifted two days and the result changes, even though the notebook's code is the same

    The cell modifies a variable that already exists in memory: each run shifts it one more day. The code you see has not changed, but the state has. That is hidden state.

  2. What does marimo do when you change a cell that other cells depend on?

    • ANothing; you have to rerun the following cells by hand
    • BIt reruns the cells that depend on it (or marks them as stale), so the state always matches the code
    • CIt deletes all the variables
    • DIt saves the notebook in .ipynb format
    Show answer

    Answer B. It reruns the cells that depend on it (or marks them as stale), so the state always matches the code

    marimo analyzes which variables each cell defines and uses and builds a dependency graph. That is why it does not allow defining the same variable in two cells: it would break the graph.

  3. Which of these is NOT a good reason to move a function from the notebook to src/?

    • AYou use it in two different notebooks
    • BIt needs tests
    • CIt is longer than three lines
    • DIt is going to run in the system that trades live
    Show answer

    Answer C. It is longer than three lines

    Size is not the criterion. Reuse, the need to test it and going to production are. A three-line function that computes position size belongs in src/ with its test.

  4. An .ipynb has cells with execution counts 1, 2, 7, 4. What does it indicate?

    • AThat the cells were not run top to bottom in a single pass: the outputs may not correspond to the code as it stands
    • BThat the notebook has four cells with errors
    • CThat it was run with four cores
    • DNothing: the counters are random
    Show answer

    Answer A. That the cells were not run top to bottom in a single pass: the outputs may not correspond to the code as it stands

    The counter shows the order in which each cell was run in the session. If it does not increase from top to bottom, someone ran cells out of order or repeated one. Restart and run everything before believing the result.

Key points

  • Editor for the code that stays (src/, tests); notebook for exploring. Both with the project's .venv interpreter.
  • A classic notebook keeps state in memory: the result depends on the order in which you ran the cells, not on the order in which they are written.
  • Before believing a number from a notebook: restart the kernel and run everything from top to bottom.
  • marimo avoids hidden state with a dependency graph and saves notebooks as .py, readable in Git.
  • Whatever is reused, tested or goes to production leaves the notebook for src/, with its test.

Resources