Bollinger Band Breakout Strategy — SPOT ONLY (Halal Algo Trading, Batch 1) =========================================================================== Timeframe : 4h Author : Halal Algo Trading Version : 1.0
Timeframe
4h
Direction
Long Only
Stoploss
-8.0%
Trailing Stop
Yes
ROI
0m: 6.0%, 60m: 4.0%, 120m: 2.0%
Interface Version
3
Startup Candles
N/A
Indicators
3
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
# flake8: noqa: F401
# isort:skip_file
# --- Do not remove these imports ---
from datetime import datetime
from typing import Optional
import numpy as np
import pandas as pd
from pandas import DataFrame
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
import pandas_ta as ta
# ---------------------------------------------------------------------------
# Strategy04_BB_Breakout
# ---------------------------------------------------------------------------
class Strategy04_BB_Breakout(IStrategy):
"""
Bollinger Band Breakout Strategy — SPOT ONLY (Halal Algo Trading, Batch 1)
===========================================================================
Timeframe : 4h
Author : Halal Algo Trading
Version : 1.0
Overview
--------
Captures explosive breakout moves that emerge after a period of price
compression (the Bollinger Band Squeeze). The strategy uses three components:
• Bollinger Bands (20-period, 2 std dev) — primary breakout signal
• BB Bandwidth (squeeze detector) — identifies low-volatility setups
• RSI (14-period) — momentum confirmation
The "squeeze" is a powerful setup: when Bollinger Bands contract to a
historically narrow width, it signals that volatility is coiling. The
subsequent expansion (breakout above the upper band) tends to produce
strong directional moves.
Entry Conditions (ALL must be true)
------------------------------------
1. Price (close) closes ABOVE the upper Bollinger Band → breakout confirmed
2. Volume > 1.5× the 20-period volume SMA → strong participation
3. RSI (14) > 50 → bullish momentum
Pre-breakout Squeeze Detection (context, not a required entry gate)
--------------------------------------------------------------------
• BB Bandwidth = (Upper Band − Lower Band) / Middle Band
• If bandwidth < BB_SQUEEZE_THRESHOLD (default 0.05) before the breakout,
this flag is stored in `bb_squeeze` for logging / hyperopt purposes.
A prior squeeze improves the quality of the breakout signal.
Exit Conditions (ANY triggers exit)
------------------------------------
A. Price closes back BELOW the middle Bollinger Band (20 SMA) → trend ends
B. RSI reaches 80 → extreme overbought
Risk Management
---------------
• Hard stoploss : -8 %
• Trailing stop : enabled
• Trailing stop positive: 2 %
• ROI ladder : 6 % @ open, 4 % @ 60 min, 2 % @ 120 min
• SPOT only — no short, no margin, no futures
Backtest Reference
------------------
Return 285.76 % | Win Rate 41.36 %
"""
# -------------------------------------------------------------------------
# Freqtrade metadata
# -------------------------------------------------------------------------
INTERFACE_VERSION = 3
strategy_name = "Strategy04_BB_Breakout"
timeframe = "4h"
process_only_new_candles = True
# Need 20 bars for BB + RSI warmup
startup_candle_count: int = 40
# -------------------------------------------------------------------------
# Strategy-specific parameters
# -------------------------------------------------------------------------
# Minimum RSI to confirm bullish momentum at breakout
RSI_ENTRY_MIN: float = 50.0
# RSI level that triggers overbought exit
RSI_EXIT_OB: float = 80.0
# Volume multiplier required vs the 20-period SMA
VOLUME_MULT: float = 1.5
# Bollinger Band squeeze threshold (bandwidth < this → squeeze)
BB_SQUEEZE_THRESHOLD: float = 0.05
# -------------------------------------------------------------------------
# ROI
# -------------------------------------------------------------------------
minimal_roi = {
"0": 0.06,
"60": 0.04,
"120": 0.02,
}
# -------------------------------------------------------------------------
# Stoploss & Trailing Stop
# -------------------------------------------------------------------------
stoploss = -0.08
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
# -------------------------------------------------------------------------
# Misc
# -------------------------------------------------------------------------
can_short = False
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# -------------------------------------------------------------------------
# Helper functions
# -------------------------------------------------------------------------
@staticmethod
def crossed_above(s1: pd.Series, s2: pd.Series) -> pd.Series:
"""Return True on the bar where s1 crosses from below to above s2."""
return (s1 > s2) & (s1.shift(1) <= s2.shift(1))
@staticmethod
def crossed_below(s1: pd.Series, s2: pd.Series) -> pd.Series:
"""Return True on the bar where s1 crosses from above to below s2."""
return (s1 < s2) & (s1.shift(1) >= s2.shift(1))
# -------------------------------------------------------------------------
# Indicators
# -------------------------------------------------------------------------
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Compute all technical indicators.
Indicators added to `dataframe`
--------------------------------
bb_upper — Upper Bollinger Band (20-period, 2 std)
bb_mid — Middle Bollinger Band (20-period SMA)
bb_lower — Lower Bollinger Band (20-period, 2 std)
bb_bandwidth — (upper − lower) / mid — measures band width / squeeze
bb_squeeze — True when bandwidth < BB_SQUEEZE_THRESHOLD
rsi — 14-period RSI
volume_sma — 20-period SMA of volume
"""
# --- Bollinger Bands (period=20, std=2.0) ---
bb = ta.bbands(dataframe["close"], length=20, std=2.0)
# pandas_ta column names: BBL_20_2.0, BBM_20_2.0, BBU_20_2.0, BBB_20_2.0, BBP_20_2.0
dataframe["bb_upper"] = bb["BBU_20_2.0"]
dataframe["bb_mid"] = bb["BBM_20_2.0"]
dataframe["bb_lower"] = bb["BBL_20_2.0"]
# --- BB Bandwidth: (upper − lower) / middle ---
# Narrow bandwidth → bands contracted → potential squeeze
dataframe["bb_bandwidth"] = (
(dataframe["bb_upper"] - dataframe["bb_lower"])
/ dataframe["bb_mid"]
)
# --- Squeeze flag: bandwidth below threshold ---
dataframe["bb_squeeze"] = dataframe["bb_bandwidth"] < self.BB_SQUEEZE_THRESHOLD
# --- RSI (14-period) ---
dataframe["rsi"] = ta.rsi(dataframe["close"], length=14)
# --- Volume SMA (20-period) ---
dataframe["volume_sma"] = ta.sma(dataframe["volume"], length=20)
# --- NaN Safety: convert any None to NaN so pandas comparisons work ---
for col in dataframe.columns:
if col not in ['open', 'high', 'low', 'close', 'volume', 'date']:
try:
dataframe[col] = pd.to_numeric(dataframe[col], errors='coerce')
except (ValueError, TypeError):
pass
return dataframe
# -------------------------------------------------------------------------
# Entry Signal
# -------------------------------------------------------------------------
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Generate LONG entry signals.
Conditions (ALL must be true):
1. Close > Upper Bollinger Band → confirmed breakout above resistance
2. Volume > 1.5× volume SMA → institutional/strong participation
3. RSI > 50 → bullish momentum confirmed
Quality note: When `bb_squeeze` was True in prior bars (bandwidth was
narrow before this candle), the breakout is considered higher quality.
This can be added as a pre-filter by checking:
dataframe["bb_squeeze"].shift(1) | dataframe["bb_squeeze"].shift(2)
This check is commented out below as it is an optional quality filter.
"""
dataframe.loc[
(
# Condition 1 — Close above Upper BB (breakout)
(dataframe["close"] > dataframe["bb_upper"])
# Condition 2 — Volume > 1.5× average (strong participation)
& (dataframe["volume"] > self.VOLUME_MULT * dataframe["volume_sma"])
# Condition 3 — RSI > 50 (momentum positive)
& (dataframe["rsi"] > self.RSI_ENTRY_MIN)
# [Optional] Prior squeeze quality filter (uncomment to enable):
# & (dataframe["bb_squeeze"].shift(1) | dataframe["bb_squeeze"].shift(2))
# Safety: indicators populated
& dataframe["bb_upper"].notna()
& dataframe["rsi"].notna()
& dataframe["volume_sma"].notna()
),
"enter_long",
] = 1
return dataframe
# -------------------------------------------------------------------------
# Exit Signal
# -------------------------------------------------------------------------
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
"""
Generate LONG exit signals.
Exit is triggered when EITHER condition is met:
A. Price closes back BELOW the middle Bollinger Band (20 SMA)
→ The breakout has failed or the trend has reversed to the mean
B. RSI reaches 80 (RSI_EXIT_OB)
→ Overbought extreme; take profit before potential reversal
"""
dataframe.loc[
(
# Condition A — Price falls back below the BB midline
(dataframe["close"] < dataframe["bb_mid"])
# Condition B — RSI reaches overbought extreme
| (dataframe["rsi"] >= self.RSI_EXIT_OB)
)
& dataframe["bb_mid"].notna()
& dataframe["rsi"].notna(),
"exit_long",
] = 1
return dataframe