QuantScript live trading example: Mean Reversion Grid System for EURUSD 1-minute. Bollinger Band-based grid with reactive trade logic, stop-loss, and pyramiding.
// ─────────────────────────────────────────────────────────────────────────────
// 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;
}