Lección 0.6.1 · 45 min
Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
Un gráfico de trading responde a una pregunta: cuánto ha ganado, cuánto ha llegado a perder, cómo se reparten los días. Esta lección construye los tres gráficos que más se repiten en el curso con las tres librerías que más se usan, y las reglas para que no engañen.
- Módulo
- 0.6 · Visualización
- Duración
- 45 min
Lecciones del nivel 0
0.1 · Bienvenida y expectativas
0.2 · Entorno de trabajo moderno
- 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
- 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
- 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
- 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
- 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
- 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
0.3 · Fundamentos del lenguaje
- 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
- 0.3.2 Cadenas, f-strings y formato de números y fechas
- 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
- 0.3.4 Control de flujo: if, elif, else, for, while y match-case
- 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
- 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
0.4 · Python intermedio
- 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
- 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
- 0.4.3 Iteradores, generadores, decoradores y context managers
- 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
- 0.4.5 Logging profesional y testing con pytest
- 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
0.5 · Computación numérica y datos
- 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
- 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
- 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
- 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
- 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
0.6 · Visualización
0.7 · Matemáticas y estadística mínimas
Un gráfico, una pregunta
Un gráfico de trading no es decoración: es la forma más rápida de responder a una pregunta concreta sobre una serie. Antes de escribir código conviene saber cuál es, porque la pregunta decide el tipo de gráfico, la escala y lo que sobra.
| Pregunta | Gráfico | Librería habitual |
|---|---|---|
| ¿Cuánto ha ganado o perdido a lo largo del tiempo? | Línea de precio o de capital, escala logarítmica | Matplotlib |
| ¿Cuánto ha llegado a perder desde un máximo? | Drawdown en área, debajo de la curva de capital | Matplotlib |
| ¿Cómo se reparten las rentabilidades diarias? | Histograma | Seaborn |
| ¿Qué periodo fue más volátil? | Barras por año o línea de volatilidad móvil | Seaborn, Matplotlib |
| ¿Qué hizo el precio dentro de cada sesión? | Velas (OHLC) | Plotly, mplfinance |
Las tres librerías no compiten: se complementan. Matplotlib es la base, la que más control da y la que usan casi todas las demás por debajo. Seaborn se apoya en Matplotlib y resuelve en una línea los gráficos estadísticos. Plotly dibuja en el navegador, con zoom y cursor, y es la opción natural para explorar velas o montar un panel (lección 0.6.2).
Los datos son los de siempre: los precios diarios de SINT, el ETF ficticio del curso. La lectura con pandas se vio en la lección 0.5.2; aquí se calcula además el drawdown, la caída desde el máximo alcanzado hasta ese día:
import pandas as pd
precios = pd.read_csv("sint_diario.csv", parse_dates=["fecha"],
index_col="fecha")
cierre = precios["cierre"]
drawdown = cierre / cierre.cummax() - 1
print(f"Sesiones: {len(cierre)}")
print(f"Drawdown máximo: {drawdown.min():.1%} "
f"el {drawdown.idxmin():%Y-%m-%d}")Sesiones: 1303
Drawdown máximo: -25.0% el 2022-10-20cummax() lleva el máximo acumulado hasta cada día; dividir el cierre entre él y restar 1 da la distancia a ese
máximo, siempre negativa o cero. El peor momento de SINT es una caída del 25,0 % en octubre de 2022.
Matplotlib: figuras y ejes
Matplotlib tiene dos formas de usarse. La implícita (plt.plot(…), plt.ylabel(…)) dibuja sobre «los ejes
actuales», un estado global que pyplot mantiene por detrás. Es cómoda para un gráfico rápido, pero con dos paneles ya
no está claro a cuál va cada instrucción. La explícita u orientada a objetos crea los objetos y los nombra:
Figure: el lienzo completo, con su tamaño y su forma de guardarse.Axes: cada panel dentro de la figura, con sus ejes X e Y, su título y sus series. Un gráfico de precio con el drawdown debajo es una figura con dosAxes.
plt.subplots() devuelve las dos cosas a la vez. A partir de ahí, todo se hace con métodos de cada panel:
import matplotlib.pyplot as plt
from matplotlib.ticker import PercentFormatter
fig, (ax_precio, ax_dd) = plt.subplots(
2, 1, sharex=True, figsize=(10, 6),
height_ratios=(3, 1), layout="constrained",
)
ax_precio.plot(cierre.index, cierre, linewidth=1)
ax_precio.set_yscale("log")
ax_precio.set_ylabel("Cierre (escala log)")
ax_precio.set_title("SINT · cierre diario 2021-2025")
ax_dd.fill_between(drawdown.index, drawdown, 0, alpha=0.4)
ax_dd.yaxis.set_major_formatter(PercentFormatter(1.0, decimals=0))
ax_dd.set_ylabel("Drawdown")
fig.axes[<Axes: title={'center': 'SINT · cierre diario 2021-2025'}, ylabel='Cierre (escala log)'>, <Axes: ylabel='Drawdown'>]Las líneas resaltadas hacen casi todo el trabajo: dos filas y una columna (2, 1), el eje X compartido
(sharex=True: al hacer zoom en un panel, el otro lo sigue), el panel de precio tres veces más alto que el de
drawdown y layout="constrained" para que los rótulos no se pisen. La última línea pide a la figura la lista de sus
paneles: dos objetos Axes, cada uno con su título y su etiqueta.
La figura de la cabecera muestra el resultado con los mismos datos, dibujado con Vega para la web (cierres semanales y el peor drawdown de cada semana, para que pese poco). Se lee de arriba abajo: la caída de 2022 se ve en el precio y se mide en el panel de abajo.
Dos decisiones de esta figura se repiten en todo el curso:
- Escala logarítmica en el precio. En escala lineal, una subida de 50 a 100 ocupa la misma altura que una de 100 a 150, aunque la primera es un 100 % y la segunda un 50 %. En escala logarítmica, la misma variación porcentual ocupa la misma altura en cualquier nivel. Con SINT, que se mueve entre 95 y 150, la diferencia es pequeña; con un índice de veinte años, que multiplica su valor por diez, es la diferencia entre ver las caídas antiguas o no verlas.
- El drawdown debajo, no encima. La curva de precio dice cuánto se ganó; el drawdown, cuánto se sufrió por el camino. Con el eje X compartido se ve a la vez cuándo empezó la caída, cuánto duró y cuándo se recuperó el máximo.
Guardar la figura es otro método del objeto. dpi fija la resolución del PNG:
from pathlib import Path
fig.savefig("sint_drawdown.png", dpi=150)
Path("sint_drawdown.png").exists()TrueEn un notebook la figura aparece sola al final de la celda; en un script hace falta plt.show() para abrir la
ventana, o solo savefig() si el gráfico es para un informe. Para que todos los gráficos de tu repositorio tengan el
mismo aspecto, un archivo de estilo (plt.style.use("mi_estilo.mplstyle")) fija colores, fuentes y retícula una
sola vez.
Seaborn: distribuciones en una línea
Seaborn trabaja sobre Matplotlib y piensa en datos, no en líneas: se le pasa una serie o un DataFrame y elige el
gráfico estadístico adecuado. El histograma de las rentabilidades diarias —qué fracción de días sube o baja cuánto—
es el gráfico estadístico más útil del trading:
import seaborn as sns
rent = cierre.pct_change().dropna()
fig, ax = plt.subplots(figsize=(8, 4))
sns.histplot(rent, bins=60, stat="density", ax=ax)
ax.xaxis.set_major_formatter(PercentFormatter(1.0, decimals=0))
ax.set_xlabel("Rentabilidad diaria")
print(f"Días: {len(rent)} · barras: {len(ax.patches)}")Días: 1302 · barras: 60Dos detalles. El primero, ax=ax: Seaborn dibuja sobre los ejes que se le pasan, igual que Matplotlib. Sin ese
argumento usaría los «ejes actuales» de pyplot, y con varios gráficos en el mismo notebook eso acaba mal. El segundo,
stat="density": la altura de cada barra se normaliza para que el área total sea 1, y así el histograma se puede
comparar con una curva de densidad. Ese gráfico, con la normal superpuesta, está en la lección 0.7.1, que es donde
se explica qué dice su forma.
La segunda pregunta típica es por categorías: ¿qué año fue más volátil? La volatilidad anualizada (la desviación
típica diaria multiplicada por , lección 0.7.1) se calcula con un groupby por año y Seaborn la dibuja
como barras:
import numpy as np
vol_anual = rent.groupby(rent.index.year).std() * np.sqrt(252)
fig, ax = plt.subplots(figsize=(6, 3))
sns.barplot(x=vol_anual.index, y=vol_anual.values, color="0.6",
ax=ax)
ax.yaxis.set_major_formatter(PercentFormatter(1.0, decimals=0))
ax.set_ylabel("Volatilidad anualizada")
vol_anual.round(3)fecha
2021 0.160
2022 0.248
2023 0.149
2024 0.150
2025 0.179
Name: cierre, dtype: float64El año 2022, con un 24,8 %, destaca sobre una base del 15–16 %: es el episodio de tensión que también se ve en el drawdown. 2025 sube al 17,9 % por un segundo episodio, más corto, en primavera. Una barra por año y un solo color: la comparación es la longitud, y nada más compite por la atención.
Velas con Plotly
Una vela resume una sesión con cuatro precios: apertura, máximo, mínimo y cierre (OHLC). El cuerpo va de la apertura al cierre; la mecha, del mínimo al máximo. Si el cierre queda por encima de la apertura, la vela es alcista; si queda por debajo, bajista. Con sesenta sesiones se ve la estructura de los últimos tres meses; con cinco años, las velas se convierten en una mancha y vale más una línea.
Plotly construye la figura como un objeto (go.Figure) con una o varias trazas. La traza de velas es
go.Candlestick:
import plotly.graph_objects as go
ult = precios.tail(60)
fig = go.Figure(go.Candlestick(
x=ult.index, open=ult["apertura"], high=ult["maximo"],
low=ult["minimo"], close=ult["cierre"], name="SINT",
))
fig.update_layout(template="plotly_dark", height=450,
xaxis_rangeslider_visible=False,
title="SINT · últimas 60 sesiones")
fig.update_xaxes(rangebreaks=[dict(bounds=["sat", "mon"])])
fig.show()fig.show() abre el gráfico en el navegador o lo pinta en el notebook, con zoom, desplazamiento y un cursor que
muestra los cuatro precios de cada vela. xaxis_rangeslider_visible=False quita la barra de desplazamiento que
Plotly añade por defecto debajo de las velas. rangebreaks quita los fines de semana: con un eje de fechas continuo,
Plotly dejaría un hueco cada sábado y domingo. Los festivos de un mercado real (SINT no los tiene) se quitan igual,
con dict(values=[…]) y la lista de fechas.
Estos son los datos que se acaban de dibujar, dibujados con Vega:
Para compartir el gráfico sin Python, se guarda como HTML. Con include_plotlyjs="cdn" el archivo pesa unos pocos
KB y carga la librería de Internet al abrirse; sin ese argumento lleva la librería dentro y funciona sin conexión:
alcistas = int((ult["cierre"] >= ult["apertura"]).sum())
print(f"Velas: {len(fig.data[0].x)} · alcistas: {alcistas} · "
f"bajistas: {len(ult) - alcistas}")
print(f"Del {ult.index[0]:%Y-%m-%d} al {ult.index[-1]:%Y-%m-%d}")
fig.write_html("velas_sint.html", include_plotlyjs="cdn")Velas: 60 · alcistas: 29 · bajistas: 31
Del 2025-10-09 al 2025-12-31La alternativa en Matplotlib es mplfinance, una librería pensada solo para gráficos de precios: velas, volumen y
medias móviles en una llamada. Exige las columnas con nombres en inglés (Open, High, Low, Close, Volume) y
un índice de fechas. No forma parte del entorno del curso; si la instalas (uv add mplfinance), el equivalente es:
import mplfinance as mpf
ohlc = ult.rename(columns={
"apertura": "Open", "maximo": "High", "minimo": "Low",
"cierre": "Close", "volumen": "Volume",
})
mpf.plot(ohlc, type="candle", volume=True, style="nightclouds")mplfinance genera imágenes estáticas, útiles para un informe en PDF; Plotly, gráficos interactivos, mejores para explorar. Ninguna de las dos convierte un gráfico de velas en una señal: las «figuras» que se ven en ellas son, mientras no se demuestre lo contrario con datos (nivel 2), patrones que el ojo encuentra en el ruido.
Gráficos que no engañan
Un gráfico mal construido puede hacer que una estrategia mediocre parezca buena. Estas reglas se aplican a todos los gráficos del curso:
- Escala logarítmica en series largas. Precios y curvas de capital de más de un par de años, siempre en logarítmica. En lineal, el final de la serie domina y las caídas del principio desaparecen.
- El drawdown, debajo del capital. Una curva de capital sin su drawdown oculta lo que se sufrió para conseguirla.
- El periodo completo, y dicho. Empezar el gráfico justo después de una caída es la forma más barata de mejorar un resultado. El título o el pie dicen de qué fecha a qué fecha.
- Unidades en los ejes. Euros, porcentaje, puntos básicos: un eje sin unidades no se puede leer.
- Comparaciones en la misma escala. Dos estrategias en el mismo panel y con el mismo eje Y; dos ejes Y en un mismo gráfico permiten hacer que dos series se crucen donde convenga.
- Sin tartas ni 3D. El ojo compara longitudes mucho mejor que ángulos o volúmenes. El peso de cada activo de una cartera se lee mejor en barras horizontales ordenadas que en una tarta, y el 3D solo añade distorsión.
- Colores que todos distinguen. Azul para lo que sube y naranja para lo que baja, como en las figuras de este curso, en lugar de verde y rojo, que una parte de los lectores no distingue.
Ejercicios
- Añade al panel de precio una media móvil de 200 sesiones y al panel de drawdown una línea horizontal en −10 %. Comprueba cuántos paneles tiene la figura y cuántas líneas tiene el panel de precio.
Solución
fig, (ax_precio, ax_dd) = plt.subplots(
2, 1, sharex=True, figsize=(10, 6), height_ratios=(3, 1),
)
ax_precio.plot(cierre.index, cierre, linewidth=1, label="Cierre")
media = cierre.rolling(200).mean()
ax_precio.plot(media.index, media, linewidth=1, label="Media 200")
ax_precio.set_yscale("log")
ax_precio.legend()
ax_dd.fill_between(drawdown.index, drawdown, 0, alpha=0.4)
ax_dd.axhline(-0.10, linestyle="--", linewidth=1)
print(f"Paneles: {len(fig.axes)}")
print(f"Líneas en el panel de precio: {len(ax_precio.lines)}")Paneles: 2
Líneas en el panel de precio: 2axhline dibuja una recta horizontal que cruza todo el panel, sea cual sea el rango de fechas. La leyenda
(label= y ax.legend()) solo hace falta cuando hay más de una serie en el mismo panel.
- Comprueba numéricamente por qué se usa la escala logarítmica: calcula la altura, en escala logarítmica, de una subida de 50 a 100 y de una de 100 a 200. ¿Y en escala lineal?
Solución
for desde, hasta in ((50, 100), (100, 200)):
log_alto = np.log10(hasta) - np.log10(desde)
print(f"{desde} → {hasta}: lineal {hasta - desde}, "
f"log {log_alto:.3f}")50 → 100: lineal 50, log 0.301
100 → 200: lineal 100, log 0.301Las dos subidas son del 100 % y en escala logarítmica ocupan exactamente la misma altura (log10(2) ≈ 0,301). En escala lineal, la segunda ocupa el doble: un gráfico lineal exagera lo reciente si el precio ha subido.
- Añade el volumen debajo de las velas con
make_subplots(dos filas, eje X compartido, la de velas tres veces más alta). ¿Cuántas trazas tiene la figura?
Solución
from plotly.subplots import make_subplots
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
row_heights=[0.75, 0.25], vertical_spacing=0.03)
fig.add_trace(go.Candlestick(
x=ult.index, open=ult["apertura"], high=ult["maximo"],
low=ult["minimo"], close=ult["cierre"], name="SINT",
), row=1, col=1)
fig.add_trace(go.Bar(x=ult.index, y=ult["volumen"],
name="Volumen"), row=2, col=1)
fig.update_layout(template="plotly_dark",
xaxis_rangeslider_visible=False)
fig.update_xaxes(rangebreaks=[dict(bounds=["sat", "mon"])])
len(fig.data)2Dos trazas, una por panel. update_xaxes sin argumentos de fila se aplica a todos los ejes X, así que los dos
paneles pierden los huecos de fin de semana.
Respuesta plegada en cada pregunta
Autoevaluación
En la API orientada a objetos de Matplotlib, ¿qué es
ax?- ALa ventana donde se muestra el gráfico
- BUn objeto
Axes: un área de dibujo con sus propios ejes, título y series, dentro de unaFigure - CEl eje X del gráfico
- DUn alias de
matplotlib.pyplot
Ver respuesta
Respuesta B. Un objeto
Axes: un área de dibujo con sus propios ejes, título y series, dentro de unaFigureLa
Figurees el lienzo; cadaAxeses un panel con sus ejes. Una figura puede tener varios (precio y drawdown), y cada método (plot,set_yscale,set_ylabel) se llama sobre el panel que corresponde, sin depender de cuál es el «actual».Una serie de precios pasa de 20 a 400 en quince años. ¿Qué escala del eje Y permite comparar una caída del 30 % en el año 2 con otra del 30 % en el año 14?
- ALineal, porque es la más honesta
- BLogarítmica, porque la misma variación porcentual ocupa la misma altura en cualquier nivel de precio
- CCualquiera, el gráfico es el mismo
- DUna escala que empiece en cero
Ver respuesta
Respuesta B. Logarítmica, porque la misma variación porcentual ocupa la misma altura en cualquier nivel de precio
En escala lineal, la caída a precio 20 sería casi invisible y la de precio 400 parecería un desplome. En escala logarítmica las dos ocupan la misma altura, que es lo que importa para el capital.
¿Por qué el gráfico de velas de Plotly de la lección usa
rangebreaks=[dict(bounds=["sat", "mon"])]?- APara quitar los festivos
- BPara que el eje X no deje un hueco vacío cada fin de semana, en el que no hay sesión
- CPara cambiar la zona horaria
- DPara que el gráfico pese menos
Ver respuesta
Respuesta B. Para que el eje X no deje un hueco vacío cada fin de semana, en el que no hay sesión
Con un eje de fechas continuo, Plotly reserva espacio para sábados y domingos.
rangebreakslos elimina; los festivos de un mercado real habría que añadirlos convalues=[…].¿Cuál de estos gráficos es el más adecuado para comparar el peso de cuatro activos en una cartera?
- AUn gráfico de tarta en 3D
- BUn gráfico de barras horizontales ordenadas
- CUn gráfico de líneas
- DUn gráfico de velas
Ver respuesta
Respuesta B. Un gráfico de barras horizontales ordenadas
El ojo compara longitudes mucho mejor que ángulos o volúmenes. Las tartas y el 3D distorsionan las proporciones; las barras ordenadas se leen de un vistazo.
Ideas clave
- Con la API orientada a objetos cada gráfico es una
Figurecon uno o variosAxes; se crean conplt.subplots()y se modifican llamando a los métodos de cada panel. - Una serie de precios larga se dibuja en escala logarítmica, y el drawdown va debajo, en un panel que comparte el eje de fechas.
- Seaborn dibuja distribuciones y comparaciones por categoría en una línea; se le pasan unos ejes propios (
ax=) para no depender del estado global. - Plotly da gráficos interactivos (zoom, cursor) y velas con
go.Candlestick;fig.write_html()los guarda en un archivo que se abre en cualquier navegador. - Sin tartas, sin 3D y sin dos ejes Y que se cruzan: cada gráfico, una pregunta, con sus unidades y su periodo.