What Is the Stochastic Oscillator? Build %K and %D with Python

AAPL candlestick chart with Fast Stochastic percent K and percent D lines calculated in Python

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