VaultCharts
Trading Strategystockscryptofutures

Squeeze Momentum Strategy

BB inside Keltner = squeeze; enter long/short when momentum turns positive/negative. Stop at band midline; exit on momentum reversal.

What is Squeeze Momentum?

Bollinger Bands inside Keltner = squeeze; when momentum histogram turns positive (from neutral/negative) enter long; when it turns negative enter short. Stop near band midline; exit on histogram reversal or ATR trail. Uses linear regression slope as momentum. Works on stocks, crypto, and futures.

Strategy Parameters

ParameterTypeDefaultDescription
bbPeriodnumber20Bollinger Bands period
bbStdDevnumber2BB standard deviation
keltnerPeriodnumber20Keltner period
keltnerMultnumber1.5Keltner ATR multiplier
atrPeriodnumber14ATR period
momPeriodnumber12Momentum (linear regression) period

Use Cases

  • Squeeze (BB inside Keltner) then momentum
  • Momentum histogram direction
  • Stop at band midline
  • Stocks, crypto, futures

Strategy Script (JavaScript)

This strategy runs in VaultCharts using the built-in strategy engine. Below is the full script in a readable format. You can copy it or run it directly in VaultCharts.

strategy.jsVaultCharts built-in
module.exports = {
  meta: {
    name: "Squeeze Momentum",
    params: {
      bbPeriod: { type: "number", default: 20 },
      bbStdDev: { type: "number", default: 2 },
      keltnerPeriod: { type: "number", default: 20 },
      keltnerMult: { type: "number", default: 1.5 },
      atrPeriod: { type: "number", default: 14 },
      momPeriod: { type: "number", default: 12 }
    }
  },
  compute: (data, params, utils) => {
    const cleanData = data.filter(d =>
      d && Number.isFinite(d.high) && Number.isFinite(d.low) && Number.isFinite(d.close) &&
      Number.isFinite(d.open) && Number.isFinite(d.time) && d.high >= d.low && d.close > 0
    );
    if (!cleanData || cleanData.length < 80) return { signals: [] };

    const { technicalindicators: TI } = utils;
    if (!TI || !TI.BollingerBands || !TI.ATR) return { signals: [] };

    const bbPeriod = params?.bbPeriod ?? 20;
    const bbStdDev = params?.bbStdDev ?? 2;
    const kPeriod = params?.keltnerPeriod ?? 20;
    const kMult = params?.keltnerMult ?? 1.5;
    const atrPeriod = params?.atrPeriod ?? 14;
    const momPeriod = params?.momPeriod ?? 12;

    const closes = cleanData.map(d => d.close);
    const highs = cleanData.map(d => d.high);
    const lows = cleanData.map(d => d.low);

    const bb = TI.BollingerBands.calculate({ period: bbPeriod, values: closes, stdDev: bbStdDev });
    const atr = TI.ATR.calculate({ high: highs, low: lows, close: closes, period: atrPeriod });

    const ema = [];
    const keltnerUpper = [];
    const keltnerLower = [];
    const alpha = 2 / (kPeriod + 1);
    let emaVal = closes[0];
    for (let i = 0; i < cleanData.length; i++) {
      emaVal = i === 0 ? closes[0] : alpha * closes[i] + (1 - alpha) * emaVal;
      ema.push(emaVal);
      const atrIdx = Math.min(i, atr.length - 1);
      const atrVal = Number.isFinite(atr[atrIdx]) ? atr[atrIdx] : 0;
      keltnerUpper.push(emaVal + kMult * atrVal);
      keltnerLower.push(emaVal - kMult * atrVal);
    }

    const momentum = [];
    for (let i = 0; i < cleanData.length; i++) {
      if (i < momPeriod) {
        momentum.push(0);
      } else {
        const linReg = (function(x, y) {
          let n = x.length, sumX = 0, sumY = 0, sumXY = 0, sumX2 = 0;
          for (let j = 0; j < n; j++) {
            sumX += x[j]; sumY += y[j]; sumXY += x[j]*y[j]; sumX2 += x[j]*x[j];
          }
          const slope = (n*sumXY - sumX*sumY) / (n*sumX2 - sumX*sumX);
          return slope;
        })(Array.from({length: momPeriod}, (_, j) => j), closes.slice(i - momPeriod, i));
        momentum.push(linReg);
      }
    }

    const signals = [];
    let position = null;
    let wasInSqueeze = false;

    for (let i = Math.max(bbPeriod, kPeriod, atrPeriod, momPeriod); i < cleanData.length; i++) {
      const candle = cleanData[i];
      const bbIdx = i - (bbPeriod - 1);
      if (bbIdx < 0 || bbIdx >= bb.length) continue;

      const bbCurr = bb[bbIdx];
      if (!bbCurr || !Number.isFinite(bbCurr.upper) || !Number.isFinite(bbCurr.lower)) continue;

      const bbInsideKeltner = bbCurr.upper <= keltnerUpper[i] && bbCurr.lower >= keltnerLower[i];
      const inSqueeze = bbInsideKeltner;

      const momCurr = momentum[i];
      const momPrev = i > 0 ? momentum[i - 1] : 0;
      const momTurnedGreen = Number.isFinite(momCurr) && Number.isFinite(momPrev) && momCurr > 0 && momPrev <= 0;
      const momTurnedRed = Number.isFinite(momCurr) && Number.isFinite(momPrev) && momCurr < 0 && momPrev >= 0;

      const mid = (bbCurr.upper + bbCurr.lower) / 2;

      if (position === 'long') {
        if (momTurnedRed || candle.close < mid * 0.998) {
          signals.push({ type: "exit", direction: "long", time: candle.time, price: candle.close, index: i });
          position = null;
        }
        continue;
      }
      if (position === 'short') {
        if (momTurnedGreen || candle.close > mid * 1.002) {
          signals.push({ type: "exit", direction: "short", time: candle.time, price: candle.close, index: i });
          position = null;
        }
        continue;
      }

      if (wasInSqueeze && momTurnedGreen && candle.close > mid) {
        signals.push({ type: "entry", direction: "long", time: candle.time, price: candle.close, index: i });
        position = 'long';
      }
      if (wasInSqueeze && momTurnedRed && candle.close < mid) {
        signals.push({ type: "entry", direction: "short", time: candle.time, price: candle.close, index: i });
        position = 'short';
      }

      wasInSqueeze = inSqueeze;
    }
    return { signals };
  }
};

Run Squeeze Momentum in VaultCharts

VaultCharts includes this strategy as a built-in option. Backtest it, adjust parameters, and use it on your own data—all stored locally on your device.

Related Strategies

Explore More