We have learned many ways to transform price.
Momentum
→ price change
RSI
→ gains versus losses
ATR
→ movement magnitude
ADX
→ directional strength
Standard Deviation
→ dispersion
Bollinger Bands
→ center + dispersion
The Stochastic Oscillator asks a different question:
Where is the current Close
inside the recent
High-Low range?
That simple idea produces a 0–100 oscillator.
1. Start with the Recent High-Low Range
Highest High = 110
Lowest Low = 90
Current Close = 108
The recent range spans:
110 - 90
=
20
2. Measure the Close from the Bottom of the Range
Current Close
-
Lowest Low
=
108 - 90
=
18
3. Normalize That Position to 0–100
%K
=
100 ×
(Close - Lowest Low)
--------------------
(Highest High - Lowest Low)
With the example:
%K
=
100 ×
18 / 20
=
90
The Close is near the upper end of the recent range.
4. %K = 90 Does Not Mean “Price Is Rising at 90% Speed”
%K = 90
means:
Close is 90% of the way
from the recent Lowest Low
to the recent Highest High
It measures normalized position, not velocity.
5. What Do 0 and 100 Mean?
%K = 100
→ Close is at the recent Highest High
%K = 0
→ Close is at the recent Lowest Low
%K = 50
→ Close is halfway through
the recent High-Low range
6. The First %K Appears Only After the Lookback Exists
With a 14-bar lookback:
first 13 bars
→ not enough history
→ %K = None
bar 14
→ first complete range
→ first %K
7. Build Fast %K from Scratch
def stochastic_fast_k(
high_values,
low_values,
close_values,
period,
):
...
highest_high = max(
high_window
)
lowest_low = min(
low_window
)
percent_k = (
100.0
* (
close_now
- lowest_low
)
/ (
highest_high
- lowest_low
)
)
8. What If Highest High Equals Lowest Low?
Then the denominator is zero.
Highest High
-
Lowest Low
=
0
TA-Lib's Fast Stochastic defines raw %K as 0 for this flat-range case. Our educational implementation follows that convention.
9. %D Reuses a Moving Average
Fast %K
↓
SMA
↓
Fast %D
In this lesson:
%K lookback = 14
%D period = 3
These are educational settings, not universal constants.
10. Work Through a Small %D Example
%K values:
70
80
90
%D
=
(70 + 80 + 90) / 3
=
80
11. %K and %D Are Two Different Time Scales
%K
→ current normalized range position
→ reacts faster
%D
→ smoothed %K
→ reacts more slowly
12. Fast and Slow Stochastic Are Not the Same Output
TA-Lib distinguishes Fast and Slow Stochastic.
Fast Stochastic
Fast %K
→ raw normalized position
Fast %D
→ moving average of Fast %K
Slow Stochastic
Fast %K
→ smooth once
→ Slow %K
Slow %K
→ smooth again
→ Slow %D
This lesson begins with the Fast version so the original normalized-position idea remains visible.
13. Why This Is Different from RSI
RSI
→ gains vs losses
→ smoothing
→ 0–100 balance
Stochastic
→ Highest High / Lowest Low
→ Close position in range
→ 0–100 position
Same scale, different information.
14. Why This Is Different from Bollinger Bands
Bollinger Bands
→ mean
→ standard deviation
→ dispersion around center
Stochastic
→ recent High-Low extremes
→ Close position inside range
15. What Do 80 and 20 Mean?
%K > 80
→ Close is near the upper part
of the recent range
%K < 20
→ Close is near the lower part
of the recent range
These reference lines do not prove what price will do next.
16. “Overbought” Does Not Mean “Must Fall”
high %K
does not automatically mean
sell
A high reading can persist during a strong trend.
17. “Oversold” Does Not Mean “Must Rise”
low %K
does not automatically mean
buy
Turning 20 or 80 into an entry rule is a separate hypothesis.
18. A %K / %D Cross Is an Event, Not Yet a Strategy
%K crosses above %D
or
%K crosses below %D
Those are reproducible events. A strategy still needs entry timing, exit, costs, position sizing, and a baseline.
19. The AAPL Chart Uses Two Panels
Panel 1
AAPL candlesticks
Panel 2
%K
%D
80 reference
20 reference
Rising candles and %K use seagreen.
Falling candles and %D use firebrick.
The colors distinguish the lines visually; they do not give either line a bullish or bearish mathematical meaning.
20. Change the Lookback Yourself
Start with:
stochastic_period = 14
Then try:
7
and:
28
Ask:
How quickly do the
recent Highest High
and Lowest Low change?
How does %K respond?
21. Change the %D Period Yourself
Start with:
d_period = 3
Then try:
2
and:
5
A shorter %D follows %K more closely. A longer %D is smoother.
22. The Complete Python Program
This lesson introduces no new external package.
We reuse FinanceDataReader and matplotlib.
phase3_stochastic_oscillator.py
from pathlib import Path
from datetime import date, timedelta
import os
import FinanceDataReader as fdr
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle
symbol = "AAPL"
recent_trading_days = 180
stochastic_period = 14
d_period = 3
bullish_color = "seagreen"
bearish_color = "firebrick"
percent_k_color = "seagreen"
percent_d_color = "firebrick"
SCRIPT_DIR = Path(__file__).resolve().parent
os.chdir(SCRIPT_DIR)
def rolling_sma_optional(values, period):
result = [None] * len(values)
if period <= 0:
raise ValueError("period must be positive")
for i in range(period - 1, len(values)):
window = values[
i - period + 1 : i + 1
]
if any(
value is None
for value in window
):
continue
result[i] = (
sum(float(value) for value in window)
/ period
)
return result
def stochastic_fast_k(
high_values,
low_values,
close_values,
period,
):
if not (
len(high_values)
== len(low_values)
== len(close_values)
):
raise ValueError(
"High, Low, and Close must "
"have the same length."
)
if period <= 0:
raise ValueError("period must be positive")
result = [None] * len(close_values)
for i in range(period - 1, len(close_values)):
high_window = high_values[
i - period + 1 : i + 1
]
low_window = low_values[
i - period + 1 : i + 1
]
highest_high = max(
float(value)
for value in high_window
)
lowest_low = min(
float(value)
for value in low_window
)
close_now = float(
close_values[i]
)
range_width = (
highest_high
- lowest_low
)
if range_width == 0:
percent_k = 0.0
else:
percent_k = (
100.0
* (
close_now
- lowest_low
)
/ range_width
)
result[i] = percent_k
return result
def stochastic_fast_d(
percent_k_values,
period,
):
return rolling_sma_optional(
values=percent_k_values,
period=period,
)
def fast_stochastic(
high_values,
low_values,
close_values,
k_period,
d_period,
):
percent_k = stochastic_fast_k(
high_values=high_values,
low_values=low_values,
close_values=close_values,
period=k_period,
)
percent_d = stochastic_fast_d(
percent_k_values=percent_k,
period=d_period,
)
return percent_k, percent_d
def draw_candlesticks(
ax,
market_df,
body_width=0.62,
):
for x, (_, row) in enumerate(
market_df.iterrows()
):
o = float(row["Open"])
h = float(row["High"])
l = float(row["Low"])
c = float(row["Close"])
color = (
bullish_color
if c >= o
else bearish_color
)
ax.vlines(
x,
l,
h,
color=color,
linewidth=1.0,
)
bottom = min(o, c)
height = abs(c - o)
if height == 0:
height = max(
h - l,
0.01,
) * 0.02
ax.add_patch(
Rectangle(
(
x
- body_width / 2.0,
bottom,
),
body_width,
height,
facecolor=color,
edgecolor=color,
linewidth=0.8,
)
)
# ------------------------------------------------------------
# Hand-calculation self-test
# ------------------------------------------------------------
toy_high = [
10.0,
12.0,
14.0,
15.0,
16.0,
]
toy_low = [
5.0,
6.0,
8.0,
9.0,
10.0,
]
toy_close = [
8.0,
11.0,
13.0,
12.0,
15.0,
]
toy_k, toy_d = fast_stochastic(
high_values=toy_high,
low_values=toy_low,
close_values=toy_close,
k_period=3,
d_period=2,
)
expected_k2 = (
100.0
* (13.0 - 5.0)
/ (14.0 - 5.0)
)
expected_k3 = (
100.0
* (12.0 - 6.0)
/ (15.0 - 6.0)
)
expected_d3 = (
expected_k2
+ expected_k3
) / 2.0
assert toy_k[:2] == [None, None]
assert abs(toy_k[2] - expected_k2) < 1e-12
assert abs(toy_k[3] - expected_k3) < 1e-12
assert toy_d[:3] == [None, None, None]
assert abs(toy_d[3] - expected_d3) < 1e-12
flat_k = stochastic_fast_k(
high_values=[10.0, 10.0, 10.0],
low_values=[10.0, 10.0, 10.0],
close_values=[10.0, 10.0, 10.0],
period=3,
)
assert flat_k[2] == 0.0
print("Self-test")
print("=========")
print("Fast %K:", toy_k)
print("Fast %D:", toy_d)
print("Self-test: PASS")
print()
# ------------------------------------------------------------
# Market data
# ------------------------------------------------------------
today = date.today()
start_date = (
today
- timedelta(days=420)
).strftime("%Y-%m-%d")
end_date = today.strftime(
"%Y-%m-%d"
)
df = fdr.DataReader(
symbol,
start_date,
end_date,
)
df = df.tail(
recent_trading_days
).copy()
high_values = [
float(value)
for value in df["High"]
]
low_values = [
float(value)
for value in df["Low"]
]
close_values = [
float(value)
for value in df["Close"]
]
percent_k, percent_d = fast_stochastic(
high_values=high_values,
low_values=low_values,
close_values=close_values,
k_period=stochastic_period,
d_period=d_period,
)
df["%K"] = percent_k
df["%D"] = percent_d
valid_df = df.dropna(
subset=["%K", "%D"]
)
latest = valid_df.iloc[-1]
latest_date = valid_df.index[-1]
print("Latest Fast Stochastic")
print("======================")
print("Symbol:", symbol)
print(
"Date:",
latest_date.strftime("%Y-%m-%d"),
)
print(
"Close:",
f'{latest["Close"]:.2f}',
)
print(
f"%K({stochastic_period}):",
f'{latest["%K"]:.2f}',
)
print(
f"%D({d_period}):",
f'{latest["%D"]:.2f}',
)
# ------------------------------------------------------------
# Plot
# ------------------------------------------------------------
plot_df = df.tail(100).copy()
x = list(range(len(plot_df)))
fig = plt.figure(
figsize=(12, 9),
)
grid = fig.add_gridspec(
2,
1,
height_ratios=[2.0, 1.1],
hspace=0.08,
)
ax_price = fig.add_subplot(
grid[0]
)
ax_stoch = fig.add_subplot(
grid[1],
sharex=ax_price,
)
draw_candlesticks(
ax_price,
plot_df,
)
ax_price.set_title(
f"{symbol} — Fast Stochastic "
f"({stochastic_period}, {d_period})"
)
ax_price.set_ylabel("Price")
ax_price.grid(axis="y", alpha=0.20)
ax_price.tick_params(
axis="x",
labelbottom=False,
)
ax_stoch.plot(
x,
plot_df["%K"],
color=percent_k_color,
linewidth=1.7,
label="%K",
)
ax_stoch.plot(
x,
plot_df["%D"],
color=percent_d_color,
linewidth=1.7,
label="%D",
)
ax_stoch.axhline(
80.0,
linewidth=1.0,
linestyle="--",
)
ax_stoch.axhline(
20.0,
linewidth=1.0,
linestyle="--",
)
ax_stoch.set_ylim(
-5.0,
105.0,
)
ax_stoch.set_ylabel("0–100")
ax_stoch.set_xlabel("Date")
ax_stoch.grid(axis="y", alpha=0.20)
ax_stoch.legend()
step = max(1, len(plot_df) // 8)
positions = list(
range(
0,
len(plot_df),
step,
)
)
labels = [
plot_df.index[i].strftime(
"%Y-%m-%d"
)
for i in positions
]
ax_stoch.set_xticks(positions)
ax_stoch.set_xticklabels(
labels,
rotation=35,
ha="right",
)
fig.subplots_adjust(
left=0.09,
right=0.98,
top=0.94,
bottom=0.13,
)
output_file = (
SCRIPT_DIR
/ "stochastic_fast_k_d_aapl.png"
)
fig.savefig(
output_file,
dpi=140,
)
print()
print("Chart saved:")
print(output_file)
plt.show()
plt.close(fig)
23. Run the Program
python phase3_stochastic_oscillator.py
Confirm:
Self-test: PASS
The chart is saved as:
stochastic_fast_k_d_aapl.png
24. Why Stochastic Is a Good Place to End Phase 3
smoothing
→ SMA / EMA
change
→ Momentum / ROC
gain-loss normalization
→ RSI
composite smoothing
→ MACD
range magnitude
→ True Range / ATR
direction and strength
→ +DI / -DI / ADX
statistical dispersion
→ Standard Deviation
center + dispersion
→ Bollinger Bands
normalized range position
→ Stochastic
We now know enough indicator mathematics to ask:
Does a precisely defined
indicator observation
actually help predict
future outcomes?
That is the beginning of Phase 4.
Check Your Understanding
- %K measures where the current Close sits inside the recent Highest-High / Lowest-Low range.
- %K = 90 does not mean price is rising at 90% speed.
- Fast %D is a moving average of Fast %K.
- Fast and Slow Stochastic use different smoothing structures.
- RSI and Stochastic can both use 0–100 while measuring different quantities.
- The 80 and 20 lines are reference conventions, not guarantees of reversal.
- A %K/%D crossover is a measurable event but not yet a complete strategy.
What You Just Learned
recent Highest High
│
│
Current Close
│
│
recent Lowest Low
↓
normalize position
to 0–100
↓
%K
↓
SMA
↓
%D
The Stochastic Oscillator does not measure speed. It measures where the current Close sits inside a recent High-Low range.
Where Do We Go Next?
indicator observation
↓
precise condition
↓
testable hypothesis
↓
future outcome
↓
baseline
↓
backtest
That is where Phase 4 begins.
Sources
- TA-Lib — Stochastic Fast (STOCHF) — Fast %K formula, Fast %D smoothing, 0–100 output, and flat-range behavior.
- TA-Lib — Stochastic (STOCH) — Slow Stochastic structure and the distinction between FastK, SlowK, and SlowD.