Bollinger Bands
Volatility envelopes around a moving average.
Two rubber bands stretched a few standard deviations away from the average price. When price stretches one and snaps back, that's the trade. Wider bands mean the market is jittery; narrow bands mean a big move is coming.
Two rubber bands stretched k standard deviations away from the average. When price stretches one and snaps back, that's the trade. Wider bands mean the market is jittery; narrow bands mean it's coiling. Bollinger himself wrote that the bands aren't trade signals — they're a regime indicator. Most traders ignore him and use them as signals anyway.
upper = SMA(n) + k·σ; lower = SMA(n) − k·σ
Real Bollinger Bands bot, running on real Yahoo data when the symbol is available. Drag the params — the bot re-runs instantly.
This is the actual code the bot runs — not a re-explanation, not a simplified version. Whatever ships here is what executes when you press Run All in the workbench. Read it, copy it, fork it, build a better one.
const bollBot: BotDef = {
id: "boll",
name: "Bollinger Bands",
category: "trend",
glyph: "⌇",
tagline: "Volatility envelopes around a moving average.",
formula: "upper = SMA(n) + k·σ; lower = SMA(n) − k·σ",
params: [
{ key: "period", label: "Period", kind: "number", default: 20, min: 10, max: 50, step: 1 },
{ key: "stdMult", label: "Std multiplier", kind: "number", default: 2, min: 1, max: 3, step: 0.1 },
],
run: (ctx, p): BotResult => {
const period = Math.round(num(p, "period", 20));
const k = num(p, "stdMult", 2);
const px = closes(ctx.candles);
const b = bollinger(px, period, k);
const signals: Signal[] = [];
const sigArr: (1 | -1 | 0)[] = px.map(() => 0);
for (let i = 1; i < px.length; i++) {
const upPrev = b.upper[i - 1], loPrev = b.lower[i - 1];
const u = b.upper[i], l = b.lower[i];
if (u == null || l == null || upPrev == null || loPrev == null) continue;
if (px[i - 1] < loPrev && px[i] >= l) {
signals.push({ i, kind: "buy", price: px[i], label: "lower band reclaim" });
sigArr[i] = 1;
} else if (px[i - 1] > upPrev && px[i] <= u) {
signals.push({ i, kind: "sell", price: px[i], label: "upper band reject" });
sigArr[i] = -1;
}
}
const bt = backtestLongOnly(px, sigArr);
const last = px[px.length - 1];
const lu = b.upper[b.upper.length - 1] ?? last;
const ll = b.lower[b.lower.length - 1] ?? last;
const lm = b.mid[b.mid.length - 1] ?? last;
const width = (lu - ll) / lm;
return {
signals,
metrics: [
{ key: "width", label: "Width", value: `${(width * 100).toFixed(1)}%`, tone: width > 0.1 ? "warn" : "neutral", hint: "vol envelope" },
{ key: "upper", label: "Upper", value: fmtNum(lu) },
{ key: "lower", label: "Lower", value: fmtNum(ll) },
{ key: "trades", label: "Trades", value: String(bt.trades) },
],
overlay: [
{ values: b.mid, color: "var(--fg-dim)", label: "SMA" },
{ values: b.upper, color: "var(--bear-dim)", label: "Upper", dashed: true },
{ values: b.lower, color: "var(--bull-dim)", label: "Lower", dashed: true },
],
summary: `Bands width ${(width * 100).toFixed(1)}%. ${signals.length} band-touch reversals.`,
beginner:
"Two rubber bands stretched k standard deviations away from the average. When price stretches one and snaps back, that's the trade. Wider bands mean the market is jittery.",
verdict: {
side: last < ll ? "buy" : last > lu ? "sell" : "hold",
text: last < ll ? "Below lower band — coiled spring." : last > lu ? "Above upper band — overextended." : "Inside the bands — no edge.",
confidence: Math.min(1, Math.abs(last - lm) / (lu - lm || 1)),
},
equity: bt.equity,
};
},
};id— unique key the workbench uses to find the bot.params— the sliders + inputs you see on the cell.run(ctx, p)— the function that gets called with candles + your params and returns the verdict.verdict— the BUY / SELL / HOLD pill at the top of the cell.metrics— the small stat boxes shown in the cell body.
- Copy the whole block above.
- On /quant, click + Import your bot in the bot library.
- Paste, hit save. It hot-loads into your workspace.
- Edit any param defaults or logic to your taste — it's now yours.
- ·Range-bound markets. Touches at the bands are reversion entries.
- ·Volatility expansion plays. Bollinger 'squeezes' (very narrow bands) often precede big moves.
- ·Combined with RSI for confirmation. Lower-band touch + oversold RSI = stronger reversal signal.
- ·Strong trends. Price 'walks the band' for weeks during a parabolic move; every touch is a losing fade.
- ·Tight-band false signals. Ranges look like coils until they don't.
- ·Default 20/2 settings. The 2-σ assumption breaks for skewed return distributions.
BUY when price reclaims the lower band from below. SELL when it rejects the upper band from above. Confidence is the distance from the mid-line, scaled by half-band width. Width of the bands (as % of mid) is a separate volatility regime indicator.
Why 20 periods, 2 standard deviations?+
What's a Bollinger squeeze?+
Catch oversold bounces, fade overbought spikes.
Bet on snap-back when the price wanders too far.
Linear regression line + std-deviation envelopes.
The Turtle trade — buy n-day highs, sell n-day lows.
Buy when fast moving average crosses above slow.
Momentum from EMA difference & its signal line.