Envelopes (Moving Average Envelopes)

Haiyue
12min

I. What is the Envelopes Indicator

Envelopes, also known as Moving Average Envelopes, is a classic technical analysis indicator. It creates a price “envelope” channel by placing a parallel line at a fixed percentage offset above and below a moving average.

Envelopes belongs to the Volatility / Bands overlay indicator category and has no single attributed inventor — it is one of the earliest and most fundamental channel indicators in technical analysis, dating back to mid-20th century technical analysis practices.

Envelopes consists of three lines:

  • Middle Line: Simple Moving Average (SMA) of the closing price
  • Upper Envelope: Middle Line x (1 + Percentage)
  • Lower Envelope: Middle Line x (1 - Percentage)

The default parameters are period n=20n = 20, percentage offset p=2.5%p = 2.5\%, and the data source is the closing price.

Note

The key difference between Envelopes and Bollinger Bands is: Envelopes uses a fixed percentage offset with a constant bandwidth, while Bollinger Bands uses a standard deviation offset with bandwidth that adapts to volatility. This means Envelopes is simpler but less adaptive than Bollinger Bands in markets with dramatic volatility changes.


II. Mathematical Principles and Calculation

2.1 Core Formulas

Let the closing price series be C1,C2,,CtC_1, C_2, \ldots, C_t, the period be nn, and the percentage offset be pp (in decimal form, e.g., 2.5% -> 0.025).

Middle Line:

Middlet=SMA(C,n)=1ni=0n1Cti\text{Middle}_t = \text{SMA}(C, n) = \frac{1}{n} \sum_{i=0}^{n-1} C_{t-i}

Upper Envelope:

Uppert=Middlet×(1+p)\text{Upper}_t = \text{Middle}_t \times (1 + p)

Lower Envelope:

Lowert=Middlet×(1p)\text{Lower}_t = \text{Middle}_t \times (1 - p)

2.2 Formula Variants

Some implementations use addition/subtraction instead of multiplication/division:

Uppert=Middlet+Middlet×p=Middlet×(1+p)\text{Upper}_t = \text{Middle}_t + \text{Middle}_t \times p = \text{Middle}_t \times (1 + p)

The two notations are mathematically equivalent.

Additionally, the middle line can use EMA instead of SMA:

Middlet=EMA(C,n)\text{Middle}_t = \text{EMA}(C, n)

The EMA version is more sensitive to price changes.

2.3 Channel Width

The channel width of Envelopes is proportional to the middle line:

Widtht=UppertLowert=2p×Middlet\text{Width}_t = \text{Upper}_t - \text{Lower}_t = 2p \times \text{Middle}_t

The relative width is always constant:

WidthtMiddlet=2p\frac{\text{Width}_t}{\text{Middle}_t} = 2p

This means the relative channel width of Envelopes never changes (e.g., with p=2.5%p = 2.5\%, the width is always 5% of the middle line), and it cannot reflect changes in volatility.

2.4 Calculation Steps

  1. Calculate the SMA of closing prices (period nn) -> middle line
  2. Upper Envelope = Middle Line x (1+p/100)(1 + p/100)
  3. Lower Envelope = Middle Line x (1p/100)(1 - p/100)
Tip

The calculation is extremely straightforward — essentially just shifting the moving average up and down by a percentage. This simplicity is both the strength of Envelopes (easy to understand and implement) and its limitation (does not account for volatility changes).


III. Python Implementation

import numpy as np
import pandas as pd

def envelopes(close: pd.Series, period: int = 20, percent: float = 2.5,
              ma_type: str = 'sma') -> pd.DataFrame:
    """
    Calculate Moving Average Envelopes.

    Parameters:
        close   : closing price series
        period  : moving average period, default 20
        percent : percentage offset, default 2.5 (i.e., 2.5%)
        ma_type : moving average type, 'sma' or 'ema'

    Returns:
        DataFrame with columns: middle, upper, lower
    """
    p = percent / 100.0

    # Calculate the middle line
    if ma_type == 'ema':
        middle = close.ewm(span=period, adjust=False).mean()
    else:
        middle = close.rolling(window=period).mean()

    # Upper and lower envelopes
    upper = middle * (1 + p)
    lower = middle * (1 - p)

    return pd.DataFrame({
        'middle': middle,
        'upper': upper,
        'lower': lower
    })


def envelope_signals(close: pd.Series, upper: pd.Series,
                     lower: pd.Series, middle: pd.Series) -> pd.Series:
    """
    Generate trading signals based on Envelopes.

    Signal logic:
        - Price breaks below the lower envelope -> Oversold (potential buy)
        - Price breaks above the upper envelope -> Overbought (potential sell)
        - Price crosses the middle line -> Trend confirmation
    """
    signals = pd.Series('Neutral', index=close.index)

    # Oversold / Overbought signals
    signals[close < lower] = 'Oversold'
    signals[close > upper] = 'Overbought'

    # Middle line crossover signals (when not overbought/oversold)
    cross_up = (close > middle) & (close.shift(1) <= middle.shift(1))
    cross_down = (close < middle) & (close.shift(1) >= middle.shift(1))
    signals[cross_up & (signals == 'Neutral')] = 'Cross Above Middle'
    signals[cross_down & (signals == 'Neutral')] = 'Cross Below Middle'

    return signals


# ============ Usage Example ============
if __name__ == '__main__':
    np.random.seed(42)
    n_days = 100

    # Generate simulated OHLCV data
    base_price = 100 + np.cumsum(np.random.randn(n_days) * 0.5)
    df = pd.DataFrame({
        'open':   base_price + np.random.randn(n_days) * 0.3,
        'high':   base_price + np.abs(np.random.randn(n_days) * 0.8),
        'low':    base_price - np.abs(np.random.randn(n_days) * 0.8),
        'close':  base_price,
        'volume': np.random.randint(1000, 10000, n_days)
    })

    # Calculate Envelopes (SMA version)
    env_sma = envelopes(df['close'], period=20, percent=2.5, ma_type='sma')
    result = pd.concat([df[['close']], env_sma], axis=1)

    print("=== Envelopes (SMA, 2.5%) Results (last 10 rows) ===")
    print(result.tail(10).to_string())

    # Calculate Envelopes (EMA version)
    env_ema = envelopes(df['close'], period=20, percent=2.5, ma_type='ema')
    print("\n=== Envelopes (EMA, 2.5%) Results (last 5 rows) ===")
    cols = env_ema.add_suffix('_ema')
    print(pd.concat([df[['close']], cols], axis=1).tail(5).to_string())

    # Generate trading signals
    signals = envelope_signals(df['close'], env_sma['upper'],
                               env_sma['lower'], env_sma['middle'])
    print("\n=== Signal Statistics ===")
    print(signals.value_counts())

    # Display selected signals
    signal_rows = df[['close']].copy()
    signal_rows['upper'] = env_sma['upper']
    signal_rows['lower'] = env_sma['lower']
    signal_rows['signal'] = signals
    non_neutral = signal_rows[signal_rows['signal'] != 'Neutral']
    print(f"\n=== Non-Neutral Signals (total: {len(non_neutral)}) ===")
    print(non_neutral.head(10).to_string())

IV. Problems the Indicator Solves

4.1 Overbought / Oversold Identification

The most basic use of Envelopes is identifying overbought and oversold price conditions:

  • Price breaks above the upper envelope: Price has deviated too far from the moving average — possibly overbought
  • Price breaks below the lower envelope: Price has deviated too far from the moving average — possibly oversold

This logic is based on the mean reversion principle — price cannot deviate from the moving average indefinitely and will eventually revert.

4.2 Mean Reversion Trading

In range-bound markets, Envelopes provides clear trading rules:

  1. Price touches the lower envelope -> Buy
  2. Price returns to the middle line -> Take profit (or hold until the upper envelope)
  3. Price touches the upper envelope -> Sell
  4. Price returns to the middle line -> Take profit

4.3 Trend Filtering

  • Price consistently above the middle line -> Bullish trend
  • Price consistently below the middle line -> Bearish trend
  • Middle line direction (rising or falling) reflects trend direction
Warning

Envelopes uses a fixed percentage, which means that during high-volatility periods, price may frequently break through the channel producing many false signals; during low-volatility periods, price may never reach the channel boundaries, missing trading opportunities. Therefore, the percentage parameter must be adjusted according to the volatility characteristics of the asset.

4.4 Combining with Other Indicators

  • Combine with RSI to confirm overbought/oversold: When price touches the upper band and RSI > 70, the overbought signal is more reliable
  • Combine with volume for confirmation: Breakouts accompanied by increased volume are more valid
  • Combine with trend indicators for filtering: Only go long in uptrends and short in downtrends

V. Advantages, Disadvantages, and Use Cases

Advantages

AdvantageDescription
Extremely simpleOnly requires a moving average and a fixed percentage — minimal computation
Few parametersOnly two parameters: period and percentage
IntuitiveThe channel meaning is immediately clear
Good for stable marketsPerforms well with assets that have relatively stable volatility

Disadvantages

DisadvantageDescription
Not adaptive to volatilityFixed percentage cannot adjust with changing market volatility
Subjective parameter choiceThe percentage must be manually adjusted based on the asset and timeframe
Fails in high volatilityIn strong trends or high volatility, price may run outside the channel for extended periods
Superseded by Bollinger BandsBollinger Bands is an improvement over Envelopes in nearly every respect

Use Cases

  • Best suited for: Assets with stable volatility (e.g., some large-cap blue chips, stable forex pairs)
  • Moderately suited for: Mean reversion trading in range-bound markets
  • Not suited for: High-volatility assets (e.g., small-cap stocks, cryptocurrencies); trending markets

Percentage Parameter Reference

TimeframeSuggested PercentageNotes
Daily1%—3%Depends on asset volatility
Weekly3%—5%Larger timeframes need larger percentages
Minute0.1%—0.5%Shorter timeframes need smaller percentages
Tip

Rule of thumb for choosing the percentage: Backtest historical data and select a percentage that keeps the price inside the channel approximately 90%—95% of the time. If breakouts are too frequent, the percentage is too small; if breakouts almost never occur, the percentage is too large. You can also calculate the asset’s historical volatility as a reference for setting the percentage.

Comparison with Similar Indicators

IndicatorChannel WidthAdaptivenessComplexity
EnvelopesFixed percentageNoneLowest
Bollinger BandsStandard deviationAdapts to volatilityMedium
Keltner ChannelsATRAdapts to volatilityHigher

Overall, Envelopes is a good starting point for learning channel indicators, but in actual trading, adaptive indicators such as Bollinger Bands or Keltner Channels are generally recommended as alternatives.