VaultCharts
Trading Strategystocks

IV Timing Mean Reversion Strategy

Mean reversion: enter on next bar open (no look-ahead). Long or short when RSI(2)<15 in uptrend after big drop; exit when RSI>50.

What is IV Timing Mean Reversion?

Implied volatility timing for mean reversion. No look-ahead: entry is on NEXT bar open after condition; exit on close. Long (inverse=false): buy oversold in uptrend (RSI(2)<15 after big drop). Short (inverse=true): short oversold. Exits when RSI > 50. Designed for stocks with min price and volume filters.

Strategy Parameters

ParameterTypeDefaultDescription
maPeriodnumber200Trend EMA period
rsiPeriodnumber2RSI period (2 for fast)
volLookbacknumber20Volatility lookback
stdDevDropnumber2Std dev for big drop
minPricenumber20Minimum stock price
minAvgVolumenumber200000Minimum average volume
inversebooleanfalseTrue for short bias

Use Cases

  • Mean reversion in uptrend
  • RSI(2) oversold bounces
  • No look-ahead entry (next bar open)
  • Stocks only

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: "IV Timing Mean Reversion",
    params: {
      maPeriod: { type: "number", default: 200 },
      rsiPeriod: { type: "number", default: 2 },
      volLookback: { type: "number", default: 20 },
      stdDevDrop: { type: "number", default: 2 },
      minPrice: { type: "number", default: 20 },
      minAvgVolume: { type: "number", default: 200000 },
      inverse: { type: "boolean", default: false }
    }
  },
  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.volume == null || Number.isFinite(d.volume)) &&
      d.high >= d.low && d.close > 0
    );
    if (!cleanData || cleanData.length < 221) return { signals: [] };

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

    const maPeriod = params?.maPeriod ?? 200;
    const rsiPeriod = params?.rsiPeriod ?? 2;
    const volLookback = params?.volLookback ?? 20;
    const stdDevDrop = params?.stdDevDrop ?? 2;
    const minPrice = params?.minPrice ?? 20;
    const minAvgVolume = params?.minAvgVolume ?? 200000;
    const inverse = params?.inverse === true;

    const closes = cleanData.map(d => d.close);
    const volumes = cleanData.map(d => d.volume || 0);

    const ma200 = TI.EMA.calculate({ period: maPeriod, values: closes });
    const rsi = TI.RSI.calculate({ values: closes, period: rsiPeriod });

    const signals = [];
    let position = null;
    var pendingEntry = null;

    for (let i = Math.max(maPeriod, volLookback, rsiPeriod, 5); i < cleanData.length; i++) {
      const candle = cleanData[i];
      const maIdx = i - (maPeriod - 1);
      if (maIdx < 0 || maIdx >= ma200.length || i >= rsi.length) continue;

      const maVal = ma200[maIdx];
      const rsiVal = rsi[i];
      if (!Number.isFinite(maVal) || !Number.isFinite(rsiVal)) continue;

      if (pendingEntry !== null) {
        signals.push({
          type: "entry",
          direction: pendingEntry,
          time: candle.time,
          price: candle.open,
          index: i
        });
        position = pendingEntry;
        pendingEntry = null;
        continue;
      }

      const inUptrend = candle.close > maVal;
      const priceOk = candle.close >= minPrice;
      const avgVol = volumes.slice(Math.max(0, i - 20), i).reduce((a, b) => a + b, 0) / Math.min(20, i);
      const volumeOk = avgVol >= minAvgVolume;

      const returns = [];
      for (let j = Math.max(0, i - volLookback); j < i; j++) {
        if (cleanData[j - 1] && cleanData[j - 1].close > 0) {
          returns.push((cleanData[j].close - cleanData[j - 1].close) / cleanData[j - 1].close);
        }
      }
      const meanRet = returns.length ? returns.reduce((a, b) => a + b, 0) / returns.length : 0;
      const variance = returns.length ? returns.reduce((s, r) => s + (r - meanRet) ** 2, 0) / returns.length : 0;
      const stdRet = Math.sqrt(variance) || 0.0001;

      const recentHigh = Math.max(...closes.slice(Math.max(0, i - volLookback), i));
      const dropFromHigh = (recentHigh - candle.close) / (recentHigh || 1);
      const bigDrop = dropFromHigh >= stdDevDrop * stdRet;

      const entryCondition = inUptrend && priceOk && volumeOk && bigDrop && rsiVal < 15;
      const exitLong = rsiVal > 50;
      const exitShort = rsiVal > 50;

      if (position === 'long') {
        if (exitLong) {
          signals.push({ type: "exit", direction: "long", time: candle.time, price: candle.close, index: i });
          position = null;
        }
        continue;
      }
      if (position === 'short') {
        if (exitShort) {
          signals.push({ type: "exit", direction: "short", time: candle.time, price: candle.close, index: i });
          position = null;
        }
        continue;
      }

      if (entryCondition && i + 1 < cleanData.length) {
        pendingEntry = inverse ? 'short' : 'long';
      }
    }
    return { signals };
  }
};

Run IV Timing Mean Reversion 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