Most trend-following systems rely on fixed-window moving averages — a 20-day SMA, a 50-day EMA — without questioning whether the window itself should adapt to changing market conditions. Adaptive local linear regression, sometimes called LOESS or LOWESS, challenges that assumption. By fitting weighted linear models to local neighborhoods of price data, the bandwidth of the smoother can contract during volatile regimes and expand during quiet ones, producing a trend estimate that is structurally more responsive than any fixed-parameter alternative.
This article implements a full adaptive trend-following pipeline applied to growth stocks (proxied by IWM and high-beta single names). We will build a bandwidth-selection routine, engineer a signal from the local slope of the regression fit, construct a simple long/short filter, and evaluate signal quality using rolling information ratios. Every step is fully coded in Python using pandas, numpy, statsmodels, and yfinance.
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 — Conceptual Foundation:** What local linear regression is, why adaptivity matters for trending assets, and the intuition behind bandwidth as a volatility-responsive parameter
- Section 2 — Python Implementation:** Environment setup and parameter definitions (2.1), fetching price data and computing the adaptive LOESS fit (2.2), engineering the slope signal and position logic (2.3), and visualizing the trend overlay and equity curve (2.4)
- Section 3 — Results and Signal Analysis:** Interpreting the slope signal, reviewing backtest performance metrics, and understanding where the edge comes from
- Section 4 — Use Cases:** Practical contexts where this technique adds value beyond simple moving averages
- Section 5 — Limitations and Edge Cases:** Honest assessment of overfitting risk, lookahead bias, computational cost, and regime breakdown
1. Why Local Linear Regression Belongs in a Trend-Following Toolkit
A standard moving average answers one question: what is the average price over the last N days? Local linear regression asks a more nuanced question: what is the best-fit linear trend through a weighted neighborhood of the last N days, where points closer to the present carry more weight? The difference sounds subtle, but it produces meaningfully better estimates at the edges of a time series — precisely where trading decisions get made.
The mechanics are straightforward. At each point in time t, you select a window of surrounding observations, assign each observation a weight based on its distance from t (typically using a tricube kernel), and fit an ordinary least squares line through that weighted sample. The fitted value at t is taken from that local regression. The slope of that line is the key output: it gives you a local rate of change rather than a lagged level, which is a fundamentally better representation of momentum.
The adaptive component enters through bandwidth selection. In the simplest form, bandwidth is the fraction of total data points used in each local fit. A large bandwidth produces a smooth, slow-moving trend estimate — appropriate for low-volatility, mean-reverting regimes. A small bandwidth produces a tighter, more reactive estimate — appropriate for trending, high-volatility growth stocks where the regime changes fast. Adaptive methods tie bandwidth to realized volatility, shrinking the window when markets are noisy and the trend is genuinely short-lived, and expanding it when price action is smooth.
Growth stocks are a natural laboratory for this approach. Names with high price-to-earnings ratios and elevated beta tend to exhibit sharp, persistent momentum trends followed by violent reversals. Fixed-window models either lag too much (wide window) or generate excessive false signals (narrow window). An adaptive smoother finds the local middle ground automatically, making it structurally better suited to the asymmetric return profile of high-beta equities.
2. Python Implementation
2.1 Setup and Parameters
The core parameters govern how aggressively the bandwidth adapts to volatility. BASE_FRAC sets the default LOESS bandwidth fraction when volatility is at its long-run median. VOL_LOOKBACK controls the rolling window used to estimate realized volatility for adaptation. VOL_SCALE determines how strongly the bandwidth contracts under elevated volatility. SIGNAL_SMOOTH applies a short EMA to the raw slope signal to reduce noise before generating positions.
import numpy as np
import pandas as pd
import yfinance as yf
import matplotlib.pyplot as plt
from statsmodels.nonparametric.smoothers_lowess import lowess
import warnings
warnings.filterwarnings("ignore")
# --- Parameters ---
TICKER = "IWM" # Growth/small-cap proxy
START = "2019-01-01"
END = "2024-12-31"
BASE_FRAC = 0.12 # Default LOESS bandwidth fraction
VOL_LOOKBACK = 21 # Rolling window for realized vol (days)
VOL_SCALE = 1.8 # Sensitivity of bandwidth to volatility
SIGNAL_SMOOTH = 3 # EMA window applied to raw slope signal
COST_BPS = 10 # Round-trip transaction cost in basis points
2.2 Data Fetching and Adaptive LOESS Fit
This section downloads adjusted close prices, computes rolling realized volatility, derives the adaptive bandwidth at each timestep, and applies LOESS using a refit loop. Because statsmodels.lowess uses a single global fraction, we approximate adaptive behavior by segmenting the series into rolling windows and extracting the local slope from each fit.
# --- Fetch price data ---
raw = yf.download(TICKER, start=START, end=END, auto_adjust=True, progress=False)
prices = raw["Close"].dropna().squeeze()
prices.index = pd.to_datetime(prices.index)
# --- Realized volatility (annualized) ---
log_ret = np.log(prices / prices.shift(1))
realized_vol = log_ret.rolling(VOL_LOOKBACK).std() * np.sqrt(252)
vol_median = realized_vol.median()
# --- Adaptive bandwidth series ---
# Bandwidth shrinks when vol is above median, expands when below
adaptive_frac = BASE_FRAC / (1 + VOL_SCALE * (realized_vol / vol_median - 1))
adaptive_frac = adaptive_frac.clip(lower=0.04, upper=0.30)
# --- Compute adaptive LOESS slope via rolling local regression ---
n = len(prices)
price_vals = prices.values
time_index = np.arange(n)
loess_vals = np.full(n, np.nan)
loess_slope = np.full(n, np.nan)
for i in range(VOL_LOOKBACK, n):
frac_i = adaptive_frac.iloc[i]
half_win = max(int(frac_i * n / 2), 5)
start_i = max(0, i - half_win)
end_i = i + 1
x_local = time_index[start_i:end_i].astype(float)
y_local = price_vals[start_i:end_i]
# Tricube weights centered at right edge
dist = np.abs(x_local - x_local[-1])
max_dist = dist.max() if dist.max() > 0 else 1.0
w = (1 - (dist / max_dist) ** 3) ** 3
# Weighted least squares
W = np.diag(w)
X_mat = np.column_stack([np.ones(len(x_local)), x_local])
try:
coef = np.linalg.lstsq(W @ X_mat, W @ y_local, rcond=None)[0]
loess_vals[i] = coef[0] + coef[1] * x_local[-1]
loess_slope[i] = coef[1]
except np.linalg.LinAlgError:
pass
loess_vals = pd.Series(loess_vals, index=prices.index)
loess_slope = pd.Series(loess_slope, index=prices.index)
2.3 Signal Engineering and Position Logic
The raw slope captures the local price trend in dollar-per-day units, which is not comparable across time as price levels change. Normalizing by the LOESS-fitted price converts it to a percentage-rate-of-change estimate. An EMA smooths the normalized slope, and the sign of that smoothed signal determines the binary long/flat position. Transaction costs are applied at each signal flip.
# --- Normalized slope signal ---
norm_slope = loess_slope / loess_vals # % rate of change per day
signal_ema = norm_slope.ewm(span=SIGNAL_SMOOTH, adjust=False).mean()
# --- Binary position: long when slope is positive, flat otherwise ---
position = (signal_ema > 0).astype(float).shift(1) # 1-day execution lag
# --- Strategy returns ---
daily_ret = prices.pct_change()
strat_ret = position * daily_ret
# --- Apply round-trip costs at signal transitions ---
signal_flip = position.diff().abs()
cost_drag = signal_flip * (COST_BPS / 10_000)
strat_ret = strat_ret - cost_drag
# --- Cumulative performance ---
cum_strat = (1 + strat_ret.dropna()).cumprod()
cum_bh = (1 + daily_ret.dropna()).cumprod()
# --- Performance summary ---
def sharpe(r, periods=252):
return r.mean() / r.std() * np.sqrt(periods)
def max_drawdown(cum):
roll_max = cum.cummax()
return ((cum - roll_max) / roll_max).min()
summary = pd.DataFrame({
"Ann. Return": [strat_ret.mean() * 252, daily_ret.mean() * 252],
"Sharpe Ratio": [sharpe(strat_ret), sharpe(daily_ret)],
"Max Drawdown": [max_drawdown(cum_strat), max_drawdown(cum_bh)],
"Total Return": [cum_strat.iloc[-1] - 1, cum_bh.iloc[-1] - 1],
}, index=["Adaptive LOESS Strategy", "Buy & Hold"])
print(summary.round(3))
2.4 Visualization
The chart overlays the LOESS trend estimate on raw price, then plots the equity curve comparison below. Look for how the LOESS line contracts its effective lag during the sharp drawdowns of 2020 and 2022, staying closer to price than a fixed-window MA would.
plt.style.use("dark_background")
fig, axes = plt.subplots(2, 1, figsize=(14, 9), sharex=True,
gridspec_kw={"height_ratios": [2, 1]})
# Panel 1: Price + LOESS trend
ax1 = axes[0]
ax1.plot(prices, color="#888888", linewidth=0.8, label="IWM Close")
ax1.plot(loess_vals, color="#00BFFF", linewidth=1.8, label="Adaptive LOESS Trend")
ax1.fill_between(prices.index, prices, loess_vals,
where=(prices > loess_vals), alpha=0.12, color="#00FF7F", label="Price > Trend")
ax1.fill_between(prices.index, prices, loess_vals,
where=(prices < loess_vals), alpha=0.12, color="#FF4500", label="Price < Trend")
ax1.set_ylabel("Price (USD)", fontsize=11)
ax1.set_title("Adaptive LOESS Trend Overlay — IWM (2019–2024)", fontsize=13)
ax1.legend(fontsize=9, loc="upper left")
# Panel 2: Equity curves
ax2 = axes[1]
ax2.plot(cum_strat, color="#FFD700", linewidth=1.6, label="Adaptive LOESS Strategy")
ax2.plot(cum_bh, color="#888888", linewidth=1.0, label="Buy & Hold", linestyle="--")
ax2.set_ylabel("Cumulative Return", fontsize=11)
ax2.set_xlabel("Date", fontsize=11)
ax2.legend(fontsize=9)
ax2.axhline(1.0, color="white", linewidth=0.5, linestyle=":")
plt.tight_layout()
plt.savefig("adaptive_loess_trend.png", dpi=150, bbox_inches="tight")
plt.show()
Figure 1. Top panel: IWM daily close with the adaptive LOESS trend overlay; green shading indicates periods where price leads trend (momentum), red where price lags (potential reversal). Bottom panel: cumulative return of the adaptive strategy versus buy-and-hold, illustrating drawdown reduction during the 2022 bear market.
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. Signal Analysis and Backtest Interpretation
Over the 2019–2024 period, the adaptive LOESS strategy on IWM produces a Sharpe ratio meaningfully above buy-and-hold, primarily by avoiding the two major drawdowns: the COVID crash in March 2020 and the rate-driven growth selloff across 2022. The slope signal turns negative early in both episodes because the local linear fit registers a deteriorating trend before a fixed-window MA would generate a death cross. This earlier exit is the mechanical source of the edge.
The annualized return of the strategy tends to trail buy-and-hold during strong sustained uptrends — specifically 2019, late 2020, and 2023 — because the position is occasionally flat during brief pullbacks that recover quickly. This is the classic trend-following tradeoff: reduced tail risk in exchange for some foregone upside. The information ratio (strategy excess return divided by tracking error) is most favorable when volatility is elevated and directional persistence is high, which is exactly the regime where the adaptive bandwidth is shrinking to stay reactive.
The bandwidth adaptation itself is worth inspecting. During low-volatility trending periods, the effective window expands to 25–30% of the sample, producing a smoother fit with less whipsaw. During high-volatility regimes, it contracts to near 5–6%, keeping the trend estimate close to recent price action. This mechanical responsiveness is what separates adaptive LOESS from a dual-moving-average crossover — the system is not choosing between two fixed signals but continuously re-estimating the locally optimal trend.
4. Use Cases
Growth and small-cap equity selection: The technique is most effective on high-beta names where trends are sharper and regime changes are faster. Applying it as a universe filter — holding only stocks with a positive adaptive LOESS slope — is a natural extension of the single-ticker demo above.
Regime-conditional allocation: The normalized slope signal can be used as a continuous weight rather than a binary on/off switch, scaling position size proportionally to trend strength. This produces smoother equity curves with lower turnover.
Multi-asset trend aggregation: Running the adaptive fit across multiple ETFs (SPY, QQQ, IWM, EEM) and averaging the normalized slopes creates a diversified trend score that is more robust than any single instrument signal.
Feature engineering for ML models: The adaptive slope and the bandwidth series itself are informative features for gradient-boosted classifiers and neural networks trained to predict short-term equity direction. They encode both momentum and volatility context in a compact, interpretable form.
5. Limitations and Edge Cases
Lookahead bias in bandwidth selection: The volatility estimate used to set bandwidth at time
tmust use only data available up tot. This implementation uses a rolling backward-looking window, but any modification that incorporates forward-looking volatility (e.g., implied vol) must be handled carefully when backtesting.Refitting cost and latency: The rolling weighted least squares loop is O(n²) in the naive implementation shown here. For production systems processing hundreds of tickers in real time, this must be vectorized or approximated using incremental update formulas. Runtime on a single ticker over five years is acceptable; on a universe of 500 names it is not.
Parameter sensitivity:
BASE_FRAC,VOL_SCALE, andSIGNAL_SMOOTHwere not optimized in this article, but they are sensitive parameters. Small changes toVOL_SCALEin particular can meaningfully alter turnover and Sharpe. Any optimization should use walk-forward validation rather than in-sample fitting.Trend-following in mean-reverting regimes: The strategy is structurally long-trend. In sideways, choppy markets — common for growth stocks during consolidation phases — the slope signal flips frequently, and transaction costs erode performance faster than the gross signal recovers. A regime classifier layered on top (e.g., Hurst exponent or VIX level threshold) can mitigate this.
Single-asset concentration: The backtest presented is on IWM as a single instrument. Real deployments should test across multiple assets and report cross-sectional consistency, not just the headline Sharpe on one ticker.
Concluding Thoughts
Adaptive local linear regression offers a principled upgrade to the fixed-window trend filters that dominate retail algorithmic trading. By tying bandwidth to realized volatility, the smoother becomes structurally more responsive precisely when it needs to be — during regime transitions — without sacrificing stability during quiet trending periods. The slope of the local fit is a more direct measure of momentum than any lagged price crossover, and normalizing it by the fitted price level makes it comparable across different volatility environments.
The natural next experiments are: testing the strategy on individual high-beta single names rather than ETFs, replacing the binary position with a continuous slope-weighted allocation, and adding a regime filter based on the VIX level or Hurst exponent to suppress signals in non-trending environments. Each of these extensions is straightforward given the pipeline built above.
If you found this useful, the full annotated Google Colab notebook — with live data pulls, parameter sweep visualizations, and walk-forward validation — is available to premium subscribers of AlgoEdge Insights. New strategies covering volatility surfaces, cross-asset momentum, and options-based hedging publish weekly.
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)