💬 Forum

BollingerBandsStrategy

salitov1337/TragedyTradingLab/strategies/bollinger.py · first seen 2026-07-16 · repo updated 2026-05-05 · ⬇ 1 download

Basics mode: spot timeframe: 15m
Settings stoploss: -0.03 has minimal roi protections startup candle count: 150
Indicators Bollinger_Bands
Concepts breakout risk_management
Methods generate_signal
Other base_strategy strategies
15 related strategies ( identical code, similar name)

Each tile is a different kind of check — from an instant code lint to full sandboxed backtests and forward tests on recent data. Not sure what a check actually proves? See the FAQ →

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
"""Bollinger squeeze breakout strategy with vectorized entry and exit rules."""

from __future__ import annotations

from typing import Any, ClassVar

import pandas as pd

try:
    from strategies.base_strategy import BaseSignalStrategy, SignalResult, bollinger_bands
except ImportError:
    from base_strategy import BaseSignalStrategy, SignalResult, bollinger_bands


class BollingerBandsStrategy(BaseSignalStrategy):
    """Enter on squeeze breakouts and exit on a move back to the mid band."""

    bb_length: int = 20
    bb_std: float = 2.0
    squeeze_lookback: int = 100
    squeeze_factor: float = 1.05

    minimal_roi: ClassVar[dict[str, float]] = {"0": 0.04, "90": 0.02, "240": 0.0}
    stoploss: float = -0.03
    timeframe: str = "15m"
    startup_candle_count: int = 150
    protections: ClassVar[list[dict[str, Any]]] = [
        {"method": "CooldownPeriod", "stop_duration_candles": 4},
        {
            "method": "StoplossGuard",
            "lookback_period_candles": 96,
            "trade_limit": 2,
            "stop_duration_candles": 16,
            "only_per_pair": False,
        },
        {
            "method": "MaxDrawdown",
            "lookback_period_candles": 192,
            "trade_limit": 6,
            "stop_duration_candles": 32,
            "max_allowed_drawdown": 0.08,
        },
    ]

    def populate_indicators(
        self,
        dataframe: pd.DataFrame,
        metadata: dict[str, Any],
    ) -> pd.DataFrame:
        lower, middle, upper, bandwidth = bollinger_bands(
            dataframe["close"],
            length=self.bb_length,
            num_std=self.bb_std,
        )
        dataframe["bb_lower"] = lower
        dataframe["bb_middle"] = middle
        dataframe["bb_upper"] = upper
        dataframe["bb_bandwidth"] = bandwidth
        dataframe["bb_bw_min"] = bandwidth.rolling(
            window=self.squeeze_lookback,
            min_periods=self.squeeze_lookback,
        ).min()
        return dataframe

    def generate_signal(
        self,
        dataframe: pd.DataFrame,
        metadata: dict[str, Any],
    ) -> SignalResult:
        required = self.bb_length + self.squeeze_lookback
        if len(dataframe) < required:
            return SignalResult(0, 0.0, "insufficient data", {})

        df = dataframe.copy()
        if "bb_lower" not in df.columns:
            df = self.populate_indicators(df, metadata)

        last = df.iloc[-1]
        close = float(last["close"])
        lower = float(last["bb_lower"])
        upper = float(last["bb_upper"])
        bw = float(last["bb_bandwidth"]) if pd.notna(last["bb_bandwidth"]) else 0.0
        bw_min = float(last["bb_bw_min"]) if pd.notna(last["bb_bw_min"]) else 0.0
        indicators = {
            "close": close,
            "bb_lower": lower,
            "bb_middle": float(last["bb_middle"]),
            "bb_upper": upper,
            "bb_bandwidth": bw,
            "bb_bw_min": bw_min,
        }

        if bw_min <= 0.0:
            return SignalResult(0, 0.0, "squeeze history not ready", indicators)

        in_squeeze = bw <= bw_min * self.squeeze_factor
        if in_squeeze and close > upper:
            return SignalResult(1, 0.6, "squeeze breakout up", indicators)
        if in_squeeze and close < lower:
            return SignalResult(-1, 0.6, "squeeze breakout down", indicators)
        return SignalResult(0, 0.0, "no squeeze breakout", indicators)

    def populate_entry_trend(
        self,
        dataframe: pd.DataFrame,
        metadata: dict[str, Any],
    ) -> pd.DataFrame:
        df = dataframe if "bb_lower" in dataframe.columns else self.populate_indicators(dataframe, metadata)
        squeeze_ready = df["bb_bw_min"] > 0.0
        in_squeeze = squeeze_ready & (df["bb_bandwidth"] <= df["bb_bw_min"] * self.squeeze_factor)
        long_signal = in_squeeze & (df["close"] > df["bb_upper"])
        short_signal = in_squeeze & (df["close"] < df["bb_lower"])

        df["enter_long"] = long_signal.fillna(False).astype(int)
        df["enter_short"] = short_signal.fillna(False).astype(int)

        if "enter_tag" in df.columns:
            df.loc[df["enter_long"] == 1, "enter_tag"] = "BollingerBandsStrategy:breakout_up"
            df.loc[df["enter_short"] == 1, "enter_tag"] = "BollingerBandsStrategy:breakout_down"
        return df

    def populate_exit_trend(
        self,
        dataframe: pd.DataFrame,
        metadata: dict[str, Any],
    ) -> pd.DataFrame:
        df = dataframe if "bb_lower" in dataframe.columns else self.populate_indicators(dataframe, metadata)
        df["exit_long"] = (df["close"] < df["bb_middle"]).fillna(False).astype(int)
        df["exit_short"] = (df["close"] > df["bb_middle"]).fillna(False).astype(int)

        if "exit_tag" in df.columns:
            df.loc[df["exit_long"] == 1, "exit_tag"] = "BollingerBandsStrategy:midband_exit"
            df.loc[df["exit_short"] == 1, "exit_tag"] = "BollingerBandsStrategy:midband_exit"
        return df


class BollingerBandsLoosenedStrategy(BollingerBandsStrategy):
    """Looser analytics v2 for more frequent squeeze-breakout candidates."""

    squeeze_lookback: int = 50
    squeeze_factor: float = 1.25
    timeframe: str = "5m"