将核心结构、指标与分析拆到 chan/{core,indicators,analysis,pipeline};
根目录保留兼容 shim;strategies 改为从 chan 包导入;买卖点经 bsp_macd 与 MACD 接合。
Co-authored-by: Cursor <cursoragent@cursor.com>
260 lines
9.1 KiB
Python
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 chan.pipeline.ChanLun import ChanLun
|
|
from chan.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])
|