Timeframe
15m
Direction
Long Only
Stoploss
-12.0%
Trailing Stop
No
ROI
0m: 1.0%, 60m: 2.0%, 180m: 3.0%
Interface Version
N/A
Startup Candles
200
Indicators
5
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 GridLikeStrategy(IStrategy):
can_short: bool = True
timeframe = '15m'
# Mean-reversion ROI with small targets per layer
minimal_roi = {
"0": 0.01,
"60": 0.02,
"180": 0.03,
}
stoploss = -0.12
# Use custom ATR-based stoploss to limit one-way trend risk
use_custom_stoploss = True
trailing_stop = False
# Allow limited position adjustments (DCA) to simulate grid layering
position_adjustment_enable = True
# Max additional entries after the initial one (e.g., 2 extra layers)
max_entry_position_adjustment = 2
startup_candle_count = 200
# --- Hyperoptable parameters to enable default buy/sell spaces ---
buy_adx_max = IntParameter(20, 35, default=25, space='buy')
buy_ema200_ratio = DecimalParameter(0.90, 1.00, decimals=3, default=0.92, space='buy')
sell_bb_mid_offset = DecimalParameter(0.0, 0.02, decimals=3, default=0.0, space='sell')
sell_adx_min = IntParameter(25, 40, default=30, space='sell')
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Bollinger bands as core grid bands
bb_upper, bb_mid, bb_lower = ta.BBANDS(
dataframe['close'], timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0
)
dataframe['bb_upper'] = bb_upper
dataframe['bb_mid'] = bb_mid
dataframe['bb_lower'] = bb_lower
# Volatility for dynamic grid steps and stoploss
dataframe['atr'] = ta.ATR(dataframe, timeperiod=14)
# Trend guards to avoid heavy DCA against strong trends
dataframe['ema200'] = ta.EMA(dataframe, timeperiod=200)
dataframe['adx'] = ta.ADX(dataframe, timeperiod=14)
dataframe['rsi'] = ta.RSI(dataframe, timeperiod=14)
# Volume sanity
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.12
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 _grid_long_conditions(self, df: DataFrame) -> DataFrame:
# Grid anchoring: below mid and below lower band are potential buy zones
# Add ATR-stepped layers below lower band
layer1 = (df['close'] <= df['bb_lower'])
layer2 = (df['close'] <= (df['bb_lower'] - 0.5 * df['atr']))
layer3 = (df['close'] <= (df['bb_lower'] - 1.0 * df['atr']))
# Trend/vol filters: avoid strong downtrends (adx high and price << ema200)
guard = (
(df['close'] >= float(self.buy_ema200_ratio.value) * df['ema200']) |
(df['adx'] < int(self.buy_adx_max.value))
)
vol_ok = (df['volume'] > 0) & (df['volume'] > df['volume_mean_slow'])
cond = (df['close'] < df['bb_mid']) & (layer1 | layer2 | layer3) & guard & vol_ok
return cond
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Base long entry on grid condition
dataframe.loc[
self._grid_long_conditions(dataframe),
'enter_long'
] = 1
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
# Mean reversion exits near/above midline or upper band
exit_mean = (dataframe['close'] >= dataframe['bb_mid'] * (1 + float(self.sell_bb_mid_offset.value)))
exit_strong = (dataframe['close'] >= dataframe['bb_upper'])
# Emergency: trend flips down hard
emergency = (
(dataframe['adx'] > int(self.sell_adx_min.value)) & (dataframe['close'] < 0.98 * dataframe['ema200'])
)
dataframe.loc[(exit_mean | exit_strong | emergency), 'exit_long'] = 1
return dataframe
# --- DCA settings ---
# Freqtrade uses `position_adjustment_enable`/`max_entry_position_adjustment`.
# Optional: You can define `custom_entry_position_adjustment` for variable DCA sizing.
# --- ATR-based custom stoploss to limit one-way trend blowups ---
atr_sl_multiplier: float = 2.2
def custom_stoploss(
self,
pair: str,
trade, # type: ignore[override]
current_time,
current_rate: float,
current_profit: float,
after_fill: bool,
**kwargs,
) -> float | None:
try:
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
last = df.iloc[-1].squeeze()
atr = float(last.get('atr', 0.0))
close = float(last.get('close', 0.0))
ema200 = float(last.get('ema200', 0.0))
adx = float(last.get('adx', 0.0))
except Exception:
return None
if atr <= 0 or close <= 0:
return None
# Base ATR stop
stop_price = close - self.atr_sl_multiplier * atr
# If market turns into strong downtrend, tighten further toward EMA200 threshold
if adx >= 30 and close < ema200:
# raise stop closer (reduce distance)
stop_price = max(stop_price, 0.98 * ema200)
if stop_price >= current_rate:
return None
distance = abs((current_rate - stop_price) / current_rate)
return float(distance) if distance > 0 else None