Hammer and Shooting Star were one-candle shapes. Engulfing introduces something new: we need two neighboring candles.
one-candle pattern
→ measure one candle
two-candle pattern
→ compare previous and current candles
This lesson also marks another transition in our Python code. The candle drawing code is now long enough that repeating every common line in every future lesson would hide the new idea.
So we will build one reusable base file. Nothing is hidden: the full base code and the full lesson code are both shown below.
1. Why Split the Code into Two Files?
Look at what keeps repeating:
download recent OHLC data
measure body and wicks
draw candlesticks
format dates
save PNG
show chart
Engulfing itself does not need to redefine all of those jobs.
The new idea in this lesson is only:
compare previous body
with current body
So our folder will contain:
practice-folder/
├─ alphesta_candlestick_core_v1.py
└─ alphesta_phase2_07_bullish_bearish_engulfing_python_v1_1.py
2. What Is a Python Module?
A Python file can contain reusable functions. Another Python file can import those functions and use them.
That reusable file is often called a module.
core file
→ reusable functions
lesson file
→ imports those functions
→ adds today's new logic
This is not about making the code mysterious. It is about keeping repeated code in one understandable place.
3. The Engulfing Rule Still Stays Visible
The main lesson function is:
def classify_engulfing(
previous_row,
current_row,
):
Bullish Engulfing:
previous bearish
AND
current bullish
AND
current Open <= previous Close
AND
current Close >= previous Open
Bearish Engulfing reverses those relationships.
4. Engulfing the Body Does Not Mean Engulfing the Wicks
This lesson compares the Open-Close bodies.
we require:
current body covers previous body
we do NOT require:
current High > previous High
AND
current Low < previous Low
Keeping that definition explicit matters because different sources can use different rules.
5. Complete Reusable Core Code
Create a new file named:
alphesta_candlestick_core_v1.py
Copy the complete code below into it.
from pathlib import Path
from datetime import date, timedelta
import os
import FinanceDataReader as fdr
import matplotlib.pyplot as plt
# ============================================================
# ALPHESTA CANDLESTICK CORE v1
# Reusable building blocks for Phase 2
# ============================================================
def set_working_folder(script_file):
"""
Use the folder containing the lesson Python file
as the working folder.
"""
script_dir = Path(script_file).resolve().parent
os.chdir(script_dir)
print("Working folder:")
print(script_dir)
print()
return script_dir
def download_recent_ohlc(
symbol,
calendar_days=220,
trading_days=120,
):
"""
Download recent daily OHLC data and keep
the most recent trading rows.
"""
today = date.today()
start_date = (
today - timedelta(days=calendar_days)
).strftime("%Y-%m-%d")
end_date = today.strftime("%Y-%m-%d")
df = fdr.DataReader(
symbol,
start_date,
end_date,
)
df = df.tail(trading_days)
if df.empty:
raise ValueError(
"No market data was downloaded."
)
print("Rows available:", len(df))
print(
"First date:",
df.index[0].strftime("%Y-%m-%d"),
)
print(
"Last date :",
df.index[-1].strftime("%Y-%m-%d"),
)
print()
return df
def measure_candle(row):
"""
Convert one OHLC row into reusable
candle measurements.
"""
open_price = float(row["Open"])
high_price = float(row["High"])
low_price = float(row["Low"])
close_price = float(row["Close"])
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
)
if full_range > 0:
body_share = (
body_size / full_range
)
body_bottom_position = (
(body_bottom - low_price)
/ full_range
)
body_top_position = (
(body_top - low_price)
/ full_range
)
else:
body_share = 0.0
body_bottom_position = 0.0
body_top_position = 0.0
if body_size > 0:
upper_wick_to_body = (
upper_wick / body_size
)
lower_wick_to_body = (
lower_wick / body_size
)
else:
upper_wick_to_body = None
lower_wick_to_body = None
return {
"open": open_price,
"high": high_price,
"low": low_price,
"close": close_price,
"body_top": body_top,
"body_bottom": body_bottom,
"body_size": body_size,
"full_range": full_range,
"upper_wick": upper_wick,
"lower_wick": lower_wick,
"body_share": body_share,
"body_bottom_position":
body_bottom_position,
"body_top_position":
body_top_position,
"upper_wick_to_body":
upper_wick_to_body,
"lower_wick_to_body":
lower_wick_to_body,
}
def make_chart_window(
dataframe,
target_position=None,
before=29,
after=10,
fallback_rows=40,
):
"""
Keep a small chart window around a target row.
If there is no target, use the latest rows.
"""
if target_position is None:
return dataframe.tail(
fallback_rows
).copy()
window_start = max(
0,
target_position - before,
)
window_end = min(
len(dataframe),
target_position + after + 1,
)
return dataframe.iloc[
window_start:window_end
].copy()
def draw_candlestick_chart(
dataframe,
output_file,
title,
highlight_dates=None,
highlight_label=None,
marker_side="below",
candle_width=0.6,
date_tick_step=4,
figure_width=11,
figure_height=6.5,
figure_dpi=100,
save_dpi=120,
title_fontsize=20,
label_fontsize=14,
tick_fontsize=10,
annotation_fontsize=11,
):
"""
Draw a candlestick chart from scratch.
highlight_dates can contain one or more dates.
"""
fig, ax = plt.subplots(
figsize=(
figure_width,
figure_height,
),
dpi=figure_dpi,
)
print(
"Figure size (inches):",
fig.get_size_inches(),
)
print(
"Figure DPI:",
fig.get_dpi(),
)
print()
# ----------------------------------------
# Draw every candlestick
# ----------------------------------------
for i, (
date_index,
row,
) in enumerate(
dataframe.iterrows()
):
m = measure_candle(row)
candle_color = (
"green"
if m["close"] >= m["open"]
else "red"
)
visible_body_height = (
m["body_size"]
if m["body_size"] > 0
else max(
m["full_range"] * 0.01,
0.01,
)
)
# Wick
ax.vlines(
x=i,
ymin=m["low"],
ymax=m["high"],
color=candle_color,
linewidth=1.6,
)
# Body
ax.bar(
x=i,
height=visible_body_height,
bottom=m["body_bottom"],
width=candle_width,
color=candle_color,
edgecolor=candle_color,
)
# ----------------------------------------
# Highlight one or more target candles
# ----------------------------------------
if highlight_dates:
valid_dates = [
d
for d in highlight_dates
if d in dataframe.index
]
if valid_dates:
x_values = [
dataframe.index.get_loc(d)
for d in valid_dates
]
pair_low = float(
dataframe.loc[
valid_dates,
"Low",
].min()
)
pair_high = float(
dataframe.loc[
valid_dates,
"High",
].max()
)
chart_range = (
float(
dataframe["High"].max()
)
- float(
dataframe["Low"].min()
)
)
marker_offset = (
chart_range * 0.04
if chart_range > 0
else 1
)
if marker_side == "above":
y_value = (
pair_high
+ marker_offset
)
text_y = (
pair_high
+ marker_offset * 3
)
marker = "v"
arrow_y = pair_high
else:
y_value = (
pair_low
- marker_offset
)
text_y = (
pair_low
- marker_offset * 3
)
marker = "^"
arrow_y = pair_low
ax.scatter(
x_values,
[
y_value
for _ in x_values
],
marker=marker,
s=75,
label=highlight_label,
)
if highlight_label:
center_x = (
min(x_values)
+ max(x_values)
) / 2
ax.annotate(
highlight_label,
xy=(
center_x,
arrow_y,
),
xytext=(
center_x,
text_y,
),
ha="center",
fontsize=
annotation_fontsize,
arrowprops=dict(
arrowstyle="->",
),
)
ax.legend(
fontsize=
annotation_fontsize,
)
# ----------------------------------------
# Date labels
# ----------------------------------------
tick_positions = list(
range(
0,
len(dataframe),
date_tick_step,
)
)
tick_labels = [
dataframe.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",
)
# ----------------------------------------
# Readability
# ----------------------------------------
ax.set_title(
title,
fontsize=title_fontsize,
)
ax.set_ylabel(
"Price",
fontsize=label_fontsize,
)
ax.tick_params(
axis="both",
labelsize=tick_fontsize,
)
ax.grid(
axis="y",
alpha=0.20,
)
ax.set_xlim(
-1,
len(dataframe),
)
# Avoid tight_layout() so memory use stays predictable.
fig.subplots_adjust(
left=0.10,
right=0.97,
top=0.90,
bottom=0.23,
)
fig.savefig(
output_file,
dpi=save_dpi,
)
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()
# Release figure memory.
plt.close(fig)
6. Complete Phase 2-7 Lesson Code
In the same folder, create:
alphesta_phase2_07_bullish_bearish_engulfing_python_v1_1.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-7
# Bullish and Bearish Engulfing
# ============================================================
# ------------------------------------------------------------
# 1. Settings
# ------------------------------------------------------------
symbol = "AAPL"
recent_trading_days = 120
# ------------------------------------------------------------
# 2. LESSON FUNCTION
# This is the new logic for Phase 2-7.
# ------------------------------------------------------------
def classify_engulfing(
previous_row,
current_row,
):
"""
Compare two neighboring candle bodies.
Returns:
"Bullish engulfing"
"Bearish engulfing"
None
"""
previous = measure_candle(
previous_row
)
current = measure_candle(
current_row
)
previous_is_bearish = (
previous["close"]
< previous["open"]
)
previous_is_bullish = (
previous["close"]
> previous["open"]
)
current_is_bullish = (
current["close"]
> current["open"]
)
current_is_bearish = (
current["close"]
< current["open"]
)
is_bullish_engulfing = (
previous_is_bearish
and current_is_bullish
and current["open"]
<= previous["close"]
and current["close"]
>= previous["open"]
)
is_bearish_engulfing = (
previous_is_bullish
and current_is_bearish
and current["open"]
>= previous["close"]
and current["close"]
<= previous["open"]
)
if is_bullish_engulfing:
return "Bullish engulfing"
if is_bearish_engulfing:
return "Bearish engulfing"
return None
# ------------------------------------------------------------
# 3. Set working folder
# ------------------------------------------------------------
SCRIPT_DIR = set_working_folder(
__file__
)
# ------------------------------------------------------------
# 4. Download recent OHLC data
# ------------------------------------------------------------
df = download_recent_ohlc(
symbol=symbol,
calendar_days=220,
trading_days=recent_trading_days,
)
if len(df) < 2:
raise ValueError(
"At least two trading days are needed "
"to compare neighboring candles."
)
# ------------------------------------------------------------
# 5. Scan every neighboring candle pair
# ------------------------------------------------------------
engulfing_candidates = []
for i in range(
1,
len(df),
):
previous_row = df.iloc[i - 1]
current_row = df.iloc[i]
pattern_name = classify_engulfing(
previous_row,
current_row,
)
if pattern_name is not None:
engulfing_candidates.append(
{
"type": pattern_name,
"previous_date":
df.index[i - 1],
"date":
df.index[i],
}
)
# ------------------------------------------------------------
# 6. Print the results
# ------------------------------------------------------------
print("Educational engulfing rule:")
print()
print("Bullish engulfing:")
print(
"1. Previous body is bearish"
)
print(
"2. Current body is bullish"
)
print(
"3. Current Open <= previous Close"
)
print(
"4. Current Close >= previous Open"
)
print()
print("Bearish engulfing:")
print(
"1. Previous body is bullish"
)
print(
"2. Current body is bearish"
)
print(
"3. Current Open >= previous Close"
)
print(
"4. Current Close <= previous Open"
)
print()
print(
"Engulfing candidates found:",
len(engulfing_candidates),
)
print()
for candidate in engulfing_candidates:
print(
candidate["date"].strftime(
"%Y-%m-%d"
),
"|",
candidate["type"],
"| previous:",
candidate[
"previous_date"
].strftime(
"%Y-%m-%d"
),
)
print()
# ------------------------------------------------------------
# 7. Select the most recent candidate
# ------------------------------------------------------------
target_position = None
highlight_dates = None
highlight_label = None
if engulfing_candidates:
target = engulfing_candidates[-1]
target_position = (
df.index.get_loc(
target["date"]
)
)
highlight_dates = [
target["previous_date"],
target["date"],
]
highlight_label = (
target["type"]
)
print(
"Most recent candidate:",
target["type"],
)
print(
"Previous date:",
target[
"previous_date"
].strftime(
"%Y-%m-%d"
),
)
print(
"Current date :",
target["date"].strftime(
"%Y-%m-%d"
),
)
print()
else:
print(
"No recent candle pair matched "
"this exact educational rule."
)
print(
"That is a valid result. "
"Do not force a pattern."
)
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
/ "engulfing_candlestick_candidate.png"
)
draw_candlestick_chart(
dataframe=chart_df,
output_file=output_file,
title=(
f"{symbol} — "
"Bullish and Bearish Engulfing"
),
highlight_dates=highlight_dates,
highlight_label=highlight_label,
marker_side="below",
)
7. Run the Lesson File
Run:
python alphesta_phase2_07_bullish_bearish_engulfing_python_v1_1.py
The program will:
download recent AAPL data
→ compare neighboring candles
→ print Engulfing candidates
→ select the most recent candidate
→ draw the chart
→ save the PNG
The image is saved beside the Python files as:
engulfing_candlestick_candidate.png
8. Change One Boundary Yourself
In classify_engulfing(), find:
current["open"]
<= previous["close"]
Change the inclusive comparison to:
current["open"]
< previous["close"]
Do the corresponding strict-boundary change for the opposite edge. Then rerun the program.
Ask:
Did fewer pairs qualify?
Which pairs disappeared?
Does equality matter
in recent AAPL data?
9. Why This Structure Helps the Next Lessons
The reusable core can now remain unchanged.
Phase 2-8 can replace the lesson-specific logic with something like:
classify_context_pattern(...)
Phase 2-9 can focus on:
is_doji(...)
The reader still has the complete working program:
shared core
+
complete lesson file
=
complete runnable experiment
What You Just Learned
repeated mechanics
→ reusable module
new candlestick idea
→ small lesson function
two neighboring candles
→ Engulfing classification
This is the structure we will reuse through the rest of Phase 2.