Lección 7.6.1 · 65 min
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.
- 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, 7.2.1 · Docker y docker-compose: imágenes reproducibles con uv, 7.2.2 · VPS, nube y latencia; CI/CD con GitHub Actions, 7.3.1 · Registro de experimentos, modelos y datos: MLflow, DVC y una ejecución reproducida de principio a fin, 2.2.4 · Diario de investigación y registro de experimentos contra el data snooping, 2.5.4 · Deflated Sharpe Ratio y Probability of Backtest Overfitting (PBO)
Lecciones del nivel 7
7.1 · Ingeniería de software para trading
7.2 · Infraestructura y despliegue
7.3 · MLOps para modelos de trading
- 7.3.1 Registro de experimentos, modelos y datos: MLflow, DVC y una ejecución reproducida de principio a fin
- 7.3.2 Reentrenamiento programado, validación automática y champion/challenger
- 7.3.3 Monitorización del modelo en vivo y desactivación automática
- 7.3.4 Servir modelos: ONNX, inferencia en CPU o GPU y el coste de los LLM en producción
7.4 · Regulación, ética y fiscalidad
- 7.4.1 MiFID II: clientes, idoneidad y trading algorítmico
- 7.4.2 ESMA y CFD: apalancamiento, cierre y saldo negativo
- 7.4.3 PRIIPs, KID y ETFs estadounidenses
- 7.4.4 MiCA después del transitorio: comprobar un proveedor en los registros
- 7.4.5 Abuso de mercado: spoofing, layering y el bot que los imita sin querer
- 7.4.6 Fiscalidad del trading en España
- 7.4.7 Ética: datos personales, inteligencia artificial y la venta de señales
7.5 · Carrera profesional quant
7.6 · Capstone final
- 7.6.1 Capstone: requisitos comunes, rúbrica del tribunal y preparación de la defensa
- 7.6.2 Capstone C1: un sistema de futuros multi-activo, de la previsión a la cuenta de papel
- 7.6.3 Capstone C2: libro de opciones sistemático, superficie de volatilidad y griegas en tiempo real
- 7.6.4 Capstone C3: cartera de acciones y ETFs con ML, HRP y fiscalidad española
- 7.6.5 Capstone C4: una plataforma de investigación con LLMs y datos alternativos
- 7.6.6 Capstone C5: market making o arbitraje en cripto con control de contraparte
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.
Fuente 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.
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("*/*")})))22 ficheros; carpetas: .github configs datos experimentos memoria papel src testsCada 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:
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())12 de 12 reglas en verde
f515f1f segunda prueba
0b89df7 primera prueba
aec5b60 plantillaLa 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:
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)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 ruffOnce 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.pyen el segundo commit, pero la clave sigue en el primero y cualquiera que clone el repositorio la recupera congit log -p. La única respuesta es tratarla como comprometida: revocarla en el broker, reescribir el historial (congit 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:
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']})")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.
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)){'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 |
Errores que suspenden por sí solos:
- Anticipación demostrable: el resultado usa información que no estaba disponible en la fecha de la decisión.
- Resultados que no se reproducen con el comando documentado.
- Backtest sin costes.
- Credenciales en el repositorio o en su historial.
- Ocultar el número de pruebas o seleccionar el periodo de evaluación a posteriori.
- 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.
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))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
- 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
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))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.
- 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
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}")el DSR baja de 0,95 al declarar 8 pruebas
60 pruebas: mejor e0133, DSR 0.810Con 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.
- 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.
Respuesta plegada en cada pregunta
Autoevaluación
Un alumno borró en el último commit un fichero
.envcon la clave de su broker que había subido tres semanas antes. ¿Qué dice el auditor y qué debe hacer el alumno?- ANada: el fichero ya no está en el repositorio
- BSolo hay que añadir
.enval.gitignore - CBasta con hacer el repositorio privado
- DEl 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
Ver respuesta
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.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?
- ASu 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
- BNada: el Sharpe es el mismo
- CEl DSR sube, porque hay más datos
- DSolo cambia si las variantes son de familias distintas
Ver respuesta
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.
¿Por qué el paper trading de cuatro semanas corre en paralelo a la redacción de la memoria y no al final?
- APorque así el alumno tiene menos trabajo
- BPorque 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
- CPorque el broker solo abre cuentas de papel en esas fechas
- DPorque la memoria no necesita los resultados del papel
Ver respuesta
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.
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?
- A8,4, notable
- BLa nota baja un punto por el error
- CSuspenso: un backtest sin costes es uno de los errores que suspenden por sí solos, sea cual sea la nota ponderada
- DAprobado, porque la media supera el 5
Ver respuesta
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.
En la defensa, un miembro del tribunal pide ejecutar
make reproduceen 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?- AUn error del tribunal; no afecta a la nota
- BAlgo 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
- CQue el ordenador del tribunal es más lento
- DQue Docker no estaba instalado; basta con explicarlo
Ver respuesta
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.