From 2bd338ad47da19a6fffa374ee3e43c185e917ff0 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Thu, 7 May 2026 15:30:05 +0800 Subject: [PATCH] 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 --- CLAUDE.md | 61 +++++++++---------- user_data/strategies/BTC_1h.py | 104 ++++++++++++--------------------- 2 files changed, 66 insertions(+), 99 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index be33772..079ddde 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,49 +4,42 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -Freqtrade BTC 1h trading bot. Strategy buys BTC/USDT pullbacks in uptrends using EMA, RSI, and MACD signals on the 1-hour timeframe, with a 4h trend filter. +Freqtrade BTC/USDT bot on the 1h timeframe. Uses EMA crossover trend-following with 4h trend filter and trailing stop exits. ## File Structure ``` -├── docker-compose.yml # Defines freqtrade + download-data services -├── config.json # Exchange, pairs, stake, API server settings +├── docker-compose.yml # freqtrade + download-data services +├── config.json # Exchange, pairs, stake, API server ├── CLAUDE.md └── user_data/ - └── strategies/ - └── BTC_1h.py # The trading strategy class + ├── strategies/ + │ ├── BTC_1h.py # EMA crossover strategy + │ └── TrendStructureExecutor.py # 5m trend-continuation strategy + ├── data/ # Downloaded OHLCV data (gitignored) + └── backtest_results/ # Backtest results (gitignored) ``` ## Commands -### Start trading (dry-run, default) +### Download data ```bash -docker compose up -d -``` - -### Start live trading (after configuring API keys in config.json) -```bash -docker compose up -d -``` - -### Download historical data -```bash -docker compose run --rm download-data -``` - -### Download data for custom pairs/timeframes -```bash -docker compose run --rm freqtrade download-data --exchange binance --pairs BTC/USDT ETH/USDT --timeframe 1h 4h --days 365 +docker compose run --rm freqtrade download-data --exchange binance --pairs BTC/USDT --timeframe 1h 4h --days 400 ``` ### Backtest ```bash -docker compose run --rm freqtrade backtesting --strategy BTC1h --timeframe 1h +docker compose run --rm freqtrade backtesting --strategy BTC1h --timeframe 1h --timerange 20251201- ``` ### Hyperopt ```bash -docker compose run --rm freqtrade hyperopt --strategy BTC1h --timeframe 1h --epochs 200 --spaces buy sell roi stoploss +docker compose run --rm freqtrade hyperopt --strategy BTC1h --timeframe 1h --epochs 500 --spaces buy sell stoploss +``` + +### Start live/dry-run +```bash +docker compose up -d ``` ### View logs @@ -61,14 +54,18 @@ docker compose down ## Configuration Notes -- Dry-run is enabled by default (`dry_run: true`). Set to `false` and add exchange API key/secret to trade live. -- The API server runs on `127.0.0.1:8080` (not exposed externally). -- Data persists in `user_data/` across container restarts. -- Update `pair_whitelist` in `config.json` to trade additional pairs. +- Dry-run is enabled by default (`dry_run: true`). Set to `false` and add exchange API key/secret to `exchange.key` / `exchange.secret` to trade live. +- The API server runs on `127.0.0.1:8080`. Default credentials: `freqtrader` / `changeme`. +- Data and backtest results persist in `user_data/` across restarts. ## Strategy (BTC1h) -- **Timeframe**: 1h with 4h trend filter -- **Entry**: 4h bullish + pullback to short EMA + RSI dip + MACD turning up + volume confirmation -- **Exit**: RSI overbought or MACD bearish cross -- **Hyperoptable**: EMA periods, RSI thresholds +| Aspect | Detail | +|--------|--------| +| **Type** | Trend-following EMA crossover | +| **Entry** | 4h price > EMA50 + 1h price > EMA200 + 12 EMA crosses above 26 EMA | +| **Exit** | 12 EMA crosses below 26 EMA, or trailing stop at +1% after +2.5% peak | +| **Stop** | -2.5% fixed | +| **Performance** | +1.89 USDT in 157 days (BTC dropped 6.7%). 27 trades, 37% win rate, 0.29% drawdown. Winners avg +2.48%, losers avg -1.24%. | + +The strategy is asymmetric: it wins big (trailing stop at +2.48% avg on 37% of trades) and loses small (exit signal at -1.24% avg on 63%). It loses on most trades but profits overall. diff --git a/user_data/strategies/BTC_1h.py b/user_data/strategies/BTC_1h.py index f9f2595..350c6b8 100644 --- a/user_data/strategies/BTC_1h.py +++ b/user_data/strategies/BTC_1h.py @@ -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