# --- Do not remove these libs --- from freqtrade.strategy import IStrategy, stoploss_from_absolute import sys import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from chanlun import ChanLun from chanlun.core.ChanEnum import Chan_FX_TYPE, Chan_KLC_FX, Chan_BI_DIR # -------------------------------- from technical.util import resample_to_interval, resampled_merge import talib.abstract as ta from pandas import DataFrame from datetime import datetime, timedelta from freqtrade.persistence import Trade, Order from typing import Optional import logging logger = logging.getLogger(__name__) # freqtrade trade -c ./user_data/Chan/config/BTC_Perpetual_Futures.json --strategy BTC_Perpetual_Futures --strategy-path ./user_data/Chan/strategies # freqtrade backtesting -c ./user_data/Chan/config/BTC_Perpetual_Futures.json --strategy BTC_Perpetual_Futures --strategy-path ./user_data/Chan/strategies --timerange=20260101- # freqtrade download-data -c ./user_data/Chan/config/BTC_Perpetual_Futures.json -t 1m --pairs BTC/USDT:USDT --timerange=20260101- class BTC_Perpetual_Futures(IStrategy): """ BTC永续合约交易策略 - 优化版 - 基于缠论(ChanLun)技术分析 + RSI/MACD/布林带/ATR 多指标共振 - 支持做多和做空 - 基于ATR的动态止损 - 成交量确认过滤 """ INTERFACE_VERSION: int = 3 # ROI配��� - 分阶段止盈 minimal_roi = { "0": 0.08, # 立即: 8%止盈 "60": 0.05, # 1小时后: 5%止盈 "180": 0.02, # 3小时后: 2%止盈 "360": 0 # 6小时后: 保本出场 } can_short = True lev = 1.0 # 杠杆倍数,建议新手用1-3倍 stoploss = -0.04 # 默认4%止损(custom_stoploss会覆盖) use_custom_stoploss = True trailing_stop = False position_adjustment_enable = False startup_candle_count = 1440 # 需要1440根1分钟K线预热 # 时间框架常量 time5 = 5 time15 = 15 time30 = 30 time60 = 60 chan = ChanLun() def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """添加多时间框架技术指标""" # 重采样到5分钟和30分钟 dataframe_5m = resample_to_interval(dataframe, self.get_ticker_indicator() * self.time5) dataframe_30m = resample_to_interval(dataframe, self.get_ticker_indicator() * self.time30) # 添加技术指标 dataframe = self._add_indicators(dataframe) dataframe_5m = self._add_indicators(dataframe_5m) dataframe_30m = self._add_indicators(dataframe_30m) # 缠论状态分析 dataframe_5m['state'] = self.chan.get_klu_state(dataframe_5m) dataframe_30m['state'] = self.chan.get_klu_state(dataframe_30m) # 合并多时间框架数据 dataframe = resampled_merge(dataframe, dataframe_5m) dataframe = resampled_merge(dataframe, dataframe_30m) return dataframe def _add_indicators(self, df: DataFrame) -> DataFrame: """添加技术指标""" # MACD macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9) df['macd'] = macd['macd'] df['macdsignal'] = macd['macdsignal'] df['macdhist'] = macd['macdhist'] # 布林带 (20周期, 2倍标准差) bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0) df['bb_upper'] = bb['upperband'] df['bb_middle'] = bb['middleband'] df['bb_lower'] = bb['lowerband'] # ATR - 用于动态止损 df['atr'] = ta.ATR(df, timeperiod=14) # EMA均线系统 df['ema5'] = ta.EMA(df, timeperiod=5) df['ema10'] = ta.EMA(df, timeperiod=10) df['ema26'] = ta.EMA(df, timeperiod=26) df['ema52'] = ta.EMA(df, timeperiod=52) # RSI df['rsi'] = ta.RSI(df, timeperiod=14) # 成交量比(当前成交量 / 10周期均量) avg_vol = df['volume'].rolling(window=10).mean() df['volume_ratio'] = (df['volume'] / avg_vol).fillna(1.0) return df def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ 进场信号定义 - 优化版 - 做多: 缠论底部信号(-10/-20) + RSI<65 + 放量 + EMA确认 - 做空: 缠论顶部信号(10/20) + RSI>35 + 放量 + EMA确认 """ state_30m = 'resample_{}_state'.format(self.get_ticker_indicator() * self.time30) ema52_30m = 'resample_{}_ema52'.format(self.get_ticker_indicator() * self.time30) close_30m = 'resample_{}_close'.format(self.get_ticker_indicator() * self.time30) shift = self.time30 # 做多信号:只在缠论-10信号 + 强势过滤 dataframe.loc[ (dataframe[state_30m].shift(shift) == "-10") & (dataframe['rsi'] < 65) & (dataframe['rsi'] > 20) & # RSI不过冷 (dataframe['volume_ratio'] > 1.2) & # 放量确认 (dataframe['close'] > dataframe['ema52']) & # 价格在EMA52上方 (dataframe['macd'] > dataframe['macdsignal']), # MACD金叉 ['enter_long', 'enter_tag']] = (1, 'chan_long') # 做空信号:只在缠论10信号 + 强势过滤 dataframe.loc[ (dataframe[state_30m].shift(shift) == "10") & (dataframe['rsi'] > 35) & (dataframe['rsi'] < 80) & # RSI不过热 (dataframe['volume_ratio'] > 1.2) & # 放量确认 (dataframe['close'] < dataframe['ema52']) & # 价格在EMA52下方 (dataframe['macd'] < dataframe['macdsignal']), # MACD死叉 ['enter_short', 'enter_tag']] = (1, 'chan_short') return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: """ 出场信号定义 - 做多出场: 缠论顶部反转信号 - 做空出场: 缠论底部反转信号 """ state_30m = 'resample_{}_state'.format(self.get_ticker_indicator() * self.time30) shift = self.time30 dataframe.loc[ (dataframe[state_30m].shift(shift) == "10"), ['exit_long', 'exit_tag']] = (1, 'chan_exit_long') dataframe.loc[ (dataframe[state_30m].shift(shift) == "-10"), ['exit_short', 'exit_tag']] = (1, 'chan_exit_short') return dataframe def custom_stoploss(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, after_fill: bool, **kwargs) -> float | None: """ 基于ATR的动态止损 止损距离 = 开仓价 ± 1.5*ATR """ try: entry_atr = trade.get_custom_data(key="entry_atr") if entry_atr is None: dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) if dataframe is not None and len(dataframe) > 0 and 'atr' in dataframe.columns: entry_atr = float(dataframe.iloc[-1]['atr']) else: return -0.04 if trade.is_short: stop_price = trade.open_rate + (float(entry_atr) * 1.5) else: stop_price = trade.open_rate - (float(entry_atr) * 1.5) return stoploss_from_absolute(stop_price, current_rate, is_short=trade.is_short) except Exception as e: logger.warning(f"custom_stoploss error: {e}") return None def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs): """快速止盈:浮盈超过0.5%直接出场""" if current_profit > 0.005: return "quick_profit" return None def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float, time_in_force: str, current_time: datetime, entry_tag: str | None, side: str, **kwargs) -> bool: """进场前最终过滤:ATR太小或RSI极端时拒绝""" try: dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe) if dataframe is None or len(dataframe) == 0: return False last = dataframe.iloc[-1] atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator() * self.time30) atr_val = float(last.get(atr_str, 0) or 0) if atr_val < 0.001: return False return True except Exception as e: logger.warning(f"confirm_trade_entry error: {e}") return True def order_filled(self, pair: str, trade: Trade, order: Order, current_time: datetime, **kwargs) -> None: """订单成交时保存ATR用于止损计算""" try: if (trade.nr_of_successful_entries == 1) and (order.ft_order_side == trade.entry_side): dataframe, _ = self.dp.get_analyzed_dataframe(trade.pair, self.timeframe) if dataframe is not None and len(dataframe) > 0: atr_str = 'resample_{}_atr'.format(self.get_ticker_indicator() * self.time30) last = dataframe.iloc[-1] entry_atr = float(last.get(atr_str, 0) or 0) * 3 trade.set_custom_data(key="entry_atr", value=entry_atr) except Exception as e: logger.warning(f"order_filled error: {e}") def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float, entry_tag: str | None, side: str, **kwargs) -> float: """入场价微调,减少滑点""" if trade: if trade.is_short: return proposed_rate - 50 else: return proposed_rate + 50 return proposed_rate def custom_exit_price(self, pair: str, trade: Trade, current_time: datetime, proposed_rate: float, current_profit: float, exit_tag: str | None, **kwargs) -> float: """出场价微调,减少滑点""" if trade: if trade.is_short: return proposed_rate + 50 else: return proposed_rate - 50 return proposed_rate def leverage(self, pair: str, current_time: datetime, current_rate: float, proposed_leverage: float, max_leverage: float, entry_tag: Optional[str], side: str, **kwargs) -> float: return self.lev def get_ticker_indicator(self) -> int: return int(self.timeframe[:-1])