← All Bots

OPEX Gamma Scalper

FREEOpen SourcestockMIT License

Exploits options expiry week gamma pinning. The options tail wags the stock dog.

1Downloads
3Views
2.19%Best Return
0.88Sharpe Ratio

How It Works

Every month on options expiry (OPEX), market makers must delta-hedge billions of dollars in options contracts. This creates predictable price behavior:

  • Gamma pinning: Prices get "pinned" to max-pain strike prices
  • Volatility expansion then crush: Vol rises into OPEX then collapses after
  • Mean reversion: Price dislocations during OPEX week revert faster than normal

The strategy: Detect OPEX-week conditions (elevated short-term vol + price dislocation from the 10-day mean) and trade the mean reversion. The gamma exposure forces prices back to equilibrium.

Used by: Citadel, Jane Street, and every options market maker. Now you have the same logic in a bot.

Deploy OPEX Gamma Scalper Live

Download the bot, connect TradingView for charts, and use Alpaca or IBKR to execute trades.

Free charts · Paper trading available · Takes 5 minutes

Backtest Results

Backtested against real historical market data from Yahoo Finance.

SymbolPeriodReturnSharpeMax DrawdownWin RateTrades
QQQ2025-02-24 — 2026-02-232.19%0.88-2.37%100%3
SPY2025-02-24 — 2026-02-230.78%0.95-0.34%100%4

Configuration

lookback5
volExpansion1.5
meanReversionTarget0.5
positionPct80
Symbols: SPY QQQ

Source Code

#!/usr/bin/env node
// OPEX Gamma Scalper — JC Trading Bots
// https://trading.jc.holdings/bot/opex-gamma-scalper
// 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 opex-gamma-scalper.js                    # Print signals
//   ALPACA_KEY=x ALPACA_SECRET=y node opex-gamma-scalper.js  # Paper trade
//   ALPACA_KEY=x ALPACA_SECRET=y LIVE=1 node opex-gamma-scalper.js  # REAL money

// ─── Configuration ─────────────────────────────────────────
const CONFIG = {
  "lookback": 5,
  "volExpansion": 1.5,
  "meanReversionTarget": 0.5,
  "positionPct": 80
};
const SYMBOLS = ["SPY","QQQ"];

// ─── 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 = "OPEX Gamma Scalper";
const DATA_RANGE = "3mo";

function evaluate(candles, config) {
  const lookback = config.lookback || 5;
  if (candles.length < lookback + 14) return { action: "hold", reason: "Need more data" };

  const currentATR = atr(candles, 14);
  const recentSlice = candles.slice(-lookback);
  let recentATRSum = 0;
  for (let i = 0; i < recentSlice.length; i++) {
    const idx = candles.length - lookback + i;
    recentATRSum += atr(candles.slice(0, idx + 1), 14);
  }
  const recentATR = recentATRSum / lookback;
  const volExpansion = recentATR / currentATR;

  const closes = candles.map(c => c.close);
  const mean = sma(closes, 10);
  const price = closes[closes.length - 1];
  const deviation = mean ? (price - mean) / mean : 0;

  // OPEX weeks (3rd Friday) have elevated vol + mean reversion
  if (volExpansion > (config.volExpansion || 1.5) && Math.abs(deviation) > 0.01) {
    if (deviation < 0) {
      return { action: "buy", reason: "OPEX-like conditions: vol expanded " + volExpansion.toFixed(2) + "x, price " + (deviation * 100).toFixed(2) + "% below mean. Gamma pin expected." };
    } else {
      return { action: "sell", reason: "OPEX-like conditions: vol expanded " + volExpansion.toFixed(2) + "x, price " + (deviation * 100).toFixed(2) + "% above mean. Mean reversion." };
    }
  }
  return { action: "hold", reason: "Vol expansion: " + volExpansion.toFixed(2) + "x | Price deviation: " + (deviation * 100).toFixed(2) + "% from 10d mean" };
}

// ─── 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. MIT License. Free forever. Modify and deploy however you want.

Where to Run This Bot

Compare All Brokers →

Deploy this bot →Get TradingView Free