Timeframe
15m
Direction
Long & Short
Stoploss
-9.0%
Trailing Stop
Yes
ROI
0m: 8.5%, 45m: 4.5%, 90m: 2.0%, 180m: 0.0%
Interface Version
3
Startup Candles
500
Indicators
5
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
"""
RegimeSwitchingHybrid_v6_1_Fett
Fett ueberarbeitete Hybrid-Strategie – Phase 20
Bessere Exits, fixter Custom Stoploss, echtes Trailing, Short-Logik, besseres Risk:Reward
Timeframe: 15m | HTF: 1h
Fixes vs. Original-Entwurf:
- Klassenname: Punkt durch Unterstrich ersetzt (Python-Syntax)
- **kwargs in confirm_trade_entry und custom_stoploss (Freqtrade-Contract)
- merge_informative_pair Import hinzugefuegt
- enter_tag Ueberschreibungs-Bug behoben (Priorisierung: trend vor range)
- trailing_stop auf True gesetzt (Frequtrade-native, ergaenzt custom_stoploss)
"""
import logging
import sys
from datetime import datetime, timedelta
from typing import Optional
import talib.abstract as ta
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, merge_informative_pair
import freqtrade.vendor.qtpylib.indicators as qtpylib
from pandas import DataFrame
sys.path.insert(0, "/freqtrade/shared")
from primo_signal import primo_gate_allows
from fleetguard_v1 import FleetGuard, FleetGuardConfig
logger = logging.getLogger(__name__)
class RegimeSwitchingHybrid_v6_1_Fett(IStrategy):
INTERFACE_VERSION = 3
timeframe = "15m"
informative_timeframe = "1h"
can_short = True
# Aggressiveres ROI fuer besseres RR
minimal_roi = {
"0": 0.085,
"45": 0.045,
"90": 0.02,
"180": 0,
}
# Failsafe stoploss — custom_stoploss() ist die eigentliche SL-Logik.
# 9% Notaus, falls custom_stoploss() versagt.
stoploss = -0.09
use_custom_stoploss = True
trailing_stop = True
trailing_stop_positive = 0.01
trailing_stop_positive_offset = 0.02
trailing_only_offset_is_reached = True
startup_candle_count = 500
# ---- FleetGuard v1 entry safety (conservative limits) ----
_fleetguard = FleetGuard(FleetGuardConfig(
max_open_trades=3,
max_open_shorts=2,
max_open_longs=2,
))
@property
def protections(self):
return [
{"method": "CooldownPeriod", "stop_duration_candles": 5},
{"method": "StoplossGuard", "lookback_period_candles": 60,
"trade_limit": 3, "stop_duration_candles": 60,
"only_per_pair": False, "only_per_side": True},
{"method": "MaxDrawdown", "lookback_period_candles": 480,
"trade_limit": 20, "stop_duration_candles": 96,
"max_allowed_drawdown": 0.06},
{"method": "LowProfitPairs", "lookback_period_candles": 1440,
"trade_limit": 2, "stop_duration_candles": 60,
"required_profit": -0.01},
]
# Hyperoptable parameters (Buy space)
adx_rel_threshold = DecimalParameter(0.8, 1.4, default=1.0, space="buy")
rsi_oversold = IntParameter(20, 40, default=25, space="buy")
# Fixed parameters
rsi_overbought = 68
# ATR Multipliers (Sell space) — enger als v6 fuer besseres RR
atr_sl_trend = 3.5
atr_sl_range = DecimalParameter(2.0, 4.0, default=2.8, space="sell", optimize=True)
atr_tp_trend = DecimalParameter(1.0, 2.5, default=1.8, space="sell", optimize=True)
def informative_pairs(self):
pairs = self.dp.current_whitelist()
return [(pair, self.informative_timeframe) for pair in pairs]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
if not self.dp:
return dataframe
informative = self.dp.get_pair_dataframe(
pair=metadata['pair'], timeframe=self.informative_timeframe
)
informative['ema200'] = ta.EMA(informative, timeperiod=200)
informative['adx'] = ta.ADX(informative)
informative['rsi'] = ta.RSI(informative)
dataframe = merge_informative_pair(
dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True
)
# Local indicators
dataframe['adx'] = ta.ADX(dataframe)
dataframe['adx_sma'] = dataframe['adx'].rolling(window=50).mean()
dataframe['adx_rel'] = dataframe['adx'] / dataframe['adx_sma']
dataframe['rsi'] = ta.RSI(dataframe)
dataframe['ema50'] = ta.EMA(dataframe, timeperiod=50)
dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
# ATR calculation for dynamic stops
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
dataframe['atr_pct'] = dataframe['atr'] / dataframe['close']
bollinger = qtpylib.bollinger_bands(
qtpylib.typical_price(dataframe), window=20, stds=2
)
dataframe['bb_lowerband'] = bollinger['lower']
dataframe['bb_middleband'] = bollinger['mid']
dataframe['bb_upperband'] = bollinger['upper']
dataframe['volume_mean'] = dataframe['volume'].rolling(window=30).mean()
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
ema200_htf = dataframe[f'ema200_{self.informative_timeframe}']
pair = metadata.get("pair")
long_gate = primo_gate_allows(pair, "long")
short_gate = primo_gate_allows(pair, "short")
# --- LONG ENTRIES ---
# Trend Regime Pullback (Long)
trend_long = (
(dataframe['adx_rel'] > self.adx_rel_threshold.value) &
(dataframe['close'] > ema200_htf) &
(dataframe['close'] > dataframe['ema200']) &
(dataframe['close'] < dataframe['ema50']) &
(dataframe['rsi'] < 50) &
(dataframe['volume'] > dataframe['volume_mean']) &
long_gate
)
# Range Regime Reversion (Long)
range_long = (
(dataframe['adx_rel'] <= self.adx_rel_threshold.value) &
(dataframe['rsi'] < self.rsi_oversold.value) &
(dataframe['close'] < dataframe['bb_lowerband']) &
(dataframe['volume'] > dataframe['volume_mean']) &
long_gate
)
# Combine long entries — trend prioritized over range via mask
long_entries = trend_long | range_long
dataframe.loc[long_entries, 'enter_long'] = 1
# enter_tag: trend wins when both match (set range first, then overwrite with trend)
dataframe.loc[long_entries, 'enter_tag'] = 'range_reversion_long'
dataframe.loc[trend_long, 'enter_tag'] = 'trend_pullback_long'
# --- SHORT ENTRIES ---
# Trend Regime Pullback (Short)
trend_short = (
(dataframe['adx_rel'] > self.adx_rel_threshold.value) &
(dataframe['close'] < ema200_htf) &
(dataframe['close'] < dataframe['ema200']) &
(dataframe['close'] > dataframe['ema50']) &
(dataframe['rsi'] > 50) &
(dataframe['volume'] > dataframe['volume_mean']) &
short_gate
)
# Range Regime Reversion (Short)
range_short = (
(dataframe['adx_rel'] <= self.adx_rel_threshold.value) &
(dataframe['rsi'] > 75) &
(dataframe['close'] > dataframe['bb_upperband']) &
(dataframe['volume'] > dataframe['volume_mean']) &
short_gate
)
# Combine short entries — trend prioritized over range via mask
short_entries = trend_short | range_short
dataframe.loc[short_entries, 'enter_short'] = 1
dataframe.loc[short_entries, 'enter_tag'] = 'range_reversion_short'
dataframe.loc[trend_short, 'enter_tag'] = 'trend_pullback_short'
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Long exits
dataframe.loc[
(dataframe['rsi'] > self.rsi_overbought) |
(dataframe['close'] > dataframe['bb_upperband']),
'exit_long'
] = 1
# Short exits
dataframe.loc[
(dataframe['rsi'] < 30) |
(dataframe['close'] < dataframe['bb_lowerband']),
'exit_short'
] = 1
return dataframe
def confirm_trade_entry(self, pair: str, order_type: str, amount: float,
rate: float, time_in_force: str, current_time: datetime,
entry_tag: Optional[str], side: str, **kwargs) -> bool:
"""FleetGuard entry safety check with real trade data."""
open_trades = []
recent_closed = []
current_drawdown = 0.0
try:
from freqtrade.persistence import Trade
for t in Trade.get_trades_proxy(is_open=True):
open_trades.append({"pair": t.pair, "is_short": t.is_short})
cutoff = current_time - timedelta(hours=24)
for t in Trade.get_trades_proxy(is_open=False):
if t.close_date and t.close_date >= cutoff:
recent_closed.append({
"pair": t.pair,
"is_short": t.is_short,
"close_profit": t.close_profit or 0.0,
})
total_profit = Trade.get_total_closed_profit()
starting_balance = (self.wallets.get_starting_balance()
if hasattr(self, 'wallets') and self.wallets else 1000.0)
if starting_balance > 0:
current_drawdown = abs(min(0, total_profit / starting_balance))
except Exception as e:
logger.warning(f"FleetGuard data gathering fallback: {e}")
try:
from freqtrade.persistence import Trade
for t in Trade.get_trades_proxy(is_open=True):
open_trades.append({"pair": t.pair, "is_short": t.is_short})
except Exception:
pass
allowed, reason = self._fleetguard.check_entry(
pair=pair, side=side, open_trades=open_trades,
recent_closed_trades=recent_closed, current_drawdown_pct=current_drawdown
)
if not allowed:
logger.info(f"FleetGuard REJECT: {pair} {side} — {reason}")
return False
return True
def custom_stoploss(self, pair: str, trade, current_time, current_rate,
current_profit: float, **kwargs) -> float:
"""ATR-based dynamic stoploss with regime-aware trailing."""
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if dataframe.empty:
return self.stoploss
last = dataframe.iloc[-1]
atr_pct = last['atr_pct']
adx_rel = last.get('adx_rel', 1.0)
is_trend = adx_rel > self.adx_rel_threshold.value
if is_trend:
sl_distance = atr_pct * self.atr_sl_trend
tp_trigger = atr_pct * self.atr_tp_trend.value
if current_profit > tp_trigger:
# Trailing: SL folgt dem Gewinn, aber nie weiter als sl_distance zurueck
return max(-sl_distance, current_profit - sl_distance)
return -sl_distance
else:
# Range-Regime: engerer Stop
sl_distance = atr_pct * self.atr_sl_range.value
return -sl_distance