184 lines
8.3 KiB
Markdown
184 lines
8.3 KiB
Markdown
```python
|
||
|
||
class FreqaiExampleStrategy(IStrategy):
|
||
minimal_roi = {}
|
||
stoploss = -0.1
|
||
trailing_stop = True
|
||
process_only_new_candles = True
|
||
use_exit_signal = True
|
||
startup_candle_count: int = 100 # 增加数据需求
|
||
can_short = False
|
||
|
||
# Hyperopt 参数
|
||
buy_rsi = IntParameter(low=10, high=50, default=27, space="buy", optimize=False, load=True)
|
||
sell_rsi = IntParameter(low=50, high=90, default=59, space="sell", optimize=False, load=True)
|
||
roi_0 = DecimalParameter(low=0.01, high=0.2, default=0.038, space="roi", optimize=True, load=True)
|
||
roi_15 = DecimalParameter(low=0.005, high=0.1, default=0.027, space="roi", optimize=True, load=True)
|
||
roi_30 = DecimalParameter(low=0.001, high=0.05, default=0.009, space="roi", optimize=True, load=True)
|
||
stoploss_param = DecimalParameter(low=-0.25, high=-0.05, default=-0.1, space="stoploss", optimize=True, load=True)
|
||
|
||
# 保护机制
|
||
protections = [
|
||
{"method": "StoplossGuard", "stop_duration": 60, "lookback_period": 120},
|
||
{"method": "MaxDrawdown", "lookback_period": 120, "max_allowed_drawdown": 0.05}
|
||
]
|
||
|
||
# FreqAI 配置
|
||
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,
|
||
"n_splits": 5 # 添加交叉验证
|
||
},
|
||
"model_training_parameters": {
|
||
"n_estimators": 200,
|
||
"learning_rate": 0.05,
|
||
"num_leaves": 10,
|
||
"min_child_weight": 1,
|
||
"verbose": -1,
|
||
},
|
||
}
|
||
|
||
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"]
|
||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||
dataframe["&-buy_rsi"] = (dataframe["close"].shift(-label_period) / dataframe["close"] - 1) * 100 # 修改目标为收益率
|
||
|
||
for col in ["&-buy_rsi", "%-volatility"]:
|
||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0)
|
||
dataframe[col] = dataframe[col].ffill()
|
||
if dataframe[col].isna().any():
|
||
logger.warning(f"目标列 {col} 包含 NaN,填充为 0")
|
||
dataframe[col] = dataframe[col].fillna(0)
|
||
logger.info(f"目标列 {col} 统计:\n{dataframe[col].describe().to_string()}")
|
||
except Exception as e:
|
||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||
raise
|
||
|
||
return dataframe
|
||
|
||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
logger.info(f"处理交易对:{metadata['pair']}, 数据形状:{dataframe.shape}")
|
||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||
|
||
# 计算传统指标
|
||
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)
|
||
|
||
# 检查FreqAI预测列
|
||
if "&-buy_rsi_pred" in dataframe.columns:
|
||
logger.info(f"&-buy_rsi_pred 统计:均值={dataframe['&-buy_rsi_pred'].mean():.2f}, 标准差={dataframe['&-buy_rsi_pred'].std():.2f}")
|
||
dataframe["buy_rsi_trend"] = np.where(
|
||
dataframe["&-buy_rsi_pred"] > dataframe["&-buy_rsi_pred"].shift(1), 1, 0
|
||
)
|
||
dataframe["&-sell_rsi_pred"] = dataframe["&-buy_rsi_pred"] + 30
|
||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20).std()
|
||
dataframe["&-stoploss_pred"] = -0.1 - (dataframe["%-volatility"] * 10).clip(0, 0.25)
|
||
dataframe["&-roi_0_pred"] = (dataframe["&-buy_rsi_pred"] / 1000).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,填充为默认值")
|
||
mean_value = dataframe[col].mean()
|
||
if pd.isna(mean_value):
|
||
mean_value = {
|
||
"&-buy_rsi_pred": 30,
|
||
"&-sell_rsi_pred": 70,
|
||
"&-stoploss_pred": -0.1,
|
||
"&-roi_0_pred": 0.05
|
||
}.get(col, 0)
|
||
dataframe[col] = dataframe[col].fillna(mean_value)
|
||
else:
|
||
logger.warning(f"&-buy_rsi_pred 列缺失,使用默认值初始化")
|
||
dataframe["buy_rsi_trend"] = 0
|
||
dataframe["&-buy_rsi_pred"] = 30
|
||
dataframe["&-sell_rsi_pred"] = 70
|
||
dataframe["&-stoploss_pred"] = -0.1
|
||
dataframe["&-roi_0_pred"] = 0.05
|
||
|
||
# 动态参数设置
|
||
try:
|
||
last_valid_idx = dataframe["&-stoploss_pred"].last_valid_index()
|
||
if last_valid_idx is None:
|
||
raise ValueError("没有有效的预测数据")
|
||
self.stoploss = float(np.clip(dataframe["&-stoploss_pred"].iloc[last_valid_idx], -0.25, -0.05))
|
||
self.buy_rsi.value = int(np.clip(dataframe["&-buy_rsi_pred"].iloc[last_valid_idx], 10, 50))
|
||
self.sell_rsi.value = int(np.clip(dataframe["&-sell_rsi_pred"].iloc[last_valid_idx], 50, 90))
|
||
self.roi_0.value = float(np.clip(dataframe["&-roi_0_pred"].iloc[last_valid_idx], 0.01, 0.2))
|
||
self.minimal_roi = {
|
||
0: self.roi_0.value,
|
||
15: self.roi_15.value,
|
||
30: self.roi_30.value,
|
||
60: 0.0
|
||
}
|
||
logger.info(f"动态参数设置: buy_rsi={self.buy_rsi.value}, sell_rsi={self.sell_rsi.value}, stoploss={self.stoploss:.2%}")
|
||
except Exception as e:
|
||
logger.error(f"动态参数设置失败,使用默认值: {str(e)}")
|
||
self.stoploss = -0.1
|
||
self.buy_rsi.value = 27
|
||
self.sell_rsi.value = 59
|
||
self.minimal_roi = {0: 0.038, 15: 0.027, 30: 0.009, 60: 0.0}
|
||
|
||
dataframe = dataframe.replace([np.inf, -np.inf], 0)
|
||
dataframe = dataframe.ffill()
|
||
dataframe = dataframe.fillna(0)
|
||
|
||
logger.info(f"do_predict 分布:\n{dataframe['do_predict'].value_counts().to_string()}")
|
||
|
||
return dataframe
|
||
|
||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||
enter_long_conditions = [
|
||
qtpylib.crossed_above(df["rsi"], df["&-buy_rsi_pred"] - 5), # 放宽RSI条件
|
||
df["tema"] > df["tema"].shift(1),
|
||
df["volume"] > 0,
|
||
df["do_predict"] == 1
|
||
]
|
||
if enter_long_conditions:
|
||
condition_met = reduce(lambda x, y: x & y, enter_long_conditions)
|
||
df.loc[condition_met, ["enter_long", "enter_tag"]] = (1, "long")
|
||
if condition_met.any():
|
||
logger.info(f"买入信号触发:{metadata['pair']},时间={df.index[condition_met][-1]}")
|
||
else:
|
||
logger.debug(f"买入条件未满足:{metadata['pair']},do_predict={df['do_predict'].iloc[-1]}, rsi={df['rsi'].iloc[-1]:.2f}, buy_rsi_pred={df['&-buy_rsi_pred'].iloc[-1]:.2f}")
|
||
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["bb_lowerband"],
|
||
df["volume"] > 0,
|
||
df["do_predict"] == 1
|
||
]
|
||
time_exit = (df["date"] >= df["date"].shift(1) + pd.Timedelta(days=2))
|
||
df.loc[
|
||
(reduce(lambda x, y: x & y, exit_long_conditions)) | time_exit,
|
||
"exit_long"
|
||
] = 1
|
||
return df
|
||
```
|
||
```
|
||
```
|
||
```
|
||
```
|
||
```
|
||
```
|
||
```
|