QuantScript — mean-reversion-grid-eurusd-live.qs
// ─────────────────────────────────────────────────────────────────────────────
// Mean Reversion Grid System — EURUSD 1-Minute (Live Mode)
// ─────────────────────────────────────────────────────────────────────────────
//
// HOW IT WORKS:
//   This strategy builds a symmetric grid of price levels around a moving
//   mean (Bollinger Band midline).  Grid spacing is derived from the BB width
//   so it automatically adapts to EURUSD's low-volatility 1-minute action.
//
//   • When price drops into a buy grid level, a long entry is placed.
//   • When price rises back to the corresponding sell grid level above the
//     mean, that position is closed for a profit.
//   • If price breaks too far below the outermost grid level, a stop-loss
//     closes everything to cap downside.
//
//   Pyramiding is enabled so multiple grid levels can fill simultaneously
//   during a dip — each level is tracked independently.
//
// ARCHITECTURE:
//   1. candles() fetches 250 historical bars from DemoTrader for indicator warmup.
//   2. event.onCandle() iterates ALL bars on every execution for chart
//      plotting and computes TA indicators (Bollinger Bands, grid levels).
//      Trade signal results are stored in module-level variables.
//   3. event.onNewCandle() fires reactively ONLY when new candles are appended
//      to the FrameModel (live edge). It uses the TA results from
//      event.onCandle() to generate trade signals.
//   4. In live mode, the CLI polls DemoTrader every 60s for new candles.
//      Each poll triggers a full re-execution: event.onCandle replays all bars
//      (updating the chart), then onNewCandle fires for the new candle(s) only.
//   5. trade.buy/trade.closePosition calls are dispatched
//      to the DemoTrader broker by the BrokerStateSync. Grid levels determine
//      WHEN to enter (price at/beyond level), not a specific limit price.
//
// INTENDED USE:
//   Symbol: EURUSD  |  Timeframe: 1 minute  |  Mode: Live
//   Broker: DemoTrader (credentials in config)
//   OHLC provider: DemoTrader (polled every 60s)
//
// ─────────────────────────────────────────────────────────────────────────────

mode("live", "Mean Reversion Grid Live");

candles("EURUSD", timeframe: "1m", bars: 40, provider: "demotrader");

broker("sim", {
  initial_capital: 100000,
  commission: 0.02,
  commission_type: "percent"
});

broker("mybrokeraccount1");

risk(max_position_pct: 50, daily_loss_limit: 3);

// ── Grid Parameters ──────────────────────────────────────────────────────────
const bbLen    = input.int(20, "BB Length", minval: 5, maxval: 50, group: "Grid");
const bbMult   = input.float(2.0, "BB Multiplier", minval: 0.5, maxval: 4.0, step: 0.1, group: "Grid");
const levels   = input.int(5, "Grid Levels (each side)", minval: 1, maxval: 10, group: "Grid");
const gridFrac = input.float(3.0, "Grid Fraction (of BB width)", minval: 0.5, maxval: 5.0, step: 0.5, group: "Grid");

// ── Risk Management ──────────────────────────────────────────────────────────
const maxPos   = input.int(20, "Max Total Trades", minval: 1, maxval: 50, group: "Risk");
const useStop  = input.bool(true, "Use Stop Loss", group: "Risk");
const stopFrac = input.float(1.0, "Stop Loss (× BB width)", minval: 0.1, maxval: 5.0, step: 0.1, group: "Risk");
const useTP    = input.bool(true, "Take Profit at Grid Target", group: "Risk");

// ── State: track which grid levels have been submitted (boolean array) ───────
var gridPlaced = [false, false, false, false, false, false, false, false, false, false, false];  // index 0 unused; 1..levels

// ── Shared signal state (onCandle → onNewCandle) ─────────────────────────────
var sigBbMid    = 0;
var sigStep     = 0;
var sigStopLevel = 0;
var sigClose    = 0;
var sigCandleIndex = 0;

// ── Chart Plotting & TA Computation ──────────────────────────────────────────
event.onCandle((candle) => {
  if (candleIndex < bbLen) { return; }

  let close = candle.close;

  // ── Bollinger Bands ────────────────────────────────────────────────
  let [bbLo, bbMid, bbHi] = ta.bbands(close, bbLen, bbMult);

  let bbHalfWidth = (bbHi - bbLo) / 2;
  let step = bbHalfWidth * gridFrac / levels;

  // ── Stop loss level ────────────────────────────────────────────────
  let outerBuyLevel = bbMid - step * levels;
  let stopLevel = outerBuyLevel - bbHalfWidth * stopFrac;

  // ── Store signals for onNewCandle ─────────────────────────────────────
  sigBbMid     = bbMid;
  sigStep      = step;
  sigStopLevel = stopLevel;
  sigClose     = close;
  sigCandleIndex  = candleIndex;

  // ── Chart: plot each grid level ────────────────────────────────────
  for (let i = 1; i <= levels; i += 1) {
    let buyPrice  = bbMid - step * i;
    let sellPrice = bbMid + step * i;
    chart.plot(buyPrice,  "Buy " + i,  "#4CAF50");
    chart.plot(sellPrice, "Sell " + i, "#F44336");
  }

  // ── Overlay plots ──────────────────────────────────────────────────
  chart.plot(bbMid, "Mean", "#FF9800");
  chart.plot(bbLo,  "BB Lo", "#2196F3");
  chart.plot(bbHi,  "BB Hi", "#2196F3");
});

// ── Reactive Trade Logic ─────────────────────────────────────────────────────
event.onNewCandle((candle) => {
  console.log("[onNewCandle] O=" + candle.open + " H=" + candle.high + " L=" + candle.low + " C=" + candle.close + " isCurrent=" + candle.isCurrent);

  if (sigCandleIndex < bbLen) { return; }

  let bbMid     = sigBbMid;
  let step      = sigStep;
  let stopLevel = sigStopLevel;
  let close     = sigClose;

  // ── STOP LOSS: flatten if price breaks below outer grid + buffer ────
  if (useStop && close < stopLevel) {
    for (const pos of trade.positions()) {
      trade.closePosition({ positionId: pos.id });
    }
    gridPlaced = [false, false, false, false, false, false, false, false, false, false, false];
    console.log("  → STOP LOSS triggered @ " + close);
    return;
  }

  // ── Grid entries & exits ───────────────────────────────────────────
  for (let i = 1; i <= levels; i += 1) {
    let buyPrice  = bbMid - step * i;
    let sellPrice = bbMid + step * i;

    let isSubmitted = gridPlaced[i];
    let captureBand = step * 0.5;
    let atBuy  = close <= buyPrice + captureBand && close > buyPrice - captureBand;
    let atSell = close >= sellPrice;

    // ── ENTRY ────────────────────────────────────────────────────────
    if (atBuy && !isSubmitted && trade.positions().length < maxPos) {
      trade.buy({
        symbol: "EURUSD", qty: 0.1, comment: "GridBuy_" + i,
        reason: "Grid buy L" + i + ": price at BB grid level (bbMid=" + bbMid + ", step=" + step + ")",
        context: { level: i, close: close },
        tags: ["mean_reversion", "grid_buy"]
      });
      gridPlaced[i] = true;
      console.log("  → BUY ENTRY level " + i + " @ ~" + Math.round(buyPrice * 10000) / 10000 + " (candle=" + sigCandleIndex + ")");
    }

    // ── EXIT ─────────────────────────────────────────────────────────
    let posObj = findPositionByPrefix("GridBuy_" + i);
    if (useTP && atSell && posObj != null) {
      trade.closePosition({ positionId: posObj.id });
      gridPlaced[i] = false;
      console.log("  → EXIT level " + i + " @ " + sellPrice + " (candle=" + sigCandleIndex + ")");
    }
  }

  let inBuyZone = close <= bbMid - step * 0.5;
  chart.plotshape(inBuyZone, "Buy Zone", "circle", "#4CAF50");
});

// ── Helper Functions ───────────────────────────────────────────────────────────
function findPositionByPrefix(prefix) {
  for (const pos of trade.positions()) {
    if (pos.comment.startsWith(prefix)) {
      return pos;
    }
  }
  return null;
}

← Back to all examples