ETH 1H Quality Strategy — trend-following with momentum confirmation.
Timeframe
1h
Direction
Long Only
Stoploss
-10.0%
Trailing Stop
No
ROI
0m: 10000.0%, 60m: 3.0%, 120m: 1.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
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter, BooleanParameter
import pandas as pd
import talib.abstract as ta
from datetime import datetime
class Eth1hQualityStrategy(IStrategy):
"""
ETH 1H Quality Strategy — trend-following with momentum confirmation.
Entry: price above EMA + RSI recovering from oversold zone.
Exit: at least 2 of 3 conditions (EMA break, RSI overbought, MACD bearish).
Stop: ATR-based dynamic stop loss, tightened after profit.
Designed for 30-365 day backtesting with 10-50 trades per month.
All tunable parameters use IntParameter/DecimalParameter for hyperopt.
"""
INTERFACE_VERSION = 3
timeframe = '1h'
max_open_trades = 1
stake_amount = 'unlimited'
stoploss = -0.10
trailing_stop = False
minimal_roi = {
'120': 0.01,
'60': 0.03,
'0': 100,
}
# -- Trend (buy) --
trend_ema_period = IntParameter(20, 50, default=30, space='buy')
# -- RSI (buy / sell) --
rsi_period = IntParameter(10, 20, default=14, space='buy')
rsi_oversold = IntParameter(30, 45, default=38, space='buy')
rsi_overbought = IntParameter(60, 80, default=70, space='sell')
# -- Volume filter (buy, off by default to keep trade frequency up) --
volume_ma_period = IntParameter(15, 30, default=20, space='buy')
volume_multiplier = DecimalParameter(1.2, 2.0, default=1.5, decimals=1, space='buy')
use_volume_filter = BooleanParameter(default=False, space='buy')
# -- Exit toggles (sell) --
use_ema_exit = BooleanParameter(default=True, space='sell')
use_rsi_exit = BooleanParameter(default=True, space='sell')
use_macd_exit = BooleanParameter(default=False, space='sell')
# -- ATR stop multiplier (in 'buy' space since 'stoploss' only handles built-in stoploss) --
atr_multiplier = DecimalParameter(1.5, 3.0, default=2.0, decimals=1, space='buy')
ATR_PERIOD = 14
# -----------------------------------------------------------------
# Indicators
# -----------------------------------------------------------------
def populate_indicators(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe['atr'] = ta.ATR(dataframe, timeperiod=self.ATR_PERIOD)
for p in self.trend_ema_period.range:
dataframe[f'ema_{p}'] = ta.EMA(dataframe, timeperiod=p)
for p in self.rsi_period.range:
dataframe[f'rsi_{p}'] = ta.RSI(dataframe, timeperiod=p)
macd = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9)
dataframe['macd'] = macd[0]
dataframe['macd_signal'] = macd[1]
for p in self.volume_ma_period.range:
dataframe[f'vol_ma_{p}'] = ta.SMA(dataframe['volume'], timeperiod=p)
return dataframe
# -----------------------------------------------------------------
# Entry
# -----------------------------------------------------------------
def populate_entry_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe.loc[:, 'enter_long'] = 0
ema_p = self.trend_ema_period.value
rsi_p = self.rsi_period.value
rsi_t = self.rsi_oversold.value
rsi = dataframe[f'rsi_{rsi_p}']
above_ema = dataframe['close'] > dataframe[f'ema_{ema_p}']
rsi_was_low = rsi.shift(1) < rsi_t
rsi_recovering = (rsi > rsi.shift(1)) & (rsi.shift(1) < rsi_t + 5)
entry = above_ema & rsi_was_low & rsi_recovering
if self.use_volume_filter.value:
vol_p = self.volume_ma_period.value
vol_m = self.volume_multiplier.value
entry &= dataframe['volume'] > dataframe[f'vol_ma_{vol_p}'] * vol_m
dataframe.loc[entry, 'enter_long'] = 1
return dataframe
# -----------------------------------------------------------------
# Exit
# -----------------------------------------------------------------
def populate_exit_trend(self, dataframe: pd.DataFrame, metadata: dict) -> pd.DataFrame:
dataframe.loc[:, 'exit_long'] = 0
ema_p = self.trend_ema_period.value
rsi_p = self.rsi_period.value
rsi_t = self.rsi_overbought.value
signals = []
if self.use_ema_exit.value:
signals.append(dataframe['close'] < dataframe[f'ema_{ema_p}'])
if self.use_rsi_exit.value:
signals.append(dataframe[f'rsi_{rsi_p}'] > rsi_t)
if self.use_macd_exit.value:
signals.append(dataframe['macd'] < dataframe['macd_signal'])
if len(signals) >= 2:
hits = signals[0].astype(int)
for s in signals[1:]:
hits = hits + s.astype(int)
dataframe.loc[hits >= 2, 'exit_long'] = 1
elif len(signals) == 1:
dataframe.loc[signals[0], 'exit_long'] = 1
return dataframe
# -----------------------------------------------------------------
# Dynamic stop loss (ATR-based)
# -----------------------------------------------------------------
def custom_stoploss(self, pair: str, trade, current_time: datetime,
current_rate: float, current_profit: float, **kwargs) -> float:
if current_profit > 0.02:
return -0.015
if self.dp:
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
if not df.empty:
atr = df.iloc[-1]['atr']
price = df.iloc[-1]['close']
if price > 0 and atr > 0:
pct = (atr * self.atr_multiplier.value) / price
return -min(pct, 0.08)
return -0.03