Files
Chan/strategies/CryptoFutures1m5mStrategy.py
T

264 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
from freqtrade.strategy import IStrategy, merge_informative_pair
from pandas import DataFrame
import pandas as pd
import talib.abstract as ta
import numpy as np
from datetime import datetime
from typing import Optional
from freqtrade.persistence import Trade
import warnings
# 抑制 pandas FutureWarning 关于 fillna 的隐式降级警告
# 这个警告来自 freqtrade 库的 strategy_helper.py
warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*')
# 或者启用未来行为(推荐)
pd.set_option('future.no_silent_downcasting', True)
# 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<EMA26<EMA50 + ADX 25-50 + RSI 30-48
3. ATR自适应波动率过滤:ATR < 长期均值 * 1.5(避免极端波动)
4. 1分钟精确入场:顶背离 / EMA死叉 / 熊市回调
5. 双重MACD确认(5分钟+1分钟MACD柱状图均为负)
6. trailing_stop_positive_offset = 0.030
7. 时间止损:持仓过久且亏损时提前退出
"""
INTERFACE_VERSION = 3
timeframe = '1m'
informative_timeframe = '5m'
can_short = True
# 止损止盈
stoploss = -0.025 # 2.5% 硬止损
trailing_stop = True
trailing_stop_positive = 0.008
trailing_stop_positive_offset = 0.030
trailing_only_offset_is_reached = True
# 不使用custom_stoploss(会干扰trailing_stop
use_custom_stoploss = False
# 完全禁用 exit_signal
use_exit_signal = False
process_only_new_candles = True
startup_candle_count: int = 1100
def informative_pairs(self):
return [
("SOL/USDT:USDT", "5m"),
]
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> 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)
# RSI5分钟)
informative['rsi_5m'] = ta.RSI(informative['close'], timeperiod=14)
# ATR5分钟)
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].astype(bool).fillna(False)
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].astype(float).fillna(0.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