← All Bots

S&P 500 DCA

FREEOpen SourcestockMIT License

Dollar-cost averaging into the S&P 500. Buys every 5 trading days regardless of price — the simplest proven strategy.

1Downloads
3Views
1.09%Best Return
1.85Sharpe Ratio

How It Works

The ultimate passive strategy: buy a fixed percentage of capital every 5 trading days, no matter what the market is doing. Historically, DCA into the S&P 500 has outperformed most active strategies over 10+ year periods. This bot automates what index fund investors do manually — removing emotion from the equation.

Deploy S&P 500 DCA 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
SPY2025-02-24 — 2026-02-231.09%1.85-0.31%100%1

Configuration

intervalCandles5
amountPct5
maxPositions50
Symbols: SPY

Source Code

#!/usr/bin/env node
// S&P 500 DCA — JC Trading Bots
// https://trading.jc.holdings/bot/s-p-500-dca-xfw7
// 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 s-p-500-dca-xfw7.js                    # Print signals
//   ALPACA_KEY=x ALPACA_SECRET=y node s-p-500-dca-xfw7.js  # Paper trade
//   ALPACA_KEY=x ALPACA_SECRET=y LIVE=1 node s-p-500-dca-xfw7.js  # REAL money

// ─── Configuration ─────────────────────────────────────────
const CONFIG = {
  "intervalCandles": 5,
  "amountPct": 5,
  "maxPositions": 50
};
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 = "S&P 500 Dollar Cost Averager";
const DATA_RANGE = "3mo";

function evaluate(candles, config) {
  // DCA buys every N bars regardless of price
  const interval = config.buyInterval || 5;
  const price = candles[candles.length - 1].close;
  const barIndex = candles.length;

  // Simple: if today is a buy day (every N trading days), buy
  if (barIndex % interval === 0) {
    return { action: "buy", reason: "DCA buy day (every " + interval + " trading days). Price: $" + price.toFixed(2) };
  }
  return { action: "hold", reason: "Next buy in " + (interval - (barIndex % interval)) + " trading days. 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); });
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