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
"""
RegimeAdaptiveBNB — BNB MR with regime-DETECTED stake scaling
Paradigm: mean-reversion (regime-conditional sizing)
Hypothesis: BNBSizedConviction r9 proved RSI-depth conviction sizing
lifted robust marginally (0.079→0.098). r20-r21 fork
experiments showed BNB sizing doesn't compose linearly
inside multi-pair containers. The remaining sizing question
is structural: does REGIME-CONDITIONAL sizing (large in
favorable regimes, small in unfavorable) outperform
SIGNAL-CONDITIONAL sizing (large at deep RSI)? v0.4.0 r7
ablation found vol-target sizing was risk-control not edge,
but it never tested regime-detected sizing — it relied on
ATR (proxy for vol) which inversely correlates with regime
but isn't regime-direct.
This strategy uses 1d EMA200 slope+magnitude to classify
regime in real-time (no peek-ahead): bull (slope_up AND
close > EMA200*1.10), winter (slope_down AND close <
EMA200*0.90), neutral (everything else). Then scales stake:
- bull: 1.5x (lean into favorable regime)
- neutral: 1.0x (baseline)
- winter: 0.5x (de-risk)
Same RSI<25 entry as BNBSized. Tests: does regime-detected
sizing lift robust > BNBSized's 0.098?
Parent: root (regime-detection-conditional-sizing variant of BNB MR;
distinct from BNBSized's RSI-depth-conditional sizing)
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 RegimeAdaptiveBNB(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
startup_candle_count: int = 250
pair_basket = ["BNB/USDT"]
test_timeranges = [
("bull_2021", "20210101-20211231"),
("winter_2022", "20220101-20221231"),
("recovery_23_25", "20230101-20251231"),
("full_5y", "20210101-20251231"),
]
@informative("1d")
def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
dataframe["ema200_slope_up"] = (
dataframe["ema200"] > dataframe["ema200"].shift(7)
).astype(int)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
# Regime classification: 2 (bull) | 1 (neutral) | 0 (winter)
is_bull = (
(dataframe["ema200_slope_up_1d"] == 1)
& (dataframe["close"] > dataframe["ema200_1d"] * 1.10)
)
is_winter = (
(dataframe["ema200_slope_up_1d"] == 0)
& (dataframe["close"] < dataframe["ema200_1d"] * 0.90)
)
dataframe["regime"] = 1
dataframe.loc[is_bull, "regime"] = 2
dataframe.loc[is_winter, "regime"] = 0
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Same RSI<25 entry as BNBSized (proven local optimum).
dataframe.loc[
(dataframe["rsi"] < 25)
& (dataframe["close"] > dataframe["ema200_1d"] * 0.85),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[dataframe["rsi"] > 55, "exit_long"] = 1
return dataframe
def custom_stake_amount(
self,
pair: str,
current_time,
current_rate: float,
proposed_stake: float,
min_stake,
max_stake: float,
leverage: float,
entry_tag: str,
side: str,
**kwargs,
) -> float:
# r26: COMPOSE regime sizing × RSI-conviction sizing.
# regime_scale: bull 1.5 / neutral 1.0 / winter 0.5
# rsi_scale: 25/RSI clamped [0.5, 2.0] (BNBSized r9 formula)
# final = regime_scale × rsi_scale, then re-clamped [0.25, 3.0]
# to prevent extreme stakes. r25 finding: regime sizing lifts
# bull & winter individually but recovery dominated robust;
# composing should let RSI-conviction lift recovery while
# regime keeps bull/winter shape.
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if df.empty or "regime" not in df.columns or "rsi" not in df.columns:
return proposed_stake
regime = df["regime"].iloc[-1]
rsi_now = df["rsi"].iloc[-1]
if regime != regime or rsi_now != rsi_now or rsi_now <= 0:
return proposed_stake
# r28: revert r27 aggressive 2x/0.25x → r26 baseline 1.5x/0.5x.
# r27 finding (joins v0.4.0 r17-r20 + v0.4.1 r17): aggressive
# sizing Pareto MOVES (more profit, more DD, ≈ same Sharpe);
# 1.5x/0.5x is the Pareto-equal-to-BNBSized optimum point.
if regime == 2:
regime_scale = 1.5
elif regime == 0:
regime_scale = 0.5
else:
regime_scale = 1.0
rsi_scale = 25.0 / max(float(rsi_now), 5.0)
rsi_scale = max(0.5, min(2.0, rsi_scale))
scale = regime_scale * rsi_scale
scale = max(0.25, min(3.0, scale))
stake = proposed_stake * scale
return max(min_stake or 0.0, min(max_stake, stake))