Build Your First Candlestick Chart from Scratch with Python

AAPL candlestick chart built from OHLC data with Python and matplotlib

In the previous lesson, you built one candlestick from one row of OHLC data. Now we will do one new thing: repeat the same drawing logic for every row in a DataFrame.

one OHLC row
→ one candlestick

many OHLC rows
→ many candlesticks
→ one candlestick chart

We will still build the chart ourselves with matplotlib. No candlestick library will do the work for us yet.

What you will finish: a multi-day AAPL candlestick chart that appears on your screen and is also saved beside your Python file.

1. Start from the One-Candle Idea

In Phase 2-1, one candle needed only two drawing commands:

ax.vlines(...)
→ draw the wick from Low to High

ax.bar(...)
→ draw the body from Open to Close

We are not replacing that logic. We are going to repeat it.

row 0 → candle 0
row 1 → candle 1
row 2 → candle 2
...
row 19 → candle 19

That repeated work is exactly what a for loop is for.

2. Create a New Python File

Create:

candlestick_chart_from_scratch.py

As before, make the folder containing the Python file the working folder:

from pathlib import Path
import os

SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)

This keeps the code and the generated PNG together.

3. Get Several Days of AAPL Data

import FinanceDataReader as fdr
import matplotlib.pyplot as plt

symbol = "AAPL"
start_date = "2025-01-02"
end_date = "2025-01-31"

df = fdr.DataReader(symbol, start_date, end_date)

Before drawing, look at the data:

print(df[["Open", "High", "Low", "Close"]].head())

Each row still contains the four prices needed to draw one candle.

4. The New Building Block: a for Loop

Here is the key line:

for i, (date, row) in enumerate(df.iterrows()):

df.iterrows()

iterrows() gives Python one DataFrame row at a time.

first row
second row
third row
...

For each row, date is the DataFrame index and row contains Open, High, Low, Close, Volume, and the other columns.

enumerate(...)

enumerate() adds a counting number:

0
1
2
3
...

We store that number in i. It becomes the x-position of each candle.

i = 0 → first candle
i = 1 → second candle
i = 2 → third candle

5. Read OHLC Inside the Loop

open_price = float(row["Open"])
high_price = float(row["High"])
low_price = float(row["Low"])
close_price = float(row["Close"])

Then use the same direction and body calculations as before:

is_up_candle = close_price >= open_price
candle_color = "green" if is_up_candle else "red"

body_bottom = min(open_price, close_price)
body_height = abs(close_price - open_price)

6. Draw Each Candle at Its Own x-Position

The important change is that x is now i.

ax.vlines(
    x=i,
    ymin=low_price,
    ymax=high_price,
    color=candle_color,
    linewidth=1.5,
)
ax.bar(
    x=i,
    height=body_height,
    bottom=body_bottom,
    width=candle_width,
    color=candle_color,
    edgecolor=candle_color,
)

Because i changes every time the loop runs, each candle moves one position to the right.

7. Control the Space Between Candles

candle_width = 0.6

A smaller value makes thinner candle bodies. A larger value makes them wider. Keep 0.6 for now.

8. Put Dates on the x-Axis

If every date is printed, labels can overlap. So show one date every three candles:

date_tick_step = 3
tick_positions = list(range(0, len(df), date_tick_step))

This creates positions such as:

0, 3, 6, 9, 12, ...

Then convert those positions into readable dates:

tick_labels = [
    df.index[i].strftime("%Y-%m-%d")
    for i in tick_positions
]

strftime() changes a date into text. Here %Y is year, %m is month, and %d is day.

9. Make the Chart Easier to Read

A correct chart is not enough. The title, price numbers, and date labels also need to be readable.

figure_width = 12
figure_height = 7

title_fontsize = 20
label_fontsize = 14
tick_fontsize = 12

Whole chart size: figsize

fig, ax = plt.subplots(
    figsize=(figure_width, figure_height)
)

figsize controls the size of the whole figure. A wider figure gives many candles more horizontal room.

Title size: fontsize

ax.set_title(
    f"{symbol} Candlestick Chart",
    fontsize=title_fontsize,
)

Axis-label size

ax.set_ylabel(
    "Price",
    fontsize=label_fontsize,
)

Date and price-number size

ax.tick_params(
    axis="both",
    labelsize=tick_fontsize,
)
figsize
→ size of the whole chart

fontsize
→ title or axis-label text size

tick_params(labelsize=...)
→ date and price-number text size

There is no single perfect font size. The best value depends on the figure size and the number of candles.

10. Complete Code

from pathlib import Path
import os

import FinanceDataReader as fdr
import matplotlib.pyplot as plt

SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)

print("Working folder:")
print(SCRIPT_DIR)
print()

symbol = "AAPL"
start_date = "2025-01-02"
end_date = "2025-01-31"

candle_width = 0.6
figure_width = 12
figure_height = 7
title_fontsize = 20
label_fontsize = 14
tick_fontsize = 12
date_tick_step = 3

df = fdr.DataReader(symbol, start_date, end_date)

print("Rows downloaded:", len(df))
print()
print(df[["Open", "High", "Low", "Close"]].head())
print()

fig, ax = plt.subplots(
    figsize=(figure_width, figure_height)
)

for i, (date, row) in enumerate(df.iterrows()):
    open_price = float(row["Open"])
    high_price = float(row["High"])
    low_price = float(row["Low"])
    close_price = float(row["Close"])

    is_up_candle = close_price >= open_price
    candle_color = "green" if is_up_candle else "red"

    body_bottom = min(open_price, close_price)
    body_height = abs(close_price - open_price)

    ax.vlines(
        x=i,
        ymin=low_price,
        ymax=high_price,
        color=candle_color,
        linewidth=1.5,
    )

    ax.bar(
        x=i,
        height=body_height,
        bottom=body_bottom,
        width=candle_width,
        color=candle_color,
        edgecolor=candle_color,
    )

tick_positions = list(
    range(0, len(df), date_tick_step)
)

tick_labels = [
    df.index[i].strftime("%Y-%m-%d")
    for i in tick_positions
]

ax.set_xticks(tick_positions)
ax.set_xticklabels(
    tick_labels,
    rotation=45,
    ha="right",
)

ax.set_title(
    f"{symbol} Candlestick Chart",
    fontsize=title_fontsize,
)

ax.set_ylabel(
    "Price",
    fontsize=label_fontsize,
)

ax.tick_params(
    axis="both",
    labelsize=tick_fontsize,
)

ax.grid(axis="y", alpha=0.25)
ax.set_xlim(-1, len(df))

plt.tight_layout()

output_file = (
    SCRIPT_DIR
    / "candlestick_chart_from_scratch.png"
)

plt.savefig(
    output_file,
    dpi=160,
    bbox_inches="tight",
)

print("Chart saved:")
print(output_file)
print()
print("A chart window will now open.")
print("Close the chart window when you are finished viewing it.")

plt.show()

11. Run the File

python candlestick_chart_from_scratch.py

The terminal should show the working folder, the number of downloaded rows, and the first few OHLC rows. Then the chart appears on screen.

The script also saves:

candlestick_chart_from_scratch.png

in the same folder as the Python file.

your-practice-folder/
├─ candlestick_chart_from_scratch.py
└─ candlestick_chart_from_scratch.png

12. Change One Thing Yourself

Find:

tick_fontsize = 12

Change it to:

tick_fontsize = 16

Run the script again. The candles and market data do not change. Only the date and price-number text becomes larger.

market data
→ controls what the chart says

display settings
→ control how clearly you can read it

What You Just Learned

one DataFrame row
→ one wick + one body

for loop
→ repeat for every row

different x-position
→ candles line up across time

date labels
→ x-axis becomes readable

font settings
→ chart becomes easier to read

A candlestick chart is simply the one-candle building block repeated across time.

Where Do We Go Next?

You can now build the shape of a candlestick chart yourself. The next question is more important than drawing: what does one candle actually tell us about what happened during that trading period?

In the next lesson, we will return to one candle and learn how Open, High, Low, and Close describe the movement between buyers and sellers without jumping straight to patterns or predictions.