---
title: "0.6.1 Charts with Matplotlib, Seaborn and Plotly"
description: "Matplotlib with figures and axes, distributions with Seaborn and candlesticks with Plotly on SINT prices, with the rules of a good trading chart."
url: "https://pythonparatrading.com/en/python-trading-course/level-0/matplotlib-seaborn-plotly-charts/"
lang: "en"
image: "https://pythonparatrading.com/og/en/python-trading-course/level-0/matplotlib-seaborn-plotly-charts.png"
---

# 0.6.1 · Object-oriented Matplotlib, Seaborn and Plotly: price, drawdown and candlestick charts

> A trading chart answers a question: how much it has gained, how much it has lost at worst, how the days are distributed. This lesson builds the three charts that recur most in the course with the three most used libraries, plus the rules that keep them from misleading.

- Course: [Level 0: Python from scratch and a data mindset](https://pythonparatrading.com/en/python-trading-course/level-0/)
- Module: 0.6 · Visualisation
- Duration: 45 min
- Prerequisites: [0.5.2 · pandas 3.0: Series, DataFrame, time indexes, Copy-on-Write, the str type and pd.col()](https://pythonparatrading.com/en/python-trading-course/level-0/pandas-3-series-and-dataframe/)

## By the end of this lesson

- Create a Matplotlib figure with the object-oriented API (`fig`, `ax`) with the price on a logarithmic scale and the drawdown below, on axes that share dates.
- Plot with Seaborn the distribution of daily returns and a bar chart by year on axes of your own.
- Build an interactive candlestick chart with `plotly.graph_objects.Candlestick` without weekend gaps and save it as HTML.
- Choose the right chart type and scale for each question and recognize charts that mislead.

## One chart, one question

A trading chart is not decoration: it is the fastest way to answer a specific question about a series. Before writing
code, it pays to know which question it is, because the question decides the chart type, the scale and what is
superfluous.

| Question | Chart | Usual library |
|---|---|---|
| How much has it gained or lost over time? | Price or equity line, logarithmic scale | Matplotlib |
| How much has it lost at worst from a peak? | Drawdown as an area, below the equity curve | Matplotlib |
| How are daily returns distributed? | Histogram | Seaborn |
| Which period was most volatile? | Bars by year or rolling volatility line | Seaborn, Matplotlib |
| What did the price do within each session? | Candlesticks (OHLC) | Plotly, mplfinance |

The three libraries do not compete: they complement each other. **Matplotlib** is the base, the one that gives the most
control and the one almost all others use under the hood. **Seaborn** builds on Matplotlib and produces statistical
charts in one line. **Plotly** draws in the browser, with zoom and cursor, and is the natural choice for exploring
candlesticks or building a dashboard (lesson 0.6.2).

The data is the usual: the daily prices of SINT, the course's fictitious ETF. Reading it with pandas was covered in
lesson 0.5.2; here we also compute the **drawdown**, the drop from the peak reached up to that day:

```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"Sessions: {len(cierre)}")
print(f"Maximum drawdown: {drawdown.min():.1%} "
      f"on {drawdown.idxmin():%Y-%m-%d}")
```
```output
Sessions: 1303
Maximum drawdown: -25.0% on 2022-10-20
```

`cummax()` carries the running maximum up to each day; dividing the close by it and subtracting 1 gives the distance to
that peak, always negative or zero. SINT's worst moment is a 25.0 % drop in October 2022.

## Matplotlib: figures and axes

Matplotlib can be used in two ways. The **implicit** one (`plt.plot(…)`, `plt.ylabel(…)`) draws on "the current
axes", a global state that pyplot keeps behind the scenes. It is convenient for a quick chart, but with two panels it is
no longer clear which instruction goes to which. The **explicit or object-oriented** one creates the objects and names
them:

- `Figure`: the whole canvas, with its size and the way it is saved.
- `Axes`: each panel inside the figure, with its X and Y axes, its title and its series. A price chart with the
  drawdown below is a figure with two `Axes`.

`plt.subplots()` returns both at once. From there on, everything is done with each panel's methods:

```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("Close (log scale)")
ax_precio.set_title("SINT · daily close 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 · daily close 2021-2025'}, ylabel='Close (log scale)'>, <Axes: ylabel='Drawdown'>]
```

The highlighted lines do almost all the work: two rows and one column (`2, 1`), the shared X axis (`sharex=True`: when
you zoom in one panel, the other follows), the price panel three times taller than the drawdown panel and
`layout="constrained"` so the labels do not overlap. The last line asks the figure for the list of its panels: two
`Axes` objects, each with its title and label.

The figure at the top shows the result with the same data, drawn with Vega for the web (weekly closes and the worst
drawdown of each week, to keep it light). It is read from top to bottom: the 2022 decline is seen in the price and
measured in the panel below.

Two decisions in this figure recur throughout the course:

- **Logarithmic scale on the price.** On a linear scale, a rise from 50 to 100 takes up the same height as one from
  100 to 150, even though the first is 100 % and the second 50 %. On a logarithmic scale, the same percentage change
  takes up the same height at any level. With SINT, which moves between 95 and 150, the difference is small; with a
  twenty-year index that multiplies its value by ten, it is the difference between seeing the old drops or not seeing
  them.
- **The drawdown below, not above.** The price curve says how much was gained; the drawdown, how much was suffered
  along the way. With the shared X axis you see at once when the decline started, how long it lasted and when the peak
  was recovered.

Saving the figure is another method of the object. `dpi` sets the PNG resolution:

```python
from pathlib import Path

fig.savefig("sint_drawdown.png", dpi=150)
Path("sint_drawdown.png").exists()
```
```output
True
```

In a notebook the figure appears on its own at the end of the cell; in a script you need `plt.show()` to open the
window, or just `savefig()` if the chart is for a report. To give all the charts in your repository the same look, a
style file (`plt.style.use("mi_estilo.mplstyle")`) sets colors, fonts and grid once.

## Seaborn: distributions in one line

Seaborn works on top of Matplotlib and thinks in data, not in lines: you pass it a series or a `DataFrame` and it picks
the right statistical chart. The histogram of daily returns (what fraction of days goes up or down by how much) is the
most useful statistical chart in 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("Daily return")
print(f"Days: {len(rent)} · bars: {len(ax.patches)}")
```
```output
Days: 1302 · bars: 60
```

Two details. The first, `ax=ax`: Seaborn draws on the axes it is given, just like Matplotlib. Without that argument it
would use pyplot's "current axes", and with several charts in the same notebook that ends badly. The second,
`stat="density"`: the height of each bar is normalized so the total area is 1, so the histogram can be compared with a
density curve. That chart, with the normal distribution overlaid, is in lesson 0.7.1, which is where its shape is
interpreted.

The second typical question is by category: which year was most volatile? The annualized volatility (the daily
standard deviation multiplied by $\sqrt{252}$, lesson 0.7.1) is computed with a `groupby` by year and Seaborn draws it
as bars:

```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("Annualized volatility")
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
```

The year 2022, at 24.8 %, stands out over a baseline of 15–16 %: it is the stress episode that also shows in the
drawdown. 2025 rises to 17.9 % because of a second, shorter episode in the spring. One bar per year and a single color:
the comparison is the length, and nothing else competes for attention.

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

`sns.set_theme()` changes the style of **all** Matplotlib charts in the session, not only Seaborn's. If you mix the two
libraries and want a consistent look, set the style once at the start and do not change it halfway through the
notebook.

</aside>

## Candlesticks with Plotly

A candlestick summarizes a session with four prices: open, high, low and close (OHLC). The body goes from the open to
the close; the wick, from the low to the high. If the close is above the open, the candle is bullish; if it is below,
bearish. With sixty sessions you see the structure of the last three months; with five years, the candles turn into a
smudge and a line is worth more.

Plotly builds the figure as an object (`go.Figure`) with one or more traces. The candlestick trace is
`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 · last 60 sessions")
fig.update_xaxes(rangebreaks=[dict(bounds=["sat", "mon"])])
fig.show()
```

`fig.show()` opens the chart in the browser or renders it in the notebook, with zoom, panning and a cursor that shows
the four prices of each candle. `xaxis_rangeslider_visible=False` removes the range slider that Plotly adds by default
below the candles. `rangebreaks` removes the weekends: with a continuous date axis, Plotly would leave a gap every
Saturday and Sunday. The holidays of a real market (SINT has none) are removed the same way, with `dict(values=[…])`
and the list of dates.

These are the data just plotted, drawn with Vega:

*Fig. — SINT daily candles, last 60 sessions of 2025 (blue, close above the open; orange, below). The same data as the Plotly chart, drawn with Vega.* Candlestick chart of 60 sessions, from 9 October to 31 December 2025, between 135 and 151. The price moves between 136 and 141 until early November, rises to 149.74 on 20 November, pulls back to 139.23 on 26 December and closes the year at 142.22, highlighted. 29 blue candles and 31 orange ones.

To share the chart without Python, save it as HTML. With `include_plotlyjs="cdn"` the file weighs a few KB and loads
the library from the Internet when opened; without that argument it carries the library inside and works offline:

```python
alcistas = int((ult["cierre"] >= ult["apertura"]).sum())
print(f"Candles: {len(fig.data[0].x)} · bullish: {alcistas} · "
      f"bearish: {len(ult) - alcistas}")
print(f"From {ult.index[0]:%Y-%m-%d} to {ult.index[-1]:%Y-%m-%d}")
fig.write_html("velas_sint.html", include_plotlyjs="cdn")
```
```output
Candles: 60 · bullish: 29 · bearish: 31
From 2025-10-09 to 2025-12-31
```

The Matplotlib alternative is **mplfinance**, a library designed only for price charts: candlesticks, volume and moving
averages in one call. It requires columns with English names (`Open`, `High`, `Low`, `Close`, `Volume`) and a date
index. It is not part of the course environment; if you install it (`uv add mplfinance`), the equivalent is:

```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 generates static images, useful for a PDF report; Plotly, interactive charts, better for exploring. Neither
turns a candlestick chart into a signal: the "patterns" seen in them are, until proven otherwise with data (level 2),
shapes the eye finds in the noise.

## Charts that do not mislead

A badly built chart can make a mediocre strategy look good. These rules apply to all the charts in the course:

- **Logarithmic scale on long series.** Prices and equity curves of more than a couple of years, always logarithmic.
  On a linear scale, the end of the series dominates and the early drops disappear.
- **The drawdown, below the equity curve.** An equity curve without its drawdown hides what was suffered to achieve
  it.
- **The full period, and stated.** Starting the chart right after a drop is the cheapest way to improve a result. The
  title or the caption says from which date to which date.
- **Units on the axes.** Euros, percent, basis points: an axis without units cannot be read.
- **Comparisons on the same scale.** Two strategies in the same panel and with the same Y axis; two Y axes on the same
  chart let you make two series cross wherever it suits you.
- **No pie charts or 3D.** The eye compares lengths much better than angles or volumes. The weight of each asset in a
  portfolio reads better in sorted horizontal bars than in a pie, and 3D only adds distortion.
- **Colors everyone can tell apart.** Blue for what goes up and orange for what goes down, as in this course's figures,
  instead of green and red, which some readers cannot distinguish.

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

A pretty backtest chart proves nothing. If the equity curve comes from a strategy with look-ahead bias (lesson 0.1.1)
or without costs (lesson 0.1.2), the chart only draws the error in more detail.

</aside>

## Exercises

1. Add a 200-session moving average to the price panel and a horizontal line at −10 % to the drawdown panel. Check how
   many panels the figure has and how many lines the price panel has.

**Solution.**

```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="Close")
media = cierre.rolling(200).mean()
ax_precio.plot(media.index, media, linewidth=1, label="200-session average")
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"Panels: {len(fig.axes)}")
print(f"Lines in the price panel: {len(ax_precio.lines)}")
```
```output
Panels: 2
Lines in the price panel: 2
```

`axhline` draws a horizontal line across the whole panel, whatever the date range. The legend (`label=` and
`ax.legend()`) is only needed when there is more than one series in the same panel.

2. Check numerically why the logarithmic scale is used: compute the height, on a logarithmic scale, of a rise from 50
   to 100 and of one from 100 to 200. And on a linear scale?

**Solution.**

```python
for desde, hasta in ((50, 100), (100, 200)):
    log_alto = np.log10(hasta) - np.log10(desde)
    print(f"{desde} → {hasta}: linear {hasta - desde}, "
          f"log {log_alto:.3f}")
```
```output
50 → 100: linear 50, log 0.301
100 → 200: linear 100, log 0.301
```

Both rises are 100 % and on a logarithmic scale they take up exactly the same height (log10(2) ≈ 0.301). On a linear
scale, the second takes up twice as much: a linear chart exaggerates the recent part if the price has risen.

3. Add the volume below the candlesticks with `make_subplots` (two rows, shared X axis, the candlestick row three times
   taller). How many traces does the figure have?

**Solution.**

```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="Volume"), 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
```

Two traces, one per panel. `update_xaxes` without row arguments applies to all X axes, so both panels lose the weekend
gaps.

## Self-assessment

1. In Matplotlib's object-oriented API, what is `ax`?

   - A. The window where the chart is shown
   - B. The X axis of the chart
   - C. An `Axes` object: a plotting area with its own axes, title and series, inside a `Figure`
   - D. An alias of `matplotlib.pyplot`

   *Answer C: An `Axes` object: a plotting area with its own axes, title and series, inside a `Figure`.* The `Figure` is the canvas; each `Axes` is a panel with its axes. A figure can have several (price and drawdown), and each method (`plot`, `set_yscale`, `set_ylabel`) is called on the right panel, without depending on which one is "current".

2. A price series goes from 20 to 400 in fifteen years. Which Y-axis scale lets you compare a 30 % drop in year 2 with another 30 % drop in year 14?

   - A. Linear, because it is the most honest
   - B. Any of them, the chart is the same
   - C. A scale that starts at zero
   - D. Logarithmic, because the same percentage change takes up the same height at any price level

   *Answer D: Logarithmic, because the same percentage change takes up the same height at any price level.* On a linear scale, the drop at price 20 would be almost invisible and the one at price 400 would look like a crash. On a logarithmic scale both take up the same height, which is what matters for capital.

3. Why does the lesson's Plotly candlestick chart use `rangebreaks=[dict(bounds=["sat", "mon"])]`?

   - A. So the X axis does not leave an empty gap every weekend, when there is no session
   - B. To remove holidays
   - C. To change the time zone
   - D. To make the chart lighter

   *Answer A: So the X axis does not leave an empty gap every weekend, when there is no session.* With a continuous date axis, Plotly reserves space for Saturdays and Sundays. `rangebreaks` removes them; the holidays of a real market would have to be added with `values=[…]`.

4. Which of these charts is the most suitable for comparing the weights of four assets in a portfolio?

   - A. A 3D pie chart
   - B. A sorted horizontal bar chart
   - C. A line chart
   - D. A candlestick chart

   *Answer B: A sorted horizontal bar chart.* The eye compares lengths much better than angles or volumes. Pie charts and 3D distort proportions; sorted bars are read at a glance.

## Key points

- With the object-oriented API every chart is a `Figure` with one or more `Axes`; they are created with `plt.subplots()` and modified by calling each panel's methods.
- A long price series is drawn on a logarithmic scale, and the drawdown goes below, in a panel that shares the date axis.
- Seaborn draws distributions and comparisons by category in one line; pass it axes of your own (`ax=`) so it does not depend on global state.
- Plotly gives interactive charts (zoom, cursor) and candlesticks with `go.Candlestick`; `fig.write_html()` saves them to a file that opens in any browser.
- No pie charts, no 3D and no two crossing Y axes: each chart, one question, with its units and its period.

## Resources

### Research

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

### Documentation

- [Matplotlib: quick start guide (explicit and implicit styles)](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: candlestick charts in Python](https://plotly.com/python/candlestick-charts/) · plotly.com
- [mplfinance: candlesticks and OHLC on Matplotlib](https://github.com/matplotlib/mplfinance) · github.com

### Data

- [Level 0 data: daily prices of the fictitious ETF SINT (CSV)](https://pythonparatrading.com/curso/datos/nivel-0/sint_diario.csv) · Download · sint_diario.csv

Previous lesson: [0.5.5 · DuckDB and essential SQL: SELECT, JOIN, GROUP BY and window functions on Parquet](https://pythonparatrading.com/en/python-trading-course/level-0/duckdb-sql-on-parquet/)

Next lesson: [0.6.2 · Quick dashboards with Streamlit: a monitoring dashboard in a single script](https://pythonparatrading.com/en/python-trading-course/level-0/streamlit-dashboards/)
