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
| Parameter | Type | Default | Description |
|---|---|---|---|
| maPeriod | number | 200 | Trend EMA period |
| rsiPeriod | number | 2 | RSI period (2 for fast) |
| volLookback | number | 20 | Volatility lookback |
| stdDevDrop | number | 2 | Std dev for big drop |
| minPrice | number | 20 | Minimum stock price |
| minAvgVolume | number | 200000 | Minimum average volume |
| inverse | boolean | false | True 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.
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.