Skip to Main Content

Price Gap Detection Automation

This is a complex example from our internal library that detects gaps in price action and illustrates them on the chart for you visually.

Price Gap Detection Automation
describe_indicator('Gap Detector', 'price', { shortName: 'Gap Det' });
 
const gapFactor = input('Gap factor', 0.5, { min: 0.01, max: 50 });
 
// An object containing all the necessary data for each gap
const gapObject = () => ({
	topPrice: null,
	bottomPrice: null,
	ascending: null,
	topLine: series_of(null),
	bottomLine: series_of(null)
});
 
const maxGaps = 10;
 
// An array with deep copies of the gap object. One for each gap
const gapArray = [];
 
for (let gapIndex = 0; gapIndex < maxGaps; gapIndex++) {
	gapArray.push(gapObject());
}
 
let gapNumber = 0;
 
 
// When a new gap is needed we check if there is a free one or else we use the oldest one
const getAvailableGapIndex = () => {
	// If there are non-used gap slots return the first one and update the gapNumber
	if (gapNumber < maxGaps) {
		return gapNumber++;
		// else return the first one which is the oldest
	}
 
	return 0;
};
 
const removeGap = index => {
	// Keep gap lines. We still want them to paint them
	const topLine = gapArray[index].topLine;
	const bottomLine = gapArray[index].bottomLine;
	// Remove the gap item from the array
	gapArray.splice(index, 1);
	// Add an empty gap item at the end of the array to keep the original size
	gapArray.push(gapObject());
	// Add the lines of the deleted gap
	gapArray[gapArray.length - 1].topLine = topLine;
	gapArray[gapArray.length - 1].bottomLine = bottomLine;
	// Update gapNumber
	gapNumber--;
};
 
const priceAtr = atr(14);
 
for (let candleIndex = 1; candleIndex < close.length; candleIndex++) {
	// Check if there is a gap
	const gapSize = priceAtr[candleIndex] !== null ? gapFactor * priceAtr[candleIndex] : null;
	const isUpGap = gapSize !== null && low[candleIndex] - high[candleIndex - 1] > gapSize;
	const isDownGap = gapSize !== null && low[candleIndex - 1] - high[candleIndex] > gapSize;
 
	if (isUpGap || isDownGap) {
		const availableGapIndex = getAvailableGapIndex();
		// Remove last values of the lines to separate them with the new one
		gapArray[availableGapIndex].topLine[candleIndex - 1] = null;
		gapArray[availableGapIndex].bottomLine[candleIndex - 1] = null;
 
		if (isUpGap) {
			gapArray[availableGapIndex].ascending = true;
			gapArray[availableGapIndex].topPrice = low[candleIndex];
			gapArray[availableGapIndex].bottomPrice = high[candleIndex - 1];
		}
		else if (isDownGap) {
			gapArray[availableGapIndex].ascending = false;
			gapArray[availableGapIndex].topPrice = low[candleIndex - 1];
			gapArray[availableGapIndex].bottomPrice = high[candleIndex];
		}
	}
 
	// Update the values of the gaps
	for (let gapIndex = 0; gapIndex < gapNumber; gapIndex++) {
		// Check if the gap should shrink due to wick entrance
		// Do not shrink if a new gap completely covers this one
		if (
			(high[candleIndex - 1] < gapArray[gapIndex].bottomPrice && low[candleIndex] > gapArray[gapIndex].topPrice) ||
			(low[candleIndex - 1] > gapArray[gapIndex].topPrice && high[candleIndex] < gapArray[gapIndex].bottomPrice)
		) {
			// Toggle this gap direction since the price is now in the other side
			gapArray[gapIndex].ascending = !gapArray[gapIndex].ascending;
		}
		else if (gapArray[gapIndex].ascending && gapArray[gapIndex].topPrice > low[candleIndex]) {
			gapArray[gapIndex].topPrice = low[candleIndex];
		}
		else if (!gapArray[gapIndex].ascending && gapArray[gapIndex].bottomPrice < high[candleIndex]) {
			gapArray[gapIndex].bottomPrice = high[candleIndex];
		}
 
		// Remove gap if it is fully covered by the prices
		if (gapArray[gapIndex].topPrice <= gapArray[gapIndex].bottomPrice) {
			// Keep the latest values for the lines before removing current gap
			gapArray[gapIndex].topLine[candleIndex] = gapArray[gapIndex].topLine[candleIndex - 1];
			gapArray[gapIndex].bottomLine[candleIndex] = gapArray[gapIndex].bottomLine[candleIndex - 1];
 
			removeGap(gapIndex);
 
			// All the following items have shifted due to the deletion. Shift the index too
			gapIndex--;
		}
		else {
			// Update the lines of the gaps
			gapArray[gapIndex].topLine[candleIndex] = gapArray[gapIndex].topPrice;
			gapArray[gapIndex].bottomLine[candleIndex] = gapArray[gapIndex].bottomPrice;
		}
	}
}
 
fill(
	paint(gapArray[0].topLine, { name: 'A/Top', color: '#1b65bf' }),
	paint(gapArray[0].bottomLine, { name: 'A/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'A'
);
 
fill(
	paint(gapArray[1].topLine, { name: 'B/Top', color: '#1b65bf' }),
	paint(gapArray[1].bottomLine, { name: 'B/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'B'
);
 
fill(
	paint(gapArray[2].topLine, { name: 'C/Top', color: '#1b65bf' }),
	paint(gapArray[2].bottomLine, { name: 'C/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'C'
);
 
fill(
	paint(gapArray[3].topLine, { name: 'D/Top', color: '#1b65bf' }),
	paint(gapArray[3].bottomLine, { name: 'D/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'D'
);
 
fill(
	paint(gapArray[4].topLine, { name: 'E/Top', color: '#1b65bf' }),
	paint(gapArray[4].bottomLine, { name: 'E/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'E'
);
 
fill(
	paint(gapArray[5].topLine, { name: 'F/Top', color: '#1b65bf' }),
	paint(gapArray[5].bottomLine, { name: 'F/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'F'
);
 
fill(
	paint(gapArray[6].topLine, { name: 'G/Top', color: '#1b65bf' }),
	paint(gapArray[6].bottomLine, { name: 'G/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'G'
);
 
fill(
	paint(gapArray[7].topLine, { name: 'H/Top', color: '#1b65bf' }),
	paint(gapArray[7].bottomLine, { name: 'H/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'H'
);
 
fill(
	paint(gapArray[8].topLine, { name: 'I/Top', color: '#1b65bf' }),
	paint(gapArray[8].bottomLine, { name: 'I/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'I'
);
 
fill(
	paint(gapArray[9].topLine, { name: 'J/Top', color: '#1b65bf' }),
	paint(gapArray[9].bottomLine, { name: 'J/Bot', color: '#1b65bf' }),
	'#1b65bf',
	undefined,
	'J'
);