VaultCharts
Trading Strategycryptostocksforex

Volatility Expansion Breakout Strategy

Trades volatility expansion after prolonged compression with trend confirmation. Works well on crypto, equities, and forex.

What is Volatility Expansion Breakout?

Trades volatility expansion after prolonged compression with trend confirmation. Waits for compression (narrow Bollinger Bands + low ATR) then trades the breakout in the direction of the EMA trend. Exits when price crosses back to band middle or trend flips.

Strategy Parameters

ParameterTypeDefaultDescription
bbPeriodnumber20Bollinger Bands period
emaPeriodnumber100EMA period for trend
atrPeriodnumber14ATR period

Use Cases

  • Consolidation breakouts
  • Trend-aligned expansion
  • BB width + ATR compression
  • EMA trend filter

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: "Volatility Expansion",
    params: {
      bbPeriod: { type: "number", default: 20 },
      emaPeriod: { type: "number", default: 100 },
      atrPeriod: { type: "number", default: 14 }
    }
  },
  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) &&
      d.high >= d.low &&
      d.close > 0
    );
    
    if (!cleanData || cleanData.length < 150) {
      console.warn('[Volatility Expansion] Insufficient data:', cleanData?.length || 0);
      return { signals: [] };
    }
    
    const { technicalindicators: TI } = utils;
    if (!TI || !TI.EMA || !TI.ATR) {
      console.warn('[Volatility Expansion] Technical indicators not available');
      return { signals: [] };
    }
    
    const bbPeriod = params?.bbPeriod ?? 20;
    const emaPeriod = params?.emaPeriod ?? 100;
    const atrPeriod = params?.atrPeriod ?? 14;
    
    const closes = cleanData.map(d => d.close);
    const highs = cleanData.map(d => d.high);
    const lows = cleanData.map(d => d.low);
    
    // BollingerBands - try multiple API forms
    let bb;
    try {
      if (TI.BollingerBands && TI.BollingerBands.calculate) {
        bb = TI.BollingerBands.calculate({ period: bbPeriod, values: closes, stdDev: 2 });
      } else if (TI.BollingerBands && typeof TI.BollingerBands === 'function') {
        const bbIndicator = new TI.BollingerBands({ period: bbPeriod, values: closes, stdDev: 2 });
        bb = bbIndicator.getResult();
      } else if (TI.BBANDS) {
        bb = TI.BBANDS.calculate({ period: bbPeriod, values: closes, stdDev: 2 });
      } else {
        console.warn('[Volatility Expansion] BollingerBands not available');
        return { signals: [] };
      }
    } catch (err) {
      console.error('[Volatility Expansion] BollingerBands error:', err);
      return { signals: [] };
    }
    
    const ema = TI.EMA.calculate({ period: emaPeriod, values: closes });
    const atr = TI.ATR.calculate({ high: highs, low: lows, close: closes, period: atrPeriod });
    
    console.log('[Volatility Expansion] Indicator lengths:', {
      dataLength: cleanData.length,
      bbLength: bb.length,
      emaLength: ema.length,
      atrLength: atr.length
    });
    
    const signals = [];
    
    // Use EMA as base since it's the slowest indicator (100)
    const emaStartOffset = emaPeriod - 1;
    
    let checkedCount = 0;
    let skippedCount = 0;
    
    // Iterate over EMA array indices
    for (let i = 1; i < ema.length; i++) {
      const dataIdx = i + emaStartOffset; // Derive data index from EMA
      
      if (dataIdx >= cleanData.length) break;
      
      const candle = cleanData[dataIdx];
      if (!candle || candle.time === undefined) continue;
      
      // Align other indicators to this dataIdx
      const bbIdx = dataIdx - (bbPeriod - 1);
      const atrIdx = dataIdx - (atrPeriod - 1);
      const emaIdx = i; // Already aligned (EMA base)
      
      // Bounds check
      if (bbIdx < 1 || atrIdx < 1 || emaIdx < 1) {
        skippedCount++;
        continue;
      }
      if (bbIdx >= bb.length || atrIdx >= atr.length || emaIdx >= ema.length) {
        skippedCount++;
        continue;
      }
      
      const bbCurr = bb[bbIdx];
      const bbPrev = bb[bbIdx - 1];
      
      if (!bbCurr || !bbPrev || 
          !Number.isFinite(bbCurr.upper) || !Number.isFinite(bbCurr.lower) || !Number.isFinite(bbCurr.middle)) {
        skippedCount++;
        continue;
      }
      
      const atrCurr = atr[atrIdx];
      const atrPrev = atr[atrIdx - 1];
      if (!Number.isFinite(atrCurr) || !Number.isFinite(atrPrev)) {
        skippedCount++;
        continue;
      }
      
      const emaCurr = ema[emaIdx];
      const emaPrev = ema[emaIdx - 1];
      if (!Number.isFinite(emaCurr) || !Number.isFinite(emaPrev)) {
        skippedCount++;
        continue;
      }
      
      checkedCount++;
      
      const width = bbCurr.upper - bbCurr.lower;
      const prevWidth = bbPrev.upper - bbPrev.lower;
      const compression = width < prevWidth && atrCurr < atrPrev;
      
      const bullishBias = emaCurr > emaPrev;
      const bearishBias = emaCurr < emaPrev;
      
      const breakoutUp = candle.close > bbCurr.upper;
      const breakoutDown = candle.close < bbCurr.lower;
      
      // 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';
      
      // Entry signals
      if (!inLongPosition && !inShortPosition && bullishBias && (compression || breakoutUp)) {
        signals.push({ type: "entry", direction: "long", time: candle.time, price: candle.close, index: dataIdx });
      }
      if (!inLongPosition && !inShortPosition && bearishBias && (compression || breakoutDown)) {
        signals.push({ type: "entry", direction: "short", time: candle.time, price: candle.close, index: dataIdx });
      }
      
      // Exit signals
      if (inLongPosition && (candle.close < bbCurr.middle || !bullishBias)) {
        signals.push({ type: "exit", direction: "long", time: candle.time, price: candle.close, index: dataIdx });
      }
      if (inShortPosition && (candle.close > bbCurr.middle || !bearishBias)) {
        signals.push({ type: "exit", direction: "short", time: candle.time, price: candle.close, index: dataIdx });
      }
    }
    
    console.log('[Volatility Expansion] Analysis:', {
      checkedCount,
      skippedCount,
      signalsGenerated: signals.length
    });
    
    return { signals };
  }
};

Run Volatility Expansion Breakout 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