Timeframe
1h
Direction
Long Only
Stoploss
-99.0%
Trailing Stop
No
ROI
0m: 10000.0%
Interface Version
3
Startup Candles
N/A
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
PerPairMR — first per-pair-conditional strategy: BNB→RSI MR, alts→BB-lower MR
Paradigm: mean-reversion (per-pair conditional)
Hypothesis: This run's strongest findings were both MR-flavored:
(a) BNBMeanRevertSharp r0: RSI<25 + 1d EMA200*0.85 gate
on BNB gave clean cross-regime positive (robust 0.079)
(b) AltsBollLowerMR r10: BB-lower + RSI<35 + slope-up
filter on SOL+AVAX gave bull Sharpe 1.33 (highest in
run) but winter-fragile
v0.4.0's "MR is BNB-skewed" finding plus v0.4.1's r6 fork
confirmed RSI doesn't generalize beyond BNB. But Bollinger
excursion DOES work on SOL+AVAX bull. So each MR signal has
its preferred pair shape — different signal families fit
different volatility profiles. v0.4.1's per-pair-conditional
entries (via metadata['pair']) lets us EXPRESS this finding
structurally: one strategy that uses RSI mechanism on BNB
and BB-lower mechanism on SOL+AVAX. The aggregate is then
two independent edge sources, not a forced single mechanism.
Critically — both branches use the same winter defense
(1d slope-up + close>EMA200) so the entire strategy is
regime-gated. Equal-weight sizing per the v0.4.0 r7 / v0.4.1
r9 ablation findings.
Parent: root (synthesis of BNBMeanRevertSharp + AltsBollLowerMR
structures, novel per-pair conditional architecture)
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 PerPairMR(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
# r18: expand to full 5-pair. BTC+ETH branches use Donchian-48 sustained
# break + 4h macro filter (transfer of AltsBollBreak r1 finding) since
# majors don't have the same MR profile. This makes the strategy
# paradigm-mixed AND universe-conditional simultaneously.
pair_basket = ["BTC/USDT", "ETH/USDT", "BNB/USDT", "SOL/USDT", "AVAX/USDT"]
test_timeranges = [
("bull_2021", "20210101-20211231"),
("winter_2022", "20220101-20221231"),
("recovery_23_25", "20230101-20251231"),
("full_5y", "20210101-20251231"),
]
@informative("4h")
def populate_indicators_4h(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# r14: 4h trend filter for alts branch — kills sideways-recovery
# entries that were SOL -0.14, AVAX -0.05.
dataframe["ema50"] = ta.EMA(dataframe, timeperiod=50)
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
return dataframe
@informative("1d")
def populate_indicators_1d(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe["ema200"] = ta.EMA(dataframe, timeperiod=200)
# r28: revert r27 14-bar → 7-bar slope. r27 finding: 14-bar slope
# is over-restrictive (robust 0.052→0.036). 7-bar window is the
# local optimum.
dataframe["ema200_slope_up"] = (
dataframe["ema200"] > dataframe["ema200"].shift(7)
).astype(int)
return dataframe
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# All branches need RSI; alts also need Bollinger; majors need
# Donchian-48 + volume.
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
bb_period = 20
bb_std = 2.0
sma = dataframe["close"].rolling(bb_period).mean()
std = dataframe["close"].rolling(bb_period).std()
dataframe["bb_lower"] = sma - bb_std * std
dataframe["bb_mid"] = sma
dataframe["donchian_high_48"] = dataframe["high"].rolling(48).max().shift(1)
dataframe["sma50"] = ta.SMA(dataframe, timeperiod=50)
dataframe["volume_sma20"] = dataframe["volume"].rolling(20).mean()
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata.get("pair", "")
if pair == "BNB/USDT":
# BNB-RSI mechanism:
dataframe.loc[
(dataframe["rsi"] < 25)
& (dataframe["close"] > dataframe["ema200_1d"] * 0.85),
"enter_long",
] = 1
elif pair in ("SOL/USDT", "AVAX/USDT"):
# Alts BB-lower MR with full regime gate.
dataframe.loc[
(dataframe["close"] < dataframe["bb_lower"])
& (dataframe["rsi"] < 35)
& (dataframe["ema200_slope_up_1d"] == 1)
& (dataframe["close"] > dataframe["ema200_1d"])
& (dataframe["ema50_4h"] > dataframe["ema200_4h"]),
"enter_long",
] = 1
elif pair in ("BTC/USDT", "ETH/USDT"):
# r18: majors Donchian-48 sustained break + 4h macro + volume.
# Different paradigm (breakout, not MR) for majors which have
# smaller MR opportunity space. Strategy now paradigm-conditional.
prior_above = dataframe["close"].shift(1) > dataframe["donchian_high_48"].shift(1)
dataframe.loc[
(dataframe["close"] > dataframe["donchian_high_48"])
& prior_above
& (dataframe["ema50_4h"] > dataframe["ema200_4h"])
& (dataframe["ema200_slope_up_1d"] == 1)
& (dataframe["volume"] > 1.3 * dataframe["volume_sma20"]),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
pair = metadata.get("pair", "")
if pair == "BNB/USDT":
dataframe.loc[dataframe["rsi"] > 55, "exit_long"] = 1
elif pair in ("SOL/USDT", "AVAX/USDT"):
dataframe.loc[dataframe["close"] > dataframe["bb_mid"], "exit_long"] = 1
elif pair in ("BTC/USDT", "ETH/USDT"):
# Majors patient SMA50 exit (paradigm-specific from
# AltsBollBreak r1 finding — breakouts ride trends).
dataframe.loc[dataframe["close"] < dataframe["sma50"], "exit_long"] = 1
return dataframe
# r22: removed custom_stake_amount entirely. r20+r21 transfer
# experiment finding: BNB sizing doesn't compose linearly inside
# multi-pair strategy. Plain 25/RSI: robust 0.052→0.038 (worse).
# Regime-aware (slope_up gate): robust 0.052→0.044 (better than
# plain but still worse than baseline). Net: sizing is best applied
# at strategy isolation (single-pair) rather than embedded in
# multi-pair containers. Cross-pair attribution is meaningfully
# different from single-pair attribution.