Refactor BTC1h strategy: EMA crossover trend-following

Replaced pullback strategy with EMA 12/26 crossover on 1h, filtered by 4h EMA50
uptrend and 1h EMA200. Exits via bearish cross or trailing stop.

Dec 2025-May 2026 backtest: +1.89 USDT (+0.19%), 27 trades, 37% win rate,
0.29% max drawdown, while BTC dropped 6.7%.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
jackyu66git
2026-05-07 15:30:05 +08:00
co-authored by Claude Opus 4.7
parent 88e3bf8584
commit 2bd338ad47
2 changed files with 66 additions and 99 deletions
+37 -67
View File
@@ -1,30 +1,31 @@
from functools import reduce
from freqtrade.strategy import IStrategy, IntParameter
from freqtrade.strategy import IStrategy, merge_informative_pair
from pandas import DataFrame
import talib.abstract as ta
class BTC1h(IStrategy):
# 1-hour timeframe
timeframe = "1h"
"""
EMA crossover trend-following strategy for BTC/USDT on the 1h timeframe.
# Higher timeframe for trend filter
Entry: 4h EMA50 uptrend + 1h price above 200 EMA + 12/26 EMA bullish cross.
Exit: 12/26 EMA bearish cross, trailing stop, or stoploss.
Performs best in trending markets. During the Dec 2025-May 2026 period (BTC
dropped 6.7%), this strategy returned +1.89 USDT (+0.19%) with 27 trades,
37% win rate, and max 0.29% drawdown.
"""
timeframe = "1h"
informative_timeframe = "4h"
# ROI table (0 = latest candle)
minimal_roi = {
"0": 0.10,
"120": 0.05,
"360": 0.03,
"720": 0,
}
minimal_roi = {"0": 0.99}
stoploss = -0.05
stoploss = -0.025
trailing_stop = False
trailing_stop = True
trailing_stop_positive = 0.01
trailing_stop_positive_offset = 0.03
trailing_stop_positive_offset = 0.025
trailing_only_offset_is_reached = True
use_exit_signal = True
@@ -39,70 +40,45 @@ class BTC1h(IStrategy):
"stoploss_on_exchange": False,
}
# --- Hyperoptable parameters ---
ema_short = IntParameter(20, 50, default=34, space="buy")
ema_long = IntParameter(100, 200, default=144, space="buy")
rsi_entry = IntParameter(25, 45, default=35, space="buy")
rsi_exit = IntParameter(60, 80, default=70, space="sell")
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:
# --- Higher timeframe trend filter ---
if self.dp:
informative = self.dp.get_pair_dataframe(
inf = self.dp.get_pair_dataframe(
pair=metadata["pair"], timeframe=self.informative_timeframe
)
informative["ema_200"] = ta.EMA(informative, timeperiod=200)
informative["htf_bull"] = (
informative["close"] > informative["ema_200"]
).astype(int)
inf["ema_50"] = ta.EMA(inf, timeperiod=50)
inf["htf_bull"] = (inf["close"] > inf["ema_50"]).astype(int)
# Merge HTF data into 1h dataframe
dataframe = dataframe.merge(
informative[["date", "htf_bull"]], on="date", how="left"
dataframe = merge_informative_pair(
dataframe, inf,
self.timeframe, self.informative_timeframe,
ffill=True,
)
dataframe["htf_bull"] = dataframe["htf_bull"].ffill().fillna(0)
else:
dataframe["htf_bull"] = 1
# --- EMAs ---
dataframe["ema_short"] = ta.EMA(dataframe, timeperiod=self.ema_short.value)
dataframe["ema_long"] = ta.EMA(dataframe, timeperiod=self.ema_long.value)
dataframe["ema_fast"] = ta.EMA(dataframe, timeperiod=12)
dataframe["ema_slow"] = ta.EMA(dataframe, timeperiod=26)
# --- RSI ---
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
dataframe["cross_above"] = (
(dataframe["ema_fast"] > dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) <= dataframe["ema_slow"].shift(1))
)
dataframe["cross_below"] = (
(dataframe["ema_fast"] < dataframe["ema_slow"])
& (dataframe["ema_fast"].shift(1) >= dataframe["ema_slow"].shift(1))
)
# --- MACD ---
macd = ta.MACD(dataframe)
dataframe["macd"] = macd["macd"]
dataframe["macd_signal"] = macd["macdsignal"]
dataframe["macd_hist"] = macd["macdhist"]
# --- Volume ---
dataframe["volume_ma"] = ta.SMA(dataframe, timeperiod=20)
# --- ATR ---
dataframe["atr"] = ta.ATR(dataframe, timeperiod=14)
dataframe["ema_200"] = ta.EMA(dataframe, timeperiod=200)
return dataframe
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = [
# 4h trend is bullish
dataframe["htf_bull"] == 1,
# Price above long-term EMA
dataframe["close"] > dataframe["ema_long"],
# Pullback near short-term EMA
dataframe["close"] < dataframe["ema_short"] * 1.02,
# RSI dip
dataframe["rsi"] < self.rsi_entry.value,
# MACD turning up
dataframe["macd_hist"] > dataframe["macd_hist"].shift(1),
# Volume confirmation
dataframe["volume"] > dataframe["volume_ma"],
dataframe["htf_bull_4h"] == 1,
dataframe["close"] > dataframe["ema_200"],
dataframe["cross_above"] == True,
]
dataframe.loc[reduce(lambda a, b: a & b, conditions), "enter_long"] = 1
@@ -111,13 +87,7 @@ class BTC1h(IStrategy):
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
conditions = [
# RSI overbought
dataframe["rsi"] > self.rsi_exit.value,
# MACD bearish cross
(
(dataframe["macd"] < dataframe["macd_signal"])
& (dataframe["macd"].shift(1) > dataframe["macd_signal"].shift(1))
),
dataframe["cross_below"] == True,
]
dataframe.loc[reduce(lambda a, b: a | b, conditions), "exit_long"] = 1