Average Daily Range (ADR)

Description

ADR Candle (Average Daily Range) A visual ADR indicator inspired by Jimmy Trades that displays a dynamic candle on your chart showing how much of the average daily range has been consumed in the current session. How It Works The indicator draws a single candle to the right of price action with two layers: Solid Fill — Represents the Average Daily Range (calculated over a configurable lookback period). This is the "expected" move for the day, anchored from today's low. Hollow Outline — Represents today's actual high-to-low range. When today's range is below 100% ADR, the outline sits inside the solid fill — showing how much room is left in the typical daily move. When today's range exceeds 100% ADR, the outline extends beyond the solid fill — the hollow portion represents how far price has pushed past the average range. Features Real-time ADR candle with solid body (expected range) and hollow wick (actual range) Info table displaying asset, timeframe, ADR value, and current % of ADR Color-coded volatility condition (Low / Medium / High) with configurable thresholds Optional ADR level lines projected across the chart Fully customizable colors, positioning, bar width, and offset Works on any intraday timeframe Inputs ADR Lookback — Number of days used to calculate the average (default: 14) High / Low Vol Thresholds — Define when the condition label switches between Low, Medium, and High volatility Bar Offset & Width — Control candle placement and size Show ADR Lines — Toggle horizontal dashed lines at the ADR levels Full color customization for bull/bear candles, ADR lines, and volatility states

Categories & Tags

Comments (0)

0/2000

Loading comments…

Source code

function calculate(bars, ctx) {
  // ─── INPUTS ───
  const lookback = ctx.input('ADR Lookback', 14, { min: 1, max: 500, step: 1 });
  const precision = ctx.input('Decimal Precision', 2, { min: 0, max: 6, step: 1 });
  const highThreshold = ctx.input('High Vol Threshold %', 100, { min: 50, max: 200, step: 5 });
  const lowThreshold = ctx.input('Low Vol Threshold %', 50, { min: 10, max: 100, step: 5 });
  const tablePos = ctx.input('Table Position', 'top_right', {
    options: [
      { label: 'Top Right', value: 'top_right' },
      { label: 'Top Left', value: 'top_left' },
      { label: 'Top Center', value: 'top_center' },
      { label: 'Bottom Right', value: 'bottom_right' },
      { label: 'Bottom Left', value: 'bottom_left' },
      { label: 'Middle Right', value: 'middle_right' },
    ]
  });
  const barOffset = ctx.input('Bar Offset (from right)', 3, { min: 1, max: 20, step: 1 });
  const barWidth = ctx.input('Bar Width (bars)', 2, { min: 1, max: 6, step: 1 });
  const showLines = ctx.input('Show ADR Lines', false);
  const bullColor = ctx.input('Bull Color', '#2962ff');
  const bearColor = ctx.input('Bear Color', '#ef4444');
  const adrLineColor = ctx.input('ADR Line Color', '#f59e0b');
  const highVolColor = ctx.input('High Vol Color', '#ef4444');
  const medVolColor = ctx.input('Med Vol Color', '#f59e0b');
  const lowVolColor = ctx.input('Low Vol Color', '#22c55e');

  if (bars.length < 2) return;

  // ─── DETECT TIMEFRAME ───
  const t1 = new Date(bars[bars.length - 1].timestamp).getTime();
  const t2 = new Date(bars[bars.length - 2].timestamp).getTime();
  const barSecs = Math.abs(t1 - t2) / 1000;
  let tfLabel = '—';
  if (barSecs <= 6) tfLabel = '5s';
  else if (barSecs <= 16) tfLabel = '15s';
  else if (barSecs <= 35) tfLabel = '30s';
  else if (barSecs <= 90) tfLabel = '1m';
  else if (barSecs <= 400) tfLabel = '5m';
  else if (barSecs <= 1200) tfLabel = '15m';
  else if (barSecs <= 5000) tfLabel = '1H';
  else if (barSecs <= 20000) tfLabel = '4H';
  else if (barSecs <= 100000) tfLabel = '1D';
  else tfLabel = '1W';

  // ─── DETECT SYMBOL ───
  const fullSym = ctx.syminfo.symbol || '';
  const asset = fullSym.replace(/[A-Z]\d+$/i, '').replace(/\d+$/, '') || ctx.symbol || '??';

  // ─── GET DAILY DATA ───
  let daily;
  try {
    daily = ctx.request('1D');
  } catch (e) {
    ctx.log('ADR requires an intraday timeframe');
    return;
  }

  const confirmedBars = daily.bars;
  if (!confirmedBars || confirmedBars.length < 1) return;

  // ─── CALCULATE ADR ───
  const usableBars = confirmedBars.slice(-lookback);
  let totalRange = 0;
  for (let i = 0; i < usableBars.length; i++) {
    totalRange += usableBars[i].high - usableBars[i].low;
  }
  const adr = usableBars.length > 0 ? totalRange / usableBars.length : 0;

  // ─── TODAY'S RANGE ───
  let todayHigh, todayLow, todayOpen, todayClose;
  if (daily.forming) {
    todayHigh = daily.forming.high;
    todayLow = daily.forming.low;
    todayOpen = daily.forming.open;
    todayClose = daily.forming.close;
  } else {
    const lastDay = confirmedBars[confirmedBars.length - 1];
    todayHigh = lastDay.high;
    todayLow = lastDay.low;
    todayOpen = lastDay.open;
    todayClose = lastDay.close;
  }

  const todayRange = todayHigh - todayLow;
  const pctOfADR = adr > 0 ? (todayRange / adr) * 100 : 0;
  const isBullish = todayClose >= todayOpen;

  // ─── VOLATILITY STATE ───
  let volState, volColor;
  if (pctOfADR >= highThreshold) {
    volState = 'HIGH'; volColor = highVolColor;
  } else if (pctOfADR <= lowThreshold) {
    volState = 'LOW'; volColor = lowVolColor;
  } else {
    volState = 'MED'; volColor = medVolColor;
  }

  // ─── ADR CANDLE GEOMETRY ───
  // Body (solid) = ADR amount, anchored from today's low
  const bodyTop = todayLow + adr;
  const bodyBottom = todayLow;
  // Wick (outline) = today's ACTUAL range (high to low)
  const wickTop = todayHigh;
  const wickBottom = todayLow;

  const lastBarIdx = bars.length - 1;
  const candleCenter = lastBarIdx + barOffset;
  const halfWidth = Math.max(1, Math.floor(barWidth / 2));
  const candleLeft = candleCenter - halfWidth;
  const candleRight = candleCenter + halfWidth;

  const bodyColor = isBullish ? bullColor : bearColor;

  // STEP 1: Draw BODY first (solid fill) — the ADR expected range
  ctx.box(candleLeft, bodyTop, candleRight, bodyBottom, {
    borderColor: bodyColor,
    fillColor: bodyColor,
    borderWidth: 1,
    borderStyle: 'solid',
  });

  // STEP 2: Draw WICK on top (outline only) — today's actual range
  // When < 100%: wick is SMALLER than body (body extends above showing ADR target)
  // When > 100%: wick extends BEYOND body (shows exceeded ADR)
  ctx.box(candleLeft, wickTop, candleRight, wickBottom, {
    borderColor: 'rgba(200,200,200,0.6)',
    fillColor: 'rgba(0,0,0,0)',
    borderWidth: 1,
    borderStyle: 'solid',
  });

  // ─── ADR LEVEL LINES ───
  if (showLines) {
    ctx.line(lastBarIdx - 30, bodyTop, candleRight + 2, bodyTop, {
      color: adrLineColor, lineWidth: 1, lineStyle: 'dashed',
    });
    ctx.line(lastBarIdx - 30, bodyBottom, candleRight + 2, bodyBottom, {
      color: adrLineColor, lineWidth: 1, lineStyle: 'dashed',
    });
  }

  // ─── % LABEL ───
  ctx.label(candleCenter, wickBottom, pctOfADR.toFixed(0), {
    color: bodyColor, textColor: '#ffffff', size: 10, position: 'below', style: 'label_down',
  });

  // ─── TABLE ───
  const tbl = ctx.table(tablePos, 4, 3, {
    bgcolor: 'rgba(13,17,28,0.92)',
    borderColor: 'rgba(255,255,255,0.15)',
    borderWidth: 1,
    textColor: '#aaa',
    fontSize: 11,
    cellPadding: 5,
    paddingTop: 40,
    paddingBottom: 0,
  });

  ctx.cell(tbl, 0, 0, 'Asset', { textColor: '#ccc', fontSize: 10, bgcolor: 'rgba(255,255,255,0.08)', align: 'center' });
  ctx.cell(tbl, 0, 1, 'TF', { textColor: '#ccc', fontSize: 10, bgcolor: 'rgba(255,255,255,0.08)', align: 'center' });
  ctx.cell(tbl, 0, 2, 'ADR', { textColor: '#ccc', fontSize: 10, bgcolor: 'rgba(255,255,255,0.08)', align: 'center' });
  ctx.cell(tbl, 0, 3, '% of ADR', { textColor: '#ccc', fontSize: 10, bgcolor: 'rgba(255,255,255,0.08)', align: 'center' });

  ctx.cell(tbl, 1, 0, asset, { textColor: '#fff', fontSize: 11, bgcolor: 'rgba(50,50,120,0.5)', align: 'center' });
  ctx.cell(tbl, 1, 1, tfLabel, { textColor: '#fff', fontSize: 11, bgcolor: 'rgba(50,50,120,0.5)', align: 'center' });
  ctx.cell(tbl, 1, 2, adr.toFixed(precision), { textColor: '#fff', fontSize: 11, bgcolor: 'rgba(50,50,120,0.5)', align: 'center' });
  ctx.cell(tbl, 1, 3, '%' + pctOfADR.toFixed(1), { textColor: '#fff', fontSize: 11, bgcolor: 'rgba(50,50,120,0.5)', align: 'center' });

  const condText = 'Condition: ' + (volState === 'HIGH' ? 'High Volatility' : volState === 'LOW' ? 'Low Volatility' : 'Medium Volatility');
  ctx.cell(tbl, 2, 0, '', { bgcolor: volColor, align: 'center' });
  ctx.cell(tbl, 2, 1, condText, { textColor: '#000', fontSize: 10, bgcolor: volColor, align: 'center' });
  ctx.cell(tbl, 2, 2, '', { bgcolor: volColor, align: 'center' });
  ctx.cell(tbl, 2, 3, '', { bgcolor: volColor, align: 'center' });
}