What Is a Doji? How Small Is “Small” in Python?

AFTER RUNNING THE PYTHON CODE AND UPLOADING THE PNG TO BLOGGER, INSERT THE REAL IMAGE HERE AS THE FIRST BODY IMAGE.
Recent AAPL candlestick chart with a Doji candidate found by a Python body-size threshold

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