Description

INITIAL BALANCE

Comments (0)

0/2000

Loading comments…

Source code

function calculate(bars, ctx) {
  // ══════════════════════════════════════
  // INPUTS
  // ══════════════════════════════════════
  const displayMode = ctx.input('Display Mode', 'Today', {
    options: [
      { label: 'Today', value: 'Today' },
      { label: 'Two Days', value: 'Two Days' },
      { label: '5 Days', value: '5 Days' },
      { label: 'All', value: 'All' },
      { label: 'Hidden', value: 'Hidden' },
    ],
  });

  const sessionStartStr = ctx.input('Session Start (HHMM)', '0930');
  const sessionEndStr = ctx.input('Session End (HHMM)', '1630');
  const timezone = ctx.input('Timezone UTC Offset', 'UTC-5', {
    options: [
      { label: 'UTC-10', value: 'UTC-10' },
      { label: 'UTC-8', value: 'UTC-8' },
      { label: 'UTC-7', value: 'UTC-7' },
      { label: 'UTC-6', value: 'UTC-6' },
      { label: 'UTC-5', value: 'UTC-5' },
      { label: 'UTC-4', value: 'UTC-4' },
      { label: 'UTC-3', value: 'UTC-3' },
      { label: 'UTC+0', value: 'UTC+0' },
      { label: 'UTC+1', value: 'UTC+1' },
      { label: 'UTC+2', value: 'UTC+2' },
      { label: 'UTC+3', value: 'UTC+3' },
      { label: 'UTC+3:30', value: 'UTC+3:30' },
      { label: 'UTC+4', value: 'UTC+4' },
      { label: 'UTC+5', value: 'UTC+5' },
      { label: 'UTC+5:30', value: 'UTC+5:30' },
      { label: 'UTC+5:45', value: 'UTC+5:45' },
      { label: 'UTC+6', value: 'UTC+6' },
      { label: 'UTC+6:30', value: 'UTC+6:30' },
      { label: 'UTC+7', value: 'UTC+7' },
      { label: 'UTC+8', value: 'UTC+8' },
      { label: 'UTC+9', value: 'UTC+9' },
      { label: 'UTC+9:30', value: 'UTC+9:30' },
      { label: 'UTC+10', value: 'UTC+10' },
      { label: 'UTC+11', value: 'UTC+11' },
      { label: 'UTC+12', value: 'UTC+12' },
      { label: 'UTC+12:45', value: 'UTC+12:45' },
      { label: 'UTC+13', value: 'UTC+13' },
    ],
  });

  const ibMethod = ctx.input('IB Method', 'First Bar', {
    options: [
      { label: 'First Bar (Opening Range)', value: 'firstBar' },
      { label: 'First 30 min', value: 'first30' },
      { label: 'First 60 min', value: 'first60' },
      { label: 'First 90 min', value: 'first90' },
      { label: 'First 120 min', value: 'first120' },
    ],
  });

  const showLabels = ctx.input('Show Labels', true);
  const fontSize = ctx.input('Font Size', 11, { min: 8, max: 16, step: 1 });
  const lineWidth = ctx.input('Line Width', 1, { min: 1, max: 5, step: 1 });
  const lineStyle = ctx.input('Line Style', 'dashed', {
    options: [
      { label: 'Solid', value: 'solid' },
      { label: 'Dashed', value: 'dashed' },
      { label: 'Dotted', value: 'dotted' },
    ],
  });

  const ibColor = ctx.input('Initial Balance Color', '#2196f3');
  const extendShade = ctx.input('Extend Shaded Region', false);
  const extLevels = ctx.input('Extension Levels', 2, { min: 0, max: 10, step: 1 });
  const extLevelColor = ctx.input('Extension Level Color', '#ffffff');
  const alertBreakout = ctx.input('Close outside range (first time)', false);

  // ══════════════════════════════════════
  // HELPER: Convert timezone string to numeric offset in minutes
  // ══════════════════════════════════════
  function tzOffsetMinutes(tz) {
    const sign = tz.includes('+') ? 1 : -1;
    const num = tz.replace('UTC', '');
    if (num === '' || num === '-0' || num === '+0') return 0;
    const parts = num.split(':');
    const h = Math.abs(parseFloat(parts[0]));
    const m = parts.length > 1 ? parseFloat(parts[1]) : 0;
    return sign * (h * 60 + m);
  }

  const tzOffset = tzOffsetMinutes(timezone);

  function minutesFromMidnight(barIndex) {
    const barTime = new Date(bars[barIndex].timestamp);
    const utcMinutes = barTime.getUTCHours() * 60 + barTime.getUTCMinutes();
    let localMinutes = utcMinutes + tzOffset;
    localMinutes = ((localMinutes % 1440) + 1440) % 1440;
    return localMinutes;
  }

  // ══════════════════════════════════════
  // PARSE SESSION HOURS
  // ══════════════════════════════════════
  function parseHHMM(str) {
    const h = parseInt(str.substring(0, 2), 10);
    const m = parseInt(str.substring(2, 4), 10);
    return h * 60 + m;
  }

  const sessionStartMinutes = parseHHMM(sessionStartStr);
  const sessionEndMinutes = parseHHMM(sessionEndStr);

  function isInSession(barIndex) {
    const mins = minutesFromMidnight(barIndex);
    if (sessionStartMinutes <= sessionEndMinutes) {
      return mins >= sessionStartMinutes && mins < sessionEndMinutes;
    }
    return mins >= sessionStartMinutes || mins < sessionEndMinutes;
  }

  function isSessionStart(barIndex) {
    if (barIndex === 0) return isInSession(barIndex);
    return isInSession(barIndex) && !isInSession(barIndex - 1);
  }

  // ══════════════════════════════════════
  // SESSION DURATION IN MINUTES
  // ══════════════════════════════════════
  let sessionMinutesDuration = sessionEndMinutes - sessionStartMinutes;
  if (sessionMinutesDuration <= 0) sessionMinutesDuration += 1440;

  // ══════════════════════════════════════
  // TIME-BASED IB DURATION (in minutes)
  // ══════════════════════════════════════
  const ibDurationMap = {
    firstBar: 0,
    first30: 30,
    first60: 60,
    first90: 90,
    first120: 120,
  };
  const ibDurationMinutes = ibDurationMap[ibMethod] || 0;

  // ══════════════════════════════════════
  // MAIN LOGIC
  // ══════════════════════════════════════
  if (displayMode === 'Hidden') return;

  const last = bars.length - 1;
  if (last < 0) return;

  // ══════════════════════════════════════
  // FIND SESSIONS FRESH EACH RUN (deterministic)
  // ══════════════════════════════════════
  const sessions = [];

  // First, find all session-start bars
  const sessionStartBars = [];
  for (let i = 0; i < bars.length; i++) {
    if (isSessionStart(i)) {
      sessionStartBars.push(i);
    }
  }

  // For each session, compute the IB range
  for (let si = 0; si < sessionStartBars.length; si++) {
    const startBar = sessionStartBars[si];

    // Collect all bars in this session
    const sessionAllBars = [];
    for (let j = startBar; j < bars.length && isInSession(j); j++) {
      sessionAllBars.push(j);
    }
    if (sessionAllBars.length === 0) continue;

    const sessionEndBar = sessionAllBars[sessionAllBars.length - 1];

    // Determine which bars form the Initial Balance
    let ibBars = [];

    if (ibDurationMinutes === 0) {
      // "First Bar" — just the opening bar of the session
      ibBars = [startBar];
    } else {
      // Time-based IB: collect bars within the first N minutes of the session
      const sessionStartMins = minutesFromMidnight(startBar);
      const ibCutoffMins = sessionStartMins + ibDurationMinutes;

      for (const bIdx of sessionAllBars) {
        const barMins = minutesFromMidnight(bIdx);

        // Handle midnight wrap
        let timeSinceSessionStart = barMins - sessionStartMins;
        if (timeSinceSessionStart < 0) timeSinceSessionStart += 1440;

        if (timeSinceSessionStart < ibDurationMinutes) {
          ibBars.push(bIdx);
        } else {
          break;
        }
      }
    }

    // Compute IB high/low from the selected IB bars
    let ibHigh = -Infinity;
    let ibLow = Infinity;
    let ibHighBar = startBar;
    let ibLowBar = startBar;

    for (const bIdx of ibBars) {
      if (bars[bIdx].high > ibHigh) {
        ibHigh = bars[bIdx].high;
        ibHighBar = bIdx;
      }
      if (bars[bIdx].low < ibLow) {
        ibLow = bars[bIdx].low;
        ibLowBar = bIdx;
      }
    }

    if (ibHigh === -Infinity || ibLow === Infinity) continue;

    const ibMedian = (ibHigh + ibLow) / 2;

    sessions.push({
      startBar: startBar,
      endBar: sessionEndBar,
      high: ibHigh,
      low: ibLow,
      median: ibMedian,
      highBar: ibHighBar,
      lowBar: ibLowBar,
    });
  }

  // ══════════════════════════════════════
  // DISPLAY MODE FILTERING
  // ══════════════════════════════════════
  let visibleCount = 999;
  if (displayMode === 'Today') visibleCount = 1;
  else if (displayMode === 'Two Days') visibleCount = 2;
  else if (displayMode === '5 Days') visibleCount = 5;

  let visibleSessions = sessions.slice(-visibleCount);

  // ══════════════════════════════════════
  // DRAW
  // ══════════════════════════════════════
  const endDraw = last;

  for (const sess of visibleSessions) {
    const start = sess.startBar;
    const end = Math.max(sess.endBar, endDraw);
    const high = sess.high;
    const low = sess.low;
    const median = sess.median;

    // Log for debugging
    ctx.log('Session start bar', start, 'IB High:', high.toFixed(2), 'IB Low:', low.toFixed(2), 'Session bars:', (sess.endBar - sess.startBar + 1));

    // ── IB Box (shaded range) ──
    ctx.box(start, high, end, low, {
      fillColor: extendShade ? 'rgba(33,150,243,0.12)' : 'rgba(33,150,243,0.08)',
      borderColor: 'transparent',
      extend: 'right',
    });

    // ── High line ──
    ctx.line(start, high, end, high, {
      color: ibColor,
      lineWidth: lineWidth,
      lineStyle: lineStyle,
      extend: 'right',
    });

    // ── Low line ──
    ctx.line(start, low, end, low, {
      color: ibColor,
      lineWidth: lineWidth,
      lineStyle: lineStyle,
      extend: 'right',
    });

    // ── Median line ──
    ctx.line(start, median, end, median, {
      color: ibColor,
      lineWidth: lineWidth,
      lineStyle: lineStyle === 'solid' ? 'dashed' : lineStyle,
      extend: 'right',
    });

    // ── Labels ──
    if (showLabels) {
      ctx.label(start, high, 'IB High (' + high.toFixed(2) + ')', {
        color: ibColor,
        textColor: '#ffffff',
        size: fontSize,
        position: 'above',
      });
      ctx.label(start, low, 'IB Low (' + low.toFixed(2) + ')', {
        color: ibColor,
        textColor: '#ffffff',
        size: fontSize,
        position: 'below',
      });
      ctx.label(start, median, '50% (' + median.toFixed(2) + ')', {
        color: ibColor,
        textColor: '#ffffff',
        size: fontSize,
        position: 'above',
      });
    }

    // ── Extension levels ──
    if (extLevels > 0) {
      const range = high - low;
      for (let i = 1; i <= extLevels; i++) {
        const extAbove = high + (range / 2) * i;
        const extBelow = low - (range / 2) * i;

        ctx.line(start, extAbove, end, extAbove, {
          color: extLevelColor,
          lineWidth: lineWidth,
          lineStyle: lineStyle,
          extend: 'right',
        });
        ctx.line(start, extBelow, end, extBelow, {
          color: extLevelColor,
          lineWidth: lineWidth,
          lineStyle: lineStyle,
          extend: 'right',
        });

        if (showLabels) {
          ctx.label(start, extAbove, 'Ext: ' + i + ' (' + extAbove.toFixed(2) + ')', {
            color: extLevelColor,
            textColor: '#ffffff',
            size: fontSize,
            position: 'above',
          });
          ctx.label(start, extBelow, 'Ext: ' + i + ' (' + extBelow.toFixed(2) + ')', {
            color: extLevelColor,
            textColor: '#ffffff',
            size: fontSize,
            position: 'below',
          });
        }
      }
    }

    // ── Breakout alert ──
    if (alertBreakout) {
      const isBreakout =
        (bars[end].close > high && bars[end].low < high) ||
        (bars[end].close < low && bars[end].high > low);

      if (isBreakout) {
        ctx.label(end, high + (high - low) * 0.02, '[A]', {
          color: '#ffffff',
          textColor: ibColor,
          size: fontSize,
          position: 'below',
        });
      }
    }
  }
}