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.

Fig. — SINT, 2021–2025: cierre semanal en escala logarítmica y, debajo, su drawdown. La misma serie de la celda de Matplotlib, dibujada con Vega
Lecciones del nivel 0
  1. 0.1 · Bienvenida y expectativas

    1. 0.1.1 Trading sistemático, algorítmico y cuantitativo frente al discrecional
    2. 0.1.2 Expectativas realistas: costes, tiempo y riesgo de pérdida
    3. 0.1.3 Estructura del curso, tests de entrada, repositorio personal y aprendizaje con IA
  2. 0.2 · Entorno de trabajo moderno

    1. 0.2.1 Instalación de Python con uv: versiones y ciclo de soporte
    2. 0.2.2 Entornos virtuales y dependencias: uv init, uv add, pyproject.toml y lockfiles
    3. 0.2.3 Editores (VS Code, Cursor, PyCharm) y notebooks (Jupyter, JupyterLab, marimo)
    4. 0.2.4 Terminal básica, rutas, variables de entorno y ficheros .env
    5. 0.2.5 Git y GitHub: commits, ramas, pull requests y .gitignore
    6. 0.2.6 Introducción a los asistentes de código con IA: prompts, revisión y tests
  3. 0.3 · Fundamentos del lenguaje

    1. 0.3.1 Variables, tipos y operadores; coma flotante y decimal para precios y ticks
    2. 0.3.2 Cadenas, f-strings y formato de números y fechas
    3. 0.3.3 Listas, tuplas, diccionarios, conjuntos y comprensiones
    4. 0.3.4 Control de flujo: if, elif, else, for, while y match-case
    5. 0.3.5 Funciones: argumentos, *args y **kwargs, lambdas y ámbito
    6. 0.3.6 Excepciones; módulos y paquetes, con los imports perezosos de Python 3.15
  4. 0.4 · Python intermedio

    1. 0.4.1 Programación orientada a objetos: clases, herencia, composición y dataclasses
    2. 0.4.2 Tipado: type hints, mypy o ty y Pydantic para configuraciones
    3. 0.4.3 Iteradores, generadores, decoradores y context managers
    4. 0.4.4 Ficheros (CSV, JSON, YAML, TOML) y fechas con zonas horarias
    5. 0.4.5 Logging profesional y testing con pytest
    6. 0.4.6 Concurrencia: threading, multiprocessing, asyncio y free-threading
  5. 0.5 · Computación numérica y datos

    1. 0.5.1 NumPy 2: arrays, broadcasting, vectorización, álgebra lineal y generadores aleatorios
    2. 0.5.2 pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()
    3. 0.5.3 Selección, filtrado, groupby, merge, pivot, resample y ventanas móviles
    4. 0.5.4 Polars: API lazy frente a eager, expresiones, rendimiento y cuándo elegirlo frente a pandas
    5. 0.5.5 DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet
  6. 0.6 · Visualización

    1. 0.6.1 Matplotlib orientado a objetos, Seaborn y Plotly: gráficos de precios, drawdown y velas
    2. 0.6.2 Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script
  7. 0.7 · Matemáticas y estadística mínimas

    1. 0.7.1 Rentabilidades simples y logarítmicas, interés compuesto y estadística descriptiva
    2. 0.7.2 Probabilidad y distribuciones, correlación frente a causalidad y regresión simple
    3. 0.7.3 Álgebra lineal intuitiva: vectores y matrices como preparación para carteras

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.

PreguntaGráficoLibrería habitual
¿Cuánto ha ganado o perdido a lo largo del tiempo?Línea de precio o de capital, escala logarítmicaMatplotlib
¿Cuánto ha llegado a perder desde un máximo?Drawdown en área, debajo de la curva de capitalMatplotlib
¿Cómo se reparten las rentabilidades diarias?HistogramaSeaborn
¿Qué periodo fue más volátil?Barras por año o línea de volatilidad móvilSeaborn, 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:

IN [01]python
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}")
OUT [01]Precios y drawdown de SINT
Sesiones: 1303
Drawdown máximo: -25.0% el 2022-10-20

cummax() 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 dos Axes.

plt.subplots() devuelve las dos cosas a la vez. A partir de ahí, todo se hace con métodos de cada panel:

IN [02]python
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
OUT [02]Precio y drawdown en dos paneles
[<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:

IN [03]python
from pathlib import Path

fig.savefig("sint_drawdown.png", dpi=150)
Path("sint_drawdown.png").exists()
OUT [03]Guardar la figura
True

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

IN [04]python
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)}")
OUT [04]Histograma de rentabilidades
Días: 1302 · barras: 60

Dos 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 252\sqrt{252}, lección 0.7.1) se calcula con un groupby por año y Seaborn la dibuja como barras:

IN [05]python
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)
OUT [05]Volatilidad por año
fecha
2021    0.160
2022    0.248
2023    0.149
2024    0.150
2025    0.179
Name: cierre, dtype: float64

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

IN [06]python
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:

Fig. — Velas diarias de SINT, últimas 60 sesiones de 2025 (azul, cierre por encima de la apertura; naranja, por debajo). Los mismos datos del gráfico de Plotly, 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:

IN [07]python
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")
OUT [07]Comprobar y guardar el gráfico
Velas: 60 · alcistas: 29 · bajistas: 31
Del 2025-10-09 al 2025-12-31

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

IN [08]velas_mplfinance.pypython
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

  1. 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
IN [09]python
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)}")
OUT [09]Media de 200 sesiones y umbral de drawdown
Paneles: 2
Líneas en el panel de precio: 2

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

  1. 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
IN [10]python
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}")
OUT [10]Misma variación, misma altura
50 → 100: lineal 50, log 0.301
100 → 200: lineal 100, log 0.301

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

  1. 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
IN [11]python
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)
OUT [11]Velas y volumen
2

Dos 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

  1. 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 una Figure
    • 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 una Figure

    La Figure es el lienzo; cada Axes es 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».

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

  3. ¿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. rangebreaks los elimina; los festivos de un mercado real habría que añadirlos con values=[…].

  4. ¿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 Figure con uno o varios Axes; se crean con plt.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.

Recursos