Files
Chan/strategies/ElliottWaveBTCStrategy.py
T
PorterandCursor 2c1232555e refactor: 缠论引擎迁入 chan/ 分层解耦,指标外置
将核心结构、指标与分析拆到 chan/{core,indicators,analysis,pipeline};
根目录保留兼容 shim;strategies 改为从 chan 包导入;买卖点经 bsp_macd 与 MACD 接合。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-03 14:47:13 +08:00

241 lines
8.1 KiB
Python

"""
ChanLun Wave Strategy for BTC Perpetual Futures
基于缠论波浪策略 V8
核心逻辑:
- 使用Chan库计算KLC-based缠论分型
- 只做空头(在下跌趋势中做空反弹)
- 顶分型确认 + RSI > 55 + 趋势确认 → 做空
- 空头出场:底分型 + RSI < 40
策略设计:
- 短周期(5m)为主,长周期(1h/1d)确认趋势
- 使用Chan库KLC分型确认入场
- RSI > 55 做空条件,RSI < 40 出场条件
- 不做多头(下跌趋势中做多风险太大)
作者: AI Assistant
"""
import sys
import os
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from chan.pipeline.ChanLun import ChanLun
from freqtrade.strategy import IStrategy
from pandas import DataFrame
import pandas as pd
import numpy as np
import talib.abstract as ta
import logging
from datetime import datetime
from typing import Optional
logger = logging.getLogger(__name__)
class ElliottWaveBTCStrategy(IStrategy):
INTERFACE_VERSION = 3
can_short = True
stoploss = -0.02
minimal_roi = {
"0": 0.06,
"120": 0.03,
"360": 0.01
}
trailing_stop = True
trailing_stop_positive = 0.02
trailing_stop_positive_offset = 0.08
trailing_only_offset_is_reached = True
startup_candle_count = 500
position_adjustment_enable = False
pair = 'BTC/USDT:USDT'
timeframe = '5m'
chan = ChanLun()
def informative_pairs(self):
return [
(self.pair, '5m'),
(self.pair, '1h'),
(self.pair, '1d'),
]
def _add_indicators(self, df: DataFrame) -> DataFrame:
df['ema20'] = ta.EMA(df, timeperiod=20)
df['ema50'] = ta.EMA(df, timeperiod=50)
df['ema200'] = ta.EMA(df, timeperiod=200)
df['rsi'] = ta.RSI(df, timeperiod=14)
df['atr'] = ta.ATR(df, timeperiod=14)
macd = ta.MACD(df, fastperiod=12, slowperiod=26, signalperiod=9)
df['macd'] = macd['macd']
df['macdsignal'] = macd['macdsignal']
df['macdhist'] = macd['macdhist']
# Chan库指标
df['ema52'] = ta.EMA(df, timeperiod=52)
df['ema104'] = ta.EMA(df, timeperiod=104)
df['ema24'] = ta.EMA(df, timeperiod=24)
df['ema26'] = ta.EMA(df, timeperiod=26)
df['volume_sma'] = ta.SMA(df, timeperiod=20)
df['volume_ratio'] = df['volume'] / df['volume_sma']
bb = ta.BBANDS(df, timeperiod=20, nbdevup=2.0, nbdevdn=2.0, matype=0)
df['bb2633upper'] = bb['upperband']
df['bb2633lower'] = bb['lowerband']
df['bb2633middle'] = bb['middleband']
return df
def _get_dataframe(self, timeframe: str) -> DataFrame:
return self.dp.get_pair_dataframe(pair=self.pair, timeframe=timeframe)
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe = self._add_indicators(dataframe)
df_1h = self._get_dataframe('1h')
df_1d = self._get_dataframe('1d')
if len(df_1h) > 0:
df_1h = self._add_indicators(df_1h)
df_1h['chan_state'] = self.chan.get_klu_state(df_1h)
dataframe['1h_ema200'] = df_1h['ema200'].reindex(dataframe.index, method='ffill')
dataframe['1h_trend_up'] = (df_1h['close'] > df_1h['ema200']).reindex(dataframe.index, method='ffill')
dataframe['1h_trend_down'] = (df_1h['close'] < df_1h['ema200']).reindex(dataframe.index, method='ffill')
dataframe['1h_chan_state'] = df_1h['chan_state'].reindex(dataframe.index, method='ffill')
else:
dataframe['1h_ema200'] = dataframe['ema200']
dataframe['1h_trend_up'] = True
dataframe['1h_trend_down'] = True
dataframe['1h_chan_state'] = '00'
if len(df_1d) > 0:
df_1d = self._add_indicators(df_1d)
df_1d['chan_state'] = self.chan.get_klu_state(df_1d)
dataframe['1d_ema200'] = df_1d['ema200'].reindex(dataframe.index, method='ffill')
dataframe['1d_trend_up'] = (df_1d['close'] > df_1d['ema200']).reindex(dataframe.index, method='ffill')
dataframe['1d_trend_down'] = (df_1d['close'] < df_1d['ema200']).reindex(dataframe.index, method='ffill')
dataframe['1d_rsi'] = df_1d['rsi'].reindex(dataframe.index, method='ffill')
dataframe['1d_chan_state'] = df_1d['chan_state'].reindex(dataframe.index, method='ffill')
else:
dataframe['1d_ema200'] = dataframe['ema200']
dataframe['1d_trend_up'] = True
dataframe['1d_trend_down'] = True
dataframe['1d_rsi'] = 50
dataframe['1d_chan_state'] = '00'
# 缠论分型(使用Chan库)
dataframe['chan_state'] = self.chan.get_klu_state(dataframe)
dataframe = self._generate_signals(dataframe)
return dataframe
def _generate_signals(self, df: DataFrame) -> DataFrame:
"""缠论分型 + 趋势确认 - 做空为主"""
n = len(df)
if n < 10:
return df
# 延迟分型状态(避免未来数据)
df['_fx'] = df['chan_state'].shift(1).fillna('00')
# 1h趋势
hourly_down = df['1h_trend_down'].fillna(False)
hourly_up = df['1h_trend_up'].fillna(False)
# MACD方向
macd_cross_down = (df['macd'] < df['macdsignal']) & (df['macd'].shift(1) >= df['macdsignal'].shift(1))
# === 空头信号(下跌趋势中做空)===
# 条件1: 顶分型 + RSI > 55 + 1h下跌趋势
short_cond1 = (
(df['_fx'] == '10') &
(df['rsi'] > 55) &
hourly_down
)
# 条件2: 1h共振顶分型 + RSI > 55
short_cond2 = (
(df['_fx'] == '10') &
(df['1h_chan_state'].fillna('00') == '10') &
(df['rsi'] > 55)
)
# 条件3: 顶分型 + MACD死叉 + RSI > 60
short_cond3 = (
(df['_fx'] == '10') &
macd_cross_down &
(df['rsi'] > 60)
)
df['chan_short'] = (short_cond1 | short_cond2 | short_cond3).astype(bool)
# === 多头信号(仅在1h上涨趋势中做多,且很少)===
# 只在1d和1h同时上涨时才做多,且需要强确认
daily_up = df['1d_trend_up'].fillna(False)
long_cond = (
(df['_fx'] == '-10') &
(df['rsi'] < 30) & # 极低RSI才做多
hourly_up &
daily_up
)
# 1h和1d共振底分型
long_cond2 = (
(df['_fx'] == '-10') &
(df['rsi'] < 30) &
(df['1h_chan_state'].fillna('00') == '-10') &
(df['1d_chan_state'].fillna('00') == '-10')
)
df['chan_long'] = (long_cond | long_cond2).astype(bool)
return df
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['enter_long'] = 0
dataframe['enter_short'] = 0
dataframe['enter_tag'] = ''
if 'chan_long' not in dataframe.columns:
return dataframe
dataframe.loc[dataframe['chan_long'], 'enter_long'] = 1
dataframe.loc[dataframe['chan_long'], 'enter_tag'] = 'chan_long'
dataframe.loc[dataframe['chan_short'], 'enter_short'] = 1
dataframe.loc[dataframe['chan_short'], 'enter_tag'] = 'chan_short'
return dataframe
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
dataframe['exit_long'] = 0
dataframe['exit_short'] = 0
if len(dataframe) < 2:
return dataframe
if 'chan_state' not in dataframe.columns:
return dataframe
df = dataframe.copy()
df['_fx'] = df['chan_state'].shift(1).fillna('00')
# 空头出场:底分型 + RSI < 40(仅在明显反弹时出场)
dataframe['exit_short'] = ((df['_fx'] == '-10') & (df['rsi'] < 40)).astype(int)
# 多头出场:顶分型 + RSI > 60
dataframe['exit_long'] = ((df['_fx'] == '10') & (df['rsi'] > 60)).astype(int)
return dataframe
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 2.0