A Doji is often described as a candle with almost no body.
But Python cannot understand the word almost. We have to turn it into a number.
“very small body”
↓
measure the body
↓
compare it with the full candle range
↓
choose a threshold
This is an important step. We are turning a visual idea into a rule that Python can test.
1. Keep the Same Core from Phase 2-7
Put these two files in the same folder:
practice-folder/
├─ alphesta_candlestick_core_v1.py
└─ phase2_09_doji_threshold.py
The Core still handles the repeated jobs:
download recent OHLC data
measure the candle
draw the chart
save the PNG
2. Which File Should You Run?
Keep both files in the same folder, but run the Phase 2-9 lesson file, not the Core file.
DO NOT run directly:
alphesta_candlestick_core_v1.py
RUN this file:
phase2_09_doji_threshold.py
If you use the terminal:
python phase2_09_doji_threshold.py
3. Why Open == Close Is Too Strict
We could define a Doji like this:
Open == Close
But real market prices are continuous numbers. A candle can have a tiny body even when Open and Close are not exactly equal.
A more useful question is:
How large is the body
compared with the whole candle?
4. Measure Body Share
The Core already gives us:
body_size
full_range
body_share
The idea is:
body_share
=
body_size / full_range
Suppose:
body size = 1
full range = 10
Then:
body share = 1 / 10
= 0.10
= 10%
5. Turn “Small” into a Threshold
We will begin with:
max_doji_body_share = 0.10
That means:
body uses 10% or less
of the full High-Low range
→ Doji candidate
The word candidate matters. The value 0.10 is a rule for this experiment, not a law of the market.
6. The New Function Is Small
Because the repeated work is already in the Core, the new idea fits inside one short function:
def is_doji(
measurements,
max_body_share,
):
...
Its main test is:
measurements["body_share"]
<=
max_body_share
If that condition is true, the function returns True.
Otherwise, it returns False.
7. Why We Check the Full Range First
The ratio only makes sense when:
High - Low > 0
So the function first checks:
if measurements["full_range"] <= 0:
return False
This prevents a zero-range candle from being classified as a Doji just because its body is also zero.
8. Complete Phase 2-9 Python Code
Keep alphesta_candlestick_core_v1.py in the same folder.
Create:
phase2_09_doji_threshold.py
Copy the complete code below.
from alphesta_candlestick_core_v1 import (
set_working_folder,
download_recent_ohlc,
measure_candle,
make_chart_window,
draw_candlestick_chart,
)
# ============================================================
# Phase 2-9
# What Is a Doji?
# How Small Is "Small" in Python?
# ============================================================
# ------------------------------------------------------------
# 1. Settings
# ------------------------------------------------------------
symbol = "AAPL"
recent_trading_days = 120
# 0.10 means:
# body size <= 10% of the full High-Low range.
max_doji_body_share = 0.10
# ------------------------------------------------------------
# 2. LESSON FUNCTION
# Turn "a very small body" into a visible rule.
# ------------------------------------------------------------
def is_doji(
measurements,
max_body_share,
):
"""
Return True when the candle body is small enough
relative to the full High-Low range.
This is an educational threshold rule,
not a universal market standard.
"""
# A zero-range candle has no usable
# High-Low range for this ratio.
if measurements["full_range"] <= 0:
return False
return (
measurements["body_share"]
<= max_body_share
)
# ------------------------------------------------------------
# 3. Set the working folder
# ------------------------------------------------------------
SCRIPT_DIR = set_working_folder(
__file__
)
# ------------------------------------------------------------
# 4. Download recent OHLC data
# ------------------------------------------------------------
df = download_recent_ohlc(
symbol=symbol,
calendar_days=260,
trading_days=recent_trading_days,
)
# ------------------------------------------------------------
# 5. Scan recent candles
# ------------------------------------------------------------
doji_candidates = []
for i in range(len(df)):
row = df.iloc[i]
date_index = df.index[i]
measurements = measure_candle(
row
)
if is_doji(
measurements=measurements,
max_body_share=max_doji_body_share,
):
doji_candidates.append(
{
"date": date_index,
"measurements": measurements,
}
)
# ------------------------------------------------------------
# 6. Print the rule and matches
# ------------------------------------------------------------
print("Doji rule:")
print()
print(
"body share <=",
f"{max_doji_body_share:.2f}",
)
print(
"which means body size <=",
f"{max_doji_body_share * 100:.0f}%",
"of the full High-Low range",
)
print()
print(
"Doji candidates found:",
len(doji_candidates),
)
print()
for candidate in doji_candidates:
m = candidate["measurements"]
print(
candidate["date"].strftime(
"%Y-%m-%d"
),
"| body share:",
f'{m["body_share"]:.3f}',
"| body:",
f'{m["body_size"]:.2f}',
"| range:",
f'{m["full_range"]:.2f}',
)
print()
# ------------------------------------------------------------
# 7. Select the most recent candidate
# ------------------------------------------------------------
target_position = None
target_date = None
target_label = None
if doji_candidates:
target = doji_candidates[-1]
target_date = target["date"]
target_position = (
df.index.get_loc(
target_date
)
)
m = target["measurements"]
target_label = (
"Doji candidate\n"
f'body share: {m["body_share"]:.1%}'
)
print("Most recent candidate:")
print(
"Date:",
target_date.strftime(
"%Y-%m-%d"
),
)
print(
"Open :",
f'{m["open"]:.2f}',
)
print(
"High :",
f'{m["high"]:.2f}',
)
print(
"Low :",
f'{m["low"]:.2f}',
)
print(
"Close:",
f'{m["close"]:.2f}',
)
print(
"Body size:",
f'{m["body_size"]:.2f}',
)
print(
"Full range:",
f'{m["full_range"]:.2f}',
)
print(
"Body share:",
f'{m["body_share"]:.1%}',
)
print()
else:
print(
"No recent candle matched "
"this exact Doji threshold."
)
print(
"That is a valid result. "
"Do not loosen the rule only "
"to force a candidate."
)
print()
# ------------------------------------------------------------
# 8. Build a small chart window
# ------------------------------------------------------------
chart_df = make_chart_window(
dataframe=df,
target_position=target_position,
before=29,
after=10,
fallback_rows=40,
)
# ------------------------------------------------------------
# 9. Draw and save the chart
# ------------------------------------------------------------
output_file = (
SCRIPT_DIR
/ "doji_candlestick_candidate.png"
)
highlight_dates = (
[target_date]
if target_date is not None
else None
)
draw_candlestick_chart(
dataframe=chart_df,
output_file=output_file,
title=(
f"{symbol} — "
"Doji Threshold Experiment"
),
highlight_dates=highlight_dates,
highlight_label=target_label,
marker_side="below",
)
9. Run the Program
Run the lesson file:
python phase2_09_doji_threshold.py
The program will:
download recent AAPL data
→ measure each candle
→ calculate body share
→ apply the Doji threshold
→ print the candidates
→ highlight the latest candidate
→ save the chart
The image is saved as:
doji_candlestick_candidate.png
10. Change the Threshold Yourself
Start with:
max_doji_body_share = 0.10
Then try:
max_doji_body_share = 0.05
and:
max_doji_body_share = 0.15
Each time, rerun the program and compare:
How many candidates appear?
Which candles disappear at 5%?
Which extra candles appear at 15%?
A smaller threshold is stricter. A larger threshold is looser.
11. This Is Bigger Than Doji
The important lesson is not just one candlestick name.
continuous number
→ ratio
→ threshold
→ classification
We will use this structure again when we begin technical indicators.
What You Just Learned
Doji
≠ Open must equal Close exactly
Doji candidate
= very small body
defined with an explicit ratio threshold
You also learned that a threshold is a parameter. You can change it, rerun the same code, and observe what changes.
Where Do We Go Next?
We now have several pattern rules written as Python logic. In Phase 2-10, we will bring them together into one small candlestick pattern scanner.
one pattern rule
→ reusable function
many pattern functions
→ pattern scanner
Previous: Phase 2-8 — Same Shape, Different Meaning: Hammer, Hanging Man, Inverted Hammer, and Shooting Star
Next: Phase 2-10 — Build Your First Candlestick Pattern Scanner with Python