← Back to Home
BBKC Squeeze BNB-USD and SOL-USD Hourly Tutorial

BBKC Squeeze BNB-USD and SOL-USD Hourly Tutorial

This tutorial uses BBKCSqueezeStrategy from the Mega Backtrader Strategy Pack. The package includes the complete strategy source and the backtest runner used to produce these results.

The Bollinger/Keltner squeeze strategy returned 39.71% on BNB-USD while BNB buy-and-hold lost 27.23%. On SOL-USD, the strategy returned 37.40% while buy-and-hold lost 60.64%.

The Result First

BBKCSqueezeStrategy hourly backtest results

Each backtest started with $10,000 and used 1-hour bars from August 14, 2025 at 12:00 UTC through August 14, 2026 at 12:00 UTC, 0.10% commission per execution, 95% position sizing, and long-only execution. Every run ended with zero open trades.

What We Are Building

A squeeze exists when narrow Bollinger Bands fit inside Keltner Channels. The strategy waits for price to escape the channel, enters in the breakout direction, and manages the position with a 1% trailing stop.

Step 0: Install and Download Hourly Data

Install the minimal dependencies:

pip install backtrader yfinance pandas matplotlib

Then fetch and normalize the hourly OHLCV frame:

from datetime import datetime, timezone

import backtrader as bt
import pandas as pd
import yfinance as yf

symbol = "BNB-USD"
data = yf.download(
    symbol,
    start=datetime(2025, 8, 14, 12, tzinfo=timezone.utc),
    end=datetime(2026, 8, 14, 13, tzinfo=timezone.utc),
    interval="1h",
    auto_adjust=False,
    progress=False,
)

if isinstance(data.columns, pd.MultiIndex):
    data.columns = data.columns.get_level_values(0)
data = data[["Open", "High", "Low", "Close", "Volume"]].dropna()

Step 1: choose short hourly lookbacks

The Bollinger and ATR windows use seven hourly bars; the Keltner center uses a 30-hour EMA.

class BBKCSqueezeStrategy(bt.Strategy):
    params = (
        ('bband_period', 7),
        ('bband_devfactor', 1.0),
        ('keltner_period', 30),
        ('keltner_atr_period', 7),
        ('keltner_atr_multiplier', 1.0),
        ('trail_percent', 0.01),
    )

Step 2: build Bollinger Bands

Bollinger width responds to standard deviation. A one-standard-deviation setting is intentionally tighter than the common two-deviation default.

def __init__(self):
    self.close = self.data.close
    self.bband = bt.indicators.BollingerBands(
        self.data,
        period=self.p.bband_period,
        devfactor=self.p.bband_devfactor,
    )

Step 3: build Keltner Channels

Keltner width is ATR-based, so comparing the two envelopes asks whether statistical volatility has compressed inside a range-based volatility measure.

self.atr = bt.indicators.ATR(
    self.data, period=self.p.keltner_atr_period
)
self.keltner_mid = bt.indicators.EMA(
    self.close, period=self.p.keltner_period
)
self.keltner_top = self.keltner_mid + self.atr * self.p.keltner_atr_multiplier
self.keltner_bot = self.keltner_mid - self.atr * self.p.keltner_atr_multiplier

Step 4: detect compression

Both Bollinger boundaries must sit inside the corresponding Keltner boundaries.

is_squeeze = (
    self.bband.top[0] < self.keltner_top[0]
    and self.bband.bot[0] > self.keltner_bot[0]
)

Step 5: enter the breakout

The measured package run was long-only, so only the upside branch could open a new position. Add --allow-short to test the downside branch as a separate experiment.

if not self.position and is_squeeze:
    if self.close[0] > self.keltner_top[0]:
        self.order = self.buy()
    elif self.close[0] < self.keltner_bot[0]:
        self.order = self.sell()

Step 6: trail the completed entry

Backtrader's trailing stop follows favorable movement and exits after a 1% reversal from the running reference level.

def notify_order(self, order):
    if order.status in [order.Submitted, order.Accepted]:
        return
    if order.status == order.Completed and order.isbuy():
        self.sell(
            exectype=bt.Order.StopTrail,
            trailpercent=self.p.trail_percent,
        )
    self.order = None

Connect the Strategy to Backtrader

Replace StrategyClass with the class built above. This is the minimal engine configuration: $10,000 starting cash, 0.10% commission, and 95% position sizing.

cerebro = bt.Cerebro()
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001)  # 0.10% per execution
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.adddata(bt.feeds.PandasData(dataname=data))
cerebro.addstrategy(StrategyClass)

result = cerebro.run()[0]
print(f"Final value: ${cerebro.broker.getvalue():,.2f}")

Complete Copy-and-Run Script

The following script includes the complete strategy, data download, long-only execution rule, analyzers, and printed results. Save it as a .py file and run it directly.

import math
from datetime import datetime, timezone

import backtrader as bt
import backtrader.indicators as btind
import pandas as pd
import yfinance as yf


# Complete strategy implementation

class BBKCSqueezeStrategy(bt.Strategy):
    """
    A strategy that enters on a breakout after a period of low volatility
    defined by Bollinger Bands being inside Keltner Channels, and uses a trailing stop.
    1. Identify Squeeze: Bollinger Bands are within Keltner Channels.
    2. Enter on Breakout: Price closes outside the Keltner Channels.
    3. Exit: A trailing stop-loss order is placed upon entry.
    """
    params = (
        ('bband_period', 7),
        ('bband_devfactor', 1.0),
        ('keltner_period', 30),
        ('keltner_atr_period', 7),
        ('keltner_atr_multiplier', 1.0),
        ('trail_percent', 0.01),
        ('printlog', False),  # For optimization
    )

    def log(self, txt, dt=None, doprint=False):
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.datetime(0)
            print(f"{dt.isoformat()} - {txt}")

    def __init__(self):
        self.order = None
        self.dataclose = self.datas[0].close

        # Add Bollinger Bands indicator
        self.bband = bt.indicators.BollingerBands(
            self.datas[0],
            period=self.p.bband_period,
            devfactor=self.p.bband_devfactor
        )

        # Add Keltner Channels
        self.atr = bt.indicators.ATR(self.datas[0], period=self.p.keltner_atr_period)
        self.keltner_mid = bt.indicators.EMA(self.dataclose, period=self.p.keltner_period)
        self.keltner_top = self.keltner_mid + (self.atr * self.p.keltner_atr_multiplier)
        self.keltner_bot = self.keltner_mid - (self.atr * self.p.keltner_atr_multiplier)

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return

        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f"BUY EXECUTED at {order.executed.price:.2f}")
                self.sell(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)
            elif order.issell():
                self.log(f"SELL EXECUTED at {order.executed.price:.2f}")
                self.buy(exectype=bt.Order.StopTrail, trailpercent=self.p.trail_percent)

        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log(f"Order {order.getstatusname()}")

        self.order = None

    def next(self):
        if self.order:
            return

        # Wait for sufficient data
        if len(self) < max(self.p.bband_period, self.p.keltner_period, self.p.keltner_atr_period):
            return

        # Check for Keltner Channel and Bollinger Band overlap for squeeze
        # Squeeze occurs when BBands are inside Keltner Channels
        is_squeeze = (self.bband.top[0] < self.keltner_top[0] and
                      self.bband.bot[0] > self.keltner_bot[0])

        if not self.position:
            if is_squeeze:
                # Breakout to the upside (close above Keltner top)
                if self.dataclose[0] > self.keltner_top[0]:
                    self.log(f"SQUEEZE BREAKOUT UP at {self.dataclose[0]:.2f}")
                    self.order = self.buy()
                # Breakout to the downside (close below Keltner bottom)
                elif self.dataclose[0] < self.keltner_bot[0]:
                    self.log(f"SQUEEZE BREAKOUT DOWN at {self.dataclose[0]:.2f}")
                    self.order = self.sell()


# Keep this tutorial backtest long-only.
original_sell = bt.Strategy.sell

def long_only_sell(self, *args, **kwargs):
    if self.position.size <= 0:
        return None
    size = kwargs.get("size")
    if size is None or size > self.position.size:
        kwargs["size"] = self.position.size
    return original_sell(self, *args, **kwargs)

bt.Strategy.sell = long_only_sell


# Download the exact hourly test window.
symbol = "BNB-USD"
data = yf.download(
    symbol,
    start=datetime(2025, 8, 14, 12, tzinfo=timezone.utc),
    end=datetime(2026, 8, 14, 13, tzinfo=timezone.utc),
    interval="1h",
    auto_adjust=False,
    progress=False,
)

if isinstance(data.columns, pd.MultiIndex):
    data.columns = data.columns.get_level_values(0)
data = data[["Open", "High", "Low", "Close", "Volume"]].dropna()
if getattr(data.index, "tz", None) is not None:
    data.index = data.index.tz_localize(None)


# Configure and run Backtrader.
cerebro = bt.Cerebro()
cerebro.broker.setcash(10_000)
cerebro.broker.setcommission(commission=0.001)
cerebro.addsizer(bt.sizers.PercentSizer, percents=95)
cerebro.adddata(bt.feeds.PandasData(dataname=data))
cerebro.addstrategy(BBKCSqueezeStrategy)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="drawdown")
cerebro.addanalyzer(bt.analyzers.TradeAnalyzer, _name="trades")

result = cerebro.run()[0]
final_value = cerebro.broker.getvalue()
total_return = (final_value / 10_000 - 1) * 100
buy_hold_prices = data.loc[data.index.hour == 0, "Close"]
buy_hold_return = (buy_hold_prices.iloc[-1] / buy_hold_prices.iloc[0] - 1) * 100
excess_return = total_return - buy_hold_return
drawdown = result.analyzers.drawdown.get_analysis()
trades = result.analyzers.trades.get_analysis()

print(f"Final value: ${final_value:,.2f}")
print(f"Strategy return: {total_return:.2f}%")
print(f"Buy-and-hold return: {buy_hold_return:.2f}%")
print(f"Excess return: {excess_return:.2f} percentage points")
print(f"Maximum drawdown: {drawdown['max']['drawdown']:.2f}%")
print(f"Closed trades: {trades.get('total', {}).get('closed', 0)}")

To run the second backtest, change symbol = "BNB-USD" to symbol = "SOL-USD"; every other line stays the same.

Run the Packaged Backtest

If you have the Mega Backtrader Strategy Pack, this command runs the full reporting pipeline for BNB-USD and automatically creates the CSV metrics and charts used below:

python run_backtest.py --symbol BNB-USD --period 1y --interval 1h --benchmark BNB-USD --strategy-filter optimization_BBKCSqueezeStrategy.py --out hourly_crypto_results

If you have the Mega Backtrader Strategy Pack, this command runs the full reporting pipeline for SOL-USD and automatically creates the CSV metrics and charts used below:

python run_backtest.py --symbol SOL-USD --period 1y --interval 1h --benchmark SOL-USD --strategy-filter optimization_BBKCSqueezeStrategy.py --out hourly_crypto_results

Read the Charts

BNB-USD: strategy versus buy-and-hold

BBKCSqueezeStrategy BNB-USD versus buy-and-hold

BNB-USD: drawdown versus buy-and-hold

BBKCSqueezeStrategy BNB-USD drawdown versus buy-and-hold

BNB-USD: strategy return distribution

BBKCSqueezeStrategy BNB-USD return distribution

SOL-USD: strategy versus buy-and-hold

BBKCSqueezeStrategy SOL-USD versus buy-and-hold

SOL-USD: drawdown versus buy-and-hold

BBKCSqueezeStrategy SOL-USD drawdown versus buy-and-hold

SOL-USD: strategy return distribution

BBKCSqueezeStrategy SOL-USD return distribution

How to Interpret This Result

A Better Research Workflow

  1. Save the downloaded dataset and record its checksum so every future run uses the same input bars.
  2. Reserve untouched validation months, then run walk-forward rather than selecting on the entire year.
  3. Test nearby parameters. A robust idea should not collapse when a period changes by one or two bars.
  4. Add spread, slippage, funding, and exchange-specific order rules—especially for high-turnover strategies.
  5. Inspect every order lifecycle and test the short-enabled branch separately with --allow-short.
  6. Compare the one-hour configuration with time-equivalent settings on four-hour and daily bars.

Continue with the Full Package

BBKCSqueezeStrategy comes from the Mega Backtrader Strategy Pack. The package includes the complete optimization_BBKCSqueezeStrategy.py source file and the backtest runner used in this tutorial, so you can run the strategy, change its parameters, and generate the same result files yourself.

Disclaimer

This material is for research and education only. Backtests are hypothetical, sensitive to data and assumptions, and do not guarantee future performance.