You can now draw one candlestick. You can also repeat that drawing and build a full candlestick chart.
But being able to draw a candle is not the same as being able to read one.
So in this lesson, we will slow down and ask a simple question: what does one candlestick actually tell us?
We will use a recent AAPL daily candle, measure it with Python, and connect every part of the shape back to Open, High, Low, and Close.
We will not name any candlestick patterns yet. First, we want to understand the raw information.
1. One Candle Is One Time Interval
A daily candlestick summarizes one trading day.
Open
→ where the interval started
High
→ highest price reached
Low
→ lowest price reached
Close
→ where the interval ended
If you change the timeframe, the idea stays the same. A 15-minute candle summarizes 15 minutes. A weekly candle summarizes one week.
In this lesson, we will stay with daily data so the idea is easy to see.
2. Use Recent Market Data
Instead of hard-coding an old date, let Python build a recent date range when the script runs.
from datetime import date, timedelta
today = date.today()
start_date = (
today - timedelta(days=60)
).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
There are three small ideas here.
date.today()
This gives Python today's calendar date.
timedelta(days=60)
This represents a period of 60 calendar days. We ask for more calendar days than we need because markets do not trade every day.
strftime("%Y-%m-%d")
This converts the date into text such as:
2026-08-08
That is the format FinanceDataReader can use.
3. Keep the Most Recent 30 Trading Days
Download the data:
df = fdr.DataReader(
symbol,
start_date,
end_date,
)
Then keep only the most recent 30 rows:
df = df.tail(30)
tail(30) means:
give me the last 30 rows
Because daily market data normally has one row per trading day, this gives us the most recent available trading days rather than a fixed historical example.
4. Select the Most Recent Available Candle
In earlier lessons, you selected a row with:
df.iloc[0]
That means the first row.
Now use:
day = df.iloc[-1]
A negative index counts backward from the end.
df.iloc[-1]
→ last row
df.iloc[-2]
→ one row before the last
df.iloc[-3]
→ two rows before the last
So df.iloc[-1] gives us the most recent row returned by the data source.
5. The Body Tells You Where the Interval Started and Ended
Read Open and Close:
open_price = float(day["Open"])
close_price = float(day["Close"])
The body sits between those two prices.
body_top = max(open_price, close_price)
body_bottom = min(open_price, close_price)
body_size = abs(
close_price - open_price
)
If Close is above Open, the interval ended higher than it started. If Close is below Open, it ended lower.
Close > Open
→ up candle
Close < Open
→ down candle
This does not mean price moved in only one direction during the interval. The High and Low will show us that the path was wider.
6. High and Low Tell You the Full Range
Read:
high_price = float(day["High"])
low_price = float(day["Low"])
Then measure the entire price range:
full_range = high_price - low_price
Think of it this way:
High
│
│ full price range
│
Low
The body is only one part of that range. Price may have traveled above and below the body before the interval ended.
7. What Is the Upper Wick?
The upper wick is the distance from the top of the body to the High.
upper_wick = (
high_price - body_top
)
This tells us how far price traded above both the Open and Close before finishing the interval.
It is evidence that higher prices were reached. By itself, it does not tell us why price moved back from that level.
8. What Is the Lower Wick?
The lower wick is the distance from the Low to the bottom of the body.
lower_wick = (
body_bottom - low_price
)
It tells us how far price traded below both Open and Close during the interval.
Again, the wick records what happened. It is not automatically a prediction of what happens next.
9. Compare the Body with the Full Range
We can ask one more useful question: how much of the candle's full High-to-Low range is occupied by the Open-to-Close body?
body_share = (
body_size / full_range * 100
)
For example, if Python prints:
Body share: 70.0% of full range
most of the candle's total range lies inside the body.
If it prints:
Body share: 15.0% of full range
the body is small relative to the full High-to-Low movement, so the wicks make up a larger part of the candle.
We are still describing the candle, not assigning a pattern name.
10. Print the Candle Before You Interpret It
The script prints:
Date
Open
High
Low
Close
Direction
Full range
Body size
Upper wick
Lower wick
Body share
This is useful because it keeps our interpretation tied to actual numbers.
numbers first
→ shape second
→ interpretation third
That order will become increasingly important later when we study indicators and trading rules.
11. Draw the Candle and Label OHLC
We still draw the candle from scratch.
The wick:
ax.vlines(
x=0,
ymin=low_price,
ymax=high_price,
color=candle_color,
linewidth=3,
)
The body:
ax.bar(
x=0,
height=visible_body_height,
bottom=body_bottom,
width=0.55,
color=candle_color,
edgecolor=candle_color,
)
Then we add horizontal guides for High, Open, Close, and Low. This makes it easy to connect the visual shape to the four prices.
12. Keep the Text Large Enough to Read
We will keep readability settings near the top of the file:
figure_width = 8
figure_height = 8
title_fontsize = 20
label_fontsize = 14
tick_fontsize = 12
annotation_fontsize = 12
The new setting here is:
annotation_fontsize = 12
It controls the OHLC labels and the small explanation box inside the figure.
If those labels are hard to read on your screen, try:
annotation_fontsize = 16
13. Complete Code
from pathlib import Path
from datetime import date, timedelta
import os
import FinanceDataReader as fdr
import matplotlib.pyplot as plt
SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)
symbol = "AAPL"
today = date.today()
start_date = (
today - timedelta(days=60)
).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
recent_trading_days = 30
figure_width = 8
figure_height = 8
title_fontsize = 20
label_fontsize = 14
tick_fontsize = 12
annotation_fontsize = 12
df = fdr.DataReader(
symbol,
start_date,
end_date,
)
df = df.tail(recent_trading_days)
if df.empty:
raise ValueError(
"No market data was downloaded."
)
day = df.iloc[-1]
date_label = (
df.index[-1]
.strftime("%Y-%m-%d")
)
open_price = float(day["Open"])
high_price = float(day["High"])
low_price = float(day["Low"])
close_price = float(day["Close"])
is_up_candle = (
close_price >= open_price
)
direction = (
"Up candle"
if is_up_candle
else "Down candle"
)
candle_color = (
"green"
if is_up_candle
else "red"
)
body_top = max(
open_price,
close_price,
)
body_bottom = min(
open_price,
close_price,
)
body_size = abs(
close_price - open_price
)
full_range = (
high_price - low_price
)
upper_wick = (
high_price - body_top
)
lower_wick = (
body_bottom - low_price
)
body_share = (
body_size / full_range * 100
if full_range > 0
else 0
)
print("Date :", date_label)
print("Open :", open_price)
print("High :", high_price)
print("Low :", low_price)
print("Close :", close_price)
print("Direction :", direction)
print()
print("Full range :", full_range)
print("Body size :", body_size)
print("Upper wick :", upper_wick)
print("Lower wick :", lower_wick)
print(
"Body share :",
f"{body_share:.1f}% of full range"
)
fig, ax = plt.subplots(
figsize=(
figure_width,
figure_height,
)
)
ax.vlines(
x=0,
ymin=low_price,
ymax=high_price,
color=candle_color,
linewidth=3,
)
visible_body_height = (
body_size
if body_size > 0
else max(
full_range * 0.01,
0.01,
)
)
ax.bar(
x=0,
height=visible_body_height,
bottom=body_bottom,
width=0.55,
color=candle_color,
edgecolor=candle_color,
)
guide_values = [
("High", high_price),
("Open", open_price),
("Close", close_price),
("Low", low_price),
]
for label, value in guide_values:
ax.hlines(
y=value,
xmin=-0.75,
xmax=0.75,
linewidth=1,
alpha=0.35,
)
ax.text(
0.82,
value,
f"{label}: {value:.2f}",
va="center",
fontsize=annotation_fontsize,
)
summary_text = (
f"{direction}\n"
f"Body: {body_size:.2f}\n"
f"Upper wick: {upper_wick:.2f}\n"
f"Lower wick: {lower_wick:.2f}\n"
f"Body share: {body_share:.1f}%"
)
ax.text(
0.03,
0.97,
summary_text,
transform=ax.transAxes,
va="top",
fontsize=annotation_fontsize,
bbox=dict(
boxstyle="round,pad=0.5",
alpha=0.08,
),
)
padding = (
full_range * 0.15
if full_range > 0
else 1
)
ax.set_xlim(-1.0, 1.65)
ax.set_ylim(
low_price - padding,
high_price + padding,
)
ax.set_xticks([0])
ax.set_xticklabels(
[date_label],
fontsize=tick_fontsize,
)
ax.set_ylabel(
"Price",
fontsize=label_fontsize,
)
ax.set_title(
f"{symbol} — Reading One Candlestick",
fontsize=title_fontsize,
)
ax.tick_params(
axis="y",
labelsize=tick_fontsize,
)
ax.grid(
axis="y",
alpha=0.20,
)
plt.tight_layout()
output_file = (
SCRIPT_DIR
/ "one_candlestick_explained.png"
)
plt.savefig(
output_file,
dpi=160,
bbox_inches="tight",
)
print()
print("Chart saved:")
print(output_file)
plt.show()
14. Change One Candle Yourself
Find:
day = df.iloc[-1]
and:
date_label = df.index[-1].strftime(
"%Y-%m-%d"
)
Change both -1 values to -2.
day = df.iloc[-2]
date_label = df.index[-2].strftime(
"%Y-%m-%d"
)
Run the program again.
Now compare:
body size
upper wick
lower wick
direction
You are no longer just looking at candles. You are measuring how their shapes come from market data.
What One Candlestick Can Tell You
Open vs Close
→ where the interval started and ended
High vs Low
→ the full range traveled
body size
→ distance between Open and Close
upper wick
→ movement above the body
lower wick
→ movement below the body
That is already useful information.
But one candle does not tell you the exact path price took inside the interval, and it does not guarantee what the next candle will do.
This is why we should understand individual candles first, then look at how several candles relate to one another.
Where Do We Go Next?
In the next lesson, we will stop looking at one candle in isolation and begin reading a sequence of candles.
We will ask: how can we describe rising and falling price without adding an indicator yet?