VaultCharts
Trading Strategystockscryptofutures

ATR Squeeze + VWAP Bias Strategy

Advanced volatility expansion with VWAP directional bias. Uses ATR squeeze, BB width, and VWAP to avoid chop.

What is ATR Squeeze + VWAP Bias?

Advanced strategy using ATR squeeze, Bollinger Band width, VWAP bias, and expansion triggers. Filters direction using VWAP: long when price above VWAP, short when below. Only trades expansion after a squeeze (ATR at low, BB contracting). Exits when price crosses VWAP or breaks back into squeeze.

Strategy Parameters

ParameterTypeDefaultDescription
bbPeriodnumber20Bollinger Bands period
bbStdDevnumber2BB standard deviation
atrPeriodnumber14ATR period
vwapLookbacknumber20VWAP rolling period
squeezeLookbacknumber30Bars for squeeze detection

Use Cases

  • Intraday / swing with VWAP bias
  • Avoid chop with VWAP filter
  • Squeeze then expansion
  • Stocks and crypto

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: "ATR Squeeze + VWAP Bias",
    params: {
      bbPeriod: { type: "number", default: 20 },
      bbStdDev: { type: "number", default: 2 },
      atrPeriod: { type: "number", default: 14 },
      vwapLookback: { type: "number", default: 20 },
      squeezeLookback: { type: "number", default: 30 }
    }
  },
  compute: (data, params, utils) => {
    // Data sanitization
    const cleanData = data.filter(d => 
      d && 
      Number.isFinite(d.high) && 
      Number.isFinite(d.low) && 
      Number.isFinite(d.close) &&
      Number.isFinite(d.time) &&
      Number.isFinite(d.volume) &&
      d.high >= d.low &&
      d.close > 0 &&
      d.volume >= 0
    );
    
    if (!cleanData || cleanData.length < 100) {
      console.warn('[ATR Squeeze VWAP] Insufficient data:', cleanData?.length || 0);
      return { signals: [] };
    }
    
    const { technicalindicators: TI } = utils;
    if (!TI || !TI.BollingerBands || !TI.ATR) {
      console.warn('[ATR Squeeze VWAP] Technical indicators not available');
      return { signals: [] };
    }
    
    const bbPeriod = params?.bbPeriod ?? 20;
    const bbStdDev = params?.bbStdDev ?? 2;
    const atrPeriod = params?.atrPeriod ?? 14;
    const vwapLookback = params?.vwapLookback ?? 20;
    const squeezeLookback = params?.squeezeLookback ?? 30;
    
    const closes = cleanData.map(d => d.close);
    const highs = cleanData.map(d => d.high);
    const lows = cleanData.map(d => d.low);
    const volumes = cleanData.map(d => d.volume || 0);
    
    // Calculate Bollinger Bands
    let bb;
    try {
      if (TI.BollingerBands && TI.BollingerBands.calculate) {
        bb = TI.BollingerBands.calculate({ period: bbPeriod, values: closes, stdDev: bbStdDev });
      } else if (TI.BBANDS) {
        bb = TI.BBANDS.calculate({ period: bbPeriod, values: closes, stdDev: bbStdDev });
      } else {
        return { signals: [] };
      }
    } catch (err) {
      console.error('[ATR Squeeze VWAP] BollingerBands error:', err);
      return { signals: [] };
    }
    
    const atr = TI.ATR.calculate({ high: highs, low: lows, close: closes, period: atrPeriod });
    
    // Calculate VWAP (Volume Weighted Average Price)
    const vwap = [];
    let cumulativeTPV = 0; // Typical Price * Volume
    let cumulativeVolume = 0;
    
    for (let i = 0; i < cleanData.length; i++) {
      const typicalPrice = (highs[i] + lows[i] + closes[i]) / 3;
      const volume = volumes[i];
      
      if (i < vwapLookback) {
        // Simple VWAP for first period
        cumulativeTPV += typicalPrice * volume;
        cumulativeVolume += volume;
        vwap.push(cumulativeVolume > 0 ? cumulativeTPV / cumulativeVolume : closes[i]);
      } else {
        // Rolling VWAP
        cumulativeTPV = 0;
        cumulativeVolume = 0;
        for (let j = i - vwapLookback + 1; j <= i; j++) {
          const tp = (highs[j] + lows[j] + closes[j]) / 3;
          const vol = volumes[j];
          cumulativeTPV += tp * vol;
          cumulativeVolume += vol;
        }
        vwap.push(cumulativeVolume > 0 ? cumulativeTPV / cumulativeVolume : closes[i]);
      }
    }
    
    const signals = [];
    const startIdx = Math.max(bbPeriod, atrPeriod, squeezeLookback, vwapLookback);
    
    let checkedCount = 0;
    let skippedCount = 0;
    let squeezeCount = 0;
    let expansionCount = 0;
    
    for (let i = startIdx; i < cleanData.length; i++) {
      const candle = cleanData[i];
      if (!candle || candle.time === undefined) continue;
      
      const bbIdx = i - (bbPeriod - 1);
      const atrIdx = i - (atrPeriod - 1);
      
      if (bbIdx < 0 || atrIdx < 0 || bbIdx >= bb.length || atrIdx >= atr.length) {
        skippedCount++;
        continue;
      }
      if (i >= vwap.length) {
        skippedCount++;
        continue;
      }
      
      const bbCurr = bb[bbIdx];
      const atrCurr = atr[atrIdx];
      const vwapCurr = vwap[i];
      
      if (!bbCurr || !Number.isFinite(bbCurr.upper) || !Number.isFinite(bbCurr.lower) || !Number.isFinite(bbCurr.middle)) {
        skippedCount++;
        continue;
      }
      if (!Number.isFinite(atrCurr) || !Number.isFinite(vwapCurr)) {
        skippedCount++;
        continue;
      }
      if (bbCurr.middle === 0 || candle.close === 0) {
        skippedCount++;
        continue;
      }
      
      checkedCount++;
      
      // Calculate BB Width
      const bbWidth = (bbCurr.upper - bbCurr.lower) / bbCurr.middle;
      
      // Find minimum ATR over squeeze lookback period
      let minATR = atrCurr;
      for (let j = Math.max(0, i - squeezeLookback); j < i; j++) {
        const histAtrIdx = j - (atrPeriod - 1);
        if (histAtrIdx >= 0 && histAtrIdx < atr.length && atr[histAtrIdx] < minATR) {
          minATR = atr[histAtrIdx];
        }
      }
      
      // Check compression: ATR at 30-day low AND BB width contracting
      const atrAtLow = atrCurr <= minATR * 1.05; // Within 5% of minimum
      
      // Check BB width contracting (compare to previous)
      const bbPrevIdx = bbIdx - 1;
      let bbWidthContracting = false;
      if (bbPrevIdx >= 0 && bbPrevIdx < bb.length) {
        const bbPrev = bb[bbPrevIdx];
        if (bbPrev && bbPrev.middle > 0) {
          const bbWidthPrev = (bbPrev.upper - bbPrev.lower) / bbPrev.middle;
          bbWidthContracting = bbWidth < bbWidthPrev;
        }
      }
      
      // Check price near VWAP (±0.5%)
      const vwapDistance = Math.abs(candle.close - vwapCurr) / vwapCurr;
      const priceNearVWAP = vwapDistance < 0.005; // 0.5%
      
      // Compression regime: relaxed - ATR at low AND (BB contracting OR price near VWAP)
      const inSqueeze = atrAtLow && (bbWidthContracting || priceNearVWAP);
      
      if (inSqueeze) squeezeCount++;
      
      // Directional bias: price relative to VWAP
      const priceAboveVWAP = candle.close > vwapCurr;
      const priceBelowVWAP = candle.close < vwapCurr;
      
      // Get position state
      const lastSignal = signals.length > 0 ? signals[signals.length - 1] : null;
      const inLongPosition = lastSignal && lastSignal.type === 'entry' && lastSignal.direction === 'long';
      const inShortPosition = lastSignal && lastSignal.type === 'entry' && lastSignal.direction === 'short';
      
      // Calculate True Range for expansion detection
      let trueRange = highs[i] - lows[i];
      if (i > 0) {
        const tr1 = highs[i] - lows[i];
        const tr2 = Math.abs(highs[i] - closes[i - 1]);
        const tr3 = Math.abs(lows[i] - closes[i - 1]);
        trueRange = Math.max(tr1, tr2, tr3);
      }
      
      // Expansion trigger: range expansion OR volume surge (relaxed)
      const avgVolume = volumes.slice(Math.max(0, i - 20), i).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(20, i));
      const volumeSurge = volumes[i] > avgVolume * 1.2; // Reduced from 1.5 to 1.2
      const rangeExpansion = trueRange > atrCurr * 1.1; // Reduced from 1.2 to 1.1
      
      const expansionCandle = rangeExpansion || volumeSurge; // Changed from AND to OR
      
      if (expansionCandle) expansionCount++;
      
      // Entry signals: simplified - in squeeze, with directional bias, on expansion
      if (!inLongPosition && !inShortPosition && inSqueeze && expansionCandle) {
        if (priceAboveVWAP) {
          // Long only if price > VWAP
          signals.push({ type: "entry", direction: "long", time: candle.time, price: candle.close, index: i });
        } else if (priceBelowVWAP) {
          // Short only if price < VWAP
          signals.push({ type: "entry", direction: "short", time: candle.time, price: candle.close, index: i });
        }
      }
      
      // Exit signals: price crosses VWAP or breaks back into squeeze
      if (inLongPosition) {
        if (priceBelowVWAP || (inSqueeze && candle.close < bbCurr.middle)) {
          signals.push({ type: "exit", direction: "long", time: candle.time, price: candle.close, index: i });
        }
      }
      
      if (inShortPosition) {
        if (priceAboveVWAP || (inSqueeze && candle.close > bbCurr.middle)) {
          signals.push({ type: "exit", direction: "short", time: candle.time, price: candle.close, index: i });
        }
      }
    }
    
    console.log('[ATR Squeeze VWAP] Analysis:', {
      dataLength: cleanData.length,
      startIdx,
      checkedCount,
      skippedCount,
      squeezeCount,
      expansionCount,
      signalsGenerated: signals.length
    });
    
    return { signals };
  }
};

Run ATR Squeeze + VWAP Bias 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