Timeframe
1h
Direction
Long Only
Stoploss
-99.0%
Trailing Stop
No
ROI
0m: 10000.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
"""
VolBBSqueeze — 4h Bollinger-Band squeeze breakout, 1h entry on confirmation
Paradigm: volatility
Hypothesis: v0.2.0's VolSqueezeBreak was killed at Sharpe 0.17 — agent noted
squeeze paradigm needs lower frequency than 1h. v0.3.0 affordance:
move squeeze detection to 4h, then enter on 1h once break confirms.
BB squeeze = 4h BB-width in bottom quartile of last 50 4h bars.
Break = 4h close > 4h upper band after squeeze. Direction is
implicitly long (we don't short on this universe). 1d EMA200
regime gate to avoid bear-market squeeze releases that fail.
Parent: root (paradigm-inspired by v0.2.0's killed VolSqueezeBreak, restructured to MTF)
Created: pending — fill in after first commit
Status: active
Uses MTF: yes
"""
from pandas import DataFrame
import talib.abstract as ta
from freqtrade.strategy import IStrategy, informative
class VolBBSqueeze(IStrategy):
INTERFACE_VERSION = 3
timeframe = "1h"
can_short = False
minimal_roi = {"0": 100}
stoploss = -0.99
trailing_stop = False
process_only_new_candles = True
use_exit_signal = True
exit_profit_only = False
ignore_roi_if_entry_signal = False
# 4h indicators with 50-bar window need ~250 hourly bars warmup; 1d EMA200 dominates
startup_candle_count: int = 250
@informative("4h")
def populate_indicators_4h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
upper, middle, lower = ta.BBANDS(
dataframe["close"], timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0
)
dataframe["bb_upper"] = upper
dataframe["bb_middle"] = middle
dataframe["bb_lower"] = lower
# BB width relative to its own 50-bar history — squeeze when width is in bottom quartile
dataframe["bb_width"] = (upper - lower) / middle
# q33 squeeze threshold (peak via r23 / r32 — tighter q20 hurts compounding, looser q50 over-trades)
dataframe["bb_width_q33"] = dataframe["bb_width"].rolling(50).quantile(0.33)
return dataframe
@informative("1d")
def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["sma50"] = ta.SMA(dataframe, timeperiod=50)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
squeeze_then_break = (
(dataframe["bb_width_4h"].shift(1) <= dataframe["bb_width_q33_4h"].shift(1)) # was squeezed
& (dataframe["close_4h"] > dataframe["bb_upper_4h"]) # now breaking upper band
)
dataframe.loc[
squeeze_then_break
& (dataframe["close"] > dataframe["ema200_1d"]), # 1d bull regime
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
dataframe["close"] < dataframe["sma50"], # 1h trend break — patient exit
"exit_long",
] = 1
return dataframe