Buys gap-downs at market open, rides the intraday reversion.
Every morning, stocks gap up or down based on overnight news, earnings, or futures movement. In most cases, these gaps partially or fully revert during the trading day.
The strategy: When a stock gaps down more than 1.5% at the open, buy it. Hold for 3-10 bars and sell as it reverts toward the previous close.
Why it works: Overnight gaps often represent panic selling or algorithmic overreaction. Market makers and institutional traders fade these gaps for consistent profits.
Academic backing: The gap-fill phenomenon has been documented in academic literature since the 1990s.
6Downloads
10Views
3Symbols
2Backtests
Parameters
gapThreshold0.8
exitBars3
maxHold10
positionPct90
Symbols
Backtest Results
| Symbol | Period | Return | Sharpe | Max DD | Win Rate | Trades |
|---|---|---|---|---|---|---|
| AAPL | 2025-02-24 — 2026-02-23 | 25.7% | 1.37 | -12.76% | 100% | 1 |
| SPY | 2025-02-24 — 2026-02-23 | 19.81% | 1.97 | -4.65% | 100% | 1 |
Source Code
#!/usr/bin/env node
// Overnight Gap Exploiter — JC Trading Bots
// https://trading.jc.holdings/bot/overnight-gap-exploiter
// 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 overnight-gap-exploiter.js # Print signals
// ALPACA_KEY=x ALPACA_SECRET=y node overnight-gap-exploiter.js # Paper trade
// ALPACA_KEY=x ALPACA_SECRET=y LIVE=1 node overnight-gap-exploiter.js # REAL money
// ─── Configuration ─────────────────────────────────────────
const CONFIG = {
"gapThreshold": 0.8,
"exitBars": 3,
"maxHold": 10,
"positionPct": 90
};
const SYMBOLS = ["SPY","QQQ","AAPL"];
// ─── 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 = "Overnight Gap Exploiter";
const DATA_RANGE = "3mo";
function evaluate(candles, config) {
if (candles.length < 2) return { action: "hold", reason: "Need more data" };
const today = candles[candles.length - 1];
const yesterday = candles[candles.length - 2];
const gapPct = ((today.open - yesterday.close) / yesterday.close) * 100;
const threshold = config.gapThreshold || 0.8;
if (gapPct < -threshold) {
return { action: "buy", reason: "Gap DOWN " + gapPct.toFixed(2) + "% (threshold: -" + threshold + "%). Buy the dip, ride reversion." };
}
if (gapPct > threshold) {
return { action: "sell", reason: "Gap UP +" + gapPct.toFixed(2) + "% — take profits on gap-up." };
}
return { action: "hold", reason: "Gap: " + gapPct.toFixed(2) + "% (within +/-" + threshold + "% threshold)" };
}
// ─── 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); });
Download .js File
Full source code included. MIT License. Free forever.