---
title: "0.6.1 Gráficos con Matplotlib, Seaborn y Plotly"
description: "Matplotlib con figuras y ejes, distribuciones con Seaborn y velas con Plotly sobre los precios de SINT, con las reglas de un buen gráfico de trading."
url: "https://pythonparatrading.com/curso-python-trading/nivel-0/graficos-matplotlib-seaborn-y-plotly/"
lang: "es"
image: "https://pythonparatrading.com/og/curso-python-trading/nivel-0/graficos-matplotlib-seaborn-y-plotly.png"
---

# 0.6.1 · 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.

- Curso: [Nivel 0: Python desde cero y mentalidad de datos](https://pythonparatrading.com/curso-python-trading/nivel-0/)
- Módulo: 0.6 · Visualización
- Duración: 45 min
- Requisitos: [0.5.2 · pandas 3.0: Series, DataFrame, índices temporales, Copy-on-Write, tipo str y pd.col()](https://pythonparatrading.com/curso-python-trading/nivel-0/pandas-3-series-y-dataframe/)

## Al terminar esta lección

- Crear una figura de Matplotlib con la API orientada a objetos (`fig`, `ax`) con el precio en escala logarítmica y el drawdown debajo, en ejes que comparten fechas.
- Dibujar con Seaborn la distribución de las rentabilidades diarias y un gráfico de barras por año sobre unos ejes propios.
- Construir un gráfico de velas interactivo con `plotly.graph_objects.Candlestick` sin huecos de fin de semana y guardarlo en HTML.
- Elegir el tipo de gráfico y la escala adecuados para cada pregunta y reconocer los gráficos que engañan.

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

```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}")
```
```output
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:

```python {4-7}
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
```
```output
[<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:

```python
from pathlib import Path

fig.savefig("sint_drawdown.png", dpi=150)
Path("sint_drawdown.png").exists()
```
```output
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:

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

```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)
```
```output
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.

<aside class="callout" data-kind="nota">

`sns.set_theme()` cambia el estilo de **todos** los gráficos de Matplotlib de la sesión, no solo los de Seaborn. Si
mezclas las dos librerías y quieres un aspecto homogéneo, fija el estilo una vez al principio y no lo cambies a mitad
de notebook.

</aside>

## 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`:

```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.* Gráfico de velas de 60 sesiones, del 9 de octubre al 31 de diciembre de 2025, entre 135 y 151. El precio oscila entre 136 y 141 hasta principios de noviembre, sube hasta 149,74 el 20 de noviembre, retrocede hasta 139,23 el 26 de diciembre y cierra el año en 142,22, destacado. 29 velas azules y 31 naranjas.

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:

```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")
```
```output
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:

```python skip title="velas_mplfinance.py"
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.

<aside class="callout" data-kind="aviso">

Un gráfico bonito de un backtest no prueba nada. Si la curva de capital sale de una estrategia con sesgo de
anticipación (lección 0.1.1) o sin costes (lección 0.1.2), el gráfico solo dibuja el error con más detalle.

</aside>

## 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.**

```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)}")
```
```output
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.

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

```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}")
```
```output
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.

3. 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.**

```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)
```
```output
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.

## Autoevaluación

1. En la API orientada a objetos de Matplotlib, ¿qué es `ax`?

   - A. La ventana donde se muestra el gráfico
   - B. Un objeto `Axes`: un área de dibujo con sus propios ejes, título y series, dentro de una `Figure`
   - C. El eje X del gráfico
   - D. Un alias de `matplotlib.pyplot`

   *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?

   - A. Lineal, porque es la más honesta
   - B. Logarítmica, porque la misma variación porcentual ocupa la misma altura en cualquier nivel de precio
   - C. Cualquiera, el gráfico es el mismo
   - D. Una escala que empiece en cero

   *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"])]`?

   - A. Para quitar los festivos
   - B. Para que el eje X no deje un hueco vacío cada fin de semana, en el que no hay sesión
   - C. Para cambiar la zona horaria
   - D. Para que el gráfico pese menos

   *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?

   - A. Un gráfico de tarta en 3D
   - B. Un gráfico de barras horizontales ordenadas
   - C. Un gráfico de líneas
   - D. Un gráfico de velas

   *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

### Research

- [Advanced Financial Report Library in Python](https://pythonparatrading.com/advanced-reporting/) · Research-0016 · 3 min

### Documentación

- [Matplotlib: guía rápida (estilos explícito e implícito)](https://matplotlib.org/stable/users/explain/quick_start.html) · matplotlib.org
- [Seaborn: seaborn.histplot](https://seaborn.pydata.org/generated/seaborn.histplot.html) · seaborn.pydata.org
- [Plotly: gráficos de velas en Python](https://plotly.com/python/candlestick-charts/) · plotly.com
- [mplfinance: velas y OHLC sobre Matplotlib](https://github.com/matplotlib/mplfinance) · github.com

### Datos

- [Datos del nivel 0: precios diarios del ETF ficticio SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.csv) · Descarga · sint_diario.csv

Lección anterior: [0.5.5 · DuckDB y SQL esencial: SELECT, JOIN, GROUP BY y funciones de ventana sobre Parquet](https://pythonparatrading.com/curso-python-trading/nivel-0/duckdb-y-sql-sobre-parquet/)

Lección siguiente: [0.6.2 · Dashboards rápidos con Streamlit: un panel de seguimiento en un solo script](https://pythonparatrading.com/curso-python-trading/nivel-0/dashboards-con-streamlit/)
