280 lines
14 KiB
Python
280 lines
14 KiB
Python
import logging
|
||
import numpy as np
|
||
from functools import reduce
|
||
import talib.abstract as ta
|
||
from pandas import DataFrame
|
||
from technical import qtpylib
|
||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
class FreqaiPrimer(IStrategy):
|
||
minimal_roi = {
|
||
0: 0.135,
|
||
9: 0.052,
|
||
15: 0.007,
|
||
60: 0
|
||
}
|
||
stoploss = -0.263
|
||
trailing_stop = True
|
||
trailing_stop_positive = 0.324
|
||
trailing_stop_positive_offset = 0.411
|
||
trailing_only_offset_is_reached = False
|
||
max_open_trades = 4
|
||
process_only_new_candles = True
|
||
use_exit_signal = True
|
||
startup_candle_count: int = 40
|
||
can_short = False
|
||
|
||
buy_rsi = IntParameter(low=10, high=50, default=30, space="buy", optimize=False, load=True)
|
||
sell_rsi = IntParameter(low=50, high=90, default=70, space="sell", optimize=False, load=True)
|
||
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.135, space="roi", optimize=True, load=True)
|
||
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.052, space="roi", optimize=True, load=True)
|
||
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.007, space="roi", optimize=True, load=True)
|
||
stoploss_param = DecimalParameter(low=-0.35, high=-0.1, default=-0.263, space="stoploss", optimize=True, load=True)
|
||
trailing_stop_positive_param = DecimalParameter(low=0.1, high=0.5, default=0.324, space="trailing", optimize=True, load=True)
|
||
trailing_stop_positive_offset_param = DecimalParameter(low=0.2, high=0.6, default=0.411, space="trailing", optimize=True, load=True)
|
||
|
||
freqai_info = {
|
||
"model": "LightGBMRegressor",
|
||
"feature_parameters": {
|
||
"include_timeframes": ["5m", "15m", "1h"],
|
||
"include_corr_pairlist": [],
|
||
"label_period_candles": 12,
|
||
"include_shifted_candles": 3,
|
||
},
|
||
"data_split_parameters": {
|
||
"test_size": 0.2,
|
||
"shuffle": False,
|
||
},
|
||
"model_training_parameters": {
|
||
"n_estimators": 200,
|
||
"learning_rate": 0.05,
|
||
"num_leaves": 31,
|
||
"verbose": -1,
|
||
},
|
||
}
|
||
|
||
plot_config = {
|
||
"main_plot": {},
|
||
"subplots": {
|
||
"&-buy_rsi": {"&-buy_rsi": {"color": "green"}},
|
||
"&-sell_rsi": {"&-sell_rsi": {"color": "red"}},
|
||
"&-stoploss": {"&-stoploss": {"color": "purple"}},
|
||
"&-roi_0": {"&-roi_0": {"color": "orange"}},
|
||
"do_predict": {"do_predict": {"color": "brown"}},
|
||
},
|
||
}
|
||
|
||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
|
||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||
dataframe["%-mfi-period"] = ta.MFI(dataframe, timeperiod=period)
|
||
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||
dataframe["%-adx-period"] = ta.ADX(dataframe, timeperiod=period)
|
||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=period, stds=2.2)
|
||
dataframe["bb_lowerband-period"] = bollinger["lower"]
|
||
dataframe["bb_middleband-period"] = bollinger["mid"]
|
||
dataframe["bb_upperband-period"] = bollinger["upper"]
|
||
dataframe["%-bb_width-period"] = (
|
||
dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]
|
||
) / dataframe["bb_middleband-period"]
|
||
dataframe["%-close-bb_lower-period"] = dataframe["close"] / dataframe["bb_lowerband-period"]
|
||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||
dataframe["%-relative_volume-period"] = (
|
||
dataframe["volume"] / dataframe["volume"].rolling(period).mean()
|
||
)
|
||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||
dataframe = dataframe.ffill()
|
||
dataframe = dataframe.fillna(0)
|
||
return dataframe
|
||
|
||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||
dataframe["%-raw_price"] = dataframe["close"]
|
||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||
dataframe = dataframe.ffill()
|
||
dataframe = dataframe.fillna(0)
|
||
return dataframe
|
||
|
||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||
if len(dataframe["close"]) < 20:
|
||
logger.warning(f"数据不足 {len(dataframe)} 根 K 线,%-volatility 可能不完整")
|
||
dataframe["%-day_of_week"] = dataframe["date"].dt.dayofweek
|
||
dataframe["%-hour_of_day"] = dataframe["date"].dt.hour
|
||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||
return dataframe
|
||
|
||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
|
||
if "close" not in dataframe.columns:
|
||
logger.error("数据框缺少必要的 'close' 列")
|
||
raise ValueError("数据框缺少必要的 'close' 列")
|
||
|
||
try:
|
||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||
if "%-volatility" not in dataframe.columns:
|
||
logger.warning("缺少 %-volatility 列,强制重新生成")
|
||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||
|
||
# 移除 shift(-label_period),改为使用当前及过去的数据
|
||
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||
dataframe["&-buy_rsi"] = dataframe["&-buy_rsi"].rolling(window=label_period).mean().ffill().bfill()
|
||
|
||
for col in ["&-buy_rsi", "%-volatility"]:
|
||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
|
||
dataframe[col] = dataframe[col].ffill()
|
||
dataframe[col] = dataframe[col].fillna(0)
|
||
if dataframe[col].isna().any():
|
||
logger.warning(f"目标列 {col} 仍包含 NaN,数据预览:\n{dataframe[col].tail(10)}")
|
||
except Exception as e:
|
||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||
raise
|
||
|
||
logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}")
|
||
return dataframe
|
||
|
||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
logger.info(f"处理交易对:{metadata['pair']}")
|
||
logger.debug(f"输入特征列:{list(dataframe.columns)}")
|
||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||
logger.debug(f"FreqAI 输出特征列:{list(dataframe.columns)}")
|
||
|
||
dataframe["rsi"] = ta.RSI(dataframe, timeperiod=14)
|
||
bollinger = qtpylib.bollinger_bands(qtpylib.typical_price(dataframe), window=20, stds=2)
|
||
dataframe["bb_lowerband"] = bollinger["lower"]
|
||
dataframe["bb_middleband"] = bollinger["mid"]
|
||
dataframe["bb_upperband"] = bollinger["upper"]
|
||
dataframe["tema"] = ta.TEMA(dataframe, timeperiod=9)
|
||
|
||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||
# 使用滚动窗口而非未来函数来生成 up_or_down 列
|
||
dataframe["up_or_down"] = np.where(
|
||
dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0
|
||
)
|
||
|
||
if "&-buy_rsi" in dataframe.columns:
|
||
if "%-volatility" not in dataframe.columns:
|
||
logger.warning("缺少 %-volatility 列,强制重新生成")
|
||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0)
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].ffill()
|
||
dataframe["%-volatility"] = dataframe["%-volatility"].fillna(0)
|
||
|
||
dataframe["&-sell_rsi"] = dataframe["&-buy_rsi"] + 30
|
||
dataframe["&-stoploss"] = self.stoploss - (dataframe["%-volatility"] * 5).clip(-0.05, 0.05)
|
||
dataframe["&-roi_0"] = (dataframe["close"].rolling(window=label_period).mean() / dataframe["close"] - 1).clip(0, 0.2)
|
||
|
||
for col in ["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0"]:
|
||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
|
||
dataframe[col] = dataframe[col].ffill()
|
||
dataframe[col] = dataframe[col].fillna(0)
|
||
|
||
dataframe["buy_rsi_pred"] = dataframe["&-buy_rsi"].rolling(5).mean().clip(10, 50)
|
||
dataframe["sell_rsi_pred"] = dataframe["&-sell_rsi"].rolling(5).mean().clip(50, 90)
|
||
dataframe["stoploss_pred"] = dataframe["&-stoploss"].clip(-0.35, -0.1)
|
||
dataframe["roi_0_pred"] = dataframe["&-roi_0"].clip(0.01, 0.2)
|
||
|
||
for col in ["buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred"]:
|
||
if dataframe[col].isna().any():
|
||
logger.warning(f"列 {col} 包含 NaN,填充为默认值")
|
||
dataframe[col] = dataframe[col].ffill()
|
||
dataframe[col] = dataframe[col].fillna(dataframe[col].mean())
|
||
|
||
dataframe["trailing_stop_positive"] = (dataframe["roi_0_pred"] * 0.5).clip(0.01, 0.3)
|
||
dataframe["trailing_stop_positive_offset"] = (dataframe["roi_0_pred"] * 0.75).clip(0.02, 0.4)
|
||
|
||
self.buy_rsi.value = float(dataframe["buy_rsi_pred"].iloc[-1])
|
||
self.sell_rsi.value = float(dataframe["sell_rsi_pred"].iloc[-1])
|
||
self.stoploss = float(self.stoploss_param.value)
|
||
self.minimal_roi = {
|
||
0: float(self.roi_0.value),
|
||
15: float(self.roi_15.value),
|
||
30: float(self.roi_30.value),
|
||
60: 0
|
||
}
|
||
self.trailing_stop_positive = float(self.trailing_stop_positive_param.value)
|
||
self.trailing_stop_positive_offset = float(self.trailing_stop_positive_offset_param.value)
|
||
|
||
logger.info(f"动态参数:buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, "
|
||
f"stoploss={self.stoploss}, trailing_stop_positive={self.trailing_stop_positive}")
|
||
else:
|
||
logger.warning(f"&-buy_rsi 列缺失,跳过 FreqAI 预测逻辑,检查 freqai.start 输出")
|
||
|
||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||
dataframe = dataframe.ffill()
|
||
dataframe = dataframe.fillna(0)
|
||
|
||
logger.info(f"up_or_down 值统计:\n{dataframe['up_or_down'].value_counts().to_string()}")
|
||
logger.info(f"do_predict 值统计:\n{dataframe['do_predict'].value_counts().to_string()}")
|
||
logger.debug(f"最终特征列:{list(dataframe.columns)}")
|
||
|
||
return dataframe
|
||
|
||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||
enter_long_conditions = [
|
||
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]),
|
||
df["tema"] > df["tema"].shift(1),
|
||
df["volume"] > 0,
|
||
df["do_predict"] == 1,
|
||
df["up_or_down"] == 1
|
||
]
|
||
if enter_long_conditions:
|
||
df.loc[
|
||
reduce(lambda x, y: x & y, enter_long_conditions),
|
||
["enter_long", "enter_tag"]
|
||
] = (1, "long")
|
||
return df
|
||
|
||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||
exit_long_conditions = [
|
||
qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"]),
|
||
(df["close"] < df["close"].shift(1) * 0.97),
|
||
df["volume"] > 0,
|
||
df["do_predict"] == 1,
|
||
df["up_or_down"] == 0
|
||
]
|
||
if exit_long_conditions:
|
||
df.loc[
|
||
reduce(lambda x, y: x & y, exit_long_conditions),
|
||
"exit_long"
|
||
] = 1
|
||
return df
|
||
|
||
def confirm_trade_entry(
|
||
self, pair: str, order_type: str, amount: float, rate: float,
|
||
time_in_force: str, current_time, entry_tag, side: str, **kwargs
|
||
) -> bool:
|
||
try:
|
||
df, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||
if df is None or df.empty:
|
||
logger.warning(f"无法获取 {pair} 的分析数据,拒绝交易")
|
||
return False
|
||
|
||
last_candle = df.iloc[-1].squeeze()
|
||
if "close" not in last_candle or np.isnan(last_candle["close"]):
|
||
logger.warning(f"{pair} 的最新 K 线缺少有效 close 价格,拒绝交易")
|
||
return False
|
||
|
||
if side == "long":
|
||
max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值
|
||
if rate > max_rate:
|
||
logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}")
|
||
return False
|
||
elif side == "short":
|
||
logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})")
|
||
return False
|
||
|
||
logger.debug(f"确认 {pair} 的交易:side={side}, rate={rate}, close={last_candle['close']}")
|
||
return True
|
||
except Exception as e:
|
||
logger.error(f"确认 {pair} 交易时出错:{str(e)}")
|
||
return False
|