How It Works
Volatility is the one thing in markets that is truly mean-reverting. After every spike (earnings, Fed meetings, geopolitical events), vol always crushes back down.
The pattern: VIX spikes to 30+ then crushes back to 15 within weeks. This bot detects ATR spikes above 1.8x the average and buys the underlying, expecting prices to stabilize and recover as vol compresses.
The edge: This is essentially what options sellers do — harvest the vol premium. But instead of selling options (risky), this bot buys the asset itself when vol is elevated (much safer).
Backtest Results
Backtested against real historical market data from Yahoo Finance.
| Symbol | Period | Return | Sharpe | Max Drawdown | Win Rate | Trades |
|---|---|---|---|---|---|---|
| SPY | 2025-02-24 — 2026-02-23 | 3.08% | 0.67 | -4.34% | 50% | 2 |
Configuration
Source Code
#!/usr/bin/env node
// Volatility Crush Harvester — JC Trading Bots
// https://trading.jc.holdings/bot/volatility-crush-harvester
// License: MIT | Free forever | Modify however you want
//
// DISCLAIMER: Not financial advice. Past performance does not guarantee
// future results. Trading involves substantial risk of loss.
// Use at your own risk. No guarantees of profit.
//
// Usage:
// node volatility-crush-harvester.js # Print signals
// ALPACA_KEY=x ALPACA_SECRET=y node volatility-crush-harvester.js # Paper trade
// ALPACA_KEY=x ALPACA_SECRET=y LIVE=1 node volatility-crush-harvester.js # REAL money
// ─── Configuration ─────────────────────────────────────────
const CONFIG = {
"atrPeriod": 14,
"lookback": 50,
"spikeThreshold": 1.4,
"crushThreshold": 0.7,
"positionPct": 85
};
const SYMBOLS = ["SPY"];
// ─── Technical Indicators ───────────────────────────────────
function sma(data, period) {
if (data.length < period) return null;
const slice = data.slice(-period);
return slice.reduce((a, b) => a + b, 0) / period;
}
function ema(data, period) {
if (data.length < period) return null;
const k = 2 / (period + 1);
let val = sma(data.slice(0, period), period);
for (let i = period; i < data.length; i++) {
val = data[i] * k + val * (1 - k);
}
return val;
}
function rsi(closes, period = 14) {
if (closes.length < period + 1) return 50;
let gains = 0, losses = 0;
for (let i = closes.length - period; i < closes.length; i++) {
const diff = closes[i] - closes[i - 1];
if (diff > 0) gains += diff;
else losses -= diff;
}
if (losses === 0) return 100;
const rs = (gains / period) / (losses / period);
return 100 - (100 / (1 + rs));
}
function bollingerBands(closes, period = 20, mult = 2) {
const mid = sma(closes.slice(-period), period);
if (!mid) return { upper: null, mid: null, lower: null };
const slice = closes.slice(-period);
const variance = slice.reduce((sum, v) => sum + Math.pow(v - mid, 2), 0) / period;
const std = Math.sqrt(variance);
return { upper: mid + mult * std, mid, lower: mid - mult * std };
}
function atr(candles, period = 14) {
if (candles.length < period + 1) return 0;
let sum = 0;
for (let i = candles.length - period; i < candles.length; i++) {
const tr = Math.max(
candles[i].high - candles[i].low,
Math.abs(candles[i].high - candles[i - 1].close),
Math.abs(candles[i].low - candles[i - 1].close)
);
sum += tr;
}
return sum / period;
}
function macd(closes, fast = 12, slow = 26, sig = 9) {
if (closes.length < slow + sig) return { macd: 0, signal: 0, histogram: 0 };
const macdSeries = [];
for (let i = slow; i <= closes.length; i++) {
const slice = closes.slice(0, i);
const f = ema(slice, fast);
const s = ema(slice, slow);
if (f !== null && s !== null) macdSeries.push(f - s);
}
if (macdSeries.length < sig) return { macd: 0, signal: 0, histogram: 0 };
const macdLine = macdSeries[macdSeries.length - 1];
const signalLine = ema(macdSeries, sig);
return { macd: macdLine, signal: signalLine || 0, histogram: macdLine - (signalLine || 0) };
}
// ─── Data Fetching (Yahoo Finance) ──────────────────────────
async function fetchCandles(symbol, range = "1y", interval = "1d") {
const url = "https://query1.finance.yahoo.com/v8/finance/chart/" + symbol
+ "?range=" + range + "&interval=" + interval + "&includePrePost=false";
const res = await fetch(url, {
headers: { "User-Agent": "Mozilla/5.0 JCTradingBot/1.0" }
});
const json = await res.json();
const result = json.chart.result[0];
const ts = result.timestamp;
const q = result.indicators.quote[0];
const candles = [];
for (let i = 0; i < ts.length; i++) {
if (q.close[i] == null) continue;
candles.push({
timestamp: ts[i] * 1000,
date: new Date(ts[i] * 1000).toISOString().split("T")[0],
open: q.open[i],
high: q.high[i],
low: q.low[i],
close: q.close[i],
volume: q.volume[i] || 0,
});
}
return candles;
}
// ─── Alpaca Integration (optional) ──────────────────────────
// Set ALPACA_KEY and ALPACA_SECRET env vars to enable trading.
// Add LIVE=1 for real money (default is paper trading).
async function alpacaTrade(symbol, action, reason) {
const key = process.env.ALPACA_KEY;
const secret = process.env.ALPACA_SECRET;
if (!key || !secret) {
console.log("[DRY RUN] " + action.toUpperCase() + " " + symbol + " — " + reason);
return;
}
const baseUrl = process.env.LIVE === "1"
? "https://api.alpaca.markets"
: "https://paper-api.alpaca.markets";
// Get account buying power
const acct = await fetch(baseUrl + "/v2/account", {
headers: { "APCA-API-KEY-ID": key, "APCA-API-SECRET-KEY": secret }
}).then(r => r.json());
if (action === "buy") {
const budget = parseFloat(acct.buying_power) * 0.9;
if (budget < 10) { console.log("Insufficient funds: $" + acct.buying_power); return; }
const price = await fetch(baseUrl + "/v2/stocks/" + symbol + "/quotes/latest", {
headers: { "APCA-API-KEY-ID": key, "APCA-API-SECRET-KEY": secret }
}).then(r => r.json());
const qty = Math.floor(budget / (price.quote?.ap || 999999));
if (qty < 1) { console.log("Price too high for budget"); return; }
const order = await fetch(baseUrl + "/v2/orders", {
method: "POST",
headers: { "APCA-API-KEY-ID": key, "APCA-API-SECRET-KEY": secret, "Content-Type": "application/json" },
body: JSON.stringify({ symbol, qty: String(qty), side: "buy", type: "market", time_in_force: "day" })
}).then(r => r.json());
console.log("ORDER PLACED: BUY " + qty + " " + symbol + " — " + reason);
console.log("Order ID: " + order.id);
} else if (action === "sell") {
// Close position
const close = await fetch(baseUrl + "/v2/positions/" + symbol, {
method: "DELETE",
headers: { "APCA-API-KEY-ID": key, "APCA-API-SECRET-KEY": secret }
}).then(r => r.json());
console.log("POSITION CLOSED: " + symbol + " — " + reason);
}
}
const BOT_NAME = "Volatility Crush Harvester";
const DATA_RANGE = "1y";
function evaluate(candles, config) {
const period = config.atrPeriod || 14;
const lookback = config.lookback || 50;
if (candles.length < lookback + period) return { action: "hold", reason: "Need more data" };
const currentATR = atr(candles, period);
// Average ATR over lookback
let atrSum = 0;
for (let i = candles.length - lookback; i < candles.length; i++) {
atrSum += atr(candles.slice(0, i + 1), period);
}
const avgATR = atrSum / lookback;
const ratio = currentATR / avgATR;
const price = candles[candles.length - 1].close;
if (ratio > (config.spikeThreshold || 1.4)) {
return { action: "buy", reason: "Vol SPIKE detected. ATR ratio: " + ratio.toFixed(2) + "x avg. Buy the fear, harvest the crush. Price: $" + price.toFixed(2) };
}
if (ratio < (config.crushThreshold || 0.6)) {
return { action: "sell", reason: "Vol CRUSHED to " + ratio.toFixed(2) + "x avg. Take profits. Price: $" + price.toFixed(2) };
}
return { action: "hold", reason: "ATR ratio: " + ratio.toFixed(2) + "x avg — normal volatility. Price: $" + price.toFixed(2) };
}
// ─── Main ───────────────────────────────────────────────────
async function run() {
console.log("\n" + "=".repeat(60));
console.log(" " + BOT_NAME);
console.log(" " + new Date().toISOString());
console.log("=".repeat(60) + "\n");
for (const symbol of SYMBOLS) {
try {
console.log("Fetching data for " + symbol + "...");
const candles = await fetchCandles(symbol, DATA_RANGE);
console.log("Got " + candles.length + " candles (" + candles[0].date + " to " + candles[candles.length-1].date + ")");
const signal = evaluate(candles, CONFIG);
const icon = signal.action === "buy" ? "BUY" : signal.action === "sell" ? "SELL" : "HOLD";
console.log("\n " + symbol + ": " + icon + " — " + signal.reason);
if (signal.action !== "hold") {
await alpacaTrade(symbol, signal.action, signal.reason);
}
console.log("");
} catch (err) {
console.error("Error on " + symbol + ": " + err.message);
}
}
}
run().catch(err => { console.error(err); process.exit(1); });Full source code. MIT License. Free forever. Modify and deploy however you want.
Where to Run This Bot
TradingView
Charts, screeners, alerts & strategy backtesting. 30% off Premium — essential for any trader.
Open Account →ProfessionalInteractive Brokers
Low-cost global trading for stocks, options, futures. Professional-grade API.
Open Account →Prediction MarketsPolymarket
Prediction market trading. Sports, politics, crypto events. Where our arb bots operate.
Open Account →