# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement from freqtrade.strategy import IStrategy, merge_informative_pair from pandas import DataFrame import talib.abstract as ta import numpy as np from datetime import datetime from typing import Optional from freqtrade.persistence import Trade # freqtrade trade -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies # freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategy --strategy-path ./user_data/Chan/strategies --timerange=20260304- # freqtrade download-data -c ./user_data/Chan/config/Local_Test.json -t 1m 5m --data-format-ohlcv json --pairs SOL/USDT:USDT --timerange=20260201- class CryptoFutures1m5mStrategy(IStrategy): """ SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V12e (Short Only) 14个月回测 (2025-01 ~ 2026-03): +107.11%, PF 1.37, DD 23.96% 每个季度均盈利,市场下跌-54%期间持续获利 核心设计: 1. 纯做空策略 - 价格必须低于EMA200至少1%才允许做空 2. 5分钟趋势确认:EMA12 DataFrame: # ==================== 5分钟指标 ==================== inf_tf = self.informative_timeframe informative = self.dp.get_pair_dataframe(pair=metadata['pair'], timeframe=inf_tf) # EMA趋势 informative['ema12'] = ta.EMA(informative['close'], timeperiod=12) informative['ema26'] = ta.EMA(informative['close'], timeperiod=26) informative['ema50'] = ta.EMA(informative['close'], timeperiod=50) # EMA12斜率(3根K线变化率,用于确认趋势方向的动量) informative['ema12_slope'] = (informative['ema12'] - informative['ema12'].shift(3)) / informative['ema12'].shift(3) * 100 # MACD macd, macd_signal, macd_hist = ta.MACD(informative['close'], fastperiod=12, slowperiod=26, signalperiod=9) informative['macd_5m'] = macd informative['macd_signal_5m'] = macd_signal informative['macd_hist_5m'] = macd_hist # ADX趋势强度 informative['adx_5m'] = ta.ADX(informative['high'], informative['low'], informative['close'], timeperiod=14) # RSI(5分钟) informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14) # ATR(5分钟) informative['atr_5m'] = ta.ATR(informative['high'], informative['low'], informative['close'], timeperiod=14) informative['atr_pct_5m'] = informative['atr_5m'] / informative['close'] * 100 # ATR 长期均值(用于自适应波动率过滤) informative['atr_pct_ma_5m'] = informative['atr_pct_5m'].rolling(window=100).mean() # ===== EMA200 大趋势过滤 ===== informative['ema200'] = ta.EMA(informative['close'], timeperiod=200) informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100 # EMA200斜率(20根5分钟K线 = 100分钟趋势方向) informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100 # ===== 大趋势过滤(Short Only) ===== # 做空需要价格低于EMA200至少1% informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0 # 牛市暂停:EMA200上升 + 价格在EMA200上方 → 完全停止做空 informative['bull_pause'] = ( (informative['ema200_slope'] > 0) & (informative['ema200_dist_pct'] > 0) ) # ===== 5分钟趋势判断(仅Short) ===== informative['trend_bear_5m'] = ( (informative['ema12'] < informative['ema26']) & (informative['ema26'] < informative['ema50']) & (informative['ema12_slope'] < 0) & (informative['adx_5m'] > 25) & (informative['adx_5m'] < 50) & (informative['close'] < informative['ema12']) & (informative['rsi_5m'] < 48) & (informative['rsi_5m'] > 30) ) # 做空条件:短期趋势 + EMA200大趋势方向一致 + 非牛市 informative['can_long_5m'] = False informative['can_short_5m'] = ( informative['trend_bear_5m'] & informative['below_ema200'] & (~informative['bull_pause']) ) # ATR波动率过滤(自适应) informative['atr_ok_5m'] = ( (informative['atr_pct_5m'] > 0.1) & (informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 1.5) ) # 合并5分钟数据到1分钟 dataframe = merge_informative_pair(dataframe, informative, self.timeframe, inf_tf, ffill=True) # ==================== 1分钟指标 ==================== macd_1m, signal_1m, hist_1m = ta.MACD(dataframe['close'], fastperiod=12, slowperiod=26, signalperiod=9) dataframe['macd'] = macd_1m dataframe['macd_signal'] = signal_1m dataframe['macd_hist'] = hist_1m dataframe['ema9'] = ta.EMA(dataframe['close'], timeperiod=9) dataframe['ema21'] = ta.EMA(dataframe['close'], timeperiod=21) dataframe['rsi'] = ta.RSI(dataframe['close'], timeperiod=14) dataframe['vol_ma20'] = ta.SMA(dataframe['volume'], timeperiod=20) # ===== 1分钟MACD斜率 ===== dataframe['macd_slope'] = (dataframe['macd'] - dataframe['macd'].shift(3)) / 3 # ===== 1分钟做空入场信号 ===== dataframe['price_high_5'] = dataframe['high'].rolling(window=5).max() dataframe['macd_high_5'] = dataframe['macd'].rolling(window=5).max() dataframe['top_divergence'] = ( (dataframe['high'] >= dataframe['price_high_5'] * 0.999) & (dataframe['macd'] < dataframe['macd_high_5']) & (dataframe['macd_slope'] < 0) & (dataframe['macd'] < dataframe['macd_signal']) & (dataframe['volume'] > dataframe['vol_ma20'] * 0.6) ) dataframe['ema_cross_down'] = ( (dataframe['ema9'] < dataframe['ema21']) & (dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) & (dataframe['rsi'] < 55) & (dataframe['rsi'] > 35) & (dataframe['volume'] > dataframe['vol_ma20'] * 1.0) ) dataframe['is_bear_candle'] = ( (dataframe['close'] < dataframe['open']) & ((dataframe['open'] - dataframe['close']) / dataframe['open'] > 0.008) ) dataframe['bear_pullback'] = ( dataframe['is_bear_candle'].shift(2) & (dataframe['close'].shift(1) > dataframe['open'].shift(1)) & (dataframe['high'] < dataframe['high'].shift(2)) & (dataframe['close'] < dataframe['open']) & (dataframe['close'] < dataframe['ema9']) ) # ==================== 时间过滤 ==================== dataframe['hour_utc'] = dataframe['date'].dt.hour dataframe['is_bad_hour'] = dataframe['hour_utc'].isin([4, 5, 6, 7]) # 安全转换5分钟布尔列 bool_cols = [ 'can_long_5m_5m', 'can_short_5m_5m', 'trend_bear_5m_5m', 'atr_ok_5m_5m', 'below_ema200_5m', 'bull_pause_5m', ] for col in bool_cols: if col in dataframe.columns: dataframe[col] = dataframe[col].fillna(False).astype(bool) num_cols = ['atr_pct_5m_5m', 'rsi_5m_5m', 'macd_hist_5m_5m', 'atr_pct_ma_5m_5m', 'ema200_dist_pct_5m', 'ema200_slope_5m'] for col in num_cols: if col in dataframe.columns: dataframe[col] = dataframe[col].fillna(0) return dataframe def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: time_ok = ~dataframe['is_bad_hour'] atr_ok = dataframe['atr_ok_5m_5m'] # 5分钟MACD方向确认 macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0 # 1分钟MACD方向确认(双重确认) macd_bear_1m = dataframe['macd_hist'] < 0 # ===== 做空入场 ===== dataframe.loc[ (time_ok) & (atr_ok) & (dataframe['can_short_5m_5m']) & (macd_bear_5m) & (macd_bear_1m) & (dataframe['rsi'] > 30) & ( dataframe['top_divergence'] | dataframe['ema_cross_down'] | dataframe['bear_pullback'] ) & (dataframe['volume'] > 0), 'enter_short' ] = 1 return dataframe def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame: dataframe.loc[:, 'exit_long'] = 0 dataframe.loc[:, 'exit_short'] = 0 return dataframe def custom_exit(self, pair: str, trade: Trade, current_time: datetime, current_rate: float, current_profit: float, **kwargs) -> str | bool | None: """时间止损:持仓过久且亏损时提前退出""" trade_duration = (current_time - trade.open_date_utc).total_seconds() / 3600 if trade_duration > 8 and current_profit < -0.005: return 'time_stop_8h' if trade_duration > 16 and current_profit < 0: return 'time_stop_16h' 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: Optional[str], side: str, **kwargs) -> bool: """入场确认 - 时间过滤安全网""" hour_utc = current_time.utcnow().hour if current_time.tzinfo is None else current_time.hour if hour_utc in {4, 5, 6, 7}: return False return True