将根目录引擎迁入 chanlun/ 并保留兼容 shim;拆分 TF_DF 与 web 服务; 前端模块化;strategies 改用 chanlun 导入;补充 ESS 文档与 golden 回归。 Co-authored-by: Cursor <cursoragent@cursor.com>
241 lines
8.1 KiB
Python
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 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
|