---
title: "7.6.1 Capstone: requisitos comunes, rúbrica y defensa"
description: "Lo común a los cinco proyectos del capstone: plantilla de repositorio, auditor de reproducibilidad, registro de pruebas, memoria, calendario y rúbrica."
url: "https://pythonparatrading.com/curso-python-trading/nivel-7/capstone-requisitos-rubrica-y-defensa/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-7/capstone-requisitos-rubrica-y-defensa.png"
---

# 7.6.1 · Capstone: requisitos comunes, rúbrica del tribunal y preparación de la defensa

> El capstone es un proyecto de 40 a 80 horas que se defiende ante un tribunal. Esta guía reúne lo que comparten los cinco proyectos: la plantilla de repositorio, un auditor que comprueba la reproducibilidad de cualquier entrega, el registro de pruebas del que sale el Deflated Sharpe, el índice de la memoria técnica, el calendario de diez semanas, la rúbrica con su calculadora de nota y cómo preparar los veinte minutos de la defensa.

- Curso: [Nivel 7: Producción, MLOps, regulación, carrera y capstone](https://pythonparatrading.com/curso-python-trading/nivel-7/)
- Módulo: 7.6 · Capstone final
- Duración: 65 min
- Requisitos: [7.1.1 · Estructura de proyecto, configuración y calidad: del notebook al repositorio](https://pythonparatrading.com/curso-python-trading/nivel-7/estructura-de-proyecto-configuracion-y-calidad/), [7.2.1 · Docker y docker-compose: imágenes reproducibles con uv](https://pythonparatrading.com/curso-python-trading/nivel-7/docker-y-docker-compose-con-uv/), [7.2.2 · VPS, nube y latencia; CI/CD con GitHub Actions](https://pythonparatrading.com/curso-python-trading/nivel-7/vps-nube-latencia-y-ci-cd/), [7.3.1 · Registro de experimentos, modelos y datos: MLflow, DVC y una ejecución reproducida de principio a fin](https://pythonparatrading.com/curso-python-trading/nivel-7/registro-de-experimentos-modelos-y-datos/), [2.2.4 · Diario de investigación y registro de experimentos contra el data snooping](https://pythonparatrading.com/curso-python-trading/nivel-2/diario-de-investigacion-y-registro-de-experimentos/), [2.5.4 · Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)](https://pythonparatrading.com/curso-python-trading/nivel-2/deflated-sharpe-ratio-y-pbo/)

## Al terminar esta lección

- Montar el repositorio del proyecto desde una plantilla con uv, Docker, Makefile, pre-commit y CI, y explicar qué fija cada pieza.
- Auditar la reproducibilidad de un repositorio con un validador ejecutable: lockfile, imagen, comando único, semillas, datos con huella, secretos en el historial y registro de pruebas solo de añadir.
- Llevar un registro de pruebas del que salga el Deflated Sharpe con el número real de intentos y comprobar cuánto cambia si se ocultan.
- Planificar el proyecto en diez semanas con el paper trading en paralelo a la memoria, y escribir la memoria con el índice común.
- Aplicar la rúbrica del curso con su calculadora, reconocer los errores que suspenden por sí solos y preparar la defensa.

## Qué se entrega y cómo se evalúa

El capstone cierra el curso con un proyecto de 40 a 80 horas: uno de los cinco encargos C1 a C5 (guías 7.6.2 a 7.6.6),
desarrollado durante unas diez semanas y defendido ante un tribunal. Cada guía de proyecto describe el encargo, la
arquitectura, los datos de partida y una columna vertebral que ya funciona con los datos del curso. Esta reúne lo que es
igual en los cinco: cómo se monta y se audita el repositorio, cómo se registran las pruebas, qué lleva la memoria, cómo
se reparte el tiempo, cómo se puntúa y cómo se defiende.

Los requisitos comunes, tal como los fija el temario del nivel 7, son seis: un repositorio reproducible (uv y Docker),
datos documentados, validación anti-sobreajuste (walk-forward y CPCV o Deflated Sharpe), un informe de riesgos, al
menos cuatro semanas de paper trading con monitorización y una memoria técnica. Ninguno es nuevo: cada uno se enseñó en
una lección concreta, y la evaluación consiste en comprobar que el alumno los aplica juntos, en un proyecto propio y sin
que nadie le diga qué celda ejecutar.

```mermaid
flowchart TB
  P[Plantilla] --> A[Auditor en verde]
  A --> V[Validación]
  V --> D{Criterio de parada}
  D --> PT[Paper 4 semanas]
  PT --> M[Memoria]
  M --> E[Entrega]
  E --> T[Tribunal]
  T --> N[Nota]
```

La rúbrica de esta guía es **la del curso**: un criterio propio, pensado para evaluar un proyecto cuantitativo con los
mismos estándares que se exigirían en un equipo profesional, no una norma ni una certificación externas. Los pesos y
los errores que suspenden se publican antes de empezar para que nadie tenga que adivinarlos.

## La plantilla de repositorio

Todos los proyectos parten del mismo árbol. Las piezas se aprendieron en el nivel 7 (estructura y calidad en 7.1.1,
Docker en 7.2.1, CI en 7.2.2, registro de experimentos y datos en 7.3.1); aquí se juntan con los tres añadidos propios
del capstone: el manifiesto de datos, el registro de pruebas y la carpeta del diario de papel. La celda genera la
plantilla de un proyecto en una carpeta y valida cada fichero de configuración con su lector: `tomllib` para el
`pyproject.toml` y `yaml.safe_load` para compose, CI, pre-commit y la configuración. Docker no está instalado en el
entorno del curso, así que el Dockerfile y el compose se validan como texto; la imagen se construye en la CI.

```python
import hashlib
import json
import os
import re
import subprocess
import tomllib
from pathlib import Path

import yaml

IMG_PY = "python:3.14.9-slim-trixie@sha256:b347e2dc…"  # lección 7.2.1
IMG_UV = "ghcr.io/astral-sh/uv:0.13.5@sha256:32a7bb39…"

PLANTILLA = {
"pyproject.toml": """
[project]
name = "capstone"
version = "0.1.0"
requires-python = ">=3.14,<3.15"
dependencies = ["numpy>=2.5", "pandas>=3.0", "pyyaml>=6.0",
                "pydantic-settings>=2.16"]
[dependency-groups]
dev = ["pytest>=9.1", "ruff>=0.17", "pre-commit>=4.7"]
[tool.ruff]
line-length = 88
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
""",
"uv.lock": """
version = 1
requires-python = ">=3.14, <3.15"
[[package]]
name = "numpy"
version = "2.5.4"
[[package]]
name = "pandas"
version = "3.0.6"
[[package]]
name = "pyyaml"
version = "6.0.3"
[[package]]
name = "pydantic-settings"
version = "2.16.1"
""",
".python-version": "3.14\n",
"Dockerfile": f"""
FROM {IMG_PY} AS builder
COPY --from={IMG_UV} /uv /uvx /bin/
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN uv sync --locked --no-install-project --no-dev
FROM {IMG_PY}
RUN useradd --system --uid 10001 capstone
WORKDIR /app
COPY --from=builder /app/.venv /app/.venv
COPY src/ src/
COPY configs/ configs/
ENV PATH="/app/.venv/bin:$PATH" PYTHONPATH=/app/src
USER capstone
CMD ["python", "-m", "capstone.vivo"]
""",
"compose.yaml": """
services:
  sistema:
    build: .
    env_file: .env
    restart: unless-stopped
    volumes: ["./estado:/app/estado"]
  redis:
    image: redis:8.2.2-alpine
""",
"Makefile": """
.PHONY: reproduce test lint
reproduce:
\tuv run --locked python -m capstone.reproducir configs/base.yaml
test:
\tuv run --locked pytest -q
lint:
\tuv run --locked ruff check src tests
""",
".pre-commit-config.yaml": """
repos:
  - repo: local
    hooks:
      - id: ruff
        name: ruff
        entry: ruff check
        language: system
        types: [python]
      - id: registro-solo-anadir
        name: registro solo de añadir
        entry: python -m capstone.registro --comprobar
        language: system
        files: experimentos/registro_pruebas.jsonl
""",
".github/workflows/ci.yml": """
name: ci
on: [push, pull_request]
jobs:
  reproducir:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
      - uses: astral-sh/setup-uv@v7
      - run: make lint test reproduce
      - run: docker build -t capstone .
""",
".gitignore": ".env\n.venv/\nestado/\n__pycache__/\n",
".env.example": "IB_CUENTA_PAPEL=DU0000000\n",
"README.md": "# Capstone\n\nReproducir: `make reproduce`.\n",
"configs/base.yaml": "semilla: 7\ncapital: 5000000\ntau: 0.20\n",
"datos/README.md": "# Datos\n\nOrigen, licencia, coste.\n",
"datos/precios.csv": "fecha,cierre\n2025-01-02,100.0\n",
"src/capstone/__init__.py": "",
"src/capstone/reproducir.py": """
import numpy as np

def main(semilla):
    rng = np.random.default_rng(semilla)
    return rng.normal()
""",
"src/capstone/registro.py": """
import subprocess
import sys

RUTA = "experimentos/registro_pruebas.jsonl"

def comprobar():
    viejo = subprocess.run(["git", "show", f"HEAD:{RUTA}"],
                           capture_output=True, text=True).stdout
    if not open(RUTA).read().startswith(viejo):
        sys.exit("registro reescrito: solo se puede añadir")

if __name__ == "__main__" and "--comprobar" in sys.argv:
    comprobar()
""",
"tests/test_reproducir.py": """
from capstone.reproducir import main

def test_semilla():
    assert main(7) == main(7)
""",
"experimentos/registro_pruebas.jsonl": "",
"memoria/memoria.md": "# Memoria\n",
"papel/diario/.gitkeep": "",
}


def crear(raiz, ficheros):
    raiz = Path(raiz)
    for ruta, texto in ficheros.items():
        p = raiz / ruta
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(texto.lstrip("\n"))
    datos = sorted(p for p in (raiz / "datos").iterdir()
                   if p.suffix != ".md" and p.name != "MANIFEST")
    (raiz / "datos/MANIFEST").write_text("".join(
        f"{hashlib.sha256(p.read_bytes()).hexdigest()}  {p.name}\n"
        for p in datos))
    return raiz


raiz = crear("bueno", PLANTILLA)
tomllib.loads((raiz / "pyproject.toml").read_text())
for f in ("compose.yaml", ".pre-commit-config.yaml",
          ".github/workflows/ci.yml", "configs/base.yaml"):
    yaml.safe_load((raiz / f).read_text())
print(sum(p.is_file() for p in raiz.rglob("*")), "ficheros;",
      "carpetas:",
      " ".join(sorted({p.parts[1] for p in raiz.rglob("*/*")})))
```
```output
22 ficheros; carpetas: .github configs datos experimentos memoria papel src tests
```

Cada fichero tiene una función concreta, y casi todos se justifican por lo que pasa sin él:

| Fichero | Qué fija | Sin él |
|---|---|---|
| `uv.lock` y `.python-version` | versiones exactas de todo el entorno y de Python | dos máquinas instalan versiones distintas y el resultado cambia en el tercer decimal |
| `Dockerfile` | imagen con versión y resumen (`@sha256`) exactos, usuario sin privilegios, `uv sync --locked` | la imagen de mañana no es la de hoy |
| `compose.yaml` | servicios del sistema en vivo (sistema, Redis…) y el `.env` fuera de la imagen | arranques a mano, credenciales copiadas en la imagen |
| `Makefile` | un comando por tarea: `make reproduce`, `make test`, `make lint` | el tribunal tiene que adivinar cómo se ejecuta |
| `.pre-commit-config.yaml` | ruff y la comprobación del registro en cada commit | errores que llegan al repositorio |
| `.github/workflows/ci.yml` | lint, tests, reproducción y construcción de la imagen en cada push | nadie sabe si `main` funciona |
| `datos/MANIFEST` | SHA-256 de cada fichero de datos | no se puede probar que los datos son los mismos |
| `experimentos/registro_pruebas.jsonl` | cada prueba, solo de añadir | N del DSR desconocido |
| `papel/diario/` | órdenes, ejecuciones, conciliaciones y alertas de cada día | no hay evidencia de las cuatro semanas |

En el ejemplo, el `uv.lock` es un extracto con la forma del real para que el auditor tenga qué leer; en el proyecto lo
genera `uv lock` y no se edita a mano. La documentación de uv distingue dos opciones de sincronización: con `--locked`,
uv falla si el lockfile no corresponde al `pyproject.toml`; con `--frozen`, usa el lockfile sin comprobarlo. Para la
CI y la imagen, `--locked`: un lockfile desactualizado es justo lo que la CI tiene que detectar. Las imágenes van con su
resumen `@sha256` (abreviado en la celda), como en la lección 7.2.1: una etiqueta como `3.14.9-slim` se puede mover, el
resumen no. Si los datos son grandes, el manifiesto se sustituye por DVC (lección 7.3.1) y el auditor busca los ficheros
`.dvc`.

## Un auditor de reproducibilidad

La pregunta que hace el tribunal es sencilla: ¿un comando, en una máquina limpia, da las cifras de la memoria? El
auditor responde antes a todo lo que se puede comprobar sin ejecutar el proyecto. Doce reglas, cada una con el error
que evita. La lección 7.5.3 tenía un auditor parecido para publicar un portfolio; este mira lo que rompe la
reproducción y lo que la rúbrica castiga, incluido el historial de git:

```python
REG = "experimentos/registro_pruebas.jsonl"
SECRETO = re.compile(
    r"(AKIA[0-9A-Z]{16}|-----BEGIN [A-Z ]*PRIVATE KEY-----"
    r"|(?i:api[_-]?key|secret|token|password)\s*[=:]\s*"
    r"['\"]?[A-Za-z0-9/+_\-]{16,})")


def git(raiz, *args):
    return subprocess.run(["git", "-C", str(raiz), *args],
                          capture_output=True, text=True,
                          check=True).stdout


def auditar(raiz):
    raiz, r = Path(raiz), {}
    leer = lambda f: ((raiz / f).read_text()
                      if (raiz / f).exists() else "")
    py = tomllib.loads(leer("pyproject.toml") or "")
    deps = [re.split(r"[<>=!~ \[]", d)[0].lower()
            for d in py.get("project", {}).get("dependencies", [])]
    lock = leer("uv.lock")
    r["R01 lockfile con todas las dependencias"] = bool(lock) and all(
        f'name = "{d}"' in lock for d in deps)
    r["R02 versión de Python fijada"] = bool(leer(".python-version"))
    df = leer("Dockerfile")
    froms = re.findall(r"^FROM (\S+)", df, re.M)
    r["R03 imagen exacta, uv --locked y sin root"] = (
        bool(froms) and all("@sha256:" in f for f in froms)
        and "uv sync --locked" in df and "\nUSER " in df)
    r["R04 un comando: make reproduce"] = bool(
        re.search(r"^reproduce:", leer("Makefile"), re.M))
    codigo = "\n".join(p.read_text()
                       for p in (raiz / "src").rglob("*.py"))
    r["R05 semillas explícitas"] = not re.search(
        r"default_rng\(\s*\)|np\.random\.(rand|randn|normal|seed)\(",
        codigo)
    man = dict(l.split()[::-1] for l in
               leer("datos/MANIFEST").splitlines() if l.strip())
    fich = {p.name: hashlib.sha256(p.read_bytes()).hexdigest()
            for p in (raiz / "datos").glob("*")
            if p.suffix != ".md" and p.name != "MANIFEST"}
    r["R06 datos con huella"] = bool(fich) and man == fich
    seguidos = git(raiz, "ls-files").split()
    r["R07 .env fuera de git"] = (".env" in leer(".gitignore").split()
                                  and ".env" not in seguidos)
    historia = git(raiz, "log", "-p", "--all", "--format=%H")
    r["R08 sin secretos en el historial"] = not SECRETO.search(historia)
    versiones = [git(raiz, "show", f"{c}:{REG}") for c in
                 git(raiz, "log", "--format=%H", "--", REG).split()]
    r["R09 registro solo de añadir"] = all(
        nueva.startswith(vieja) for nueva, vieja
        in zip(versiones, versiones[1:]))
    ci = yaml.safe_load(leer(".github/workflows/ci.yml") or "{}")
    pasos = json.dumps(ci.get("jobs", {}))
    r["R10 la CI reproduce y construye"] = ("reproduce" in pasos
                                           and "docker build" in pasos)
    r["R11 tests"] = any((raiz / "tests").glob("test_*.py"))
    r["R12 pre-commit con ruff"] = "ruff" in leer(
        ".pre-commit-config.yaml")
    return r


def commit(raiz, mensaje, fecha):
    git(raiz, "add", "-A")
    subprocess.run(["git", "-C", str(raiz), "commit", "-q", "-m",
                    mensaje], check=True, env={
        **os.environ, "GIT_AUTHOR_DATE": fecha,
        "GIT_COMMITTER_DATE": fecha})


git(raiz, "init", "-q")
commit(raiz, "plantilla", "2026-01-05T10:00:00+00:00")
(raiz / REG).write_text('{"id": "a1", "sharpe": 0.41}\n')
commit(raiz, "primera prueba", "2026-01-12T10:00:00+00:00")
with open(raiz / REG, "a") as f:
    f.write('{"id": "b2", "sharpe": 0.63}\n')
commit(raiz, "segunda prueba", "2026-01-19T10:00:00+00:00")
res = auditar(raiz)
print(sum(res.values()), "de", len(res), "reglas en verde")
print(git(raiz, "log", "--format=%h %s").strip())
```
```output
12 de 12 reglas en verde
f515f1f segunda prueba
0b89df7 primera prueba
aec5b60 plantilla
```

La plantilla pasa las doce. Las fechas de los commits se fijan a mano en el entorno del subproceso, y la identidad del
autor sale de las variables del entorno de la lección (en el proyecto, la del alumno): con fecha e identidad fijas, los
identificadores de los commits son los mismos en cada ejecución. Ahora, un repositorio con los defectos que más se ven
en las entregas, cada uno metido en el commit en que suele aparecer:

```python
malo = dict(PLANTILLA)
malo["Dockerfile"] = ("FROM python:3.14-slim\nCOPY . /app\n"
                      "RUN pip install -r /app/requirements.txt\n"
                      'CMD ["python", "-m", "capstone.vivo"]\n')
malo["Makefile"] = "test:\n\tpytest -q\n"
malo["src/capstone/reproducir.py"] = (
    "import numpy as np\n\ndef main():\n"
    "    return np.random.default_rng().normal()\n")
malo["pyproject.toml"] = PLANTILLA["pyproject.toml"].replace(
    '"pyyaml>=6.0"', '"pyyaml>=6.0", "scipy>=1.18"')
malo.pop(".python-version")
malo[".github/workflows/ci.yml"] = (
    "name: ci\non: [push]\njobs:\n  t:\n    runs-on: ubuntu-24.04\n"
    "    steps:\n      - run: pytest\n")
malo.pop(".pre-commit-config.yaml")
rm = crear("malo", malo)
git(rm, "init", "-q")
(rm / "configs/broker.py").write_text(
    'API_KEY = "Zx81kQp0LmN4rT7vW2yB"\n')
commit(rm, "conexión al broker", "2026-01-05T10:00:00+00:00")
(rm / "configs/broker.py").unlink()            # «ya está borrada»
(rm / ".env").write_text("IB_CUENTA_PAPEL=DU1234567\n")
(rm / ".gitignore").write_text("__pycache__/\n")
(rm / REG).write_text('{"id": "a1", "sharpe": 0.41}\n'
                      '{"id": "b2", "sharpe": -0.20}\n')
commit(rm, "pruebas", "2026-01-12T10:00:00+00:00")
(rm / REG).write_text('{"id": "a1", "sharpe": 0.41}\n')  # ¡fuera!
(rm / "datos/precios.csv").write_text(
    "fecha,cierre\n2025-01-02,100.5\n")           # sin actualizar
commit(rm, "limpieza", "2026-01-19T10:00:00+00:00")
for regla, ok in auditar(rm).items():
    print("ok  " if ok else "FALLA", regla)
```
```output
FALLA R01 lockfile con todas las dependencias
FALLA R02 versión de Python fijada
FALLA R03 imagen exacta, uv --locked y sin root
FALLA R04 un comando: make reproduce
FALLA R05 semillas explícitas
FALLA R06 datos con huella
FALLA R07 .env fuera de git
FALLA R08 sin secretos en el historial
FALLA R09 registro solo de añadir
FALLA R10 la CI reproduce y construye
ok   R11 tests
FALLA R12 pre-commit con ruff
```

Once de doce fallan, y cada fallo tiene una causa concreta. La imagen usa una etiqueta que se mueve y `pip install`
de un fichero que ni siquiera existe (R03); no hay `make reproduce` (R04); la semilla no está fijada (R05); `scipy` se
añadió al `pyproject.toml` pero el lockfile no se regeneró (R01); falta la versión de Python (R02); el fichero de
precios cambió y el manifiesto no (R06); `.env` está en git (R07). Dos merecen atención aparte:

- **R08, la clave borrada.** El alumno borró `configs/broker.py` en el segundo commit, pero la clave sigue en el
  primero y cualquiera que clone el repositorio la recupera con `git log -p`. La única respuesta es tratarla como
  comprometida: revocarla en el broker, reescribir el historial (con `git filter-repo`, por ejemplo) y anotarlo en la
  memoria. El patrón del auditor es sencillo a propósito; en el proyecto conviene pasar además un detector
  especializado (gitleaks, por ejemplo) en la CI. La gestión de secretos es la de la lección 7.2.4.
- **R09, el registro reescrito.** La prueba `b2`, con un Sharpe negativo, desapareció en el último commit. El
  auditor lo ve porque cada versión del registro debe empezar por la anterior. Ocultar pruebas es uno de los errores
  que suspenden por sí solos, y el historial de git es la evidencia.

El auditor no sustituye a ejecutar `make reproduce` en una máquina limpia, que es lo que hará el tribunal: comprueba lo
que se puede comprobar sin ejecutar nada, en un segundo, y por eso conviene pasarlo en la CI y antes de cada entrega
parcial. Lo que no ve es igual de importante: una fecha del sistema escrita en el informe, una descarga de datos en
mitad del proceso, un orden de iteración que depende de un conjunto. Esos solo aparecen ejecutando dos veces y
comparando la huella del informe (guía 7.6.2).

## El registro de pruebas y el Deflated Sharpe

El registro de pruebas es el fichero que más pesa en la nota de validación, porque de él salen los dos números que el
Deflated Sharpe necesita (lección 2.5.4): cuántas pruebas se hicieron, N, y la dispersión de sus Sharpes. Cada línea es
una prueba y lleva, como mínimo:

| Campo | Contenido |
|---|---|
| `id` | identificador derivado de la configuración (un hash), para detectar repeticiones |
| `fecha` | cuándo se hizo |
| `hipotesis` | qué se esperaba y por qué, en una frase |
| `config` | parámetros completos |
| `datos` y `codigo` | huella de los datos y commit del código (lección 2.2.4) |
| `periodo` | dentro y fuera de muestra |
| `resultado` | Sharpe, número de observaciones, asimetría y curtosis; o «descartada» y por qué |

Se apunta **todo**: las variantes que no llegaron a terminar, las que se descartaron por un error, las que se probaron
en otro notebook. La regla práctica es que el registro se escribe cuando se lanza la prueba, no cuando se ve el
resultado. Para ver qué está en juego, los datos del nivel 2 traen mil «estrategias» con la respuesta conocida. Un
alumno prueba las 20 variantes de dos familias, 40 pruebas; una de ellas, `e0133`, tiene ventaja de verdad:

```python
import numpy as np
import pandas as pd
from scipy.stats import norm

est = pd.read_parquet("ruido2_estrategias.parquet").pivot(
    index="fecha", columns="estrategia", values="rendimiento")
verdad = pd.read_csv("ruido2_verdad.csv", index_col="estrategia")
probadas = verdad.index[verdad["familia"].isin(["f37", "f16"])]
x = est[probadas]
sr = x.mean() / x.std()                       # Sharpe diario


def psr(sr, umbral, t, asim, curt):           # lección 2.5.4
    den = np.sqrt(1 - asim * sr + (curt - 1) / 4 * sr**2)
    return norm.cdf((sr - umbral) * np.sqrt(t - 1) / den)


def sr0(n, var_sr):
    g = np.euler_gamma
    return np.sqrt(var_sr) * ((1 - g) * norm.ppf(1 - 1 / n)
                              + g * norm.ppf(1 - 1 / (n * np.e)))


mejor = sr.idxmax()
r, T = x[mejor], len(x)
print(f"{len(probadas)} pruebas; la mejor, {mejor}: Sharpe anual "
      f"{sr[mejor] * np.sqrt(252):.2f} (verdadero "
      f"{verdad.at[mejor, 'sharpe_real']})")
for n in (1, 5, 40):
    u = sr0(n, sr.var()) if n > 1 else 0.0
    print(f"declara N = {n:2d}: umbral {u * np.sqrt(252):.2f} anual,"
          f" DSR {psr(sr[mejor], u, T, r.skew(), r.kurt() + 3):.3f}")
segunda = sr.drop(mejor).idxmax()
print(f"la segunda, {segunda}: Sharpe anual "
      f"{sr[segunda] * np.sqrt(252):.2f} (verdadero "
      f"{verdad.at[segunda, 'sharpe_real']})")
```
```output
40 pruebas; la mejor, e0133: Sharpe anual 1.46 (verdadero 0.8)
declara N =  1: umbral 0.00 anual, DSR 0.998
declara N =  5: umbral 0.51 anual, DSR 0.968
declara N = 40: umbral 0.94 anual, DSR 0.844
la segunda, e0902: Sharpe anual 0.74 (verdadero 0.0)
```

La mejor de las 40 es `e0133`, precisamente la que tiene ventaja, con un Sharpe medido de 1,46 cuando el verdadero es
0,8: la mejor de un grupo casi siempre sale inflada, también cuando es buena. Si el alumno declara una sola prueba, su
DSR es 0,998; con cinco, 0,968; con las 40 que hizo de verdad, el máximo esperado de 40 intentos sin ventaja sube a un
Sharpe anual de 0,94 y el DSR baja a 0,844, por debajo de 0,95. Con el número honesto, el resultado no es concluyente
aunque la estrategia tenga ventaja: cuatro años de datos no la separan de lo que darían 40 intentos de ruido. La
segunda mejor, `e0902`, es ruido puro con un Sharpe medido de 0,74.

Esa es la lectura que el tribunal espera: el DSR no castiga haber probado mucho, mide cuánto respaldan los datos el
resultado después de haber probado. Ocultar 39 pruebas habría convertido un 0,844 en un 0,998, y por eso el registro es
solo de añadir (R09), se versiona con el código y se cita en la memoria con su número de líneas. Si las variantes están
muy correlacionadas (todas de una familia), su dispersión es pequeña y el umbral baja solo: no hace falta inventar un N
«efectivo» para defenderse, basta con declarar el real y enseñar la dispersión. CPCV y la probabilidad de sobreajuste
(lecciones 2.5.3 y 2.5.4) usan la matriz de rendimientos de todas las pruebas, que también se guarda.

## La memoria técnica

La memoria es el documento que el tribunal lee antes de la defensa. No es un diario del proyecto ni un tutorial: es un
informe técnico que justifica cada decisión con las cifras del informe que reproduce `make reproduce`. Su índice es
común a los cinco proyectos:

| Parte | Contenido | Extensión orientativa |
|---|---|---|
| 1. Resumen | qué se construyó, resultado principal, decisión (pasar a papel o parar) y huella del informe | 1 página |
| 2. Encargo y alcance | cliente, objetivo, qué entra y qué no, entregables | 1-2 páginas |
| 3. Datos | fuentes con licencia y coste, limpieza, calendario, sesgos tratados (supervivencia, anticipación), huellas | 3-5 páginas |
| 4. Método | señal, dimensionamiento, costes, ejecución; por qué así | 4-6 páginas |
| 5. Validación | registro de pruebas (N), walk-forward, CPCV o DSR, criterio de parada escrito antes y su resultado | 3-5 páginas |
| 6. Arquitectura | componentes, despliegue, configuración, monitorización | 2-3 páginas |
| 7. Riesgos | informe de riesgos: qué puede fallar, límites, desactivación, continuidad | 2-4 páginas |
| 8. Paper trading | diario resumido, conciliaciones, incidencias, costes medidos frente a supuestos | 3-5 páginas |
| 9. Limitaciones | qué no demuestra el proyecto y qué haría falta para operarlo con dinero | 1-2 páginas |
| 10. Reproducción | comando, versión del código, huellas de datos e informe | 1 página |
| Anexos | registro de pruebas completo, configuración, glosario | sin límite |

El resumen se escribe al final y se lee primero. La parte 9 es la que distingue una buena memoria: decir con precisión
qué no demuestra el proyecto (cuatro semanas de papel no estiman un Sharpe; los datos sintéticos validan el código, no
la estrategia) protege más que cualquier cifra brillante. Una comprobación automática evita los olvidos más
frecuentes: que falte una parte o que la huella citada no sea la del informe.

```python
PARTES = ["Resumen", "Encargo y alcance", "Datos", "Método",
          "Validación", "Arquitectura", "Riesgos", "Paper trading",
          "Limitaciones", "Reproducción"]


def revisar_memoria(texto, informe):
    huella = hashlib.sha256(informe).hexdigest()[:12]
    titulos = re.findall(r"^## (?:\d+\. )?(.+)$", texto, re.M)
    faltan = [p for p in PARTES if p not in titulos]
    citada = re.search(r"huella del informe: `([0-9a-f]{12})`", texto)
    return {"faltan": faltan,
            "huella": "coincide" if citada and citada[1] == huella
            else f"no coincide (informe {huella})"}


informe = json.dumps({"sharpe_neto": 0.837, "n_pruebas": 24},
                     sort_keys=True).encode()
borrador = "\n".join(f"## {i}. {p}\n\nTexto." for i, p in
                     enumerate(PARTES, 1) if p != "Limitaciones")
borrador += "\n\nhuella del informe: `b465af1864ec`\n"
print(revisar_memoria(borrador, informe))
```
```output
{'faltan': ['Limitaciones'], 'huella': 'no coincide (informe 3e519e92608e)'}
```

El borrador del ejemplo no tiene la parte de limitaciones y cita una huella que no es la del informe: la memoria se
escribió con un informe anterior. Es un error corriente cuando la memoria se redacta durante el papel y el informe se
regenera después (por ejemplo, al recalcular el backtest con los costes medidos). La comprobación se añade al
`Makefile` como `make memoria` y a la CI.

El informe de riesgos, uno de los requisitos comunes, es la parte 7 de la memoria y se escribe antes del paper trading,
no después. Tiene tres piezas. La primera, una tabla de riesgos específicos del proyecto (no genéricos: «el mercado
puede caer» no es un riesgo de este sistema; «el roll del crudo cae en un día de poca liquidez» sí), cada uno con su
probabilidad cualitativa, su impacto en dinero o en días de operación y la defensa que tiene el sistema. La segunda,
los límites con sus valores y de dónde salen: nocional por orden, pérdida diaria, caída desde máximos, margen usado,
exposición por sector o por contraparte. La tercera, los procedimientos: qué apaga el sistema, quién lo vuelve a
encender y qué se hace ante un fallo del broker, de los datos o de la red (lecciones 4.2.5, 7.2.4 y 7.3.3). Durante el
papel, cada incidencia se anota contra esa tabla: si ocurrió algo que no estaba en ella, la tabla estaba incompleta, y
la memoria lo dice.

## Calendario: diez semanas, de 40 a 80 horas

El proyecto cabe en unas diez semanas de calendario. Las horas de dedicación son de 40 a 80 según el proyecto y el punto
de partida; las cuatro semanas de paper trading son tiempo de calendario que corre solo, mientras el alumno atiende
alertas, concilia cada día y escribe la memoria.

| Semana | Fase | Entregable verificable | Horas |
|---|---|---|---|
| 1 | Propuesta y plantilla | proyecto elegido, alcance escrito, repositorio desde la plantilla, auditor en verde | 4-8 |
| 2 | Datos | carga, limpieza, README de datos con licencias, manifiesto, tests de datos | 5-10 |
| 3 | Estrategia y costes | señal, dimensionamiento, costes; backtest reproducible | 5-10 |
| 4 | Validación | registro de pruebas, walk-forward, CPCV o DSR, criterio de parada aplicado, informe JSON | 5-10 |
| 5 | Ejecución y despliegue | conexión a la cuenta de papel, Docker, monitorización, alertas, desactivación probada | 5-10 |
| 6-9 | Paper trading | 28 días naturales sin cambiar el código de la decisión, diario y conciliación diaria | 4-8 |
| 6-9 | Memoria | partes 2 a 7 y 9; la 8 se completa con el diario | 8-16 |
| 10 | Cierre y defensa | informe de papel, resumen, ensayo, defensa | 4-8 |
| | **Total** | | **40-80** |

La figura de la cabecera dibuja el mismo calendario. Tres reglas lo hacen funcionar:

- **El papel empieza en la semana 6 o antes.** Cuatro semanas de calendario no se comprimen. Si la ejecución no está
  lista en la semana 5, lo que se recorta es alcance (menos mercados, una regla menos), no días de papel.
- **Durante el papel no se cambia el código de la decisión.** Un error que obligue a cambiarlo se corrige, se anota y
  reinicia el contador de 28 días. Cambiar umbrales de alertas o arreglar la monitorización no lo reinicia; cambiar la
  señal, el tamaño o los límites, sí.
- **El criterio de parada de la semana 4 se respeta.** Si la validación dice que el sistema no pasa, el papel se hace
  igualmente (lo que se evalúa es el sistema, no su rentabilidad), pero la memoria lo dice y no presenta el sistema como
  operable.

Cuatro semanas son 28 días naturales y, como mínimo, 20 sesiones del mercado en que opera el sistema; en cripto (C5),
28 días de operación continua.

## La rúbrica y la calculadora de nota

La rúbrica puntúa siete criterios de 0 a 10. La nota es la media ponderada por los pesos de la tabla, y seis errores
suspenden por sí solos, sea cual sea la media. Cada guía de proyecto (7.6.2 a 7.6.6) concreta qué evidencia pide cada
criterio en ese proyecto.

| Criterio | Peso | Qué se mira |
|---|---|---|
| Reproducibilidad | 15 % | uv.lock, Docker, un comando, semillas, datos versionados |
| Datos | 15 % | origen, licencia, limpieza, sesgos de supervivencia y anticipación |
| Validación anti-sobreajuste | 20 % | walk-forward, CPCV o DSR, número de pruebas registrado |
| Riesgo y ejecución | 15 % | costes, dimensionamiento, límites, kill switch |
| Paper trading | 15 % | al menos 4 semanas, monitorización y conciliación |
| Calidad del código | 10 % | estructura, tests, lint, configuración |
| Memoria técnica y defensa | 10 % | claridad, decisiones justificadas, limitaciones, respuestas |

*Fig. — Pesos de los siete criterios de la rúbrica común del capstone (criterio propio del curso).* Barras horizontales con el peso de cada criterio: reproducibilidad 15 %, datos 15 %, validación anti-sobreajuste 20 %, destacada y la mayor, riesgo y ejecución 15 %, paper trading 15 %, calidad del código 10 % y memoria y defensa 10 %.

Errores que suspenden por sí solos:

1. **Anticipación demostrable**: el resultado usa información que no estaba disponible en la fecha de la decisión.
2. **Resultados que no se reproducen** con el comando documentado.
3. **Backtest sin costes.**
4. **Credenciales** en el repositorio o en su historial.
5. **Ocultar el número de pruebas** o seleccionar el periodo de evaluación a posteriori.
6. **Menos de cuatro semanas** de paper trading.

Las escalas de cada criterio son orientativas: 0-2, ausente o incorrecto; 3-4, presente con errores importantes; 5-6,
correcto en lo esencial; 7-8, completo y justificado; 9-10, además, con algo que el tribunal no esperaba (una
comprobación adicional, un análisis de sensibilidad que cambia una decisión, una incidencia del papel bien resuelta).
La calificación sigue la escala habitual en España: suspenso por debajo de 5, aprobado de 5 a 6,9, notable de 7 a 8,9 y
sobresaliente desde 9.

```python
PESOS = {"reproducibilidad": 0.15, "datos": 0.15, "validacion": 0.20,
         "riesgo_ejecucion": 0.15, "paper_trading": 0.15,
         "codigo": 0.10, "memoria_defensa": 0.10}
SUSPENDEN = {
    "anticipacion": "anticipación demostrable",
    "no_reproduce": "no se reproduce con el comando documentado",
    "sin_costes": "backtest sin costes",
    "credenciales": "credenciales en el repositorio o el historial",
    "pruebas_ocultas": "pruebas ocultas o periodo elegido a posteriori",
    "papel_corto": "menos de 4 semanas de paper trading"}


def nota(puntos, errores=(), dias_papel=None):
    if set(puntos) != set(PESOS):
        raise ValueError(f"faltan: {set(PESOS) - set(puntos)}")
    if not all(0 <= v <= 10 for v in puntos.values()):
        raise ValueError("las puntuaciones van de 0 a 10")
    errores = set(errores)
    if dias_papel is not None and dias_papel < 28:
        errores.add("papel_corto")
    if raros := errores - set(SUSPENDEN):
        raise ValueError(f"error desconocido: {raros}")
    n = round(sum(PESOS[k] * v for k, v in puntos.items()), 2)
    if errores:
        return n, "suspenso", sorted(SUSPENDEN[e] for e in errores)
    cal = ("suspenso" if n < 5 else "aprobado" if n < 7
           else "notable" if n < 9 else "sobresaliente")
    return n, cal, []


assert abs(sum(PESOS.values()) - 1) < 1e-12
A = dict(zip(PESOS, (8, 7, 8, 7, 7, 6, 8)))
B = dict(zip(PESOS, (9, 9, 9, 8, 8, 9, 8)))
C = dict(zip(PESOS, (6, 5, 4, 6, 7, 5, 5)))
print("A:", nota(A, dias_papel=29))
print("B:", nota(B, errores=["credenciales"], dias_papel=30))
print("C:", nota(C, dias_papel=21))
print("C con 28 días:", nota(C, dias_papel=28))
```
```output
A: (7.35, 'notable', [])
B: (8.6, 'suspenso', ['credenciales en el repositorio o el historial'])
C: (5.4, 'suspenso', ['menos de 4 semanas de paper trading'])
C con 28 días: (5.4, 'aprobado', [])
```

El alumno A obtiene un 7,35, notable: sólido en todo, algo flojo en calidad del código. El B tiene la mejor media,
8,6, y suspende: hay una credencial en el historial. El C llega a un 5,4 con un paper trading de 21 días y suspende por
los días; con los 28 días habría aprobado con la misma media. La calculadora devuelve siempre la media aunque haya un
error que suspende, para que el alumno sepa qué nota tendría al corregirlo en una segunda convocatoria; y rechaza
puntuaciones fuera de escala, criterios que faltan y errores que no están en la lista, que es lo que evita que una
rúbrica se aplique de forma distinta a dos alumnos.

La figura muestra el reparto de pesos. La validación pesa más que ningún otro criterio porque es donde fallan la
mayoría de los proyectos cuantitativos; reproducibilidad y datos pesan lo mismo que el riesgo, porque sin ellos ninguna
otra cifra se puede comprobar.

## La defensa

La defensa son 20 minutos de exposición y un turno de preguntas del tribunal. Los 20 minutos no dan para contar el
proyecto entero, ni deben: el tribunal ya leyó la memoria. Sirven para demostrar que el alumno entiende sus decisiones
y sus límites. Un reparto que funciona:

| Minutos | Contenido |
|---|---|
| 0-2 | el encargo y el resultado en una frase; la decisión (operable, no operable, por qué) |
| 2-5 | datos: de dónde salen, qué se limpió, qué sesgos se trataron |
| 5-10 | método y validación: el registro (N), walk-forward, DSR o CPCV, el criterio de parada y su resultado |
| 10-14 | ejecución y paper trading: arquitectura, incidencias reales, costes medidos frente a supuestos |
| 14-17 | riesgos y límites: qué apaga el sistema y qué no demuestra el proyecto |
| 17-20 | reproducción en directo (o grabada si no hay red): `make reproduce` y la huella del informe |

La reproducción al final no es teatro: si la huella coincide, todo lo anterior queda respaldado. Si el proyecto tarda
más de unos minutos en reproducirse, se lanza al principio de la exposición y se comprueba al final.

Las preguntas del tribunal suelen ir a los mismos sitios, y conviene llevarlas preparadas con cifras del informe:

| Criterio | Preguntas típicas |
|---|---|
| Reproducibilidad | ¿Qué pasa si ejecuto esto dentro de un año? ¿Qué no está fijado? ¿Por qué `--locked` y no `--frozen`? |
| Datos | ¿Qué licencia tienen los datos y puedes publicarlos? ¿Cómo sabes que no hay anticipación en esta columna? ¿Qué mercados o valores no están y por qué? |
| Validación | ¿Cuántas pruebas hiciste de verdad, contando las descartadas? ¿Qué resultado te habría hecho parar y cuándo lo escribiste? ¿Qué Sharpe esperas fuera de muestra y con qué error? |
| Riesgo y ejecución | ¿Cuánto cuesta ejecutar en tu backtest y cuánto en el papel? ¿Qué pasa si el broker se desconecta con una orden viva? ¿Qué apaga el sistema y quién lo vuelve a encender? |
| Paper trading | ¿Qué incidencia te enseñó más? ¿Alguna diferencia con el broker que no supieras explicar? ¿Cambiaste algo durante las cuatro semanas? |
| Código | Enséñame el test que más errores te ha evitado. ¿Qué parte no tiene tests y por qué? |
| Memoria | ¿Qué no demuestra tu proyecto? Si tuvieras el doble de tiempo, ¿qué harías primero? |

Tres consejos que salen de ver defensas: contestar con la cifra cuando la pregunta la pide («el coste medido fue de 0,7
ticks por contrato frente a 0,5 supuestos»); decir «no lo sé» y cómo se sabría, en vez de improvisar; y no defender un
resultado que el propio registro desmiente. El tribunal puntúa más alto a quien reconoce un límite con precisión que a
quien presenta un Sharpe alto sin poder explicar cuántas pruebas hay detrás.

## Ejercicios

1. Añade al auditor una regla R13 que compruebe el diario de paper trading: al menos 28 días naturales entre la primera
   y la última entrada y al menos 20 sesiones distintas. Pruébala con un diario de ejemplo de 19 sesiones.

**Solución.**

```python
def regla_papel(raiz, dias_min=28, sesiones_min=20):
    fechas = sorted(pd.to_datetime(p.stem) for p in
                    (Path(raiz) / "papel/diario").glob("*.jsonl"))
    if not fechas:
        return False, "sin diario"
    dias = (fechas[-1] - fechas[0]).days + 1
    ok = dias >= dias_min and len(fechas) >= sesiones_min
    return ok, f"{dias} días, {len(fechas)} sesiones"


sesiones = pd.bdate_range("2026-03-02", periods=19)
for d in sesiones:
    (raiz / f"papel/diario/{d.date()}.jsonl").write_text("{}\n")
print("R13:", regla_papel(raiz))
```
```output
R13: (False, '25 días, 19 sesiones')
```

El diario de ejemplo tiene 19 sesiones entre el 2 y el 26 de marzo: 25 días naturales. La regla falla por los dos
lados, y el mensaje dice cuánto falta. En el proyecto, cada fichero del diario lleva las órdenes, ejecuciones,
conciliaciones y alertas del día; la regla cuenta solo los días con fichero, así que un día sin entrada (el sistema
caído y nadie lo vio) no cuenta como sesión. Un diario con huecos es en sí una incidencia que la memoria tiene que
explicar.

2. Con las 40 pruebas del ejemplo, ¿cuántas tendría que declarar el alumno para que el DSR de la mejor bajara de 0,95?
   ¿Y si añade al registro las 20 variantes de otra familia que también probó?

**Solución.**

```python
umbral_n = next(n for n in range(2, 200)
                if psr(sr[mejor], sr0(n, sr.var()), T, r.skew(),
                       r.kurt() + 3) < 0.95)
print("el DSR baja de 0,95 al declarar", umbral_n, "pruebas")
mas = verdad.index[verdad["familia"].isin(["f37", "f16", "f02"])]
sr60 = est[mas].mean() / est[mas].std()
r60 = est[sr60.idxmax()]
d60 = psr(sr60.max(), sr0(60, sr60.var()), T, r60.skew(),
          r60.kurt() + 3)
print(f"60 pruebas: mejor {sr60.idxmax()}, DSR {d60:.3f}")
```
```output
el DSR baja de 0,95 al declarar 8 pruebas
60 pruebas: mejor e0133, DSR 0.810
```

Con la dispersión de las 40 pruebas, el DSR de `e0133` baja de 0,95 al declarar 8: el alumno solo podría presentar el
resultado como concluyente si hubiera probado siete variantes o menos. Si además añade al registro las 20 variantes de
la familia `f02`, que también probó, la mejor sigue siendo `e0133` y el DSR baja a 0,810. Cada familia que se prueba y
se descarta sube el listón del máximo esperado, y eso es justo lo que tiene que pasar.

3. Un compañero propone presentar el paper trading de las semanas 7 a 10, que fueron mejores, y no el de las semanas 6
   a 9, porque «las dos ventanas tienen cuatro semanas». ¿Qué le dirías?

**Solución.**

Que es seleccionar el periodo a posteriori, uno de los errores que suspenden por sí solos. El periodo de papel se fija
antes de empezar (en el calendario de la semana 1) y se presenta entero, con sus incidencias; si se alarga, se presenta
también lo añadido. Además, con cuatro semanas el resultado económico apenas dice nada: lo que se evalúa es que el
sistema funcionó, se vigiló y se concilió cada día, y eso se ve igual de bien en una semana mala. Escoger la ventana
buena no mejora la nota de paper trading y pone en riesgo toda la evaluación. Si la semana 6 tuvo un problema que
obligó a cambiar el código de la decisión, la regla del calendario es otra: el contador de 28 días se reinicia y se
explica en la memoria.

## Autoevaluación

1. Un alumno borró en el último commit un fichero `.env` con la clave de su broker que había subido tres semanas antes. ¿Qué dice el auditor y qué debe hacer el alumno?

   - A. Nada: el fichero ya no está en el repositorio
   - B. Solo hay que añadir `.env` al `.gitignore`
   - C. Basta con hacer el repositorio privado
   - D. El auditor encuentra la clave en el historial; la clave se considera comprometida: hay que revocarla en el broker, reescribir el historial y documentarlo. Mientras siga en el historial, la entrega suspende

   *Respuesta D: El auditor encuentra la clave en el historial; la clave se considera comprometida: hay que revocarla en el broker, reescribir el historial y documentarlo. Mientras siga en el historial, la entrega suspende.* git guarda todas las versiones: cualquiera que clone el repositorio puede recuperar la clave con `git log -p`. Borrarla en un commit nuevo no la quita. Credenciales en el repositorio o en el historial es uno de los errores que suspenden por sí solos.

2. Un alumno probó 40 variantes de dos familias y presenta la mejor, con un Sharpe anual de 1,46 en cuatro años. La memoria dice que fue la única que probó. ¿Qué cambia si declara las 40?

   - A. Su DSR pasa de 0,998 (una prueba) a 0,844 (40 pruebas): con el número real, el resultado deja de ser concluyente; y ocultarlo suspende por sí solo
   - B. Nada: el Sharpe es el mismo
   - C. El DSR sube, porque hay más datos
   - D. Solo cambia si las variantes son de familias distintas

   *Respuesta A: Su DSR pasa de 0,998 (una prueba) a 0,844 (40 pruebas): con el número real, el resultado deja de ser concluyente; y ocultarlo suspende por sí solo.* El DSR compara el Sharpe con el máximo esperado de N intentos sin ventaja. Con N = 1 ese máximo es 0; con 40 intentos de esa dispersión, un Sharpe anual de 0,94. El número de pruebas es parte del resultado.

3. ¿Por qué el paper trading de cuatro semanas corre en paralelo a la redacción de la memoria y no al final?

   - A. Porque así el alumno tiene menos trabajo
   - B. Porque cuatro semanas de calendario no se pueden comprimir: si empiezan tarde, la defensa se retrasa; y el sistema no se toca durante el papel, así que ese tiempo se dedica a escribir
   - C. Porque el broker solo abre cuentas de papel en esas fechas
   - D. Porque la memoria no necesita los resultados del papel

   *Respuesta B: Porque cuatro semanas de calendario no se pueden comprimir: si empiezan tarde, la defensa se retrasa; y el sistema no se toca durante el papel, así que ese tiempo se dedica a escribir.* Las cuatro semanas son tiempo de calendario, no de dedicación: el sistema funciona solo y el alumno atiende alertas y concilia. Empezarlas en la semana 6 deja la 10 para cerrar el informe de papel, que sí entra en la memoria, y ensayar la defensa.

4. Una entrega tiene puntuaciones altas en todo (nota ponderada 8,4), pero el backtest no descuenta costes. ¿Qué nota recibe con la rúbrica del curso?

   - A. 8,4, notable
   - B. La nota baja un punto por el error
   - C. Suspenso: un backtest sin costes es uno de los errores que suspenden por sí solos, sea cual sea la nota ponderada
   - D. Aprobado, porque la media supera el 5

   *Respuesta C: Suspenso: un backtest sin costes es uno de los errores que suspenden por sí solos, sea cual sea la nota ponderada.* Los errores que suspenden por sí solos invalidan el resultado entero: un backtest sin costes no dice nada de la estrategia que se presenta. La calculadora devuelve el suspenso y el motivo.

5. En la defensa, un miembro del tribunal pide ejecutar `make reproduce` en una máquina limpia y la huella del informe no coincide con la de la memoria. ¿Qué es lo más probable y qué falla en la rúbrica?

   - A. Un error del tribunal; no afecta a la nota
   - B. Algo del resultado depende de lo que no está fijado (una semilla, una versión sin lock, un dato que se descarga en vez de leerse de una copia versionada, una fecha del sistema en el informe): si los resultados no se reproducen con el comando documentado, la entrega suspende
   - C. Que el ordenador del tribunal es más lento
   - D. Que Docker no estaba instalado; basta con explicarlo

   *Respuesta B: Algo del resultado depende de lo que no está fijado (una semilla, una versión sin lock, un dato que se descarga en vez de leerse de una copia versionada, una fecha del sistema en el informe): si los resultados no se reproducen con el comando documentado, la entrega suspende.* Reproducir es el requisito de partida: si el comando documentado no da las mismas cifras, ninguna cifra de la memoria se puede comprobar. El auditor de esta guía detecta la mayoría de las causas antes de la entrega.

## Ideas clave

- El capstone es un encargo de 40 a 80 horas en unas diez semanas de calendario; las cuatro de paper trading corren en paralelo a la memoria y no admiten cambios en el código de la decisión.
- La plantilla fija el entorno (uv.lock y `.python-version`), la imagen (Docker con versiones exactas), el comando único (`make reproduce`), la calidad (ruff, pytest, pre-commit) y la CI; Docker y compose se validan como texto y con la CI.
- El auditor de reproducibilidad comprueba doce reglas; en el repositorio de ejemplo encuentra once fallos, entre ellos una clave borrada que sigue en el historial y un registro de pruebas reescrito.
- El registro de pruebas solo se añade, y de él salen N y la dispersión del DSR: en el ejemplo, la mejor de 40 variantes pasa de un DSR de 0,998 declarando una prueba a 0,844 declarando las 40.
- La rúbrica es del curso (criterio propio, no una norma externa): siete criterios con pesos del 10 al 20 %, nota de 0 a 10 y seis errores que suspenden por sí solos.
- La defensa son 20 minutos de exposición y las preguntas del tribunal; se prepara con el comando de reproducción, el registro y la lista de preguntas típicas.

## Recursos

### Research

- [Docker para trading](https://pythonparatrading.com/docker-para-trading/) · Research-0013 · 4 min

### Documentación

- [uv: bloqueo y sincronización del proyecto (uv.lock, --locked, --frozen)](https://docs.astral.sh/uv/concepts/projects/sync/) · docs.astral.sh
- [uv: uso con Docker](https://docs.astral.sh/uv/guides/integration/docker/) · docs.astral.sh
- [pre-commit: documentación](https://pre-commit.com/) · pre-commit.com
- [GitHub Actions: sintaxis de los workflows](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions) · docs.github.com

### Libros

- [Bailey, D. H. y López de Prado, M. (2014): The Deflated Sharpe Ratio. Journal of Portfolio Management 40(5)](https://www.davidhbailey.com/dhbpapers/deflated-sharpe.pdf) · davidhbailey.com

### Datos

- [Datos del nivel 2: rendimientos de 1.000 estrategias con la respuesta conocida (Parquet)](https://pythonparatrading.com/curso/datos/nivel-2/ruido2_estrategias.parquet) · Descarga · ruido2_estrategias.parquet

Lección anterior: [7.5.4 · Certificaciones, formación y prop firms: qué certifica cada cosa y qué mide un examen](https://pythonparatrading.com/curso-python-trading/nivel-7/certificaciones-formacion-y-prop-firms/)

Lección siguiente: [7.6.2 · Capstone C1: un sistema de futuros multi-activo, de la previsión a la cuenta de papel](https://pythonparatrading.com/curso-python-trading/nivel-7/capstone-c1-futuros-multiactivo-nautilustrader/)
