Same Shape, Different Meaning: Hammer, Hanging Man, Inverted Hammer, and Shooting Star

Recent AAPL chart showing Hammer Hanging Man Inverted Hammer or Shooting Star classified by shape and preceding price context

A long lower wick can be called a Hammer. The same shape can also be called a Hanging Man.

A long upper wick can be called an Inverted Hammer. The same shape can also be called a Shooting Star.

The missing idea is context.

shape
+
what price was doing before the candle
=
pattern name

Important: In this lesson, preceding context always means what price was doing before the pattern candle appeared. It does not mean the direction price may move afterward.

Hammer
preceding context: falling
possible reversal afterward: bullish

Shooting Star
preceding context: rising
possible reversal afterward: bearish

In this lesson, we will keep the reusable candlestick core from Phase 2-7 and replace only the lesson-specific logic.

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_08_same_shape_different_context.py

2. Which File Should You Run?

You need both Python files in the same folder, but you do not run the Core file directly.

DO NOT run directly:
alphesta_candlestick_core_v1.py

RUN this file:
phase2_08_same_shape_different_context.py

Think of alphesta_candlestick_core_v1.py as a reusable toolbox. It contains functions that the Phase 2-8 lesson file imports and uses.

alphesta_candlestick_core_v1.py
        │
        │ imported by
        ▼
phase2_08_same_shape_different_context.py
        │
        ▼
downloads recent data
uses Core functions
runs Phase 2-8 logic
draws the chart
saves the PNG

So when you study Phase 2-8, open both files if you want to inspect the code, but press Run on phase2_08_same_shape_different_context.py.

If you use the terminal, run:

python phase2_08_same_shape_different_context.py

The lesson file starts with:

from alphesta_candlestick_core_v1 import (
    set_working_folder,
    download_recent_ohlc,
    measure_candle,
    make_chart_window,
    draw_candlestick_chart,
)

This means: "bring these reusable functions from the Core file into this lesson." The Core supports the lesson, while the lesson file controls what experiment is performed.

3. What Does the Core Do?

The core already handles:

recent OHLC download
candle measurements
chart-window selection
candlestick drawing
memory-safe matplotlib settings
PNG saving

We do not need to rewrite those mechanics.

4. What Changes in Phase 2-8?

Two small pieces of lesson logic are new:

get_preceding_context()

classify_preceding_context_pattern()

The first function describes what price was doing before the pattern candle. The second combines that preceding context with the candle shape.

We deliberately use the Python name preceding_context instead of the shorter context. This prevents us from confusing the direction before the candle with the possible reversal direction after it.

5. Same Lower-Wick Shape, Two Names

long lower wick
small upper wick
body near the top

That geometry can become:

falling context
→ Hammer

rising context
→ Hanging Man

6. Same Upper-Wick Shape, Two Names

long upper wick
small lower wick
body near the bottom

That geometry can become:

falling context
→ Inverted Hammer

rising context
→ Shooting Star

7. The Short-Side Patterns Become Clearer

Hanging Man and Shooting Star are commonly discussed after rising prices as bearish reversal candidates.

Hanging Man
→ rising context
→ bearish reversal candidate

Shooting Star
→ rising context
→ bearish reversal candidate

But neither label automatically means:

enter a short position now

We still have not tested confirmation, future return, resistance, volume, or risk management.

8. Why Inverted Hammer Is Different

An Inverted Hammer looks similar to a Shooting Star because both have a long upper wick.

But the preceding context is different:

Inverted Hammer
→ after falling context
→ bullish reversal candidate

Shooting Star
→ after rising context
→ bearish reversal candidate

This is exactly why the program should keep shape and context separate.

9. Define a Simple Preceding-Context Rule

Python cannot work with the vague phrase "after an uptrend" until we define what that means.

For this beginner experiment:

preceding_context_bars = 5

We compare the Close immediately before the pattern candle with the Close five rows earlier.

recent prior Close > earlier Close
→ rising

recent prior Close < earlier Close
→ falling

equal
→ flat

This is intentionally simple. It is a visible assumption, not a universal definition of trend.

10. The Preceding-Context Function

def get_preceding_context(
    dataframe,
    row_number,
    bars,
):
    ...

This is a small helper function. It receives the DataFrame, the current row number, and the size of the context window.

It returns one word:

"rising"
"falling"
"flat"

11. The Pattern Function

The main new function is:

def classify_preceding_context_pattern(
    measurements,
    context,
):
    ...

The core already gives us measurements such as:

upper_wick_to_body
lower_wick_to_body
body_bottom_position
body_top_position

The lesson function only has to combine those values with context.

12. Complete Phase 2-8 Python Code

Keep alphesta_candlestick_core_v1.py from Phase 2-7 in the same folder.

Create:

phase2_08_same_shape_different_context.py

and 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-8
# Same Shape, Different Meaning
# Hammer, Hanging Man, Inverted Hammer, Shooting Star
# ============================================================


# ------------------------------------------------------------
# 1. Settings
# ------------------------------------------------------------

symbol = "AAPL"

recent_trading_days = 120

# How many earlier bars are used
# to describe the preceding price context?
preceding_context_bars = 5

# Shared shape thresholds
min_long_wick_to_body = 2.0
max_short_wick_to_body = 1.0

# Long-lower-wick shape:
# body should sit high in the full candle range.
min_body_bottom_position = 0.60

# Long-upper-wick shape:
# body should sit low in the full candle range.
max_body_top_position = 0.40


# ------------------------------------------------------------
# 2. LESSON HELPER
#    Read the price context BEFORE the pattern candle.
# ------------------------------------------------------------

def get_preceding_context(
    dataframe,
    row_number,
    bars,
):
    """
    Compare the Close immediately before the
    pattern candle with the Close several bars earlier.

    Returns:
        "rising"
        "falling"
        "flat"
    """

    recent_close = float(
        dataframe.iloc[
            row_number - 1
        ]["Close"]
    )

    earlier_close = float(
        dataframe.iloc[
            row_number - bars
        ]["Close"]
    )

    if recent_close > earlier_close:
        return "rising"

    if recent_close < earlier_close:
        return "falling"

    return "flat"


# ------------------------------------------------------------
# 3. LESSON FUNCTION
#    Same shape + different preceding context
#    = different candlestick name
# ------------------------------------------------------------

def classify_preceding_context_pattern(
    measurements,
    preceding_context,
):
    """
    Combine candle geometry with the price context
    that existed BEFORE the pattern candle.

    Returns:
        "Hammer"
        "Hanging Man"
        "Inverted Hammer"
        "Shooting Star"
        None
    """

    # Wick/body ratios are undefined
    # when the body has zero height.
    if (
        measurements[
            "upper_wick_to_body"
        ] is None
        or measurements[
            "lower_wick_to_body"
        ] is None
    ):
        return None

    # ----------------------------------------
    # Shape A: long lower wick,
    #          small upper wick,
    #          body near the top.
    # ----------------------------------------

    is_lower_wick_shape = (
        measurements[
            "lower_wick_to_body"
        ]
        >= min_long_wick_to_body

        and measurements[
            "upper_wick_to_body"
        ]
        <= max_short_wick_to_body

        and measurements[
            "body_bottom_position"
        ]
        >= min_body_bottom_position
    )

    # ----------------------------------------
    # Shape B: long upper wick,
    #          small lower wick,
    #          body near the bottom.
    # ----------------------------------------

    is_upper_wick_shape = (
        measurements[
            "upper_wick_to_body"
        ]
        >= min_long_wick_to_body

        and measurements[
            "lower_wick_to_body"
        ]
        <= max_short_wick_to_body

        and measurements[
            "body_top_position"
        ]
        <= max_body_top_position
    )

    # ----------------------------------------
    # Same lower-wick shape,
    # different PRECEDING context.
    # ----------------------------------------

    if is_lower_wick_shape:

        if preceding_context == "falling":
            return "Hammer"

        if preceding_context == "rising":
            return "Hanging Man"

    # ----------------------------------------
    # Same upper-wick shape,
    # different PRECEDING context.
    # ----------------------------------------

    if is_upper_wick_shape:

        if preceding_context == "falling":
            return "Inverted Hammer"

        if preceding_context == "rising":
            return "Shooting Star"

    return None


# ------------------------------------------------------------
# 4. Set the working folder
# ------------------------------------------------------------

SCRIPT_DIR = set_working_folder(
    __file__
)


# ------------------------------------------------------------
# 5. Download recent OHLC data
# ------------------------------------------------------------

df = download_recent_ohlc(
    symbol=symbol,
    calendar_days=260,
    trading_days=recent_trading_days,
)

if len(df) <= preceding_context_bars:
    raise ValueError(
        "Not enough market data was downloaded "
        "for the selected preceding-context window."
    )


# ------------------------------------------------------------
# 6. Scan recent candles
# ------------------------------------------------------------

pattern_candidates = []

for i in range(
    preceding_context_bars,
    len(df),
):
    row = df.iloc[i]
    date_index = df.index[i]

    measurements = measure_candle(
        row
    )

    preceding_context = (
        get_preceding_context(
            dataframe=df,
            row_number=i,
            bars=preceding_context_bars,
        )
    )

    pattern_name = (
        classify_preceding_context_pattern(
            measurements=measurements,
            preceding_context=preceding_context,
        )
    )

    if pattern_name is not None:
        pattern_candidates.append(
            {
                "date": date_index,
                "pattern": pattern_name,
                "preceding_context":
                    preceding_context,
                "measurements":
                    measurements,
            }
        )


# ------------------------------------------------------------
# 7. Print the four-way map and matches
# ------------------------------------------------------------

print("Shape + preceding context map:")
print()

print(
    "Long lower wick + falling "
    "preceding context -> Hammer"
)

print(
    "Long lower wick + rising "
    "preceding context  -> Hanging Man"
)

print(
    "Long upper wick + falling "
    "preceding context -> Inverted Hammer"
)

print(
    "Long upper wick + rising "
    "preceding context  -> Shooting Star"
)

print()

print(
    "Pattern candidates found:",
    len(pattern_candidates),
)

print()

for candidate in pattern_candidates:

    print(
        candidate["date"].strftime(
            "%Y-%m-%d"
        ),
        "|",
        candidate["pattern"],
        "| preceding context:",
        candidate["preceding_context"],
    )

print()


# ------------------------------------------------------------
# 8. Select the most recent candidate
# ------------------------------------------------------------

target_position = None
target_date = None
target_label = None
marker_side = "below"

if pattern_candidates:

    target = pattern_candidates[-1]

    target_date = target["date"]

    target_position = (
        df.index.get_loc(
            target_date
        )
    )

    target_label = (
        f'{target["pattern"]}\n'
        f'preceding context: '
        f'{target["preceding_context"]}'
    )

    # Put the marker above upper-wick patterns
    # and below lower-wick patterns.
    if target["pattern"] in (
        "Inverted Hammer",
        "Shooting Star",
    ):
        marker_side = "above"

    print("Most recent candidate:")
    print(
        "Pattern:",
        target["pattern"],
    )
    print(
        "Date   :",
        target_date.strftime(
            "%Y-%m-%d"
        ),
    )
    print(
        "Preceding context:",
        target["preceding_context"],
    )
    print()

    m = target["measurements"]

    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()

    print(
        "Upper wick / body:",
        f'{m["upper_wick_to_body"]:.2f}',
    )
    print(
        "Lower wick / body:",
        f'{m["lower_wick_to_body"]:.2f}',
    )
    print()

else:

    print(
        "No recent candle matched "
        "the full shape + preceding-context rules."
    )

    print(
        "That is a valid result. "
        "Do not force a pattern."
    )

    print()


# ------------------------------------------------------------
# 9. Build a small chart window
# ------------------------------------------------------------

chart_df = make_chart_window(
    dataframe=df,
    target_position=target_position,
    before=29,
    after=10,
    fallback_rows=40,
)


# ------------------------------------------------------------
# 10. Draw and save the chart
# ------------------------------------------------------------

output_file = (
    SCRIPT_DIR
    / "same_shape_different_context.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} — "
        "Same Shape, Different Meaning"
    ),
    highlight_dates=highlight_dates,
    highlight_label=target_label,
    marker_side=marker_side,
)

13. Run the Program

Remember: run the Phase 2-8 lesson file, not the Core file. Both files must be in the same folder.

python phase2_08_same_shape_different_context.py

Do not use:

python alphesta_candlestick_core_v1.py

The lesson file automatically imports the Core functions it needs.

The program will:

download recent AAPL data
→ measure each candle
→ measure preceding context
→ combine shape + preceding context
→ print pattern candidates
→ highlight the latest candidate
→ save the chart

The image is saved as:

same_shape_different_context.png

14. Change the Context Window Yourself

Change:

preceding_context_bars = 5

to:

preceding_context_bars = 10

Then rerun the code.

Ask:

Did the same candle keep the same context?

Did a Hammer become a Hanging Man?

Did an Inverted Hammer become a Shooting Star?

Did some candidates disappear?

This shows that preceding context is also a parameter.

What You Just Learned

geometry
→ what the candle looks like

context
→ what price was doing before it

geometry + context
→ pattern name

You also used the modular structure introduced in Phase 2-7:

shared core
+
complete lesson file
=
complete experiment

Where Do We Go Next?

The next candle is different. A Doji has almost no body at all.

That raises a useful research question: how small does a body have to be before we call it small?

Phase 2-9 will turn that question into a threshold we can change and test.