Files
Chan/strategies/BTC_Perpetual_Futures.py
T

260 lines
9.1 KiB
Python

# --- 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 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])