Load and combine multiple datasets in QuantScript: cross-timeframe trend filters, cross-symbol regime gating (VIX, DXY), and staleness-aware alignment. Complete examples with code.
Real trading strategies rarely rely on a single price series. A trend-following strategy might use a higher-timeframe moving average for direction, a lower-timeframe for entry timing, and a cross-symbol volatility index (VIX, DXY) for regime filtering. Each dataset has its own bar calendar, its own alignment rules, and its own staleness characteristics.
QuantScript’s data.loadCandles() loads secondary OHLCV data from any symbol and timeframe, aligns it to your primary chart’s timeline, and exposes it as series you can plot, compute on, and gate your trading logic with. The alignment is closed-bar by default — a secondary bar’s values only become visible once that bar has fully closed — so your strategy never accidentally looks ahead.
All three cases use the same data.loadCandles() API. The only difference is the alignment contract you declare.
Your primary chart is declared with candles(). Every additional dataset is loaded with data.loadCandles(), which has two modes:
Without a callback, data.loadCandles() returns a FeedAccessor with .open, .high, .low, .close, .volume, and .age columns. Each column is a series aligned to your primary timeline:
// Load VIX daily data as a raw feed
var vix = data.loadCandles({ adapter: "demotrader.net", symbol: "VIX", resolution_type: "1D" });
event.onCandle((candle) => {
// vix.close, vix.high, vix.age — all aligned to primary timeline
const calm = vix.close < 20;
});With a callback, the expression is evaluated on the secondary timeframe’s native bars (not forward-filled), then the result is aligned back to your primary chart. This is equivalent to Pine Script’s request.security():
// Compute SMA on native 1h bars, then align to 15m timeline
var htfSma = data.loadCandles({ adapter: "demotrader.net", symbol: "ETHUSD", resolution_type: "1h", callback: (c) => ta.sma(c.close, 50) });
// Object callback: bind multiple named columns at once
var htf = data.loadCandles({ adapter: "demotrader.net", symbol: "ETHUSD", resolution_type: "1h", callback: (c) => ({
sma: ta.sma(c.close, 50),
mom: c.close - c.close[1]
}) });
// htf.sma, htf.mom — both aligned independentlyBoth modes support [n] historical lookback on the aligned result. Callback results are forward-filled by default; raw feeds can be configured with fill, fill_limit, and lookahead options to control staleness explicitly.
Every secondary dataset is aligned to your primary chart’s timeline using closed-bar visibility: a secondary bar’s values only become visible once that bar has fully closed. A 1h feed on a 15m chart shows the 09:00 bar’s values from the 09:45 primary bar onward — never the forming bar’s final values at 09:00. This is the backtest-correct default (Pine’s lookahead_off).
By default, values are forward-filled across gaps: the last visible value carries forward until a new secondary bar arrives. You can control staleness with fill_limit — the maximum number of primary bars a value may be carried past its arrival before becoming null:
// Daily VIX feed: carry each value at most 3 primary bars past arrival
var vix = data.loadCandles({ adapter: "demotrader.net", symbol: "VIX", resolution_type: "1D", fill_limit: 3 });
event.onCandle((candle) => {
// vix.age: 0 on arrival bar, increments while carried, null when expired
const fresh = vix.age !== null; // visible AND within the staleness cap
const calm = fresh && vix.close < 20;
});The feed.age column tells you how many primary bars have elapsed since the currently visible value first arrived. Use it to gate your logic on data freshness — a stale regime reading can never silently drive entries.
Raw feeds can also be re-viewed under a different alignment contract via data.align(feed, { fill_limit: 3 }), and current staleness queried with data.age(feed). See the API Reference for details.
A gold (XAUUSD) strategy that trades on a 15m chart, uses a 1h trend filter for direction, and gates entries on a daily VIX regime. Three datasets, three alignment contracts:
mode(mode_type: "forwardtest", script_name: "VIX Regime Gating XAUUSD");
candles(symbol: "XAUUSD", resolution_type: "15m", bars: 500, adapter: "demotrader.net");
// 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, 20),
mom: c.close - c.close[1]
}) });
// Dataset 3: VIX 1D regime (raw feed with staleness cap)
var vix = data.loadCandles({ adapter: "demotrader.net", symbol: "VIX", resolution_type: "1D", fill_limit: 3 });
event.onCandle((candle) => {
// Section 1: signals (named booleans over series)
const trendUp = htf.sma !== null && htf.mom > 0 && candle.close > htf.sma;
const calmVix = vix.age !== null && vix.close < 25;
const longEntry = ta.crossover(candle.close, ta.sma(candle.close, 20)) && trendUp && calmVix;
// Section 2: execution (mechanical signal → order mapping)
// ... trade.buy / trade.closePosition ...
});The vix.age !== null check ensures that if the VIX feed goes stale (no new daily bar has arrived within 3 primary bars), the strategy stops opening new positions until a fresh reading arrives. This is staleness as data, not imperative logic.
See the full annotated source in the Declarative Signals example (ETHUSD variant) or the VIX Regime Gating example (XAUUSD variant).
The same pattern applied to a different primary: trade gold on a 15m chart, use a 1h gold trend for direction, and gate on VIX calm. The complete strategy:
mode(mode_type: "forwardtest", script_name: "VIX Regime Gating XAUUSD");
candles(symbol: "XAUUSD", resolution_type: "15m", bars: 500, adapter: "demotrader.net");
var htf = data.loadCandles({ adapter: "demotrader.net", symbol: "XAUUSD", resolution_type: "1h", callback: (c) => ({
sma: ta.sma(c.close, 20),
mom: c.close - c.close[1]
}) });
var vix = data.loadCandles({ adapter: "demotrader.net", symbol: "VIX", resolution_type: "1D", fill_limit: 3 });
event.onCandle((candle) => {
const trendUp = htf.sma !== null && htf.mom > 0 && candle.close > htf.sma;
const calmVix = vix.age !== null && vix.close < 25;
const longEntry = ta.crossover(candle.close, ta.sma(candle.close, 20)) && trendUp && calmVix;
const longExit = ta.crossunder(candle.close, ta.sma(candle.close, 20)) || !trendUp;
// ... execution ...
});Run the full script with the CLI: npm -w @quantscript/lang run dev -- run1 qs-examples/vix-regime-gating-xauusd.qs --log-level info
fill, fill_limit, lookahead, callback modes.