Timeframe
15m
Direction
Long Only
Stoploss
-5.0%
Trailing Stop
Yes
ROI
0m: 5.0%, 120m: 2.5%, 480m: 1.2%, 1440m: 0.5%
Interface Version
3
Startup Candles
200
Indicators
4
freqtrade/freqtrade-strategies
Strategy 003 author@: Gerald Lonlas github@: https://github.com/freqtrade/freqtrade-strategies
from pandas import DataFrame
import talib.abstract as ta
from freqtrade.strategy import IStrategy, merge_informative_pair
class HalalSpotStrategy(IStrategy):
INTERFACE_VERSION = 3
timeframe = "15m"
# Higher timeframe used to confirm the broader trend before entering.
informative_timeframe = "4h"
can_short = False
minimal_roi = {
"0": 0.05, # exit at +5% immediately
"120": 0.025, # after 2h, exit at +2.5%
"480": 0.012, # after 8h, exit at +1.2%
"1440": 0.005, # after 24h, exit at +0.5%
}
stoploss = -0.05
# Trailing stop activates after a meaningful move and then protects profit.
trailing_stop = True
trailing_stop_positive = 0.015
trailing_stop_positive_offset = 0.03
trailing_only_offset_is_reached = True
process_only_new_candles = True
startup_candle_count = 200
# Protections guard against whipsaw re-entries and drawdown clusters.
@property
def protections(self):
return [
{
"method": "CooldownPeriod",
"stop_duration_candles": 4,
},
{
"method": "StoplossGuard",
"lookback_period_candles": 24,
"trade_limit": 2,
"stop_duration_candles": 12,
"only_per_pair": True,
},
{
"method": "MaxDrawdown",
"lookback_period_candles": 48,
"trade_limit": 4,
"stop_duration_candles": 12,
"max_allowed_drawdown": 0.1,
},
]
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:
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["sma_fast"] = ta.SMA(dataframe, timeperiod=20)
dataframe["sma_slow"] = ta.SMA(dataframe, timeperiod=50)
dataframe["ema_trend"] = ta.EMA(dataframe, timeperiod=200)
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
dataframe["atr_ratio"] = dataframe["atr"] / dataframe["close"]
dataframe["volume_mean"] = dataframe["volume"].rolling(20).mean()
dataframe["volume_ratio"] = dataframe["volume"] / dataframe["volume_mean"]
# Higher-timeframe trend filter keeps entries aligned with the broader move.
informative = self.dp.get_pair_dataframe(
pair=metadata["pair"], timeframe=self.informative_timeframe
)
informative["sma_trend"] = ta.SMA(informative, timeperiod=50)
informative["sma_trend_slope"] = informative["sma_trend"] - informative[
"sma_trend"
].shift(3)
dataframe = merge_informative_pair(
dataframe, informative, self.timeframe, self.informative_timeframe, ffill=True
)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
htf_close = f"close_{self.informative_timeframe}"
htf_trend = f"sma_trend_{self.informative_timeframe}"
htf_trend_slope = f"sma_trend_slope_{self.informative_timeframe}"
dataframe.loc[
(
(dataframe["rsi"] > 32)
& (dataframe["rsi"].shift(1) <= 32)
& (dataframe["sma_fast"] > dataframe["sma_slow"])
& (dataframe["close"] > dataframe["ema_trend"])
# Only buy recovering dips while the 4h trend is up.
& (dataframe[htf_close] > dataframe[htf_trend])
& (dataframe[htf_trend_slope] > 0)
& (dataframe["close"] > dataframe["open"])
& (dataframe["volume_ratio"] > 1.0)
& (dataframe["atr_ratio"] > 0.002)
& (dataframe["atr_ratio"] < 0.06)
& (dataframe["volume"] > 0)
),
"enter_long",
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe.loc[
(
(dataframe["rsi"] > 70)
| (dataframe["sma_fast"] < dataframe["sma_slow"])
| (dataframe["close"] < dataframe["ema_trend"])
),
"exit_long",
] = 1
return dataframe