策略 revert b4b647dc4 之后又进行优化, 纯freaai填充所有参数
This commit is contained in:
parent
abbada318d
commit
a117548d02
2
.gitignore
vendored
2
.gitignore
vendored
@ -125,5 +125,5 @@ data/
|
||||
!result/
|
||||
|
||||
tools/.env
|
||||
result/
|
||||
output.log
|
||||
result/
|
||||
|
||||
@ -4,47 +4,38 @@ from functools import reduce
|
||||
import talib.abstract as ta
|
||||
from pandas import DataFrame
|
||||
from technical import qtpylib
|
||||
from freqtrade.strategy import IStrategy, IntParameter, DecimalParameter
|
||||
from pandas.core.dtypes.common import is_scalar
|
||||
import pandas as pd
|
||||
|
||||
from freqtrade.strategy import IStrategy
|
||||
|
||||
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
|
||||
"""
|
||||
策略说明:
|
||||
- 所有交易信号由 FreqAI 动态预测
|
||||
- 不使用 Hyperopt 优化任何参数
|
||||
- 使用 FreqAI 提供的趋势和波动率信号进行交易决策
|
||||
"""
|
||||
|
||||
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)
|
||||
tema_period = IntParameter(low=5, high=20, default=9, space="buy", optimize=True, load=True)
|
||||
plot_config = {
|
||||
"main_plot": {},
|
||||
"subplots": {
|
||||
"Signals": {
|
||||
"enter_long": {"color": "green"},
|
||||
"exit_long": {"color": "red"}
|
||||
},
|
||||
"FreqAI Predictions": {
|
||||
"&-buy_signal": {"color": "blue"},
|
||||
"&-sell_signal": {"color": "orange"},
|
||||
"&-volatility_forecast": {"color": "purple"}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
freqai_info = {
|
||||
"model": "LightGBMRegressorMultiTarget",
|
||||
"model": "LightGBMClassifier",
|
||||
"feature_parameters": {
|
||||
"include_timeframes": ["5m", "15m", "1h"],
|
||||
"include_corr_pairlist": [],
|
||||
"label_period_candles": 12,
|
||||
"label_period_candles": 24,
|
||||
"include_shifted_candles": 3,
|
||||
},
|
||||
"data_split_parameters": {
|
||||
@ -52,357 +43,100 @@ class FreqaiPrimer(IStrategy):
|
||||
"shuffle": False,
|
||||
},
|
||||
"model_training_parameters": {
|
||||
"n_estimators": 500,
|
||||
"learning_rate": 0.01,
|
||||
"num_leaves": 64,
|
||||
"max_depth": 8,
|
||||
"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 __init__(self, config: dict):
|
||||
super().__init__(config)
|
||||
self.cycle_trade_pair = None # 跟踪当前周期的下单币对
|
||||
self.last_cycle_time = None # 记录上一个周期时间
|
||||
def bot_loop_start(self, current_time, **kwargs) -> None:
|
||||
"""
|
||||
Reset cycle_trade_pair at the start of each new cycle.
|
||||
"""
|
||||
try:
|
||||
# 检查是否进入新周期(基于 timeframe,如 5m)
|
||||
if self.last_cycle_time is None or current_time >= self.last_cycle_time + pd.Timedelta(self.timeframe):
|
||||
self.cycle_trade_pair = None
|
||||
self.last_cycle_time = current_time
|
||||
logger.debug(f"New cycle started at {current_time}, reset cycle_trade_pair")
|
||||
except Exception as e:
|
||||
logger.error(f"Error in bot_loop_start: {str(e)}")
|
||||
def clean_dataframe(self, dataframe: DataFrame, columns: list = None) -> DataFrame:
|
||||
try:
|
||||
if columns:
|
||||
valid_columns = [col for col in columns if col in dataframe.columns]
|
||||
if not valid_columns:
|
||||
logger.warning(f"None of the specified columns {columns} exist in DataFrame. Skipping cleaning.")
|
||||
return dataframe
|
||||
for col in valid_columns:
|
||||
logger.debug(f"Cleaning column {col}: NaN count before = {dataframe[col].isna().sum()}")
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||||
logger.debug(f"NaN count after = {dataframe[col].isna().sum()}")
|
||||
else:
|
||||
logger.debug(f"Cleaning entire DataFrame: NaN count before = {dataframe.isna().sum().sum()}")
|
||||
dataframe = dataframe.replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||||
logger.debug(f"NaN count after = {dataframe.isna().sum().sum()}")
|
||||
return dataframe
|
||||
except Exception as e:
|
||||
logger.error(f"Data cleaning failed: {str(e)}")
|
||||
return dataframe # 返回原始 DataFrame,避免策略崩溃
|
||||
|
||||
def feature_engineering_expand_all(self, dataframe: DataFrame, period: int, metadata: dict, **kwargs) -> DataFrame:
|
||||
try:
|
||||
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
|
||||
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 = self.clean_dataframe(dataframe, columns=["%-rsi-period", "%-mfi-period", "%-sma-period",
|
||||
"%-ema-period", "%-adx-period", "bb_lowerband-period",
|
||||
"bb_middleband-period", "bb_upperband-period",
|
||||
"%-bb_width-period", "%-close-bb_lower-period",
|
||||
"%-roc-period", "%-relative_volume-period"])
|
||||
return dataframe
|
||||
except Exception as e:
|
||||
logger.error(f"Error in feature_engineering_expand_all for {metadata['pair']}: {str(e)}")
|
||||
return dataframe
|
||||
dataframe["%-rsi-period"] = ta.RSI(dataframe, timeperiod=period)
|
||||
dataframe["%-sma-period"] = ta.SMA(dataframe, timeperiod=period)
|
||||
dataframe["%-ema-period"] = ta.EMA(dataframe, timeperiod=period)
|
||||
|
||||
def feature_engineering_expand_basic(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
try:
|
||||
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
|
||||
dataframe["%-pct-change"] = dataframe["close"].pct_change()
|
||||
dataframe["%-raw_volume"] = dataframe["volume"]
|
||||
dataframe["%-raw_price"] = dataframe["close"]
|
||||
dataframe = self.clean_dataframe(dataframe, columns=["%-pct-change", "%-raw_volume", "%-raw_price"])
|
||||
return dataframe
|
||||
except Exception as e:
|
||||
logger.error(f"Error in feature_engineering_expand_basic for {metadata['pair']}: {str(e)}")
|
||||
return dataframe
|
||||
real = ta.TYPPRICE(dataframe)
|
||||
upperband, middleband, lowerband = ta.BBANDS(real, timeperiod=period, nbdevup=2.0, nbdevdn=2.0)
|
||||
dataframe["bb_lowerband-period"] = lowerband
|
||||
dataframe["bb_upperband-period"] = upperband
|
||||
dataframe["bb_middleband-period"] = middleband
|
||||
dataframe["%-bb_width-period"] = (dataframe["bb_upperband-period"] - dataframe["bb_lowerband-period"]) / dataframe["bb_middleband-period"]
|
||||
|
||||
def feature_engineering_standard(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
try:
|
||||
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
|
||||
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).ffill().fillna(0)
|
||||
dataframe = self.clean_dataframe(dataframe, columns=["%-day_of_week", "%-hour_of_day", "%-volatility"])
|
||||
return dataframe
|
||||
except Exception as e:
|
||||
logger.error(f"Error in feature_engineering_standard for {metadata['pair']}: {str(e)}")
|
||||
return dataframe
|
||||
dataframe["%-roc-period"] = ta.ROC(dataframe, timeperiod=period)
|
||||
return dataframe
|
||||
|
||||
def set_freqai_targets(self, dataframe: DataFrame, metadata: dict, **kwargs) -> DataFrame:
|
||||
try:
|
||||
logger.info(f"设置 FreqAI 目标,交易对:{metadata['pair']}")
|
||||
if "close" not in dataframe.columns:
|
||||
logger.error("数据框缺少必要的 'close' 列")
|
||||
raise ValueError("数据框缺少必要的 'close' 列")
|
||||
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).ffill().fillna(0)
|
||||
dataframe["&-buy_rsi"] = ta.RSI(dataframe, timeperiod=14).rolling(window=label_period).mean().ffill().bfill()
|
||||
for col in ["&-buy_rsi", "%-volatility"]:
|
||||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||||
if dataframe[col].isna().any():
|
||||
logger.warning(f"目标列 {col} 仍包含 NaN,数据预览:\n{dataframe[col].tail(10)}")
|
||||
logger.info(f"目标列预览:\n{dataframe[['&-buy_rsi']].head().to_string()}")
|
||||
return dataframe
|
||||
except Exception as e:
|
||||
logger.error(f"创建 FreqAI 目标失败:{str(e)}")
|
||||
return dataframe
|
||||
"""
|
||||
使用历史窗口预测未来趋势和波动率作为辅助信号,
|
||||
避免使用任何未来数据(如 shift(-N))
|
||||
"""
|
||||
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
|
||||
# 1. 趋势强度:使用过去 N 根 K 线的收益率判断当前趋势
|
||||
dataframe["&-trend_strength"] = dataframe["close"].pct_change(label_period)
|
||||
|
||||
# 2. 波动率预测:使用过去 N 根 K 线的价格变动绝对值
|
||||
dataframe["&-volatility_forecast"] = dataframe["close"].pct_change(label_period).abs()
|
||||
|
||||
# 3. 动态 ROI 目标:基于趋势强度缩放
|
||||
dataframe["&-roi_target"] = np.where(
|
||||
dataframe["&-trend_strength"] > 0,
|
||||
dataframe["&-trend_strength"] * 1.5,
|
||||
0.01 # 最小 ROI
|
||||
)
|
||||
|
||||
# 4. 动态止损目标:基于波动率缩放
|
||||
dataframe["&-stoploss_target"] = -dataframe["&-volatility_forecast"] * 1.2
|
||||
|
||||
# 5. 市场状态识别:判断当前是震荡、趋势、超买还是超卖
|
||||
if "bb_upperband-period" in dataframe.columns and "bb_lowerband-period" in dataframe.columns:
|
||||
dataframe["&-market_condition"] = np.select([
|
||||
(dataframe["close"] > dataframe["bb_upperband-period"]),
|
||||
(dataframe["close"] < dataframe["bb_lowerband-period"]),
|
||||
(dataframe["&-trend_strength"] > 0.03),
|
||||
], [
|
||||
"overbought",
|
||||
"oversold",
|
||||
"strong_up",
|
||||
], default="sideways")
|
||||
|
||||
# 买入信号(必须存在)
|
||||
dataframe["&-buy_signal"] = np.where(dataframe["&-trend_strength"] > 0.01, 1, 0)
|
||||
|
||||
# 卖出信号(必须存在)
|
||||
dataframe["&-sell_signal"] = np.where(dataframe["&-trend_strength"] < -0.01, 1, 0)
|
||||
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_indicators(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||||
try:
|
||||
logger.debug(f"Pre-cleaning input DataFrame for {metadata['pair']}: columns={list(dataframe.columns)}")
|
||||
dataframe = self.clean_dataframe(dataframe, columns=["close", "volume", "open", "high", "low"])
|
||||
logger.info(f"Processing pair: {metadata['pair']}")
|
||||
logger.debug(f"Input columns before FreqAI: {list(dataframe.columns)}, shape={dataframe.shape}")
|
||||
|
||||
# FreqAI 预测
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
logger.debug(f"Columns after FreqAI: {list(dataframe.columns)}, shape={dataframe.shape}")
|
||||
|
||||
# 技术指标
|
||||
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=self.tema_period.value)
|
||||
dataframe["atr"] = ta.ATR(dataframe["high"], dataframe["low"], dataframe["close"], timeperiod=14)
|
||||
|
||||
# 标签生成
|
||||
label_period = self.freqai_info["feature_parameters"]["label_period_candles"]
|
||||
dataframe["up_or_down"] = np.where(
|
||||
dataframe["close"].rolling(window=label_period).mean() > dataframe["close"], 1, 0
|
||||
)
|
||||
|
||||
# FreqAI 预测后处理
|
||||
if "&-buy_rsi" in dataframe.columns:
|
||||
if "%-volatility" not in dataframe.columns:
|
||||
logger.warning("Missing %-volatility column, regenerating")
|
||||
dataframe["%-volatility"] = dataframe["close"].pct_change().rolling(20, min_periods=1).std()
|
||||
dataframe["%-volatility"] = dataframe["%-volatility"].replace([np.inf, -np.inf], 0).ffill().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)
|
||||
dataframe = self.clean_dataframe(dataframe, columns=["&-buy_rsi", "&-sell_rsi", "&-stoploss", "&-roi_0", "%-volatility"])
|
||||
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"Column {col} contains NaN values, filling with mean")
|
||||
dataframe[col] = dataframe[col].ffill().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"Dynamic parameters: 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 column missing, skipping FreqAI prediction logic. Check model training or prediction files.")
|
||||
dataframe["buy_rsi_pred"] = self.buy_rsi.default
|
||||
dataframe["sell_rsi_pred"] = self.sell_rsi.default
|
||||
dataframe["stoploss_pred"] = self.stoploss_param.default
|
||||
dataframe["roi_0_pred"] = self.roi_0.default
|
||||
dataframe["trailing_stop_positive"] = self.trailing_stop_positive_param.default
|
||||
dataframe["trailing_stop_positive_offset"] = self.trailing_stop_positive_offset_param.default
|
||||
|
||||
# 清理生成的列
|
||||
generated_columns = [
|
||||
"rsi", "bb_lowerband", "bb_middleband", "bb_upperband", "tema", "atr",
|
||||
"up_or_down", "buy_rsi_pred", "sell_rsi_pred", "stoploss_pred", "roi_0_pred",
|
||||
"trailing_stop_positive", "trailing_stop_positive_offset"
|
||||
]
|
||||
dataframe = self.clean_dataframe(dataframe, columns=generated_columns)
|
||||
|
||||
logger.info(f"up_or_down value counts:\n{dataframe['up_or_down'].value_counts().to_string()}")
|
||||
logger.info(f"do_predict value counts:\n{dataframe['do_predict'].value_counts().to_string()}")
|
||||
logger.debug(f"Final columns: {list(dataframe.columns)}, shape={dataframe.shape}")
|
||||
return dataframe
|
||||
except Exception as e:
|
||||
logger.error(f"Error processing indicators for {metadata['pair']}: {str(e)}")
|
||||
return dataframe # 返回未修改的 DataFrame,避免中断
|
||||
logger.info(f"[{metadata['pair']}] 当前可用列: {list(dataframe.columns)}")
|
||||
dataframe = self.freqai.start(dataframe, metadata, self)
|
||||
|
||||
# FreqAI 提供的预测列
|
||||
if "&-trend" in dataframe.columns:
|
||||
dataframe["in_uptrend"] = dataframe["&-trend"] > 0.5
|
||||
|
||||
return dataframe
|
||||
|
||||
def populate_entry_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
try:
|
||||
# 验证输入列
|
||||
required_columns = ["rsi", "tema", "volume", "buy_rsi_pred", "do_predict", "bb_middleband", "close", "&-buy_rsi"]
|
||||
missing_cols = [col for col in required_columns if col not in df.columns]
|
||||
if missing_cols:
|
||||
logger.error(f"Missing columns for {metadata['pair']}: {missing_cols}")
|
||||
return df
|
||||
conditions = []
|
||||
|
||||
# 处理 NaN
|
||||
if df[required_columns].isna().any().any():
|
||||
logger.warning(f"NaN values in {metadata['pair']}: {df[required_columns].isna().any()}")
|
||||
df[required_columns] = df[required_columns].ffill().fillna(0)
|
||||
if "&-buy_signal" in df.columns:
|
||||
conditions.append(df["&-buy_signal"] > 0.5)
|
||||
else:
|
||||
logger.warning("⚠️ &-buy_signal 列缺失,跳过该条件")
|
||||
|
||||
# 计算 entry_score
|
||||
df["entry_score"] = (
|
||||
(50 - df["rsi"]) * 0.5 + # RSI 超卖权重
|
||||
(df["volume"] / df["volume"].rolling(20, min_periods=1).mean()) * 0.3 + # 成交量活跃
|
||||
df["&-buy_rsi"] * 0.2 # FreqAI 预测
|
||||
)
|
||||
|
||||
# 确保 entry_score 是标量
|
||||
if not df["entry_score"].apply(is_scalar).all():
|
||||
logger.error(f"Non-scalar values in entry_score for {metadata['pair']}: {df['entry_score'].head()}")
|
||||
df["entry_score"] = df["entry_score"].apply(lambda x: x[0] if isinstance(x, (list, np.ndarray)) else x).astype(float)
|
||||
|
||||
# 买入条件(优化为 5 个,触发率 0.35%-0.42%)
|
||||
enter_long_conditions = [
|
||||
qtpylib.crossed_above(df["rsi"], df["buy_rsi_pred"]), # RSI 上穿 FreqAI 预测
|
||||
df["tema"] > df["tema"].shift(1), # TEMA 上行(趋势确认)
|
||||
df["volume"] > df["volume"].rolling(20, min_periods=1).mean() * 1.1, # 成交量略高于均量
|
||||
df["do_predict"] == 1, # FreqAI 预测有效
|
||||
df["rsi"] < 45 # RSI 放宽到 < 45
|
||||
]
|
||||
|
||||
# 合并条件
|
||||
if enter_long_conditions:
|
||||
condition = reduce(lambda x, y: x & y, enter_long_conditions)
|
||||
df.loc[condition, "enter_long"] = 1
|
||||
df.loc[condition, "enter_tag"] = "long"
|
||||
df.loc[condition, "entry_score"] = df.loc[condition, "entry_score"]
|
||||
df["enter_long"] = df["enter_long"].fillna(0).astype(int)
|
||||
df["enter_tag"] = df["enter_tag"].fillna("")
|
||||
df["entry_score"] = df["entry_score"].fillna(0.0).astype(float)
|
||||
else:
|
||||
logger.warning(f"No valid entry conditions for {metadata['pair']}")
|
||||
df["enter_long"] = 0
|
||||
df["enter_tag"] = ""
|
||||
df["entry_score"] = 0.0
|
||||
|
||||
logger.debug(f"Entry signals for {metadata['pair']}: {df['enter_long'].sum()} signals")
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.error(f"Error in populate_entry_trend for {metadata['pair']}: {str(e)}")
|
||||
if len(conditions) == 0:
|
||||
return df
|
||||
|
||||
df.loc[reduce(lambda x, y: x & y, conditions), 'enter_long'] = 1
|
||||
return df
|
||||
|
||||
def populate_exit_trend(self, df: DataFrame, metadata: dict) -> DataFrame:
|
||||
try:
|
||||
exit_long_conditions = [
|
||||
qtpylib.crossed_above(df["rsi"], df["sell_rsi_pred"]),
|
||||
(df["close"] < df["close"].shift(1) * 0.97) | (df["rsi"] > 80),
|
||||
df["volume"] > 0,
|
||||
(df["do_predict"] == 1) | (df["do_predict"] == 0),
|
||||
(df["up_or_down"] == 0) | (df["rsi"] > 70)
|
||||
]
|
||||
if exit_long_conditions:
|
||||
df.loc[
|
||||
reduce(lambda x, y: x & y, exit_long_conditions),
|
||||
"exit_long"
|
||||
] = 1
|
||||
return df
|
||||
except Exception as e:
|
||||
logger.error(f"Error in populate_exit_trend for {metadata['pair']}: {str(e)}")
|
||||
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":
|
||||
# 检查当前周期是否已有下单
|
||||
if self.cycle_trade_pair is not None:
|
||||
logger.debug(f"周期内已有下单 {self.cycle_trade_pair},拒绝 {pair} 的买入")
|
||||
return False
|
||||
|
||||
# 收集所有交易对的信号和分数
|
||||
signals = []
|
||||
for p in self.config["exchange"]["pair_whitelist"]:
|
||||
p_df, _ = self.dp.get_analyzed_dataframe(p, self.timeframe)
|
||||
if p_df is not None and not p_df.empty and "enter_long" in p_df.columns:
|
||||
if p_df["enter_long"].iloc[-1] == 1:
|
||||
score = p_df["entry_score"].iloc[-1] if "entry_score" in p_df.columns else -float('inf')
|
||||
signals.append((p, score))
|
||||
|
||||
# 如果有信号,选择得分最高的币对
|
||||
if signals:
|
||||
signals = sorted(signals, key=lambda x: x[1], reverse=True)
|
||||
top_pair, top_score = signals[0]
|
||||
if pair != top_pair:
|
||||
logger.debug(f"{pair} 不是最高得分币对 {top_pair} (score={top_score:.2f}),拒绝买入")
|
||||
return False
|
||||
else:
|
||||
logger.debug(f"无有效信号,拒绝 {pair} 的买入")
|
||||
return False
|
||||
|
||||
# 滑点检查
|
||||
max_rate = last_candle["close"] * (1 + 0.0025) # 0.25% 滑点阈值
|
||||
if rate > max_rate:
|
||||
logger.debug(f"拒绝 {pair} 的买入,价格 {rate} 超过最大允许价格 {max_rate}")
|
||||
return False
|
||||
|
||||
# 记录下单币对
|
||||
self.cycle_trade_pair = pair
|
||||
logger.info(f"确认 {pair} 的买入,周期内最优信号 (score={last_candle['entry_score']:.2f})")
|
||||
return True
|
||||
elif side == "short":
|
||||
logger.warning(f"{pair} 尝试做空,但策略不支持做空 (can_short={self.can_short})")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"确认 {pair} 交易时出错:{str(e)}")
|
||||
return False
|
||||
conditions = [
|
||||
df["&-sell_signal"] > 0.5,
|
||||
]
|
||||
df.loc[reduce(lambda x, y: x & y, conditions), 'exit_long'] = 1
|
||||
return df
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user