Exciting Update: Version 1.0.1 is now available, introducing the high-performance BacktestX Custom Script Editor. Read more

Tutorials & Presets

Step-by-step guides demonstrating how to build custom trading overlay indicators on the BacktestX Canvas.

EMA Crossover Indicator

In this tutorial, we will write a script that calculates a fast EMA (9 periods) and a slow EMA (21 periods). When the fast EMA crosses above the slow EMA, it renders a green buy arrow. When it crosses under, it renders a red sell arrow.

Key Concepts: Coordinate conversions (priceToY, barToX), path drawing operations, and indicator functions (ema).

EMA Crossover Code
const bars = ctx.bars;
const bounds = ctx.bounds;
if (!bars || bars.length < 21) return;

const closePrices = bars.map(b => b.close);
const ema9 = ctx.ema(closePrices, 9);
const ema21 = ctx.ema(closePrices, 21);

// 1. Draw EMA 9 (Red Line)
ctx.beginPath();
ctx.strokeStyle = '#ef5350';
ctx.lineWidth = 1.5;
let started9 = false;
for (let i = Math.max(0, Math.floor(bounds.startIndex) - 1); i <= Math.ceil(bounds.endIndex) + 1; i++) {
  if (isNaN(ema9[i])) continue;
  const x = ctx.barToX(i);
  const y = ctx.priceToY(ema9[i]);
  if (!started9) { ctx.moveTo(x, y); started9 = true; } else ctx.lineTo(x, y);
}
ctx.stroke();

// 2. Draw EMA 21 (Blue Line)
ctx.beginPath();
ctx.strokeStyle = '#2962ff';
ctx.lineWidth = 1.5;
let started21 = false;
for (let i = Math.max(0, Math.floor(bounds.startIndex) - 1); i <= Math.ceil(bounds.endIndex) + 1; i++) {
  if (isNaN(ema21[i])) continue;
  const x = ctx.barToX(i);
  const y = ctx.priceToY(ema21[i]);
  if (isNaN(prevFast) || isNaN(prevSlow) || isNaN(currFast) || isNaN(currSlow)) continue;
  
  const crossedUp = prevFast <= prevSlow && currFast > currSlow;
  const crossedDown = prevFast >= prevSlow && currFast < currSlow;
  const x = ctx.barToX(i);
  
  if (crossedUp) {
    const y = ctx.priceToY(bars[i].low) + 12;
    ctx.fillStyle = '#089981';
    ctx.beginPath();
    ctx.moveTo(x, y - 6);
    ctx.lineTo(x - 6, y + 6);
    ctx.lineTo(x + 6, y + 6);
    ctx.closePath();
    ctx.fill();
  } else if (crossedDown) {
    const y = ctx.priceToY(bars[i].high) - 12;
    ctx.fillStyle = '#f23645';
    ctx.beginPath();
    ctx.moveTo(x, y + 6);
    ctx.lineTo(x - 6, y - 6);
    ctx.lineTo(x + 6, y - 6);
    ctx.closePath();
    ctx.fill();
  }
}

RSI Signal Dots & Alerts

This script computes a 14-period RSI. When RSI crosses below 30 (oversold) and rises back, it places a green circle marker on the low of the candlestick. If RSI crosses above 70 (overbought), it places a red circle marker on the high. It also triggers an automated alert callback via ctx.alert() on the latest candle.

RSI Signals Code
const bars = ctx.bars;
const bounds = ctx.bounds;
if (!bars || bars.length < 15) return;

const rsi = ctx.rsi(bars.map(b => b.close), 14);

for (let i = Math.max(0, Math.floor(bounds.startIndex) - 1); i <= Math.ceil(bounds.endIndex) + 1; i++) {
  if (isNaN(rsi[i])) continue;
  const x = ctx.barToX(i);
  
  if (rsi[i] < 30) {
    // Draw Green Transparent Circle
    const y = ctx.priceToY(bars[i].low) + 10;
    ctx.fillStyle = 'rgba(8, 153, 129, 0.35)';
    ctx.beginPath();
    ctx.arc(x, y, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#089981';
    ctx.lineWidth = 1.5;
    ctx.stroke();
  } else if (rsi[i] > 70) {
    // Draw Red Transparent Circle
    const y = ctx.priceToY(bars[i].high) - 10;
    ctx.fillStyle = 'rgba(242, 54, 69, 0.35)';
    ctx.beginPath();
    ctx.arc(x, y, 6, 0, Math.PI * 2);
    ctx.fill();
    ctx.strokeStyle = '#f23645';
    ctx.lineWidth = 1.5;
    ctx.stroke();
  }
}

// Send alert notifications dynamically
const lastIdx = bars.length - 1;
if (rsi[lastIdx] < 30) {
  ctx.alert(true, `RSI Oversold Alert: ${rsi[lastIdx].toFixed(2)}`);
} else if (rsi[lastIdx] > 70) {
  ctx.alert(true, `RSI Overbought Alert: ${rsi[lastIdx].toFixed(2)}`);
}

Support/Resistance Zones & HUD HUD Table

This advanced indicator computes a support and resistance boundary by reading high and low averages, fills a translucent zone box across the viewport, and prints statistics directly on a HUD overlay dashboard using the ctx.table utilities.

Support & Resistance Zones Code
const bars = ctx.bars;
const bounds = ctx.bounds;
if (!bars || bars.length < 50) return;

// Calculate average levels
let sumHigh = 0, sumLow = 0;
for (let i = bars.length - 20; i < bars.length; i++) {
  sumHigh += bars[i].high;
  sumLow += bars[i].low;
}
const resistanceVal = sumHigh / 20;
const supportVal = sumLow / 20;

const startX = ctx.barToX(Math.max(0, Math.floor(bounds.startIndex)));
const endX = ctx.barToX(bounds.endIndex);
const yRes = ctx.priceToY(resistanceVal);
const ySup = ctx.priceToY(supportVal);

// 1. Fill Translucent Resistance zone
ctx.fillStyle = 'rgba(239, 83, 80, 0.1)';
ctx.fillRect(startX, yRes - 10, endX - startX, 20);
ctx.strokeStyle = 'rgba(239, 83, 80, 0.4)';
ctx.lineWidth = 1;
ctx.strokeRect(startX, yRes - 10, endX - startX, 20);

// 2. Fill Translucent Support zone
ctx.fillStyle = 'rgba(8, 153, 129, 0.1)';
ctx.fillRect(startX, ySup - 10, endX - startX, 20);
ctx.strokeStyle = 'rgba(8, 153, 129, 0.4)';
ctx.strokeRect(startX, ySup - 10, endX - startX, 20);

// 3. Construct overlay table grid dashboard
const tbl = ctx.table.new(3, 2, ctx.position.top_right);
tbl.cell(0, 0, 'Levels');
tbl.cell(0, 1, 'Price');
tbl.cell(1, 0, 'Resistance');
tbl.cell(1, 1, resistanceVal.toFixed(2));
tbl.cell(2, 0, 'Support');
tbl.cell(2, 1, supportVal.toFixed(2));
tbl.set_bgcolor(0, 0, '#111111');
tbl.set_bgcolor(0, 1, '#111111');