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
| Parameter | Type | Default | Description |
|---|---|---|---|
| bbPeriod | number | 20 | Bollinger Bands period |
| bbStdDev | number | 2 | BB standard deviation |
| atrPeriod | number | 14 | ATR period |
| vwapLookback | number | 20 | VWAP rolling period |
| squeezeLookback | number | 30 | Bars 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.