QuantScript — declarative-signals-cross-tf.qs
// Declarative Signals — Cross-Timeframe / Cross-Symbol Strategy
//
// Demonstrates the recommended declarative strategy style:
//
//   Section 1 — Signals:   named booleans computed fresh from series data
//                          on every bar (no flags, no counters, no memory).
//   Section 2 — Execution: fixed position-check boilerplate, then a
//                          mechanical signal → order mapping via trade.*.
//
// Data sources (three datasets, three alignment contracts):
//   - ETHUSD 15m  primary chart
//   - ETHUSD 1h   higher-timeframe trend via object callback ({ sma, mom })
//   - XAUUSD 1D   risk-regime raw feed with fill_limit: 3 + .age freshness gate

mode(mode_type: "forwardtest", script_name: "Declarative Signals Cross-TF");
candles(symbol: "ETHUSD", 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 regimeMax  = input.float(3000.0, "Gold Regime Cap", null, null, null, null, null, "Regime");
const lotSize    = input.float(0.1, "Lot Size", 0.01, null, 0.01, null, null, "Trade");

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

// ── Dataset 3: XAUUSD 1D regime (raw feed with a staleness cap) ─────────────
// fill_limit: 3 carries each daily value at most 3 primary bars past its
// arrival — a stale regime reading can never silently drive entries.
var xau = data.loadCandles({ adapter: "demotrader.net", symbol: "XAUUSD", 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) ────────────────────
  const trendUp = htf.sma !== null && htf.mom !== null && htf.mom > 0 && close > htf.sma;

  // Regime: gold reading visible AND within the 3-bar staleness cap.
  const regimeFresh = xau.age !== null;
  const calmRegime = regimeFresh && xau.close < regimeMax;

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

  // ── Plot ────────────────────────────────────────────────────────────────
  chart.plot(close, "ETH Close");
  chart.plot(htf.sma, "ETH 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 ?? "ETHUSD";
  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: ["declarative", "exit"]
    });
  }

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

← Back to all examples