Back to Strategies

Bollinger Band Squeeze Breakout

Code Strategy by 0xsDp5JzBDn·2026-07-02


// Env: BB_PERIOD, BB_STDDEV, SQUEEZE_THRESHOLD, INTERVAL, TP_PERCENT, SL_PERCENT

async function getCloses(interval, limit) {
  const res = await $.Http.get("https://fapi.binance.com/fapi/v1/klines", {
    symbol: $.Price.symbol,
    interval,
    limit
  });
  if (res.status !== 200) return null;
  const raw = JSON.parse(res.text);
  return raw.map(c => parseFloat(c[4]));
}

const BB_PERIOD = Number($.Env.BB_PERIOD || 20);
const BB_STDDEV = Number($.Env.BB_STDDEV || 2);
const SQUEEZE_THRESHOLD = Number($.Env.SQUEEZE_THRESHOLD || 1.5); // band width as % of price
const INTERVAL = $.Env.INTERVAL || "15m";
const TP_PERCENT = Number($.Env.TP_PERCENT || 2.0);
const SL_PERCENT = Number($.Env.SL_PERCENT || 1.2);

let S = (await $.Storage.get()) || {
  position: null,
  entryPrice: null,
  wasSqueezed: false
};
let nextAction = null;

if (S.position) {
  const roi = $.Strategy.roi(true, S.entryPrice, $.Price.price);
  if (roi !== null) {
    const profit = S.position === "LONG" ? roi : -roi;
    if (profit >= TP_PERCENT || profit <= -SL_PERCENT) {
      nextAction = { type: $.Action.EXIT };
    }
  }
}

if (!nextAction) {
  const closes = await getCloses(INTERVAL, BB_PERIOD * 3);
  if (closes && closes.length > BB_PERIOD) {
    const bb = new $.Ta.BollingerBands(BB_PERIOD, BB_STDDEV);
    for (const c of closes) bb.update(c);

    if (bb.isStable) {
      const bands = bb.getResult();
      const price = $.Price.price;
      const widthPercent = ((bands.upper - bands.lower) / bands.middle) * 100;
      const isSqueezed = widthPercent < SQUEEZE_THRESHOLD;

      if (!S.position) {
        if (S.wasSqueezed && price > bands.upper) {
          nextAction = { type: $.Action.LONG, side: "LONG" };
        } else if (S.wasSqueezed && price < bands.lower) {
          nextAction = { type: $.Action.SHORT, side: "SHORT" };
        }
      } else if (S.position === "LONG" && price < bands.middle) {
        nextAction = { type: $.Action.EXIT };
      } else if (S.position === "SHORT" && price > bands.middle) {
        nextAction = { type: $.Action.EXIT };
      }

      S.wasSqueezed = isSqueezed;
    }
  }
}

if (nextAction) {
  await $.Strategy.action(nextAction.type);
  if (nextAction.type === $.Action.EXIT) {
    S.position = null;
    S.entryPrice = null;
  } else {
    S.position = nextAction.side;
    S.entryPrice = $.Price.price;
  }
}

await $.Storage.set(S);