QuantScript — vix-regime-gating-xauusd.qs
// VIX Regime Gating — XAUUSD Gold Strategy
//
// Demonstrates multi-dataset loading with cross-symbol, cross-calendar data:
//   - XAUUSD 15m  primary chart (gold trading)
//   - XAUUSD 1h   higher-timeframe trend via object callback ({ sma, mom })
//   - VIX 1D      volatility regime as a raw feed with fill_limit: 3 + .age gate
//
// The VIX freshness gate ensures that a stale volatility reading can never
// silently drive entries — if the VIX feed goes stale (age becomes null),
// the strategy stops opening new positions until a fresh reading arrives.

mode(mode_type: "forwardtest", script_name: "VIX Regime Gating XAUUSD");
candles(symbol: "XAUUSD", resolution_type: "15m", bars: 500, adapter: "demotrader.net");

broker(saved: "mybrokeraccount1");
risk(max_position_pct: 20, daily_loss_limit: 3);

// ── Inputs ───────────────────────────────────────────────────────────────────
const smaLen     = input.int(20, "Primary SMA Length", 5, 100, null, null, null, "Entry");
const htfSmaLen  = input.int(20, "HTF SMA Length", 5, 100, null, null, null, "Trend");
const vixMax     = input.float(25.0, "VIX Calm Threshold", null, null, null, null, null, "Regime");
const lotSize    = input.float(0.1, "Lot Size", 0.01, null, 0.01, null, null, "Trade");

// ── Dataset 2: XAUUSD 1h trend (object callback → two aligned columns) ──────
var htf = data.loadCandles({ adapter: "demotrader.net", symbol: "XAUUSD", resolution_type: "1h", callback: (c) => ({
  sma: ta.sma(c.close, htfSmaLen),
  mom: c.close - c.close[1]
}) });

// ── Dataset 3: VIX 1D regime (raw feed with a staleness cap) ────────────────
// fill_limit: 3 means a daily VIX value is carried at most 3 primary bars past
// its arrival bar, then becomes null — a stale volatility reading can never
// silently drive entries. vix.age is 0 on the arrival bar, increments while
// carried, and is null when invisible.
var vix = data.loadCandles({ adapter: "demotrader.net", symbol: "VIX", resolution_type: "1D", fill_limit: 3 });

// ── Strategy ─────────────────────────────────────────────────────────────────
event.onCandle((candle) => {
  const close = candle.close;
  const sma = ta.sma(close, smaLen);

  // ── Section 1: signals (named booleans over series) ────────────────────
  // HTF trend: 1h SMA rising and price above it.
  const trendUp = htf.sma !== null && htf.mom !== null && htf.mom > 0 && close > htf.sma;

  // Regime: VIX reading visible AND within the 3-bar staleness cap AND calm.
  const vixFresh = vix.age !== null;
  const calmVix = vixFresh && vix.close < vixMax;

  // Entry/exit edges via ta.* — true only on the crossing bar.
  const longEntry = ta.crossover(close, sma) && trendUp && calmVix;
  const longExit = ta.crossunder(close, sma) || !trendUp;

  // ── Plot ────────────────────────────────────────────────────────────────
  chart.plot(close, "XAU Close");
  chart.plot(htf.sma, "XAU 1h SMA", "#FF980E");
  chart.plot(sma, "15m SMA", "#58A6FF");
  chart.plot(longEntry ? candle.low * 0.998 : null, "Long Entry", "#3fb950");

  // ── Section 2: execution (mechanical signal → order mapping) ───────────
  if (!candle.isLive) return;

  // Fixed position-check boilerplate: find our long position, if any.
  const symbol = frame.symbol ?? "XAUUSD";
  let longPosition = null;
  for (const pos of trade.positions()) {
    if (pos.symbol == symbol && pos.side == "buy") longPosition = pos;
  }

  if (longPosition !== null && longExit) {
    trade.closePosition({
      positionId: longPosition.id,
      reason: "15m cross under SMA or 1h trend flipped",
      tags: ["vix-gating", "exit"]
    });
  }

  if (longPosition === null && longEntry) {
    trade.buy({
      symbol, size: { lots: lotSize },
      reason: "SMA crossover, 1h trend up, VIX calm (vix.age=" + data.age(vix) + ", vix=" + vix.close + ")",
      tags: ["vix-gating", "entry"]
    });
  }
});

← Back to all examples