Cross-symbol, cross-calendar multi-dataset strategy: XAUUSD 15m primary, 1h trend via object callback, VIX daily volatility regime with fill_limit and feed.age freshness gating. Complete runnable code.
// 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"]
});
}
});