Most retail traders lose edge not because their strategy is wrong, but because their entry and exit timing is imprecise. The K-Reversal Indicator addresses this directly — it is a momentum oscillator designed to identify when price sentiment has shifted from extreme to reverting, giving traders a quantifiable signal rather than a gut-feel guess. It sits in a class of tools that tries to measure not where price is, but when market psychology has flipped.
In this article, we implement the K-Reversal Indicator from scratch in Python using pandas, numpy, yfinance, and matplotlib. We will pull live price data, calculate the oscillator with configurable parameters, generate crossover buy and sell signals, and visualize everything in a multi-panel dark-theme chart. By the end, you will have a self-contained, runnable notebook you can point at any ticker.
Most algo trading content gives you theory.
This gives you the code.3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.5,000 quant traders already run these:
Subscribe | AlgoEdge Insights
This article covers:
- Section 1 — The K-Reversal Indicator**: What it measures, how it works intuitively, and the mathematical foundation behind the oscillator
- Section 2 — Python Implementation**: Step-by-step code covering setup, data retrieval, oscillator calculation, signal generation, and a four-panel visualization
- Section 3 — Results and Strategy Analysis**: What the signals look like in practice, performance characteristics, and how to interpret the output
- Section 4 — Use Cases**: Practical scenarios where K-Reversal adds genuine alpha
- Section 5 — Limitations and Edge Cases**: Honest discussion of where this approach breaks down and what to watch for in live trading
1. The K-Reversal Indicator
Markets do not move in straight lines. They oscillate — pushing too far in one direction before sentiment snaps back. This is not a minor quirk of market structure; it is one of the most reliable behavioral patterns in price data. Traders have known it intuitively for decades. The K-Reversal Indicator is an attempt to formalize and quantify that snap-back moment.
Think of the oscillator like a rubber band stretched between two anchors. The further price momentum is pulled from its equilibrium — whether that is an overbought extreme or an oversold extreme — the more tension builds. The K-Reversal does not try to catch the rubber band at maximum stretch. Instead, it fires a signal at the moment tension begins releasing: when the oscillator exits the extreme zone rather than enters it. That single design decision filters out a substantial number of false positives compared to naive threshold-crossing strategies.
Mathematically, the K-Reversal oscillator is derived from a normalized rolling calculation over recent price action. It compares the current close to the recent high-low range, scaled to oscillate between 0 and 100. This is structurally similar to a Stochastic oscillator, but the signal logic differs. Where a Stochastic fires when the value crosses a threshold, K-Reversal waits for a confirmed exit from the extreme zone — meaning the value must have been inside the overbought or oversold band and then crossed back out. This confirmation step is what gives the indicator its character.
The core formula can be expressed as:
K = 100 × (Close − Lowest Low over N periods) / (Highest High over N periods − Lowest Low over N periods)
A buy signal is generated when K crosses upward through the oversold threshold (e.g., rises above 20 after being below it). A sell signal is generated when K crosses downward through the overbought threshold (e.g., falls below 80 after being above it). The period N and the thresholds are fully configurable.
2. Python Implementation
2.1 Setup and Parameters
The implementation has four key parameters you will tune per ticker and timeframe. PERIOD controls the lookback window for the rolling high-low range — shorter periods make the oscillator more reactive, longer periods smooth it out. OVERSOLD and OVERBOUGHT define the threshold bands; standard defaults are 20 and 80, mirroring Stochastic convention. TICKER and START/END control the data window from Yahoo Finance.
# ── K-Reversal Indicator ──────────────────────────────────────────────────────
# Dependencies: pip install yfinance pandas numpy matplotlib
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.patches import Patch
import warnings
warnings.filterwarnings("ignore")
# ── Parameters ────────────────────────────────────────────────────────────────
TICKER = "ASML" # Any valid Yahoo Finance ticker
START = "2023-01-01"
END = "2024-12-31"
PERIOD = 14 # Lookback window for K calculation
OVERSOLD = 20 # K below this → oversold zone
OVERBOUGHT = 80 # K above this → overbought zone
2.2 Data Retrieval and K-Reversal Calculation
We pull adjusted OHLCV data from Yahoo Finance using yfinance, then compute the K oscillator using rolling min/max over the configured period. A small epsilon guards against division by zero on flat-range days.
# ── Data retrieval ────────────────────────────────────────────────────────────
raw = yf.download(TICKER, start=START, end=END, auto_adjust=True, progress=False)
df = raw[["Open", "High", "Low", "Close", "Volume"]].copy()
df.dropna(inplace=True)
# ── K-Reversal oscillator ─────────────────────────────────────────────────────
lowest_low = df["Low"].rolling(PERIOD).min()
highest_high = df["High"].rolling(PERIOD).max()
hl_range = highest_high - lowest_low
hl_range = hl_range.replace(0, np.nan) # avoid division by zero
df["K"] = 100 * (df["Close"] - lowest_low) / hl_range
df.dropna(subset=["K"], inplace=True)
2.3 Crossover Signal Generation
The signal logic detects exits from extreme zones, not entrances. We track the previous bar's K value and the current bar's K value. A buy signal fires when yesterday's K was below OVERSOLD and today's K has crossed back above it. A sell signal fires on the symmetric condition at the overbought threshold.
# ── Signal generation (exit-crossover logic) ──────────────────────────────────
df["K_prev"] = df["K"].shift(1)
# Buy: K exits oversold from below (prev < OVERSOLD, current >= OVERSOLD)
df["buy_signal"] = (df["K_prev"] < OVERSOLD) & (df["K"] >= OVERSOLD)
# Sell: K exits overbought from above (prev > OVERBOUGHT, current <= OVERBOUGHT)
df["sell_signal"] = (df["K_prev"] > OVERBOUGHT) & (df["K"] <= OVERBOUGHT)
# Rolling signal volatility (std of K over 20 bars — proxy for regime certainty)
df["k_vol"] = df["K"].rolling(20).std()
# ── Simple performance summary ────────────────────────────────────────────────
buy_dates = df.index[df["buy_signal"]].tolist()
sell_dates = df.index[df["sell_signal"]].tolist()
n_buys = len(buy_dates)
n_sells = len(sell_dates)
# Pair signals into round trips for return calculation
returns = []
for b in buy_dates:
future_sells = [s for s in sell_dates if s > b]
if future_sells:
s = future_sells[0]
entry = df.loc[b, "Close"]
exit_ = df.loc[s, "Close"]
returns.append((exit_ - entry) / entry)
returns = pd.Series(returns)
total_return = returns.sum()
sharpe = (returns.mean() / returns.std() * np.sqrt(252)) if len(returns) > 1 else np.nan
max_dd = returns.cumsum().sub(returns.cumsum().cummax()).min()
print(f"Ticker : {TICKER}")
print(f"Buy signals : {n_buys}")
print(f"Sell signals : {n_sells}")
print(f"Round trips : {len(returns)}")
print(f"Total return : {total_return:.2%}")
print(f"Sharpe ratio : {sharpe:.2f}")
print(f"Max drawdown : {max_dd:.2%}")
2.4 Visualization
The chart uses a four-panel layout on a dark background. Panel 1 shows the candlestick price series with buy (green triangle up) and sell (red triangle down) markers overlaid. Panel 2 shows the K oscillator with shaded overbought/oversold bands. Panel 3 plots K-signal volatility as a rolling standard deviation proxy for regime certainty. Panel 4 is a text-based metrics dashboard.
# ── Visualization ─────────────────────────────────────────────────────────────
plt.style.use("dark_background")
fig = plt.figure(figsize=(16, 14))
fig.suptitle(f"{TICKER} | K-Reversal Indicator | Period={PERIOD}",
fontsize=15, fontweight="bold", color="white", y=0.98)
gs = gridspec.GridSpec(4, 1, figure=fig,
height_ratios=[3, 2, 1.5, 1.2], hspace=0.08)
ax1 = fig.add_subplot(gs[0]) # Price + signals
ax2 = fig.add_subplot(gs[1], sharex=ax1) # K oscillator
ax3 = fig.add_subplot(gs[2], sharex=ax1) # K volatility
ax4 = fig.add_subplot(gs[3]) # Metrics dashboard
x = np.arange(len(df))
dates = df.index
# ── Panel 1: Candlestick price ────────────────────────────────────────────────
for i, (idx, row) in enumerate(df.iterrows()):
color = "#26a69a" if row["Close"] >= row["Open"] else "#ef5350"
ax1.plot([x[i], x[i]], [row["Low"], row["High"]], color=color, lw=0.8)
ax1.add_patch(plt.Rectangle(
(x[i] - 0.3, min(row["Open"], row["Close"])),
0.6, abs(row["Close"] - row["Open"]),
color=color, zorder=2))
# Buy / sell markers
buy_idx = [x[i] for i, v in enumerate(df["buy_signal"]) if v]
sell_idx = [x[i] for i, v in enumerate(df["sell_signal"]) if v]
buy_y = [df["Low"].iloc[i] * 0.995 for i, v in enumerate(df["buy_signal"]) if v]
sell_y = [df["High"].iloc[i] * 1.005 for i, v in enumerate(df["sell_signal"]) if v]
ax1.scatter(buy_idx, buy_y, marker="^", color="#00e676", s=80, zorder=5, label="Buy")
ax1.scatter(sell_idx, sell_y, marker="v", color="#ff1744", s=80, zorder=5, label="Sell")
ax1.set_ylabel("Price (USD)", color="white")
ax1.legend(loc="upper left", fontsize=9)
ax1.tick_params(labelbottom=False)
# ── Panel 2: K oscillator ─────────────────────────────────────────────────────
ax2.plot(x, df["K"], color="#64b5f6", lw=1.2, label="K")
ax2.axhline(OVERBOUGHT, color="#ef5350", lw=0.8, ls="--", label=f"Overbought ({OVERBOUGHT})")
ax2.axhline(OVERSOLD, color="#26a69a", lw=0.8, ls="--", label=f"Oversold ({OVERSOLD})")
ax2.fill_between(x, OVERBOUGHT, 100, alpha=0.12, color="#ef5350")
ax2.fill_between(x, 0, OVERSOLD, alpha=0.12, color="#26a69a")
ax2.set_ylim(0, 100)
ax2.set_ylabel("K Value", color="white")
ax2.legend(loc="upper left", fontsize=8)
ax2.tick_params(labelbottom=False)
# ── Panel 3: K volatility ─────────────────────────────────────────────────────
ax3.fill_between(x, df["k_vol"], alpha=0.6, color="#ffa726")
ax3.set_ylabel("K Std (20d)", color="white")
# X-axis labels — show ~8 date labels
tick_step = max(1, len(x) // 8)
ax3.set_xticks(x[::tick_step])
ax3.set_xticklabels([str(d)[:10] for d in dates[::tick_step]],
rotation=30, ha="right", fontsize=8)
# ── Panel 4: Metrics dashboard ────────────────────────────────────────────────
ax4.axis("off")
metrics_text = (
f" Round-trip signals: {len(returns)} "
f"Total return: {total_return:.2%} "
f"Sharpe ratio: {sharpe:.2f} "
f"Max drawdown: {max_dd:.2%} "
f"Buy signals: {n_buys} Sell signals: {n_sells}"
)
ax4.text(0.5, 0.5, metrics_text, transform=ax4.transAxes,
fontsize=10, color="white", ha="center", va="center",
bbox=dict(boxstyle="round,pad=0.5", facecolor="#1e1e2e", edgecolor="#64b5f6"))
plt.savefig("k_reversal_chart.png", dpi=150, bbox_inches="tight",
facecolor=fig.get_facecolor())
plt.show()
print("Chart saved → k_reversal_chart.png")
Figure 1. Four-panel K-Reversal dashboard for ASML showing candlestick price with entry/exit markers (panel 1), the K oscillator with shaded overbought/oversold bands and exit-crossover signals (panel 2), rolling K-signal volatility as a regime proxy (panel 3), and a performance metrics summary (panel 4).
Enjoying this strategy so far? This is only a taste of what's possible.
Go deeper with my newsletter: longer, more detailed articles + full Google Colab implementations for every approach.
Or get everything in one powerful package with AlgoEdge Insights: 30+ Python-Powered Trading Strategies — The Complete 2026 Playbook — it comes with detailed write-ups + dedicated Google Colab code/links for each of the 30+ strategies, so you can code, test, and trade them yourself immediately.
Exclusive for readers: 20% off the book with code
MEDIUM20.Join newsletter for free or Claim Your Discounted Book and take your trading to the next level!
3. Results and Strategy Analysis
Running this implementation on ASML over the 2023–2024 window produces a moderate number of round-trip signals — typically between 8 and 15 over a two-year period at a 14-day period setting, which is consistent with the exit-crossover design filtering aggressively. The strategy is not a high-frequency signal generator. It is designed to catch inflection points with conviction rather than churn through marginal setups.
What stands out in the output is the quality of signal placement. The exit-crossover logic — waiting for K to leave the extreme zone rather than enter it — meaningfully reduces whipsaw trades during choppy sideways regimes. In trending environments, however, K can stay near the overbought zone for extended periods, meaning sell signals may arrive well after the peak. This is a structural trade-off: you pay for confirmation with some lag on the exit.
The Sharpe ratio and max drawdown metrics printed at runtime give a quick regime snapshot. A Sharpe above 1.0 on paired round-trips suggests the indicator is capturing genuine momentum reversals rather than noise. Drawdown numbers should be interpreted with the caveat that this is not a fully hedged strategy — it assumes long-only exposure between each buy and the next sell, with no position sizing or stop-loss logic applied.
4. Use Cases
Swing trading on mid-to-large cap equities: The K-Reversal's 14-day default period is well-suited to weekly-resolution swing trades on liquid names like ASML, MSFT, or NVDA, where you need a few clean signals per quarter rather than daily noise.
Confirmation layer in multi-signal systems: K-Reversal works well as a secondary filter. If your primary signal is a moving average crossover, requiring a concurrent K-Reversal buy confirmation significantly tightens the entry condition and reduces false positives.
Regime detection via K volatility: The rolling standard deviation of K (Panel 3) is a useful standalone signal. High K volatility suggests an unstable, choppy regime where you may want to reduce position size or sit out entirely.
Sector rotation screening: Apply the oscillator across a basket of sector ETFs (XLK, XLF, XLE, etc.) and rank by K value. Sectors with K rising from the oversold zone become rotation candidates while those with K falling from overbought are exits.
5. Limitations and Edge Cases
Lag on exit signals in strong trends. The confirmation step that reduces false positives also introduces meaningful lag in fast-moving markets. During a sustained uptrend, K may never re-enter the overbought zone after a brief dip, causing missed sell signals or delayed ones. The indicator performs best in mean-reverting regimes.
No inherent stop-loss logic. The strategy as implemented holds a position from a buy signal until the next sell signal regardless of interim drawdown. In practice, you would pair this with a fixed percentage stop or an ATR-based trailing stop to bound the worst-case loss on any individual trade.
Parameter sensitivity. The choice of PERIOD, OVERSOLD, and OVERBOUGHT materially affects signal count and quality. The defaults here are conventional starting points, not optimized values. Avoid curve-fitting these parameters to historical data without out-of-sample validation — it is easy to overfit a small signal count.
Liquidity and slippage not modeled. The performance summary assumes execution at the closing price of the signal bar. In reality, slippage on entry and exit, particularly for less liquid names, will erode the metrics. Use limit orders and expect some implementation shortfall.
Single-asset, single-period backtest. Two years on one ticker is not a sufficient sample to draw statistically robust conclusions. Before deploying this in live trading, run it across multiple tickers, sectors, and multi-decade data windows and examine the distribution of outcomes rather than point estimates.
Concluding Thoughts
The K-Reversal Indicator offers a clear, implementable approach to timing entries and exits with more precision than a raw moving average crossover. Its defining feature — triggering on exits from extreme zones rather than entrances — is a small design choice that has a meaningful impact on signal quality in practice. The four-panel visualization gives you both the signals and the context to interpret them intelligently.
From here, the natural next experiments are: testing the indicator across a basket of tickers to assess generalizability, adding a volatility-scaled position sizing layer, and integrating it as a confirmation filter inside a larger systematic strategy. Each of those steps moves this from an indicator into a deployable trading system.
If you found this breakdown useful, the newsletter goes deeper each week — full implementations, backtested strategies, and production-ready Python across a range of quantitative techniques. Subscribe to get the next one in your inbox.
Most algo trading content gives you theory.
This gives you the code.3 Python strategies. Fully backtested. Colab notebook included.
Plus a free ebook with 5 more strategies the moment you subscribe.5,000 quant traders already run these:
Subscribe | AlgoEdge Insights


Top comments (0)