Skip to main content

Keltner Channel Breakout

The breakout trading strategy involves using two technical indicators, Keltner Channels and On-Balance Volume (OBV), to determine entry and exit signals for trades.

Keltner Channels are a type of volatility-based envelope indicator that are calculated using an exponential moving average (EMA) and an average true range (ATR) multiplier. The upper and lower bands of the Keltner Channel are calculated by adding and subtracting the ATR multiple from the EMA, respectively. When price breaks above the upper Keltner Channel, it is seen as a bullish breakout signal, and when price breaks below the lower Keltner Channel, it is seen as a bearish breakout signal.

On-Balance Volume (OBV) is a momentum indicator that measures buying and selling pressure. OBV is calculated by adding the volume of all up days and subtracting the volume of all down days. When OBV is increasing, it suggests that buying pressure is increasing, and when OBV is decreasing, it suggests that selling pressure is increasing.

The strategy involves calculating the Keltner Channels and OBV without using built-in methods and then using them to determine entry and exit signals. Trades are only entered when there is a breakout signal, as indicated by price breaking above the upper Keltner Channel for a long trade or below the lower Keltner Channel for a short trade. Additionally, trades are only entered if there is no bag, which likely refers to a bearish trend or period of consolidation. Trades are exited when price reaches the opposite Keltner Channel or if there is a reversal signal indicated by OBV.

Overall, this strategy aims to capture trending moves in the market and avoid trading during periods of low volatility or uncertainty.

tip

This example strategy is machine generated using Gunbot AI. Review its behavior carefully in a simulated bot instance before using parts of this code in production.

// initialize customStratStore within pairLedger object
gb.data.pairLedger.customStratStore = gb.data.pairLedger.customStratStore || {};

// forced wait time reduces risk of double orders
function checkTime() {
return !gb.data.pairLedger.customStratStore.timeCheck || typeof gb.data.pairLedger.customStratStore.timeCheck !== "number"
? (gb.data.pairLedger.customStratStore.timeCheck = Date.now(), false)
: (Date.now() - gb.data.pairLedger.customStratStore.timeCheck > 8000);
}
const enoughTimePassed = checkTime();

// set timestamp for checkTime in next round
const setTimestamp = () => gb.data.pairLedger.customStratStore.timeCheck = Date.now();

// calculate Keltner Channels
const calculateKC = (period = 20, multiplier = 2) => {
const tp = gb.data.candlesHigh.map((high, i) => (high + gb.data.candlesLow[i] + gb.data.candlesClose[i]) / 3);
const atr = gb.data.candlesHigh.slice(1).reduce((acc, high, i) => {
const tr = Math.max(
high - gb.data.candlesLow[i],
Math.abs(high - gb.data.candlesClose[i]),
Math.abs(gb.data.candlesClose[i] - gb.data.candlesLow[i])
);
return acc + tr;
}, 0) / (gb.data.candlesHigh.length - 1);
const upper = tp.map((_, i) => tp.slice(i - period + 1, i + 1).reduce((acc, val) => acc + val, 0) / period + atr * multiplier);
const middle = tp.map((_, i) => tp.slice(i - period + 1, i + 1).reduce((acc, val) => acc + val, 0) / period);
const lower = tp.map((_, i) => tp.slice(i - period + 1, i + 1).reduce((acc, val) => acc + val, 0) / period - atr * multiplier);
return { upper, middle, lower };
};

// calculate On-Balance Volume (OBV)
const calculateOBV = () => {
const obv = [0];
for (let i = 1; i < gb.data.candlesClose.length; i++) {
if (gb.data.candlesClose[i] > gb.data.candlesClose[i - 1]) {
obv.push(obv[i - 1] + gb.data.candlesVolume[i]);
} else if (gb.data.candlesClose[i] < gb.data.candlesClose[i - 1]) {
obv.push(obv[i - 1] - gb.data.candlesVolume[i]);
} else {
obv.push(obv[i - 1]);
}
}
return obv;
};

// calculate indicators
const kc = calculateKC();
const obv = calculateOBV();
const currentObv = obv[obv.length - 1];
const previousObv = obv[obv.length - 2];
const currentClose = gb.data.candlesClose[gb.data.candlesClose.length - 1];
const currentHigh = gb.data.candlesHigh[gb.data.candlesHigh.length - 1];
const currentLow = gb.data.candlesLow[gb.data.candlesLow.length - 1];
const currentKcUpper = kc.upper[kc.upper.length - 1];
const currentKcMiddle = kc.middle[kc.middle.length - 1];
const currentKcLower = kc.lower[kc.lower.length - 1];

// log indicators
console.log(`Current OBV: ${currentObv}`);
console.log(`Previous OBV: ${previousObv}`);
console.log(`Current Close: ${currentClose}`);
console.log(`Current High: ${currentHigh}`);
console.log(`Current Low: ${currentLow}`);
console.log(`Current KC Upper: ${currentKcUpper}`);
console.log(`Current KC Middle: ${currentKcMiddle}`);
console.log(`Current KC Lower: ${currentKcLower}`);

if (enoughTimePassed) {
const buyConditions = currentObv > previousObv && currentClose > currentKcUpper && !gb.data.gotBag;
const sellConditions = currentObv < previousObv && currentClose < currentKcLower && gb.data.gotBag;

// fire orders when conditions are met
if (buyConditions) {
const buyAmount = gb.data.baseBalance / gb.data.ask;
gb.method.buyMarket(buyAmount, gb.data.pairName);
setTimestamp();
} else if (sellConditions) {
gb.method.sellMarket(gb.data.quoteBalance, gb.data.pairName);
setTimestamp();
}
}

// Code is machine generated, review it and run in simulator mode first