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',
});
}
}
}
}