Timeframe
4h
Direction
Long Only
Stoploss
-15.0%
Trailing Stop
No
ROI
0m: 15.0%
Interface Version
N/A
Startup Candles
210
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
from freqtrade.strategy.interface import IStrategy
from freqtrade.strategy import IntParameter, DecimalParameter
from pandas import DataFrame
import talib.abstract as ta
class SwingTradingStrategy(IStrategy):
can_short: bool = True
timeframe = '4h'
# Swing targets: larger moves and longer holds
minimal_roi = {
"0": 0.15,
}
stoploss = -0.15 # Hard floor – custom ATR stoploss will trail above this
# We'll use a custom ATR-based stop instead of classical trailing stop
use_custom_stoploss = True
trailing_stop = False
# Ensure enough candles for EMA200 and ATR(14)
startup_candle_count = 210
process_only_new_candles = True
# --- Hyperoptable parameters to enable default buy/sell spaces ---
buy_rsi_max = IntParameter(55, 80, default=70, space='buy')
buy_macd_hist_min = DecimalParameter(0.0, 0.010, decimals=3, default=0.000, space='buy')
sell_rsi_min = IntParameter(60, 90, default=80, space='sell')
sell_macd_hist_min = DecimalParameter(0.0, 0.010, decimals=3, default=0.000, space='sell')
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# MACD
macd = ta.MACD(dataframe)
dataframe['macd'] = macd['macd']
dataframe['macdsignal'] = macd['macdsignal']
dataframe['macdhist'] = macd['macdhist']
# Trend filter – avoid counter-trend swings
dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
# Momentum / sanity filter (optional but helpful)
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
# ATR for volatility-aware stop management
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
# Volume filter helpers
dataframe['volume_mean_slow'] = dataframe['volume'].rolling(30).mean()
return dataframe
def leverage(self, pair, current_time, current_rate, proposed_leverage, max_leverage, entry_tag, side, **kwargs) -> float:
stop = abs(float(self.stoploss)) if getattr(self, "stoploss", None) is not None else 0.15
base = 0.05 / stop if stop > 0 else (proposed_leverage or 1.0)
base = max(1.0, min(float(base), float(max_leverage)))
return base
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Golden cross detection
macd_cross_up = (
(dataframe['macd'] > dataframe['macdsignal']) &
(dataframe['macd'].shift(1) <= dataframe['macdsignal'].shift(1))
)
# MACD histogram threshold (momentum confirmation)
macd_hist_ok = (dataframe['macdhist'] > float(self.buy_macd_hist_min.value))
dataframe.loc[
(
macd_cross_up &
(dataframe['close'] > dataframe['ema200']) & # trend filter
(dataframe['rsi'] < int(self.buy_rsi_max.value)) & # avoid buying into extreme overbought
macd_hist_ok &
(dataframe['volume'] > 0) &
(dataframe['volume'] > dataframe['volume_mean_slow'])
),
'enter_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Dead cross detection – primary exit condition
macd_cross_down = (
(dataframe['macd'] < dataframe['macdsignal']) &
(dataframe['macd'].shift(1) >= dataframe['macdsignal'].shift(1))
)
# MACD histogram negative enough (loss of momentum)
macd_hist_weak = ((dataframe['macdsignal'] - dataframe['macd']) > float(self.sell_macd_hist_min.value))
dataframe.loc[
(
macd_cross_down |
# Safety: loss of momentum or trend break
(dataframe['rsi'] > int(self.sell_rsi_min.value)) |
macd_hist_weak |
(dataframe['close'] < dataframe['ema200'])
),
'exit_long'
] = 1
return dataframe
# --- Custom ATR-based stoploss ---
# Define a modest ATR multiplier for swings – can be hyperoptimized later
atr_multiplier: float = 2.0
def custom_stoploss(
self,
pair: str,
trade, # type: ignore[override]
current_time,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float | None:
"""
ATR-based dynamic stoploss.
For long trades: stop_price = close - atr_multiplier * ATR(14).
Convert absolute stop to distance ratio relative to current_rate.
Returning None leaves current stop unchanged.
"""
try:
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last = dataframe.iloc[-1].squeeze()
atr = float(last.get('atr', 0.0))
close = float(last.get('close', 0.0))
except Exception:
return None
if atr <= 0 or close <= 0:
return None
# Long-only logic – for futures shorting, mirror the formula accordingly
stop_price = close - self.atr_multiplier * atr
# If stop would be above current price due to extreme spikes, keep unchanged
if stop_price >= current_rate:
return None
# Convert absolute stop to relative distance (sign ignored by framework)
distance = abs((current_rate - stop_price) / current_rate)
# Keep within hard floor enforced by self.stoploss (framework will cap it)
return float(distance) if distance > 0 else None