405 lines
16 KiB
Python
405 lines
16 KiB
Python
# pragma pylint: disable=missing-docstring, invalid-name, pointless-string-statement
|
||
from freqtrade.strategy import IStrategy, merge_informative_pair, IntParameter, CategoricalParameter
|
||
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 的隐式降级警告
|
||
warnings.filterwarnings('ignore', category=FutureWarning, message='.*Downcasting object dtype arrays.*')
|
||
pd.set_option('future.no_silent_downcasting', True)
|
||
|
||
# freqtrade backtesting -c ./user_data/Chan/config/Local_Test.json --strategy CryptoFutures1m5mStrategyV4 --strategy-path ./user_data/Chan/strategies --timerange=20250101-
|
||
|
||
|
||
class CryptoFutures1m5mStrategyV4(IStrategy):
|
||
"""
|
||
SOL/USDT 合约策略 - 1分钟+5分钟双时间框架 V4 多空完全分离版
|
||
|
||
基于V3优化:
|
||
1. 多空参数完全分离
|
||
2. 分别优化做多做空的风险参数
|
||
|
||
核心设计:
|
||
1. 5分钟趋势确认 + 1分钟精确入场
|
||
2. ATR自适应波动率过滤
|
||
3. 多空trailing参数分离
|
||
"""
|
||
INTERFACE_VERSION = 3
|
||
timeframe = '1m'
|
||
informative_timeframe = '5m'
|
||
can_short = True
|
||
can_long = True
|
||
lev = 1.0
|
||
|
||
# ==================== 多空分离参数 ====================
|
||
|
||
# 做多止损 (更宽松,因为牛市回调幅度大)
|
||
stoploss_long = -0.035
|
||
|
||
# 做空止损 (相对紧凑,熊市反弹快)
|
||
stoploss_short = -0.025
|
||
|
||
# 统一下跌止损(取两者较宽松值)
|
||
stoploss = -0.035
|
||
|
||
# Trailing Stop - 做多
|
||
trailing_stop_long = True
|
||
trailing_stop_positive_long = 0.006
|
||
trailing_stop_positive_offset_long = 0.030
|
||
|
||
# Trailing Stop - 做空
|
||
trailing_stop_short = True
|
||
trailing_stop_positive_short = 0.010
|
||
trailing_stop_positive_offset_short = 0.038
|
||
|
||
# 统一设置
|
||
trailing_stop = True
|
||
trailing_stop_positive = 0.008
|
||
trailing_stop_positive_offset = 0.035
|
||
trailing_only_offset_is_reached = True
|
||
|
||
# 完全禁用 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 get_stoploss(self, side: str, trade: Optional[Trade] = None, current_rate: float = 0,
|
||
current_time: datetime = None, after_fill: bool = False, **kwargs) -> float:
|
||
"""动态获取多空不同的止损"""
|
||
if side == "long":
|
||
return self.stoploss_long
|
||
elif side == "short":
|
||
return self.stoploss_short
|
||
return self.stoploss
|
||
|
||
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)
|
||
|
||
# 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()
|
||
|
||
# ATR 短期均值 (用于做空过滤 - 更严格)
|
||
informative['atr_pct_ma_short_5m'] = informative['atr_pct_5m'].rolling(window=20).mean()
|
||
|
||
# EMA200 大趋势过滤
|
||
informative['ema200'] = ta.EMA(informative['close'], timeperiod=200)
|
||
informative['ema200_dist_pct'] = (informative['close'] - informative['ema200']) / informative['ema200'] * 100
|
||
|
||
# EMA200斜率
|
||
informative['ema200_slope'] = (informative['ema200'] - informative['ema200'].shift(20)) / informative['ema200'].shift(20) * 100
|
||
|
||
# ==================== 多空趋势判断 ====================
|
||
|
||
# 5分钟趋势判断 - 做多 (Bull)
|
||
informative['trend_bull_5m'] = (
|
||
(informative['ema12'] > informative['ema26']) &
|
||
(informative['ema26'] > informative['ema50']) &
|
||
(informative['ema12_slope'] > 0.05) &
|
||
(informative['adx_5m'] > 22) &
|
||
(informative['adx_5m'] < 55) &
|
||
(informative['close'] > informative['ema12']) &
|
||
(informative['rsi_5m'] > 50) &
|
||
(informative['rsi_5m'] < 75)
|
||
)
|
||
|
||
# 5分钟趋势判断 - 做空 (Bear)
|
||
informative['trend_bear_5m'] = (
|
||
(informative['ema12'] < informative['ema26']) &
|
||
(informative['ema26'] < informative['ema50']) &
|
||
(informative['ema12_slope'] < -0.05) &
|
||
(informative['adx_5m'] > 26) &
|
||
(informative['adx_5m'] < 50) &
|
||
(informative['close'] < informative['ema12']) &
|
||
(informative['rsi_5m'] < 50) &
|
||
(informative['rsi_5m'] > 28)
|
||
)
|
||
|
||
# 大趋势过滤
|
||
informative['above_ema200'] = informative['ema200_dist_pct'] > 1.0
|
||
informative['below_ema200'] = informative['ema200_dist_pct'] < -1.0
|
||
|
||
# 牛市/熊市环境
|
||
informative['bull_market'] = (
|
||
(informative['ema200_slope'] > 0) &
|
||
(informative['ema200_dist_pct'] > 0)
|
||
)
|
||
informative['bear_market'] = (
|
||
(informative['ema200_slope'] < 0) &
|
||
(informative['ema200_dist_pct'] < 0)
|
||
)
|
||
|
||
# 做多条件
|
||
informative['can_long_5m'] = (
|
||
informative['trend_bull_5m'] &
|
||
informative['above_ema200']
|
||
)
|
||
|
||
# 做空条件
|
||
informative['can_short_5m'] = (
|
||
informative['trend_bear_5m'] &
|
||
informative['below_ema200']
|
||
)
|
||
|
||
# ==================== 多空分离的ATR过滤 ====================
|
||
|
||
# 做多ATR过滤 - 允许更大波动(牛市波动大)
|
||
informative['atr_ok_long_5m'] = (
|
||
(informative['atr_pct_5m'] > 0.08) &
|
||
(informative['atr_pct_5m'] < informative['atr_pct_ma_5m'] * 2.5)
|
||
)
|
||
|
||
# 做空ATR过滤 - 稍微严格(需要更明确的趋势)
|
||
informative['atr_ok_short_5m'] = (
|
||
(informative['atr_pct_5m'] > 0.06) &
|
||
(informative['atr_pct_5m'] < informative['atr_pct_ma_short_5m'] * 2.0)
|
||
)
|
||
|
||
# 成交量确认
|
||
informative['volume_ma_5m'] = ta.SMA(informative['volume'], timeperiod=20)
|
||
informative['volume_ok_5m'] = informative['volume'] > informative['volume_ma_5m'] * 0.75
|
||
|
||
# 合并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
|
||
|
||
# ==================== 做空信号 ====================
|
||
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)
|
||
)
|
||
|
||
# EMA死叉 (做空)
|
||
dataframe['ema_cross_down'] = (
|
||
(dataframe['ema9'] < dataframe['ema21']) &
|
||
(dataframe['ema9'].shift(1) >= dataframe['ema21'].shift(1)) &
|
||
(dataframe['rsi'] < 58) &
|
||
(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['price_low_5'] = dataframe['low'].rolling(window=5).min()
|
||
dataframe['macd_low_5'] = dataframe['macd'].rolling(window=5).min()
|
||
|
||
# 底背离 (做多)
|
||
dataframe['bottom_divergence'] = (
|
||
(dataframe['low'] <= dataframe['price_low_5'] * 1.001) &
|
||
(dataframe['macd'] > dataframe['macd_low_5']) &
|
||
(dataframe['macd_slope'] > 0) &
|
||
(dataframe['macd'] > dataframe['macd_signal']) &
|
||
(dataframe['volume'] > dataframe['vol_ma20'] * 0.6)
|
||
)
|
||
|
||
# EMA金叉 (做多)
|
||
dataframe['ema_cross_up'] = (
|
||
(dataframe['ema9'] > dataframe['ema21']) &
|
||
(dataframe['ema9'].shift(1) <= dataframe['ema21'].shift(1)) &
|
||
(dataframe['rsi'] > 42) &
|
||
(dataframe['rsi'] < 72) &
|
||
(dataframe['volume'] > dataframe['vol_ma20'] * 1.0)
|
||
)
|
||
|
||
# 牛市回调 (做多)
|
||
dataframe['is_bull_candle'] = (
|
||
(dataframe['close'] > dataframe['open']) &
|
||
((dataframe['close'] - dataframe['open']) / dataframe['open'] > 0.008)
|
||
)
|
||
dataframe['bull_pullback'] = (
|
||
dataframe['is_bull_candle'].shift(2) &
|
||
(dataframe['close'].shift(1) < dataframe['open'].shift(1)) &
|
||
(dataframe['low'] > dataframe['low'].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_bull_5m_5m', 'trend_bear_5m_5m',
|
||
'atr_ok_long_5m_5m', 'atr_ok_short_5m_5m',
|
||
'above_ema200_5m', 'below_ema200_5m',
|
||
'bull_market_5m', 'bear_market_5m',
|
||
'volume_ok_5m_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', 'atr_pct_ma_short_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']
|
||
volume_ok = dataframe['volume_ok_5m_5m']
|
||
|
||
# ========== 做空入场 ==========
|
||
atr_ok_short = dataframe['atr_ok_short_5m_5m']
|
||
macd_bear_5m = dataframe['macd_hist_5m_5m'] < 0
|
||
macd_bear_1m = dataframe['macd_hist'] < 0
|
||
|
||
dataframe.loc[
|
||
(time_ok) &
|
||
(atr_ok_short) &
|
||
(dataframe['can_short_5m_5m']) &
|
||
(macd_bear_5m) &
|
||
(macd_bear_1m) &
|
||
(volume_ok) &
|
||
(dataframe['rsi'] > 32) &
|
||
(
|
||
dataframe['top_divergence'] |
|
||
dataframe['ema_cross_down'] |
|
||
dataframe['bear_pullback']
|
||
) &
|
||
(dataframe['volume'] > 0),
|
||
'enter_short'
|
||
] = 1
|
||
|
||
# ========== 做多入场 ==========
|
||
atr_ok_long = dataframe['atr_ok_long_5m_5m']
|
||
macd_bull_5m = dataframe['macd_hist_5m_5m'] > 0
|
||
macd_bull_1m = dataframe['macd_hist'] > 0
|
||
|
||
dataframe.loc[
|
||
(time_ok) &
|
||
(atr_ok_long) &
|
||
(dataframe['can_long_5m_5m']) &
|
||
(macd_bull_5m) &
|
||
(macd_bull_1m) &
|
||
(volume_ok) &
|
||
(dataframe['rsi'] < 72) &
|
||
(dataframe['rsi'] > 38) &
|
||
(
|
||
dataframe['bottom_divergence'] |
|
||
dataframe['ema_cross_up'] |
|
||
dataframe['bull_pullback']
|
||
) &
|
||
(dataframe['volume'] > 0),
|
||
'enter_long'
|
||
] = 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.trade_direction == 'short':
|
||
if trade_duration > 6 and current_profit < -0.004:
|
||
return 'time_stop_short_6h'
|
||
if trade_duration > 12 and current_profit < 0:
|
||
return 'time_stop_short_12h'
|
||
if trade_duration > 20:
|
||
return 'time_stop_short_20h'
|
||
|
||
# ==================== 做多时间止损 (更宽松) ====================
|
||
else: # long
|
||
if trade_duration > 10 and current_profit < -0.006:
|
||
return 'time_stop_long_10h'
|
||
if trade_duration > 20 and current_profit < 0:
|
||
return 'time_stop_long_20h'
|
||
if trade_duration > 30:
|
||
return 'time_stop_long_30h'
|
||
|
||
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
|
||
|
||
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
|