2075 lines
93 KiB
Bash
2075 lines
93 KiB
Bash
self.risk_position_multiplier = regime_config["position"]
|
||
self.risk_stoploss_multiplier = regime_config["stoploss"]
|
||
|
||
logger.info(f"[{pair}] {regime_config['label']} - 仓位倍数: {self.risk_position_multiplier}, "
|
||
f"止损倍数: {self.risk_stoploss_multiplier}")
|
||
else:
|
||
# 默认值
|
||
self.risk_position_multiplier = 1.0
|
||
self.risk_stoploss_multiplier = 1.0
|
||
|
||
logger.info(f"[{pair}] 市场趋势得分:{market_trend_score}, labels_mean:{labels_mean:.4f}, labels_std:{labels_std:.4f}")
|
||
logger.info(f"[{pair}] k_buy:{k_buy:.2f}, k_sell:{k_sell:.2f}")
|
||
logger.info(f"[{pair}] 动态买入阈值:{self.buy_threshold:.4f}, 卖出阈值:{self.sell_threshold:.4f}")
|
||
logger.info(f"[{pair}] 最新数据 - close: {dataframe['close'].iloc[-1]:.6f}, "
|
||
f"rsi: {dataframe['rsi'].iloc[-1]:.2f}, "
|
||
f"&-price_value_divergence: {dataframe['&-price_value_divergence'].iloc[-1]:.6f}, "
|
||
f"volume_z_score: {dataframe['volume_z_score'].iloc[-1]:.2f}, "
|
||
f"bb_lowerband: {dataframe['bb_lowerband'].iloc[-1]:.6f}, "
|
||
f"market_regime: {dataframe['&*-market_regime'].iloc[-1] if '&*-market_regime' in dataframe else 'N/A'}")
|
||
|
||
if not self.stats_logged:
|
||
logger.info("===== 所有币对的 labels_mean 和 labels_std 汇总 =====")
|
||
for p, stats in self.pair_stats.items():
|
||
logger.info(f"[{p}] labels_mean:{stats['labels_mean']:.4f}, labels_std:{stats['labels_std']:.4f}")
|
||
logger.info("==============================================")
|
||
self.stats_logged = True
|
||
|
||
|
||
|
||
return dataframe
|
||
|
||
def populate_entry_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
pair = metadata.get('pair', 'Unknown')
|
||
original_length = len(dataframe)
|
||
original_index = dataframe.index
|
||
|
||
conditions = []
|
||
logger.info(f"[{pair}] populate_entry_trend 被调用,原始数据长度:{original_length},时间:{dataframe.index[-1]}")
|
||
# 获取市场趋势得分
|
||
trend_score = self.get_market_trend(dataframe=dataframe, metadata=metadata)
|
||
|
||
# 动态调整成交量和 RSI 阈值
|
||
volume_z_score_min = 0.5
|
||
volume_z_score_max = 1.5
|
||
volume_z_score_threshold = self.linear_map(trend_score, 0, 100, volume_z_score_max, volume_z_score_min)
|
||
rsi_min = 35
|
||
rsi_max = 55
|
||
rsi_threshold = self.linear_map(trend_score, 0, 100, rsi_max, rsi_min)
|
||
stochrsi_min = 25
|
||
stochrsi_max = 45
|
||
stochrsi_threshold = self.linear_map(trend_score, 0, 100, stochrsi_max, stochrsi_min)
|
||
|
||
# 计算熊市信号和 STOCHRSI 超买信号
|
||
bearish_signal = self.is_bearish_market(dataframe, metadata, timeframe="1h")
|
||
bearish_signal_aligned = bearish_signal.reindex(dataframe.index, method='ffill').fillna(False)
|
||
stochrsi_overbought = self.is_stochrsi_overbought(dataframe, period=10, threshold=85)
|
||
stochrsi_overbought_aligned = stochrsi_overbought.reindex(dataframe.index, method='ffill').fillna(True)
|
||
|
||
# 检测趋势状态
|
||
trend_status = self.detect_trend_status(dataframe, metadata)
|
||
|
||
# 根据趋势状态调整入场策略
|
||
if "&-price_value_divergence" in dataframe.columns:
|
||
# 检测当前持仓订单数量(用于绿色通道判断)
|
||
open_trades = len(self.active_trades) if hasattr(self, 'active_trades') else 0
|
||
is_green_channel = (trend_status == "bullish" and open_trades <= 2 and self.GREEN_CHANNEL_ENABLED)
|
||
|
||
# 获取市场状态并调整入场条件
|
||
market_regime = dataframe["&*-market_regime"].iloc[-1] if "&*-market_regime" in dataframe.columns else 2
|
||
|
||
# 市场状态影响入场严格程度
|
||
regime_adjustments = {
|
||
0: {"threshold_mult": 1.5, "strict_mult": 0.7}, # 低波动震荡:更宽松入场
|
||
1: {"threshold_mult": 1.2, "strict_mult": 0.8}, # 正常趋势:较宽松
|
||
2: {"threshold_mult": 1.0, "strict_mult": 1.0}, # 高波动趋势:标准
|
||
3: {"threshold_mult": 0.8, "strict_mult": 1.2}, # 极端波动:更严格
|
||
4: {"threshold_mult": 0.6, "strict_mult": 1.5}, # 黑天鹅状态:最严格
|
||
}
|
||
|
||
regime_adj = regime_adjustments.get(market_regime, regime_adjustments[2])
|
||
|
||
logger.info(f"[{pair}] 市场状态: {market_regime}, 阈值调整: {regime_adj['threshold_mult']}, 严格度调整: {regime_adj['strict_mult']}")
|
||
|
||
if is_green_channel:
|
||
# 🟢 牛市绿色通道:持仓≤2个,25USDT入场,5条件需要满足4个
|
||
cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold * 1.8) # 超宽松偏离度
|
||
cond2 = (dataframe["volume_z_score"] > volume_z_score_threshold * 0.4) # 超低成交量要求
|
||
cond3 = (dataframe["rsi"] < rsi_threshold * 1.4) # 超宽松RSI
|
||
cond4 = (dataframe["close"] <= dataframe["bb_upperband"] * 1.05) # 允许上轨附近
|
||
cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold * 1.4) # 超宽松STOCHRSI
|
||
|
||
core_conditions = [cond1, cond2, cond3, cond4, cond5]
|
||
# 使用向量化操作计算满足条件的数量
|
||
satisfied_count_vector = cond1.astype(int) + cond2.astype(int) + cond3.astype(int) + cond4.astype(int) + cond5.astype(int)
|
||
buy_condition = satisfied_count_vector >= 4
|
||
|
||
# 仅在日志中使用最后一行的值
|
||
if len(dataframe) > 0:
|
||
satisfied_count = satisfied_count_vector.iloc[-1]
|
||
logger.info(f"[{pair}] 🟢 牛市绿色通道:持仓{open_trades}≤2个,25USDT入场,5条件需要满足{satisfied_count}/4个")
|
||
|
||
elif trend_status == "bullish":
|
||
# 牛市正常通道:持仓>2个,75USDT入场,必须满足全部7个条件
|
||
cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold * 1.5) # 放宽到1.5倍
|
||
cond2 = (dataframe["volume_z_score"] > volume_z_score_threshold * 0.7) # 降低成交量要求
|
||
cond3 = (dataframe["rsi"] < rsi_threshold * 1.2) # 放宽RSI要求
|
||
cond4 = (dataframe["close"] <= dataframe["bb_upperband"]) # 可以在上轨附近入场
|
||
cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold * 1.2) # 放宽STOCHRSI要求
|
||
cond6 = pd.Series([True] * len(dataframe), index=dataframe.index) # 取消熊市过滤
|
||
cond7 = pd.Series([True] * len(dataframe), index=dataframe.index) # 取消超买过滤
|
||
buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7
|
||
logger.info(f"[{pair}] 🚀 牛市正常通道:持仓{open_trades}>2个,75USDT入场,必须满足全部7个条件")
|
||
|
||
elif trend_status == "bearish":
|
||
# 下跌趋势:严格入场条件,只抄底
|
||
cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold * 0.7) # 严格到0.7倍
|
||
cond2 = (dataframe["volume_z_score"] > volume_z_score_threshold * 1.3) # 提高成交量要求
|
||
cond3 = (dataframe["rsi"] < rsi_threshold * 0.8) # 严格RSI要求
|
||
cond4 = (dataframe["close"] <= dataframe["bb_lowerband"] * 0.95) # 必须跌破下轨
|
||
cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold * 0.8) # 严格STOCHRSI要求
|
||
cond6 = ~bearish_signal_aligned # 保持熊市过滤
|
||
cond7 = ~stochrsi_overbought_aligned # 保持超买过滤
|
||
buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7
|
||
logger.info(f"[{pair}] 📉 下跌趋势策略:严格入场条件")
|
||
|
||
else: # ranging
|
||
# 震荡趋势:使用原策略
|
||
cond1 = (dataframe["&-price_value_divergence"] < self.buy_threshold)
|
||
cond2 = (dataframe["volume_z_score"] > volume_z_score_threshold)
|
||
cond3 = (dataframe["rsi"] < rsi_threshold)
|
||
cond4 = (dataframe["close"] <= dataframe["bb_lowerband"])
|
||
cond5 = (dataframe["stochrsi_k"] < stochrsi_threshold)
|
||
cond6 = ~bearish_signal_aligned
|
||
cond7 = ~stochrsi_overbought_aligned
|
||
buy_condition = cond1 & cond2 & cond3 & cond4 & cond5 & cond6 & cond7
|
||
logger.info(f"[{pair}] ⚖️ 震荡趋势策略:标准入场条件")
|
||
|
||
# 绿色通道和趋势状态的条件已经设置好buy_condition
|
||
conditions.append(buy_condition)
|
||
|
||
# 调试日志 - 仅在日志中使用最后一行的值
|
||
if len(dataframe) > 0:
|
||
divergence_value = dataframe["&-price_value_divergence"].iloc[-1]
|
||
volume_z_score_value = dataframe["volume_z_score"].iloc[-1]
|
||
rsi_value = dataframe["rsi"].iloc[-1]
|
||
stochrsi_value = dataframe["stochrsi_k"].iloc[-1]
|
||
bb_close_value = dataframe["close"].iloc[-1]
|
||
bb_lower_value = dataframe["bb_lowerband"].iloc[-1]
|
||
bb_upper_value = dataframe["bb_upperband"].iloc[-1]
|
||
|
||
# 输出关键指标以便调试
|
||
logger.info(f"[{pair}] 📊 关键指标 - 偏离度: {divergence_value:.6f}, RSI: {rsi_value:.2f}, STOCHRSI: {stochrsi_value:.2f}, 价格: {bb_close_value:.4f}, 下轨: {bb_lower_value:.4f}, 上轨: {bb_upper_value:.4f}")
|
||
logger.info(f"[{pair}] 🔍 买入阈值 - 偏离度: {self.buy_threshold:.6f}, RSI: {rsi_threshold:.2f}, STOCHRSI: {stochrsi_threshold:.2f}")
|
||
|
||
# 获取条件结果的最后一行值(仅用于日志)
|
||
cond1_last = cond1.iloc[-1]
|
||
cond2_last = cond2.iloc[-1]
|
||
cond3_last = cond3.iloc[-1]
|
||
cond4_last = cond4.iloc[-1]
|
||
cond5_last = cond5.iloc[-1]
|
||
cond6_last = cond6.iloc[-1] if trend_status != "bullish" or open_trades > 2 else False
|
||
cond7_last = cond7.iloc[-1] if trend_status != "bullish" or open_trades > 2 else False
|
||
|
||
# 定义条件名称和状态(仅用于日志)
|
||
conditions_summary = [
|
||
("&-price_value_divergence", divergence_value, "<", self.buy_threshold, cond1_last),
|
||
("volume_z_score", volume_z_score_value, ">", volume_z_score_threshold, cond2_last),
|
||
("rsi", rsi_value, "<", rsi_threshold, cond3_last),
|
||
("close <= bb_lowerband", bb_close_value, "<=", bb_lower_value, cond4_last),
|
||
("stochrsi_k", stochrsi_value, "<", stochrsi_threshold, cond5_last),
|
||
]
|
||
|
||
# 根据趋势状态添加对应的条件6和7
|
||
if trend_status != "bullish" or open_trades > 2:
|
||
conditions_summary.extend([
|
||
("非熊市", None, None, None, cond6_last),
|
||
("STOCHRSI未持续超买", None, None, None, cond7_last),
|
||
])
|
||
|
||
# 输出每个条件的状态
|
||
logger.info(f"[{pair}] === 买入条件检查 ===")
|
||
failed_conditions = []
|
||
for name, value, operator, threshold, result in conditions_summary:
|
||
status = "✅" if result else "❌"
|
||
if value is not None and threshold is not None:
|
||
logger.info(f"[{pair}] {status} {name}: {value:.6f} {operator} {threshold:.6f}")
|
||
else:
|
||
logger.info(f"[{pair}] {status} {name}")
|
||
if not result:
|
||
failed_conditions.append(name)
|
||
else:
|
||
logger.warning(f"[{pair}] &-price_value_divergence 列缺失,跳过买入信号生成")
|
||
|
||
if conditions:
|
||
combined_condition = reduce(lambda x, y: x & y, conditions)
|
||
dataframe.loc[combined_condition, 'enter_long'] = 1
|
||
|
||
# 输出每个条件的状态
|
||
logger.info(f"[{pair}] === 买入条件检查 ===")
|
||
satisfied_conditions = []
|
||
for name, value, operator, threshold, result in conditions_summary:
|
||
status = "✅" if result else "❌"
|
||
if value is not None and threshold is not None:
|
||
logger.info(f"[{pair}] {status} {name}: {value:.6f} {operator} {threshold:.6f}")
|
||
else:
|
||
logger.info(f"[{pair}] {status} {name}")
|
||
if result:
|
||
satisfied_conditions.append(name)
|
||
|
||
# 总结满足的条件
|
||
if combined_condition.any():
|
||
logger.info(f"[{pair}] ✅ 买入信号触发,满足条件: {', '.join(satisfied_conditions)},趋势得分:{trend_score:.2f}")
|
||
else:
|
||
logger.info(f"[{pair}] ❌ 买入条件未满足")
|
||
|
||
else:
|
||
logger.info(f"[{pair}] 无有效买入条件")
|
||
|
||
# 记录各条件触发率
|
||
logger.info(f"[{pair}] 各条件触发率 - "
|
||
f"cond1: {cond1.mean():.2%}, "
|
||
f"cond2: {cond2.mean():.2%}, "
|
||
f"cond3: {cond3.mean():.2%}, "
|
||
f"cond4: {cond4.mean():.2%}, "
|
||
f"cond5: {cond5.mean():.2%}, "
|
||
f"buy_condition: {buy_condition.mean():.2%}")
|
||
# 记录 enter_long 信号统计
|
||
enter_long_count = dataframe['enter_long'].sum() if 'enter_long' in dataframe.columns else 0
|
||
logger.info(f"[{pair}] enter_long 信号总数:{enter_long_count}")
|
||
|
||
# 数据完整性检查和长度验证修复
|
||
if len(dataframe) > 0:
|
||
# 验证数据长度一致性
|
||
current_length = len(dataframe)
|
||
if current_length != original_length:
|
||
logger.warning(f"[{pair}] ⚠️ DataFrame长度不匹配!原始长度: {original_length}, 当前长度: {current_length}")
|
||
|
||
# 修复数据长度
|
||
if current_length < original_length:
|
||
# 数据行数不足,需要填充
|
||
missing_rows = original_length - current_length
|
||
logger.info(f"[{pair}] 填充缺失的 {missing_rows} 行数据...")
|
||
|
||
# 创建缺失行的索引
|
||
missing_index = original_index.difference(dataframe.index)
|
||
if len(missing_index) > 0:
|
||
# 用最后一行的数据填充缺失行
|
||
last_row = dataframe.iloc[-1:].copy()
|
||
filled_rows = pd.DataFrame(index=missing_index)
|
||
for col in dataframe.columns:
|
||
filled_rows[col] = last_row[col].iloc[0] if len(last_row) > 0 else 0
|
||
|
||
# 合并数据
|
||
dataframe = pd.concat([dataframe, filled_rows])
|
||
dataframe = dataframe.reindex(original_index)
|
||
logger.info(f"[{pair}] 数据填充完成,新长度: {len(dataframe)}")
|
||
|
||
elif current_length > original_length:
|
||
# 数据行数过多,截断到原始长度
|
||
excess_rows = current_length - original_length
|
||
logger.info(f"[{pair}] 截断多余的 {excess_rows} 行数据...")
|
||
dataframe = dataframe.iloc[:original_length].copy()
|
||
dataframe = dataframe.reindex(original_index)
|
||
logger.info(f"[{pair}] 数据截断完成,新长度: {len(dataframe)}")
|
||
|
||
else:
|
||
# 长度一致但索引可能不同,重新对齐索引
|
||
dataframe = dataframe.reindex(original_index)
|
||
logger.info(f"[{pair}] 索引重新对齐完成,长度: {len(dataframe)}")
|
||
|
||
# 处理NaN值
|
||
nan_columns = [col for col in dataframe.columns if dataframe[col].isna().any()]
|
||
if nan_columns:
|
||
logger.warning(f"[{pair}] 发现NaN值的列: {nan_columns}")
|
||
for col in nan_columns:
|
||
nan_count = dataframe[col].isna().sum()
|
||
if nan_count > 0:
|
||
logger.warning(f"[{pair}] 列 {col} 有 {nan_count} 个NaN值,正在清理...")
|
||
# 使用前向填充,然后零填充
|
||
dataframe[col] = dataframe[col].ffill().fillna(0)
|
||
|
||
# 最终验证
|
||
final_length = len(dataframe)
|
||
if final_length != original_length:
|
||
logger.error(f"[{pair}] ❌ 数据长度修复失败!期望: {original_length}, 实际: {final_length}")
|
||
# 强制截断或填充到原始长度
|
||
if final_length > original_length:
|
||
dataframe = dataframe.iloc[:original_length]
|
||
elif final_length < original_length:
|
||
# 复制最后一行填充
|
||
last_row = dataframe.iloc[-1:].copy()
|
||
while len(dataframe) < original_length:
|
||
dataframe = pd.concat([dataframe, last_row])
|
||
dataframe = dataframe.iloc[:original_length]
|
||
logger.info(f"[{pair}] 强制修复完成,最终长度: {len(dataframe)}")
|
||
else:
|
||
logger.info(f"[{pair}] ✅ 数据长度验证通过,最终长度: {final_length}")
|
||
|
||
return dataframe
|
||
|
||
def populate_exit_trend(self, dataframe: DataFrame, metadata: dict) -> DataFrame:
|
||
pair = metadata.get('pair', 'Unknown')
|
||
|
||
# 记录原始数据长度和索引
|
||
original_length = len(dataframe)
|
||
original_index = dataframe.index
|
||
logger.info(f"[{pair}] populate_exit_trend 开始处理,原始数据长度:{original_length}")
|
||
|
||
conditions = []
|
||
|
||
if "&-price_value_divergence" in dataframe.columns:
|
||
# 计算额外指标:StochRSI、ADX 和短期价格变化
|
||
stochrsi = ta.STOCHRSI(dataframe, timeperiod=14, fastk_period=3, fastd_period=3)
|
||
dataframe["stochrsi_k"] = stochrsi["fastk"]
|
||
dataframe["adx"] = ta.ADX(dataframe, timeperiod=14)
|
||
# 计算短期价格涨幅(最近 5 根 K 线,约 15 分钟)
|
||
dataframe["short_term_return"] = dataframe["close"].pct_change(5, fill_method=None) * 100 # 百分比回报
|
||
|
||
# 清理新计算列的NaN值
|
||
for col in ["stochrsi_k", "adx", "short_term_return"]:
|
||
if col in dataframe.columns:
|
||
dataframe[col] = dataframe[col].replace([np.inf, -np.inf], 0).ffill().fillna(0)
|
||
|
||
# 获取市场趋势得分
|
||
trend_score = self.get_market_trend(dataframe=dataframe, metadata={'pair': pair})
|
||
|
||
# 条件 1:高阈值 &-price_value_divergence
|
||
cond1 = (
|
||
(dataframe["&-price_value_divergence"] > self.sell_threshold * float(self.exit_divergence_multiplier.value)) &
|
||
(dataframe["adx"] > float(self.exit_adx_threshold.value)) # 趋势强度过滤
|
||
)
|
||
|
||
# 条件 2:超买信号
|
||
cond2 = (
|
||
(dataframe["rsi"] > float(self.exit_rsi_threshold.value)) &
|
||
(dataframe["stochrsi_k"] > float(self.exit_stochrsi_threshold.value)) & # StochRSI 超买
|
||
(dataframe["adx"] > float(self.exit_adx_threshold_overbought.value)) # 趋势强度
|
||
)
|
||
|
||
# 条件 3:快速拉升退出
|
||
# 检测最近 5 根 K 线(约 15 分钟)涨幅超过阈值,且有最低利润要求
|
||
min_profit = float(self.exit_min_profit_threshold.value) # 最低利润要求
|
||
rapid_rise_threshold = self.linear_map(trend_score, 0, 100, float(self.exit_rapid_rise_bear.value), float(self.exit_rapid_rise_bull.value))
|
||
cond3 = (
|
||
(dataframe["short_term_return"] > rapid_rise_threshold) & # 短期快速拉升
|
||
(dataframe["close"] / dataframe["close"].shift(5) - 1 > min_profit) & # 确保最低利润
|
||
(dataframe["stochrsi_k"] > float(self.exit_stochrsi_rapid.value)) & # 超买确认
|
||
(dataframe["volume_z_score"] > float(self.exit_volume_threshold.value))
|
||
)
|
||
|
||
# 检测趋势状态
|
||
trend_status = self.detect_trend_status(dataframe, metadata)
|
||
|
||
# 根据趋势状态调整出场策略
|
||
if trend_status == "bullish":
|
||
# 上涨趋势:严格出场条件,让利润奔跑
|
||
if trend_score > float(self.exit_bullish_trend_score_max.value):
|
||
logger.info(f"[{pair}] 🚀 强劲上涨趋势,拒绝卖出")
|
||
return dataframe
|
||
|
||
# 上涨趋势下需要更强的卖出信号
|
||
cond1_bullish = (dataframe["&-price_value_divergence"] > self.sell_threshold * float(self.exit_bullish_divergence_mult.value))
|
||
cond2_bullish = (dataframe["rsi"] > float(self.exit_bullish_rsi.value)) & (dataframe["stochrsi_k"] > float(self.exit_bullish_stochrsi.value)) & (dataframe["adx"] > float(self.exit_bullish_adx.value))
|
||
cond3_bullish = (dataframe["short_term_return"] > float(self.exit_bullish_return.value)) & (dataframe["stochrsi_k"] > float(self.exit_bullish_stochrsi_rapid.value))
|
||
|
||
sell_condition = (cond1_bullish & cond2_bullish) | (cond1_bullish & cond3_bullish) | (cond2_bullish & cond3_bullish)
|
||
logger.info(f"[{pair}] 🚀 上涨趋势策略:严格出场条件")
|
||
|
||
elif trend_status == "bearish":
|
||
# 下跌趋势:宽松出场条件,快速止盈止损
|
||
cond1_bearish = (dataframe["&-price_value_divergence"] > self.sell_threshold * float(self.exit_bearish_divergence_mult.value))
|
||
cond2_bearish = (dataframe["rsi"] > float(self.exit_bearish_rsi.value)) & (dataframe["stochrsi_k"] > float(self.exit_bearish_stochrsi.value)) & (dataframe["adx"] > float(self.exit_bearish_adx.value))
|
||
cond3_bearish = (dataframe["short_term_return"] > float(self.exit_bearish_return.value)) & (dataframe["stochrsi_k"] > float(self.exit_bearish_stochrsi_rapid.value))
|
||
|
||
sell_condition = cond1_bearish | cond2_bearish | cond3_bearish # 任一条件即可卖出
|
||
logger.info(f"[{pair}] 📉 下跌趋势策略:宽松出场条件")
|
||
|
||
else: # ranging
|
||
# 震荡趋势:使用原策略
|
||
if trend_score > float(self.exit_ranging_trend_score_max.value):
|
||
logger.info(f"[{pair}] ⚖️ 震荡趋势但得分较高,拒绝卖出")
|
||
return dataframe
|
||
|
||
if trend_score > float(self.exit_ranging_trend_score_threshold.value):
|
||
sell_condition = (cond1 & cond2) | (cond1 & cond3) | (cond2 & cond3) # 中等趋势,至少两个条件满足
|
||
logger.info(f"[{pair}] ⚖️ 震荡趋势策略:标准出场条件")
|
||
else:
|
||
sell_condition = cond1 | cond2 | cond3 # 弱势趋势,任一条件满足
|
||
logger.info(f"[{pair}] ⚖️ 弱势震荡,任一条件满足")
|
||
|
||
conditions.append(sell_condition)
|
||
|
||
# 调试日志 - 仅在日志中使用最后一行的值(这是允许的用途)
|
||
if len(dataframe) > 0:
|
||
divergence_value = dataframe["&-price_value_divergence"].iloc[-1] if not dataframe["&-price_value_divergence"].isna().iloc[-1] else 0.0
|
||
rsi_value = dataframe["rsi"].iloc[-1] if not dataframe["rsi"].isna().iloc[-1] else 0.0
|
||
stochrsi_value = dataframe["stochrsi_k"].iloc[-1] if not dataframe["stochrsi_k"].isna().iloc[-1] else 0.0
|
||
adx_value = dataframe["adx"].iloc[-1] if not dataframe["adx"].isna().iloc[-1] else 0.0
|
||
short_term_return = dataframe["short_term_return"].iloc[-1] if not dataframe["short_term_return"].isna().iloc[-1] else 0.0
|
||
|
||
# 获取条件结果的最后一行值(仅用于日志)
|
||
cond1_last = cond1.iloc[-1]
|
||
cond2_last = cond2.iloc[-1]
|
||
cond3_last = cond3.iloc[-1]
|
||
|
||
logger.info(f"[{pair}] 卖出条件检查 - "
|
||
f"&-price_value_divergence={divergence_value:.6f} > {self.sell_threshold * float(self.exit_divergence_multiplier.value):.6f}: {cond1_last}, "
|
||
f"rsi={rsi_value:.2f} > {float(self.exit_rsi_threshold.value)} & stochrsi_k={stochrsi_value:.2f} > {float(self.exit_stochrsi_threshold.value)}: {cond2_last}, "
|
||
f"short_term_return={short_term_return:.2f}% > {rapid_rise_threshold:.2f}% & profit > {min_profit*100:.2f}%: {cond3_last}, "
|
||
f"adx={adx_value:.2f}, trend_score={trend_score:.2f}")
|
||
else:
|
||
logger.warning(f"[{pair}] ⚠️ &-price_value_divergence 列缺失,跳过该条件")
|
||
|
||
if len(conditions) > 0:
|
||
dataframe.loc[reduce(lambda x, y: x & y, conditions), 'exit_long'] = 1
|
||
logger.info(f"[{pair}] 出场信号触发,条件满足,趋势得分:{trend_score:.2f}")
|
||
else:
|
||
logger.info(f"[{pair}] 无有效卖出条件")
|
||
|
||
# 数据完整性检查和长度验证修复
|
||
if len(dataframe) > 0:
|
||
# 验证数据长度一致性
|
||
current_length = len(dataframe)
|
||
if current_length != original_length:
|
||
logger.warning(f"[{pair}] ⚠️ 卖出DataFrame长度不匹配!原始长度: {original_length}, 当前长度: {current_length}")
|
||
|
||
# 修复数据长度
|
||
if current_length < original_length:
|
||
# 数据行数不足,需要填充
|
||
missing_rows = original_length - current_length
|
||
logger.info(f"[{pair}] 卖出填充缺失的 {missing_rows} 行数据...")
|
||
|
||
# 创建缺失行的索引
|
||
missing_index = original_index.difference(dataframe.index)
|
||
if len(missing_index) > 0:
|
||
# 用最后一行的数据填充缺失行
|
||
last_row = dataframe.iloc[-1:].copy()
|
||
filled_rows = pd.DataFrame(index=missing_index)
|
||
for col in dataframe.columns:
|
||
filled_rows[col] = last_row[col].iloc[0] if len(last_row) > 0 else 0
|
||
|
||
# 合并数据
|
||
dataframe = pd.concat([dataframe, filled_rows])
|
||
dataframe = dataframe.reindex(original_index)
|
||
logger.info(f"[{pair}] 卖出数据填充完成,新长度: {len(dataframe)}")
|
||
|
||
elif current_length > original_length:
|
||
# 数据行数过多,截断到原始长度
|
||
excess_rows = current_length - original_length
|
||
logger.info(f"[{pair}] 卖出截断多余的 {excess_rows} 行数据...")
|
||
dataframe = dataframe.iloc[:original_length].copy()
|
||
dataframe = dataframe.reindex(original_index)
|
||
logger.info(f"[{pair}] 卖出数据截断完成,新长度: {len(dataframe)}")
|
||
|
||
else:
|
||
# 长度一致但索引可能不同,重新对齐索引
|
||
dataframe = dataframe.reindex(original_index)
|
||
logger.info(f"[{pair}] 卖出索引重新对齐完成,长度: {len(dataframe)}")
|
||
|
||
# 处理NaN值
|
||
nan_columns = [col for col in dataframe.columns if dataframe[col].isna().any()]
|
||
if nan_columns:
|
||
logger.warning(f"[{pair}] 卖出检查 - 发现NaN值的列: {nan_columns}")
|
||
for col in nan_columns:
|
||
nan_count = dataframe[col].isna().sum()
|
||
if nan_count > 0:
|
||
logger.warning(f"[{pair}] 卖出检查 - 列 {col} 有 {nan_count} 个NaN值,正在清理...")
|
||
# 使用前向填充,然后零填充
|
||
dataframe[col] = dataframe[col].ffill().fillna(0)
|
||
|
||
# 最终验证
|
||
final_length = len(dataframe)
|
||
if final_length != original_length:
|
||
logger.error(f"[{pair}] ❌ 卖出数据长度修复失败!期望: {original_length}, 实际: {final_length}")
|
||
# 强制截断或填充到原始长度
|
||
if final_length > original_length:
|
||
dataframe = dataframe.iloc[:original_length]
|
||
elif final_length < original_length:
|
||
# 复制最后一行填充
|
||
last_row = dataframe.iloc[-1:].copy()
|
||
while len(dataframe) < original_length:
|
||
dataframe = pd.concat([dataframe, last_row])
|
||
dataframe = dataframe.iloc[:original_length]
|
||
logger.info(f"[{pair}] 卖出强制修复完成,最终长度: {len(dataframe)}")
|
||
else:
|
||
logger.info(f"[{pair}] ✅ 卖出数据长度验证通过,最终长度: {final_length}")
|
||
|
||
# 记录exit_long信号
|
||
exit_long_count = dataframe['exit_long'].sum() if 'exit_long' in dataframe.columns else 0
|
||
logger.info(f"[{pair}] exit_long 信号总数:{exit_long_count}")
|
||
|
||
return dataframe
|
||
|
||
def buy_space(self):
|
||
return [
|
||
DecimalParameter(-0.1, -0.01, name="buy_threshold_min"),
|
||
DecimalParameter(-0.02, -0.001, name="buy_threshold_max"),
|
||
DecimalParameter(-0.05, -0.01, name="add_position_threshold", default=-0.02),
|
||
IntParameter(1, 10, name="cooldown_period_minutes", default=5),
|
||
IntParameter(1, 3, name="max_entry_position_adjustment", default=2)
|
||
]
|
||
|
||
def sell_space(self):
|
||
return [
|
||
DecimalParameter(0.001, 0.02, name="sell_threshold_min"),
|
||
DecimalParameter(0.02, 0.1, name="sell_threshold_max"),
|
||
DecimalParameter(0.2, 0.7, name="exit_position_ratio", default=0.5),
|
||
# 出场策略参数
|
||
DecimalParameter(1.0, 1.3, name="exit_divergence_multiplier"),
|
||
IntParameter(15, 35, name="exit_adx_threshold"),
|
||
IntParameter(55, 75, name="exit_rsi_threshold"),
|
||
IntParameter(60, 80, name="exit_stochrsi_threshold"),
|
||
IntParameter(20, 35, name="exit_adx_threshold_overbought"),
|
||
DecimalParameter(0.005, 0.05, name="exit_min_profit_threshold"),
|
||
DecimalParameter(1.5, 4.0, name="exit_rapid_rise_bull"),
|
||
DecimalParameter(2.5, 5.0, name="exit_rapid_rise_bear"),
|
||
IntParameter(65, 85, name="exit_stochrsi_rapid"),
|
||
DecimalParameter(0.5, 2.0, name="exit_volume_threshold"),
|
||
# 趋势状态相关出场参数
|
||
IntParameter(90, 98, name="exit_bullish_trend_score_max"),
|
||
DecimalParameter(1.1, 1.3, name="exit_bullish_divergence_mult"),
|
||
IntParameter(70, 85, name="exit_bullish_rsi"),
|
||
IntParameter(80, 90, name="exit_bullish_stochrsi"),
|
||
IntParameter(25, 40, name="exit_bullish_adx"),
|
||
DecimalParameter(3.0, 7.0, name="exit_bullish_return"),
|
||
IntParameter(80, 90, name="exit_bullish_stochrsi_rapid"),
|
||
IntParameter(95, 99, name="exit_ranging_trend_score_max"),
|
||
IntParameter(80, 90, name="exit_ranging_trend_score_threshold"),
|
||
DecimalParameter(0.8, 1.0, name="exit_bearish_divergence_mult"),
|
||
IntParameter(55, 65, name="exit_bearish_rsi"),
|
||
IntParameter(65, 75, name="exit_bearish_stochrsi"),
|
||
IntParameter(15, 25, name="exit_bearish_adx"),
|
||
DecimalParameter(1.0, 3.0, name="exit_bearish_return"),
|
||
IntParameter(70, 85, name="exit_bearish_stochrsi_rapid")
|
||
]
|
||
|
||
def adjust_trade_position(self, trade: Trade, current_time: datetime,
|
||
current_rate: float, current_profit: float,
|
||
min_stake: float | None, max_stake: float,
|
||
current_entry_rate: float, current_exit_rate: float,
|
||
current_entry_profit: float, current_exit_profit: float,
|
||
**kwargs) -> float | None | tuple[float | None, str | None]:
|
||
"""
|
||
动态调整仓位:支持加仓、减仓、追踪止损和最大持仓时间限制
|
||
参数:
|
||
- trade: 当前交易对象
|
||
- current_time: 当前时间
|
||
- current_rate: 当前价格
|
||
- current_profit: 当前总盈利
|
||
- min_stake: 最小下注金额
|
||
- max_stake: 最大下注金额
|
||
- current_entry_rate: 当前入场价格
|
||
- current_exit_rate: 当前退出价格
|
||
- current_entry_profit: 当前入场盈利
|
||
- current_exit_profit: 当前退出盈利
|
||
返回:
|
||
- 调整金额(正数为加仓,负数为减仓)或 None
|
||
"""
|
||
pair = trade.pair
|
||
dataframe = self.dp.get_pair_dataframe(pair, self.timeframe)
|
||
trend_score = self.get_market_trend(dataframe=dataframe, metadata={'pair': pair})
|
||
hold_time = (current_time - trade.open_date_utc).total_seconds() / 60
|
||
profit_ratio = (current_rate - trade.open_rate) / trade.open_rate
|
||
|
||
# 检测趋势状态(必须先定义才能使用)
|
||
trend_status = self.detect_trend_status(dataframe, {'pair': pair})
|
||
logger.info(f"{pair} 当前趋势状态: {trend_status}")
|
||
|
||
# 检测当前持仓订单数量
|
||
open_trades = len(self.active_trades) if hasattr(self, 'active_trades') else 0
|
||
|
||
# 牛市绿色通道判断:持仓≤2个且牛市趋势,且绿色通道开关开启
|
||
is_green_channel = (trend_status == "bullish" and open_trades <= 2 and self.GREEN_CHANNEL_ENABLED)
|
||
|
||
# 根据绿色通道调整入场金额
|
||
if is_green_channel:
|
||
initial_stake_amount = 25.0 # 绿色通道:25USDT入场
|
||
logger.info(f"{pair} 🟢 牛市绿色通道:25USDT入场,当前持仓{open_trades}个")
|
||
else:
|
||
initial_stake_amount = 75.0 # 正常通道:75USDT入场
|
||
logger.info(f"{pair} 首次入场金额: {initial_stake_amount:.2f}, 当前持仓金额: {trade.stake_amount:.2f}, "
|
||
f"加仓次数: {trade.nr_of_successful_entries - 1}, 趋势得分: {trend_score:.2f}")
|
||
|
||
# 根据趋势状态调整仓位管理参数
|
||
if trend_status == "bullish":
|
||
# 上涨趋势:积极加仓,放宽止盈
|
||
max_entry_adjustments = min(self.MAX_ENTRY_POSITION_ADJUSTMENT + 1, 5) # 允许更多加仓
|
||
add_position_threshold = self.ADD_POSITION_THRESHOLD * 1.3 # 更宽松的加仓条件
|
||
exit_position_ratio = self.EXIT_POSITION_RATIO * 1.4 # 更高的止盈目标
|
||
trailing_stop_start = self.TRAILING_STOP_START * 1.2 # 更高的启动阈值
|
||
trailing_stop_distance = self.TRAILING_STOP_DISTANCE * 1.5 # 更大的回撤容忍
|
||
logger.info(f"{pair} 🚀 上涨趋势仓位管理参数: max_entries={max_entry_adjustments}, add_thresh={add_position_threshold:.4f}, exit_ratio={exit_position_ratio:.2%}")
|
||
|
||
elif trend_status == "bearish":
|
||
# 下跌趋势:谨慎加仓,严格止盈
|
||
max_entry_adjustments = max(self.MAX_ENTRY_POSITION_ADJUSTMENT - 1, 1) # 减少加仓次数
|
||
add_position_threshold = self.ADD_POSITION_THRESHOLD * 0.7 # 更严格的加仓条件
|
||
exit_position_ratio = self.EXIT_POSITION_RATIO * 0.8 # 更低的止盈目标
|
||
trailing_stop_start = self.TRAILING_STOP_START * 0.8 # 更低的启动阈值
|
||
trailing_stop_distance = self.TRAILING_STOP_DISTANCE * 0.7 # 更严格的止损
|
||
logger.info(f"{pair} 📉 下跌趋势仓位管理参数: max_entries={max_entry_adjustments}, add_thresh={add_position_threshold:.4f}, exit_ratio={exit_position_ratio:.2%}")
|
||
|
||
else: # ranging
|
||
# 震荡趋势:使用标准参数
|
||
max_entry_adjustments = self.MAX_ENTRY_POSITION_ADJUSTMENT
|
||
add_position_threshold = self.ADD_POSITION_THRESHOLD
|
||
exit_position_ratio = self.EXIT_POSITION_RATIO
|
||
trailing_stop_start = self.TRAILING_STOP_START
|
||
trailing_stop_distance = self.TRAILING_STOP_DISTANCE
|
||
logger.info(f"{pair} ⚖️ 震荡趋势仓位管理参数: max_entries={max_entry_adjustments}, add_thresh={add_position_threshold:.4f}, exit_ratio={exit_position_ratio:.2%}")
|
||
|
||
# 加仓逻辑
|
||
if trade.nr_of_successful_entries <= max_entry_adjustments + 1:
|
||
# 动态调整加仓阈值
|
||
if trend_status == "bullish":
|
||
add_threshold = 90 - 20 * (trend_score / 100) # 上涨趋势下更宽松
|
||
elif trend_status == "bearish":
|
||
add_threshold = 70 - 30 * (trend_score / 100) # 下跌趋势下更谨慎
|
||
else:
|
||
add_threshold = 80 - 30 * (trend_score / 100) # 震荡趋势标准
|
||
|
||
if profit_ratio <= add_position_threshold and hold_time > 5 and trend_score <= add_threshold:
|
||
add_count = trade.nr_of_successful_entries - 1
|
||
|
||
# 根据趋势状态调整加仓倍数
|
||
if trend_status == "bullish":
|
||
multipliers = [1.5, 3, 6] # 上涨趋势下更保守的加仓
|
||
elif trend_status == "bearish":
|
||
multipliers = [1, 2, 4] # 下跌趋势下更激进的加仓(抄底)
|
||
else:
|
||
multipliers = [2, 4, 8] # 震荡趋势标准加仓
|
||
|
||
if add_count < len(multipliers):
|
||
multiplier = multipliers[add_count]
|
||
add_amount = initial_stake_amount * multiplier
|
||
|
||
if min_stake is not None and add_amount < min_stake:
|
||
logger.warning(f"{pair} 加仓金额 {add_amount:.2f} 低于最小下注金额 {min_stake:.2f}")
|
||
return (None, f"Add amount {add_amount:.2f} below min_stake {min_stake:.2f}")
|
||
if add_amount > max_stake:
|
||
add_amount = max_stake
|
||
|
||
logger.info(f"{pair} 趋势状态: {trend_status}, 价格下跌 {profit_ratio*100:.2f}%,触发第 {add_count + 1} 次加仓 {add_amount:.2f}")
|
||
return (add_amount, f"Trend: {trend_status}, Price dropped {profit_ratio*100:.2f}%, add {add_amount:.2f}")
|
||
|
||
# 减仓逻辑
|
||
if profit_ratio >= exit_position_ratio:
|
||
# 根据趋势状态调整减仓比例
|
||
if trend_status == "bullish":
|
||
reduce_factor = 0.5 # 上涨趋势下只减仓50%
|
||
elif trend_status == "bearish":
|
||
reduce_factor = 1.0 # 下跌趋势下全部减仓
|
||
else:
|
||
reduce_factor = 0.8 # 震荡趋势下减仓80%
|
||
|
||
reduce_amount = -trade.stake_amount * reduce_factor
|
||
logger.info(f"{pair} 趋势状态: {trend_status}, 利润 {profit_ratio*100:.2f}%,减仓 {abs(reduce_amount):.2f} ({reduce_factor*100:.0f}%)")
|
||
return (reduce_amount, f"Trend: {trend_status}, Profit {profit_ratio*100:.2f}%, reduce {abs(reduce_amount):.2f}")
|
||
|
||
# 追踪止损逻辑
|
||
if profit_ratio >= trailing_stop_start and not self.trailing_stop_enabled:
|
||
self.trailing_stop_enabled = True
|
||
trade.adjust_min_max_rates(current_rate, current_rate)
|
||
logger.info(f"{pair} 趋势状态: {trend_status}, 价格上涨超过 {trailing_stop_start*100:.1f}%,启动追踪止损")
|
||
return None
|
||
|
||
if self.trailing_stop_enabled:
|
||
max_rate = trade.max_rate or current_rate
|
||
trailing_stop_price = max_rate * (1 - trailing_stop_distance)
|
||
if current_rate < trailing_stop_price:
|
||
logger.info(f"{pair} 趋势状态: {trend_status}, 价格回落至 {trailing_stop_price:.6f},触发全部卖出")
|
||
return (-trade.stake_amount, f"Trend: {trend_status}, Trailing stop at {trailing_stop_price:.6f}")
|
||
trade.adjust_min_max_rates(current_rate, trade.min_rate)
|
||
return None
|
||
|
||
return None
|
||
|
||
def confirm_trade_entry(self, pair: str, order_type: str, amount: float, rate: float,
|
||
time_in_force: str, current_time: datetime, **kwargs) -> bool:
|
||
# 调试日志:记录输入参数
|
||
logger.info(f"[{pair}] confirm_trade_entry called with rate={rate}, type(rate)={type(rate)}, "
|
||
f"amount={amount}, order_type={order_type}, time_in_force={time_in_force}")
|
||
|
||
# 检查 rate 是否有效
|
||
if not isinstance(rate, (float, int)) or rate is None:
|
||
logger.error(f"[{pair}] Invalid rate value: {rate} (type: {type(rate)}). Skipping trade entry.")
|
||
return False
|
||
|
||
# 获取当前数据
|
||
dataframe, _ = self.dp.get_analyzed_dataframe(pair, self.timeframe)
|
||
last_candle = dataframe.iloc[-1]
|
||
|
||
market_trend_score = self.get_market_trend(dataframe=DataFrame, metadata={'pair': pair})
|
||
# 修正逻辑:趋势得分越低(熊市),冷却期越长;得分越高(牛市),冷却期越短
|
||
cooldown_period_minutes = self.COOLDOWN_PERIOD_MINUTES if market_trend_score < 50 else self.COOLDOWN_PERIOD_MINUTES // 2
|
||
|
||
if pair in self.last_entry_time:
|
||
last_time = self.last_entry_time[pair]
|
||
if (current_time - last_time).total_seconds() < cooldown_period_minutes * 60:
|
||
logger.info(f"[{pair}] 冷却期内({cooldown_period_minutes} 分钟),跳过本次入场")
|
||
return False
|
||
|
||
self.last_entry_time[pair] = current_time
|
||
self.trailing_stop_enabled = False
|
||
try:
|
||
logger.info(f"[{pair}] 确认入场,价格:{float(rate):.6f}")
|
||
except (ValueError, TypeError) as e:
|
||
logger.error(f"[{pair}] Failed to format rate: {rate} (type: {type(rate)}), error: {e}")
|
||
return False
|
||
|
||
return True
|
||
def confirm_trade_exit(self, pair: str, trade: Trade, order_type: str, amount: float,
|
||
rate: float, time_in_force: str, exit_reason: str,
|
||
current_time: datetime, **kwargs) -> bool:
|
||
adjusted_rate = rate * (1 + 0.0025)
|
||
logger.info(f"[{pair}] 退出交易,原因:{exit_reason}, 原始利润:{trade.calc_profit_ratio(rate):.2%},"f"调整后卖出价:{adjusted_rate:.6f}")
|
||
return True
|
||
|
||
def custom_roi(self, trade: Trade, current_profit: float, current_time: datetime, trade_dur: int,
|
||
current_rate: float = None, min_stake: float | None = None, max_stake: float | None = None) -> dict:
|
||
"""
|
||
动态调整 ROI 表格,基于 FreqAI 预测的 &-price_value_divergence 和 RSI。
|
||
- 负的 divergence(预测上涨)或低 RSI 时提高 ROI。
|
||
- 正的 divergence(预测下跌)或高 RSI 时降低 ROI。
|
||
- 长时间持仓降低 ROI 目标。
|
||
"""
|
||
pair = trade.pair
|
||
logger.info(f"[{pair}] 计算自定义 ROI,当前盈利: {current_profit:.2%}, 持仓时间: {trade_dur} 分钟")
|
||
|
||
# 获取最新数据
|
||
dataframe = self.dp.get_pair_dataframe(pair=pair, timeframe=self.timeframe)
|
||
dataframe = self.populate_indicators(dataframe, {'pair': pair}) # 计算指标
|
||
|
||
# 获取 FreqAI 预测和 RSI
|
||
divergence = dataframe["&-price_value_divergence"].iloc[-1] if "&-price_value_divergence" in dataframe else 0
|
||
rsi = dataframe["rsi"].iloc[-1] if "rsi" in dataframe else 50
|
||
|
||
# 计算调整系数
|
||
# 1. Divergence 调整:负值(预测上涨)-> 提高 ROI,正值(预测下跌)-> 降低 ROI
|
||
divergence_factor = self.linear_map(divergence, -0.1, 0.1, 1.2, 0.8)
|
||
|
||
# 2. RSI 调整:低 RSI(超卖)-> 提高 ROI,高 RSI(超买)-> 降低 ROI
|
||
rsi_factor = self.linear_map(rsi, 30, 70, 1.2, 0.8)
|
||
|
||
# 3. 时间调整:持仓时间越长,ROI 目标降低
|
||
time_factor = self.linear_map(trade_dur, 0, 240, 1.0, 0.7) # 4小时后 ROI 降低到 70%
|
||
|
||
# 综合调整系数
|
||
roi_factor = divergence_factor * rsi_factor * time_factor
|
||
|
||
# 默认 ROI 表格
|
||
base_roi = {
|
||
0: 0.06,
|
||
30: 0.04,
|
||
90: 0.025,
|
||
270: 0.002
|
||
}
|
||
|
||
# 动态调整 ROI,限制在 0% 到 20% 之间
|
||
dynamic_roi = {time: min(max(roi * roi_factor, 0.0), 0.2) for time, roi in base_roi.items()}
|
||
|
||
logger.info(f"[{pair}] Divergence: {divergence:.4f}, RSI: {rsi:.2f}, 持仓时间: {trade_dur} 分钟, "
|
||
f"调整系数: divergence={divergence_factor:.2f}, rsi={rsi_factor:.2f}, time={time_factor:.2f}, "
|
||
f"总系数={roi_factor:.2f}, 动态 ROI 表格: {dynamic_roi}")
|
||
return dynamic_roi
|
||
|
||
def custom_entry_price(self, pair: str, trade: Trade | None, current_time: datetime, proposed_rate: float,
|
||
entry_tag: str | None, side: str, **kwargs) -> float:
|
||
adjusted_rate = proposed_rate * (1 - 0.001)
|
||
logger.info(f"[{pair}] 自定义买入价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f})")
|
||
return adjusted_rate
|
||
|
||
def custom_exit_price(self, pair: str, trade: Trade,
|
||
current_time: datetime, proposed_rate: float,
|
||
current_profit: float, exit_tag: str | None, **kwargs) -> float:
|
||
adjusted_rate = proposed_rate * (1 + 0.0005)
|
||
logger.info(f"[{pair}] 自定义卖出价:{adjusted_rate:.6f}(原价:{proposed_rate:.6f})")
|
||
return adjusted_rate
|
||
|
||
def get_market_trend(self, dataframe: DataFrame = None, metadata: dict = None) -> int:
|
||
try:
|
||
timeframes = ["3m", "15m", "1h"]
|
||
weights = {"3m": 0.3, "15m": 0.35, "1h": 0.35}
|
||
trend_scores = {}
|
||
pair = metadata.get('pair', 'Unknown') if metadata else 'Unknown'
|
||
|
||
# 检查 pair 是否有效
|
||
if pair == 'Unknown':
|
||
logger.error(f"[{pair}] Invalid pair in metadata: {metadata}. Returning default score 50")
|
||
return 50
|
||
|
||
logger.info(f"[{pair}] 正在计算多时间框架市场趋势得分")
|
||
|
||
for tf in timeframes:
|
||
# 优先使用传入的 dataframe(如果匹配主时间框架),否则加载目标币对数据
|
||
pair_df = dataframe if tf == self.timeframe and dataframe is not None else self.dp.get_pair_dataframe(pair=pair, timeframe=tf)
|
||
|
||
min_candles = 200 if tf == "3m" else 100 if tf == "15m" else 50
|
||
if pair_df.empty or len(pair_df) < min_candles:
|
||
logger.warning(f"[{pair}] 数据不足({tf}),使用默认得分:50")
|
||
trend_scores[tf] = 50
|
||
continue
|
||
|
||
# 价格趋势
|
||
ema_short_period = 50 if tf == "3m" else 20 if tf == "15m" else 12
|
||
ema_long_period = 200 if tf == "3m" else 80 if tf == "15m" else 50
|
||
pair_df["ema_short"] = ta.EMA(pair_df, timeperiod=ema_short_period)
|
||
pair_df["ema_long"] = ta.EMA(pair_df, timeperiod=ema_long_period)
|
||
pair_df["ema_short_slope"] = (pair_df["ema_short"] - pair_df["ema_short"].shift(10)) / pair_df["ema_short"].shift(10)
|
||
|
||
price_above_ema = pair_df["close"].iloc[-1] > pair_df["ema_long"].iloc[-1]
|
||
ema_short_above_ema_long = pair_df["ema_short"].iloc[-1] > pair_df["ema_long"].iloc[-1]
|
||
ema_short_slope = pair_df["ema_short_slope"].iloc[-1]
|
||
|
||
price_score = 0
|
||
if price_above_ema:
|
||
price_score += 20
|
||
if ema_short_above_ema_long:
|
||
price_score += 20
|
||
if ema_short_slope > 0.005:
|
||
price_score += 15
|
||
elif ema_short_slope < -0.005:
|
||
price_score -= 15
|
||
|
||
# K线形态
|
||
pair_df["bullish_engulfing"] = (
|
||
(pair_df["close"].shift(1) < pair_df["open"].shift(1)) &
|
||
(pair_df["close"] > pair_df["open"]) &
|
||
(pair_df["close"] > pair_df["open"].shift(1)) &
|
||
(pair_df["open"] < pair_df["close"].shift(1))
|
||
).fillna(False)
|
||
pair_df["bearish_engulfing"] = (
|
||
(pair_df["close"].shift(1) > pair_df["open"].shift(1)) &
|
||
(pair_df["close"] < pair_df["open"]) &
|
||
(pair_df["close"] < pair_df["open"].shift(1)) &
|
||
(pair_df["open"] > pair_df["close"].shift(1))
|
||
).fillna(False)
|
||
|
||
kline_score = 0
|
||
if pair_df["bullish_engulfing"].iloc[-1]:
|
||
kline_score += 15
|
||
elif pair_df["bearish_engulfing"].iloc[-1]:
|
||
kline_score -= 15
|
||
volatility = pair_df["close"].pct_change(10, fill_method=None).std() * 100
|
||
if volatility > 0.5:
|
||
kline_score += 10 if price_score > 0 else -10
|
||
|
||
# StochRSI
|
||
stochrsi = ta.STOCHRSI(pair_df, timeperiod=14, fastk_period=3, fastd_period=3)
|
||
pair_df["stochrsi_k"] = stochrsi["fastk"]
|
||
pair_df["stochrsi_d"] = stochrsi["fastd"]
|
||
|
||
stochrsi_score = 0
|
||
stochrsi_k = pair_df["stochrsi_k"].iloc[-1]
|
||
stochrsi_d = pair_df["stochrsi_d"].iloc[-1]
|
||
if stochrsi_k > 80 and stochrsi_k < stochrsi_d:
|
||
stochrsi_score -= 15
|
||
elif stochrsi_k < 20 and stochrsi_k > stochrsi_d:
|
||
stochrsi_score += 15
|
||
elif stochrsi_k > 50:
|
||
stochrsi_score += 5
|
||
elif stochrsi_k < 50:
|
||
stochrsi_score -= 5
|
||
|
||
# 量价关系
|
||
pair_df["volume_mean_20"] = pair_df["volume"].rolling(20).mean()
|
||
pair_df["volume_std_20"] = pair_df["volume"].rolling(20).std()
|
||
pair_df["volume_z_score"] = (pair_df["volume"] - pair_df["volume_mean_20"]) / pair_df["volume_std_20"]
|
||
pair_df["adx"] = ta.ADX(pair_df, timeperiod=14)
|
||
|
||
volume_score = 0
|
||
if pair_df["volume_z_score"].iloc[-1] > 1.5:
|
||
volume_score += 10 if price_score > 0 else -10
|
||
if pair_df["adx"].iloc[-1] > 25:
|
||
volume_score += 10 if price_score > 0 else -10
|
||
|
||
# 综合得分
|
||
raw_score = price_score + kline_score + stochrsi_score + volume_score
|
||
raw_score = max(min(raw_score, 50), -50)
|
||
|
||
# 对数映射到 [0, 100]
|
||
if raw_score >= 0:
|
||
mapped_score = 50 + 50 * (np.log1p(raw_score / 50) / np.log1p(1))
|
||
else:
|
||
mapped_score = 50 * (np.log1p(-raw_score / 50) / np.log1p(1))
|
||
|
||
trend_scores[tf] = max(0, min(100, int(round(mapped_score))))
|
||
logger.info(f"[{pair}] {tf} 趋势得分:{trend_scores[tf]}, 原始得分:{raw_score}, "
|
||
f"价格得分:{price_score}, K线得分:{kline_score}, "
|
||
f"StochRSI得分:{stochrsi_score}, 量价得分:{volume_score}")
|
||
|
||
# 动态调整权重
|
||
if trend_scores.get("1h", 50) - trend_scores.get("3m", 50) > 20 or trend_scores.get("15m", 50) - trend_scores.get("3m", 50) > 20:
|
||
weights = {"3m": 0.2, "15m": 0.35, "1h": 0.45}
|
||
logger.info(f"[{pair}] 1h 趋势得分({trend_scores.get('1h', 50)})显著高于 3m({trend_scores.get('3m', 50)}),调整权重为 {weights}")
|
||
|
||
# 加权融合
|
||
final_score = sum(trend_scores[tf] * weights[tf] for tf in timeframes)
|
||
final_score = int(round(final_score))
|
||
final_score = max(0, min(100, final_score))
|
||
|
||
logger.info(f"[{pair}] 最终趋势得分:{final_score}, "
|
||
f"3m得分:{trend_scores.get('3m', 50)}, 15m得分:{trend_scores.get('15m', 50)}, "
|
||
f"1h得分:{trend_scores.get('1h', 50)}")
|
||
return final_score
|
||
|
||
except Exception as e:
|
||
logger.error(f"[{pair}] 获取市场趋势失败:{e}", exc_info=True)
|
||
return 50
|
||
|
||
def get_trend_score_with_cache(self, pair: str, timeframe: str, timestamp: int, dataframe: DataFrame, metadata: dict) -> float:
|
||
"""
|
||
三级缓存架构获取趋势得分:
|
||
1. 本地内存缓存(一级缓存)- 最快,无网络开销
|
||
2. Redis缓存(二级缓存)- 局域网共享,减少重复计算
|
||
3. 实时计算(三级缓存)- 最慢,但保证准确性
|
||
"""
|
||
|
||
# 初始化本地缓存(如果尚未初始化)
|
||
if not hasattr(self, '_local_cache'):
|
||
self._local_cache = {}
|
||
self._local_cache_stats = {'hits': 0, 'misses': 0, 'redis_hits': 0, 'computes': 0}
|
||
logger.info("🚀 初始化本地内存缓存")
|
||
|
||
# 创建 Redis 客户端(如果尚未创建)
|
||
if not hasattr(self, 'redis_client') and self.redis_url:
|
||
try:
|
||
self.redis_client = redis.from_url(self.redis_url)
|
||
logger.info("✅ Redis 客户端已成功初始化")
|
||
except Exception as e:
|
||
logger.error(f"❌ 初始化 Redis 客户端失败: {e}")
|
||
# Redis 失败时继续运行,降级为本地缓存
|
||
self.redis_client = None
|
||
|
||
# 生成统一的缓存键
|
||
strategy_name = "freqaiprimer"
|
||
timeframes_str = "3m-15m-1h"
|
||
# 将时间戳精度从秒改为分钟,去掉最后两位秒数
|
||
minute_timestamp = int(timestamp // 60) * 60 # 向下取整到分钟
|
||
cache_key = f"{strategy_name}|trend_score|{pair.replace('/', '-')}|{timeframes_str}|{minute_timestamp}"
|
||
logger.debug(f"[{pair}] 生成缓存键:{cache_key} (时间戳: {minute_timestamp})")
|
||
|
||
# 🎯 一级缓存:本地内存检查
|
||
if cache_key in self._local_cache:
|
||
cached_score = self._local_cache[cache_key]
|
||
self._local_cache_stats['hits'] += 1
|
||
# 只在调试模式下显示单个缓存命中,正常模式下只统计
|
||
logger.debug(f"[{pair}] 🟢 本地缓存命中:key={cache_key}, value={cached_score:.2f}")
|
||
return cached_score
|
||
|
||
self._local_cache_stats['misses'] += 1
|
||
logger.debug(f"[{pair}] 本地缓存未命中:key={cache_key}")
|
||
|
||
# 🎯 二级缓存:Redis检查
|
||
redis_client = self._get_redis_client()
|
||
if redis_client:
|
||
logger.debug(f"[{pair}] 尝试从 Redis 查询趋势得分,key={cache_key}")
|
||
try:
|
||
cached_score = redis_client.get(cache_key)
|
||
if cached_score is not None:
|
||
score = float(cached_score)
|
||
# 同时写入本地缓存,加速后续访问
|
||
self._local_cache[cache_key] = score
|
||
self._local_cache_stats['redis_hits'] += 1
|
||
logger.debug(f"[{pair}] 🟡 Redis 缓存命中:key={cache_key}, value={score:.2f} (已同步到本地缓存)")
|
||
return score
|
||
else:
|
||
logger.debug(f"[{pair}] 🔴 Redis 缓存未命中:key={cache_key}")
|
||
except Exception as e:
|
||
logger.error(f"[{pair}] Redis 查询失败,key={cache_key}, 错误: {e}")
|
||
# Redis 失败时继续运行,降级为本地缓存
|
||
|
||
# 🎯 三级缓存:实时计算
|
||
logger.info(f"[{pair}] 开始计算趋势得分 (时间戳: {timestamp})")
|
||
trend_score = self.get_market_trend(dataframe=dataframe, metadata=metadata)
|
||
self._local_cache_stats['computes'] += 1
|
||
logger.info(f"[{pair}] ✅ 成功计算趋势得分: {trend_score:.2f}")
|
||
|
||
# 将结果写入两级缓存
|
||
self._local_cache[cache_key] = trend_score
|
||
|
||
redis_client = self._get_redis_client()
|
||
if redis_client:
|
||
logger.debug(f"[{pair}] 尝试将趋势得分写入 Redis,key={cache_key}, value={trend_score:.2f}")
|
||
try:
|
||
redis_client.setex(cache_key, 86400, trend_score)
|
||
logger.info(f"[{pair}] ✅ 成功将趋势得分存储到 Redis,key={cache_key}, value={trend_score:.2f}")
|
||
except Exception as e:
|
||
logger.error(f"[{pair}] Redis 写入失败,key={cache_key}, 错误: {e}")
|
||
# Redis 失败时继续运行
|
||
|
||
# 定期打印缓存统计信息(更频繁地显示本轮命中统计)
|
||
total_requests = sum(self._local_cache_stats.values())
|
||
if total_requests % 50 == 0 and total_requests > 0: # 每50次显示一次统计
|
||
local_hits = self._local_cache_stats['hits']
|
||
redis_hits = self._local_cache_stats['redis_hits']
|
||
hit_rate = (local_hits + redis_hits) / total_requests * 100
|
||
logger.info(f"📊 本轮缓存统计 - 本地命中: {local_hits}次, "
|
||
f"Redis命中: {redis_hits}次, "
|
||
f"计算: {self._local_cache_stats['computes']}次, "
|
||
f"总命中率: {hit_rate:.1f}%")
|
||
|
||
return trend_score
|
||
|
||
def cleanup_local_cache(self):
|
||
"""
|
||
清理本地缓存,防止内存泄漏
|
||
"""
|
||
if hasattr(self, '_local_cache'):
|
||
cache_size = len(self._local_cache)
|
||
self._local_cache.clear()
|
||
logger.info(f"🧹 已清理本地缓存,清除条目数: {cache_size}")
|
||
|
||
def get_current_round_cache_hits(self) -> int:
|
||
"""
|
||
获取当前轮次的缓存命中总数(本地+Redis)
|
||
"""
|
||
if hasattr(self, '_local_cache_stats'):
|
||
return self._local_cache_stats['hits'] + self._local_cache_stats['redis_hits']
|
||
return 0
|
||
if hasattr(self, '_local_cache_stats'):
|
||
total_requests = sum(self._local_cache_stats.values())
|
||
if total_requests > 0:
|
||
local_hits = self._local_cache_stats['hits']
|
||
redis_hits = self._local_cache_stats['redis_hits']
|
||
return {
|
||
'local_hits': local_hits,
|
||
'redis_hits': redis_hits,
|
||
'misses': self._local_cache_stats['misses'],
|
||
'computes': self._local_cache_stats['computes'],
|
||
'total_requests': total_requests,
|
||
'local_hit_rate': local_hits / total_requests * 100,
|
||
'redis_hit_rate': redis_hits / total_requests * 100,
|
||
'overall_hit_rate': (local_hits + redis_hits) / total_requests * 100,
|
||
'current_round_hits': local_hits + redis_hits # 当前轮次总命中数
|
||
}
|
||
return {'local_hits': 0, 'redis_hits': 0, 'misses': 0, 'computes': 0, 'total_requests': 0, 'current_round_hits': 0}
|
||
|
||
def bot_loop_start(self, current_time: datetime, **kwargs) -> None:
|
||
"""
|
||
每个交易循环开始时调用,用于自动清理本地缓存
|
||
"""
|
||
if hasattr(self, '_local_cache') and self._local_cache:
|
||
cache_size = len(self._local_cache)
|
||
|
||
# 每1000次循环清理一次,或当缓存超过500条时清理
|
||
if not hasattr(self, '_loop_counter'):
|
||
self._loop_counter = 0
|
||
|
||
self._loop_counter += 1
|
||
|
||
if self._loop_counter % 1000 == 0 or cache_size > 500:
|
||
logger.info(f"🧹 自动清理本地缓存 - 循环次数: {self._loop_counter}, 缓存大小: {cache_size}")
|
||
self.cleanup_local_cache()
|
||
|
||
# 打印缓存统计
|
||
stats = self.get_cache_stats()
|
||
if stats['total_requests'] > 0:
|
||
logger.info(f"📊 缓存性能统计: 本地命中率 {stats['local_hit_rate']:.1f}%, "
|
||
f"Redis命中率 {stats['redis_hit_rate']:.1f}%, "
|
||
f"总命中率 {stats['overall_hit_rate']:.1f}%")
|
||
|
||
def bot_start(self, **kwargs) -> None:
|
||
"""
|
||
机器人启动时调用,初始化缓存相关设置
|
||
"""
|
||
logger.info("🚀 策略启动 - 初始化多级缓存系统")
|
||
|
||
# 初始化本地缓存
|
||
if not hasattr(self, '_local_cache'):
|
||
self._local_cache = {}
|
||
self._local_cache_stats = {'hits': 0, 'misses': 0, 'redis_hits': 0, 'computes': 0}
|
||
logger.info("✅ 本地内存缓存已初始化")
|
||
|
||
# 设置最大本地缓存大小
|
||
self._max_local_cache_size = 500
|
||
|
||
# 初始化循环计数器
|
||
self._loop_counter = 0
|
||
|
||
def bot_stop(self, **kwargs) -> None:
|
||
"""
|
||
机器人停止时调用,清理所有缓存
|
||
"""
|
||
logger.info("🛑 策略停止 - 清理所有缓存")
|
||
|
||
# 清理本地缓存
|
||
if hasattr(self, '_local_cache'):
|
||
cache_size = len(self._local_cache)
|
||
self.cleanup_local_cache()
|
||
|
||
# 打印最终统计
|
||
stats = self.get_cache_stats()
|
||
if stats['total_requests'] > 0:
|
||
logger.info(f"📊 最终缓存统计 - 总请求: {stats['total_requests']}, "
|
||
f"本地命中: {stats['local_hits']}, "
|
||
f"Redis命中: {stats['redis_hits']}, "
|
||
f"计算次数: {stats['computes']}, "
|
||
f"总命中率: {stats['overall_hit_rate']:.1f}%")
|
||
|
||
# 注意:由于使用延迟初始化,无需关闭持久化的Redis连接
|
||
|
||
def detect_trend_status(self, dataframe: DataFrame, metadata: dict) -> str:
|
||
"""
|
||
基于分类模型优化 first_length 的趋势检测
|
||
规则:
|
||
- 使用 LightGBMClassifier 预测最优 first_length 类别
|
||
- 类别映射:0:激进(2), 1:中性(4), 2:稳健(6), 3:保守(8), 4:极保守(10)
|
||
- 根据预测的类别动态调整段长,而非固定使用2
|
||
"""
|
||
pair = metadata.get('pair', 'Unknown')
|
||
|
||
# 检查数据完整性
|
||
if len(dataframe) == 0:
|
||
logger.warning(f"[{pair}] ⚠️ 数据为空,返回震荡趋势")
|
||
return "ranging"
|
||
|
||
try:
|
||
# 使用分类模型预测最优 first_length
|
||
# 检查所有可能的分类列(精确匹配实际列名格式)
|
||
classification_cols = [col for col in dataframe.columns
|
||
if col in ["&*-optimal_first_length", "optimal_first_length_pred"]]
|
||
if classification_cols:
|
||
logger.info(f"[{pair}] 📊 检测到分类相关列: {classification_cols}")
|
||
else:
|
||
logger.debug(f"[{pair}] 📊 未检测到分类相关列")
|
||
|
||
# 调试:显示所有包含"optimal"的列名
|
||
all_optimal_cols = [col for col in dataframe.columns if "optimal" in str(col).lower()]
|
||
if all_optimal_cols:
|
||
logger.debug(f"[{pair}] 所有包含'optimal'的列: {all_optimal_cols}")
|
||
else:
|
||
logger.debug(f"[{pair}] 未找到任何包含'optimal'的列")
|
||
|
||
if "optimal_first_length_pred" in dataframe.columns:
|
||
predicted_value = dataframe["optimal_first_length_pred"].iloc[-1]
|
||
if pd.notna(predicted_value):
|
||
# 处理浮点预测值,四舍五入到最近的整数类别
|
||
optimal_length_class = int(round(float(predicted_value)))
|
||
length_mapping = {0: 2, 1: 4, 2: 6, 3: 8, 4: 10}
|
||
first_length = length_mapping.get(optimal_length_class, 2)
|
||
logger.info(f"[{pair}] ✅ 使用 optimal_first_length_pred: first_length={first_length} (原始值: {predicted_value}, 类别: {optimal_length_class})")
|
||
else:
|
||
first_length = 2
|
||
logger.warning(f"[{pair}] ⚠️ optimal_first_length_pred 为NaN,使用默认值: {first_length}")
|
||
elif "&*-optimal_first_length" in dataframe.columns:
|
||
# 直接检查原始列
|
||
predicted_value = dataframe["&*-optimal_first_length"].iloc[-1]
|
||
if pd.notna(predicted_value):
|
||
optimal_length_class = int(round(float(predicted_value)))
|
||
length_mapping = {0: 2, 1: 4, 2: 6, 3: 8, 4: 10}
|
||
first_length = length_mapping.get(optimal_length_class, 2)
|
||
logger.info(f"[{pair}] ✅ 使用 &*-optimal_first_length: first_length={first_length} (原始值: {predicted_value}, 类别: {optimal_length_class})")
|
||
else:
|
||
first_length = 2
|
||
logger.warning(f"[{pair}] ⚠️ &*-optimal_first_length 为NaN,使用默认值: {first_length}")
|
||
else:
|
||
first_length = 2
|
||
# 只在首次或特定条件下显示警告
|
||
if not hasattr(self, '_classification_warning_shown'):
|
||
logger.warning(f"[{pair}] ⚠️ 未找到分类模型列,使用默认值: {first_length}")
|
||
self._classification_warning_shown = True
|
||
else:
|
||
logger.debug(f"[{pair}] 未找到分类模型列,使用默认值: {first_length}")
|
||
|
||
# 根据 first_length 动态调整其他段长
|
||
second_length = first_length * 3
|
||
third_length = first_length * 5
|
||
|
||
# 计算总长度
|
||
total_length_needed = first_length + second_length + third_length
|
||
|
||
# 检查数据是否充足
|
||
if len(dataframe) < total_length_needed:
|
||
logger.warning(f"[{pair}] 数据不足{total_length_needed}个周期,返回震荡趋势")
|
||
return "ranging"
|
||
|
||
# 获取所需长度的trend_score历史
|
||
trend_scores = []
|
||
actual_total_length = len(dataframe)
|
||
|
||
for i in range(-total_length_needed, 0):
|
||
# 确保索引在有效范围内
|
||
if abs(i) > actual_total_length:
|
||
logger.warning(f"[{pair}] 索引 {i} 超出数据范围,使用默认趋势得分 50")
|
||
trend_scores.append(50)
|
||
continue
|
||
|
||
# 获取历史数据片段
|
||
end_idx = i + 1 if i != -1 else None
|
||
hist_df = dataframe.iloc[:end_idx]
|
||
|
||
if hist_df.empty:
|
||
logger.warning(f"[{pair}] 历史数据片段为空,使用默认趋势得分 50")
|
||
trend_scores.append(50)
|
||
continue
|
||
|
||
# 获取时间戳 - 统一使用整数时间戳
|
||
try:
|
||
last_idx = hist_df.index[-1]
|
||
if isinstance(last_idx, pd.Timestamp):
|
||
# 确保时间戳是无时区的整数
|
||
ts = last_idx.tz_localize(None) if last_idx.tz else last_idx
|
||
timestamp = int(ts.timestamp())
|
||
elif isinstance(last_idx, (int, np.integer)):
|
||
# 如果索引已经是整数,直接使用
|
||
timestamp = int(last_idx)
|
||
elif hasattr(last_idx, 'timestamp'):
|
||
timestamp = int(last_idx.timestamp())
|
||
else:
|
||
# 使用当前时间的整数时间戳
|
||
timestamp = int(pd.Timestamp.utcnow().timestamp())
|
||
except Exception as e:
|
||
# 使用当前时间的整数时间戳作为fallback
|
||
timestamp = int(pd.Timestamp.utcnow().timestamp())
|
||
|
||
# 获取趋势得分
|
||
score = self.get_trend_score_with_cache(
|
||
pair=pair,
|
||
timeframe=self.timeframe,
|
||
timestamp=timestamp,
|
||
dataframe=hist_df,
|
||
metadata=metadata
|
||
)
|
||
trend_scores.append(score)
|
||
|
||
# 验证结果数量
|
||
if len(trend_scores) < total_length_needed:
|
||
logger.warning(f"[{pair}] 只获取到 {len(trend_scores)} 个趋势得分,需要{total_length_needed}个")
|
||
while len(trend_scores) < total_length_needed:
|
||
trend_scores.append(50)
|
||
|
||
# 分段计算加权得分
|
||
# 第一段:最近first_length个周期
|
||
segment1 = trend_scores[-first_length:]
|
||
weighted_score1 = sum(score * 10 for score in segment1) / len(segment1)
|
||
|
||
# 第二段:接下来的second_length个周期
|
||
segment2 = trend_scores[-(first_length + second_length):-first_length]
|
||
weighted_score2 = sum(score * 7 for score in segment2) / len(segment2)
|
||
|
||
# 第三段:最后的third_length个周期
|
||
segment3 = trend_scores[-total_length_needed:-(first_length + second_length)]
|
||
weighted_score3 = sum(score * 3 for score in segment3) / len(segment3)
|
||
|
||
# 计算最终加权得分
|
||
final_weighted_score = (weighted_score1 + weighted_score2 + weighted_score3) / (10 + 7 + 3)
|
||
|
||
# 将得分映射到0-100区间
|
||
final_score = max(0, min(100, final_weighted_score))
|
||
|
||
# 使用hyperopt优化的阈值判断趋势状态
|
||
bullish_threshold = self.trend_final_bullish_threshold.value
|
||
bearish_threshold = self.trend_final_bearish_threshold.value
|
||
|
||
# 判定趋势状态
|
||
if final_score >= bullish_threshold:
|
||
trend_status = "bullish"
|
||
logger.info(f"[{pair}] 🚀 检测到上涨趋势: 最终加权得分={final_score:.2f}, 阈值≥{bullish_threshold}, 段长={first_length},{second_length},{third_length}")
|
||
elif final_score <= bearish_threshold:
|
||
trend_status = "bearish"
|
||
logger.info(f"[{pair}] 📉 检测到下跌趋势: 最终加权得分={final_score:.2f}, 阈值≤{bearish_threshold}, 段长={first_length},{second_length},{third_length}")
|
||
else:
|
||
trend_status = "ranging"
|
||
logger.info(f"[{pair}] ⚖️ 检测到震荡趋势: 最终加权得分={final_score:.2f}, 阈值范围({bearish_threshold}, {bullish_threshold}), 段长={first_length},{second_length},{third_length}")
|
||
|
||
# 输出分段详细信息用于调试
|
||
logger.debug(f"[{pair}] 趋势分析详情 - "
|
||
f"第一段({first_length}个,权重10): {[f'{s:.1f}' for s in segment1]}, "
|
||
f"第二段({second_length}个,权重7): {[f'{s:.1f}' for s in segment2]}, "
|
||
f"第三段({third_length}个,权重3): {[f'{s:.1f}' for s in segment3]}")
|
||
|
||
return trend_status
|
||
|
||
except Exception as e:
|
||
logger.error(f"[{pair}] 趋势状态检测失败: {e}")
|
||
return "ranging"ubuntu@giana:~/freqtrade/freqtrade/templates$ ^C
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$ git branch
|
||
a4ba750e
|
||
base
|
||
best5m-noai-dryrun
|
||
freq-ai-2a119a011ac
|
||
freq-ai-3c3eeaf7429
|
||
freq-ai-418559b5
|
||
freq-ai-5m-2a119a011
|
||
* freq-ai-b47cd7cf21
|
||
freq-ai-da9d98f161
|
||
freq-ai-stepduration
|
||
freq-noai-02535ec1
|
||
freq-noai-02535ec1-2
|
||
freq-noai-0e4480d383
|
||
freq-noai-3e57301762d
|
||
freq-noai-3f8d47f
|
||
freq-noai-57f716b3
|
||
freq-noai-5a4d3812d
|
||
freq-noai-5aab051fb
|
||
freq-noai-75655008
|
||
freq-noai-7e68e4e3
|
||
freq-noai-a21aa0bad2
|
||
freq-noai-a4a9d6dfd
|
||
freq-noai-ade68cdc9
|
||
freq-noai-ade68cdc96
|
||
freq-noai-b368bd88d41
|
||
freq-noai-b688f82c
|
||
freq-noai-c676df73
|
||
freq-noai-ce4d5324
|
||
freq-noai-dcb91f3b
|
||
freq-noai-e736766556
|
||
freq-noai-e736766556-forHyperopt-20251104-2tnd
|
||
freq-noai-e736766556-forHyperopt-20251104-CReb
|
||
freq-noai-e736766556-forHyperopt-20251104-NuB5
|
||
freq-noai-e736766556-forHyperopt-20251104-Ritq
|
||
freq-noai-e736766556-forHyperopt-20251104-SJAH
|
||
freq-noai-e736766556-forHyperopt-20251104-hdic
|
||
freq-noai-ee803845b
|
||
freq-noai-f8672df
|
||
freq-noai-fab0ccf
|
||
freq-noai-newseed1
|
||
freqai-1hwave
|
||
freqai-customrsi
|
||
freqai-stable-42e51f8
|
||
freqai-stable0
|
||
freqai-stable1
|
||
freqai-stable2
|
||
freqai-stable2-531e8d69
|
||
freqai-stable2-c5574de0
|
||
freqai-stable2-e60b04d16
|
||
freqai-stable2-test1
|
||
freqai-stable2-test10
|
||
freqai-stable2-test2
|
||
freqai-stable2-test3
|
||
freqai-stable2-test4
|
||
freqai-stable2-test5
|
||
freqai-stable2-test6
|
||
freqai-stable2-test7
|
||
freqai-stable2-test8
|
||
freqai-stable2-test9
|
||
freqai-stable3-08712c24d3
|
||
freqai-stable3-0ee48a52
|
||
freqai-stable3-226d226
|
||
freqai-stable3-3ec7807
|
||
freqai-stable3-8210a74b-2
|
||
freqai-stable3-a4ba750e
|
||
freqai-stable3-b2cda9074
|
||
freqai-stable3-f9351a7b7
|
||
freqai-trend_score2
|
||
freqai-trendscore
|
||
freqaiprimer-4hwave
|
||
freqaiprimer-ai-dryrun
|
||
freqaiprimer-ai-test
|
||
freqaiprimer-aipure-test
|
||
freqaiprimer-deviation-test
|
||
freqaiprimer-deviation-test1
|
||
freqaiprimer-deviation-test2
|
||
freqaiprimer-hybrid-test
|
||
|
||
[22]+ Stopped git branch
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$ ^C
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$ git log
|
||
commit 18954bd2015b0fc0fe2e148567f4a0877f1e3c83 (HEAD -> freq-ai-b47cd7cf21, origin/freq-ai-b47cd7cf21)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 11:35:58 2026 +0800
|
||
|
||
dryrun update
|
||
|
||
commit ae4d2c32ae336723f71808105d7a191ddb338185
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 11:30:55 2026 +0800
|
||
|
||
避免 FreqAI 调用时出现Cannot
|
||
|
||
commit f0617f2e1f7b58637e059eb698283d803b5c7164
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 10:43:30 2026 +0800
|
||
|
||
custom_entry_price 0.995
|
||
|
||
commit b47cd7cf21a12ba92e96ae8ba1ed0c98e7143d4a (tag: 优选种子1号)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Wed Aug 20 16:07:10 2025 +0800
|
||
|
||
v
|
||
|
||
commit 9bc93e4a42da46e4e9d247f8aed32dadf13b3f7d (tag: 种子5, origin/freqaiprimer-stable2-9bc93e4a, freqaiprimer-stable2-9bc93e4a)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:47:57 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 987c761f96f31497f9a448f6c55f788859a2a27c
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:25:35 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 727cf3d340b9acbb92594ad70860b76e17cb8efe
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:16:35 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 986baf8c11618e7b3ed1fc1527752642ea523709
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:26:37 2025 +0800
|
||
|
||
优化log
|
||
|
||
commit 36e006a8a0f72729ba5616810fe809bcfe721215
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:26:04 2025 +0800
|
||
|
||
优化log
|
||
|
||
commit 81ae1e7381157440ab4419498864b025b0aa1f93
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:10:19 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit a54d24679ea0a6a64c910d39090528bd43ddb13e
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:58:00 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit b2c6fae3b08c22faa393ed5011380c4a51bb3d73
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:55:48 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit 4c5f444693bd306b0e16318e425f84ef2c1a25b6
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:45:04 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit 189add64f9b9f1f740a196cbde4b604b4e5acf29
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:38:44 2025 +0800
|
||
|
||
revert
|
||
|
||
commit 2d3c8be5bd80f0691687c360b98ea40cf781e7e7
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:24:21 2025 +0800
|
||
|
||
波动率相关特征freqai新回归属性
|
||
|
||
commit 2bbf5209bbc1e42232f76dda04d780c046660866
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:11:34 2025 +0800
|
||
|
||
波动率相关特征freqai新回归属性
|
||
|
||
commit d83213dcf70c640469966c54e9cb828dc92d01d4
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:44:36 2025 +0800
|
||
|
||
tailing stop fixed
|
||
|
||
commit f9351a7b74a88a69e129b001be2f2d6fef2156f6 (tag: 种子4, tag: 种子3)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:30:20 2025 +0800
|
||
|
||
revert to HEAD^^^^
|
||
|
||
commit 9c34c92fdeb99a2374b062898cefa5a28af777e5
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:19:15 2025 +0800
|
||
|
||
prevent_future_data_leak
|
||
|
||
commit 9b5f279603e23adf370ac3e338814939d459bf50
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:14:17 2025 +0800
|
||
|
||
prevent_future_data_leak
|
||
|
||
commit c9ec3b37271257004800e3af8010834fbdb3a8a9
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 00:57:29 2025 +0800
|
||
|
||
去掉iloc[-1]
|
||
|
||
commit f7f689c2dc3df079cdc7cd15f769693e63a9b1a7
|
||
Author: Ubuntu <ubuntu@niyon.lxd>
|
||
Date: Mon Aug 18 16:21:37 2025 +0800
|
||
|
||
Reset code state to commit 098d99e2d628dd1b740c22ba4ca815ac72ec9d05
|
||
|
||
commit 5d5f7adcfacbaa003604b89d130ef51be1f87d13
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 16:12:57 2025 +0800
|
||
|
||
优化warning
|
||
|
||
commit 098d99e2d628dd1b740c22ba4ca815ac72ec9d05
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 15:02:34 2025 +0800
|
||
|
||
关闭绿色通道
|
||
|
||
commit c188403bd3958233c8dbd43266c83b3137df04cf
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 10:42:34 2025 +0800
|
||
|
||
关闭绿色通道
|
||
|
||
commit 5edd1cace4ffd5059ef9c02e5be7608e618a4e71
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 09:03:37 2025 +0800
|
||
|
||
...skipping...
|
||
|
||
commit 189add64f9b9f1f740a196cbde4b604b4e5acf29
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:38:44 2025 +0800
|
||
|
||
revert
|
||
|
||
commit 2d3c8be5bd80f0691687c360b98ea40cf781e7e7
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:24:21 2025 +0800
|
||
|
||
波动率相关特征freqai新回归属性
|
||
|
||
commit 2bbf5209bbc1e42232f76dda04d780c046660866
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:11:34 2025 +0800
|
||
|
||
波动率相关特征freqai新回归属性
|
||
|
||
commit d83213dcf70c640469966c54e9cb828dc92d01d4
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:44:36 2025 +0800
|
||
|
||
tailing stop fixed
|
||
|
||
commit f9351a7b74a88a69e129b001be2f2d6fef2156f6 (tag: 种子4, tag: 种子3)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:30:20 2025 +0800
|
||
|
||
revert to HEAD^^^^
|
||
|
||
commit 9c34c92fdeb99a2374b062898cefa5a28af777e5
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:19:15 2025 +0800
|
||
|
||
prevent_future_data_leak
|
||
|
||
commit 9b5f279603e23adf370ac3e338814939d459bf50
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 01:14:17 2025 +0800
|
||
|
||
prevent_future_data_leak
|
||
|
||
commit c9ec3b37271257004800e3af8010834fbdb3a8a9
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 00:57:29 2025 +0800
|
||
|
||
去掉iloc[-1]
|
||
|
||
commit f7f689c2dc3df079cdc7cd15f769693e63a9b1a7
|
||
Author: Ubuntu <ubuntu@niyon.lxd>
|
||
Date: Mon Aug 18 16:21:37 2025 +0800
|
||
|
||
Reset code state to commit 098d99e2d628dd1b740c22ba4ca815ac72ec9d05
|
||
|
||
commit 5d5f7adcfacbaa003604b89d130ef51be1f87d13
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 16:12:57 2025 +0800
|
||
|
||
优化warning
|
||
|
||
commit 098d99e2d628dd1b740c22ba4ca815ac72ec9d05
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 15:02:34 2025 +0800
|
||
|
||
关闭绿色通道
|
||
|
||
commit c188403bd3958233c8dbd43266c83b3137df04cf
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 10:42:34 2025 +0800
|
||
|
||
关闭绿色通道
|
||
|
||
commit 5edd1cace4ffd5059ef9c02e5be7608e618a4e71
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Mon Aug 18 09:03:37 2025 +0800
|
||
|
||
绿色通道至少要满足4条
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$ -ai-b47cd7cf21
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
^Z
|
||
[23]+ Stopped -ai-b47cd7cf21
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$ ^C
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$ git log
|
||
commit 18954bd2015b0fc0fe2e148567f4a0877f1e3c83 (HEAD -> freq-ai-b47cd7cf21, origin/freq-ai-b47cd7cf21)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 11:35:58 2026 +0800
|
||
|
||
dryrun update
|
||
|
||
commit ae4d2c32ae336723f71808105d7a191ddb338185
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 11:30:55 2026 +0800
|
||
|
||
避免 FreqAI 调用时出现Cannot
|
||
|
||
commit f0617f2e1f7b58637e059eb698283d803b5c7164
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 10:43:30 2026 +0800
|
||
|
||
custom_entry_price 0.995
|
||
|
||
commit b47cd7cf21a12ba92e96ae8ba1ed0c98e7143d4a (tag: 优选种子1号)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Wed Aug 20 16:07:10 2025 +0800
|
||
|
||
v
|
||
|
||
commit 9bc93e4a42da46e4e9d247f8aed32dadf13b3f7d (tag: 种子5, origin/freqaiprimer-stable2-9bc93e4a, freqaiprimer-stable2-9bc93e4a)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:47:57 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 987c761f96f31497f9a448f6c55f788859a2a27c
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:25:35 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 727cf3d340b9acbb92594ad70860b76e17cb8efe
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:16:35 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 986baf8c11618e7b3ed1fc1527752642ea523709
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:26:37 2025 +0800
|
||
|
||
优化log
|
||
|
||
commit 36e006a8a0f72729ba5616810fe809bcfe721215
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:26:04 2025 +0800
|
||
|
||
优化log
|
||
|
||
commit 81ae1e7381157440ab4419498864b025b0aa1f93
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:10:19 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit a54d24679ea0a6a64c910d39090528bd43ddb13e
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:58:00 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit b2c6fae3b08c22faa393ed5011380c4a51bb3d73
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:55:48 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit 4c5f444693bd306b0e16318e425f84ef2c1a25b6
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:45:04 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
:
|
||
commit 18954bd2015b0fc0fe2e148567f4a0877f1e3c83 (HEAD -> freq-ai-b47cd7cf21, origin/freq-ai-b47cd7cf21)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 11:35:58 2026 +0800
|
||
|
||
dryrun update
|
||
|
||
commit ae4d2c32ae336723f71808105d7a191ddb338185
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 11:30:55 2026 +0800
|
||
|
||
避免 FreqAI 调用时出现Cannot
|
||
|
||
commit f0617f2e1f7b58637e059eb698283d803b5c7164
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Thu Jan 1 10:43:30 2026 +0800
|
||
|
||
custom_entry_price 0.995
|
||
|
||
commit b47cd7cf21a12ba92e96ae8ba1ed0c98e7143d4a (tag: 优选种子1号)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Wed Aug 20 16:07:10 2025 +0800
|
||
|
||
v
|
||
|
||
commit 9bc93e4a42da46e4e9d247f8aed32dadf13b3f7d (tag: 种子5, origin/freqaiprimer-stable2-9bc93e4a, freqaiprimer-stable2-9bc93e4a)
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:47:57 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 987c761f96f31497f9a448f6c55f788859a2a27c
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:25:35 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 727cf3d340b9acbb92594ad70860b76e17cb8efe
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 08:16:35 2025 +0800
|
||
|
||
风险等级改成了波动率
|
||
|
||
commit 986baf8c11618e7b3ed1fc1527752642ea523709
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:26:37 2025 +0800
|
||
|
||
优化log
|
||
|
||
commit 36e006a8a0f72729ba5616810fe809bcfe721215
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:26:04 2025 +0800
|
||
|
||
优化log
|
||
|
||
commit 81ae1e7381157440ab4419498864b025b0aa1f93
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 03:10:19 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit a54d24679ea0a6a64c910d39090528bd43ddb13e
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:58:00 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit b2c6fae3b08c22faa393ed5011380c4a51bb3d73
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:55:48 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
|
||
commit 4c5f444693bd306b0e16318e425f84ef2c1a25b6
|
||
Author: zhangkun9038@dingtalk.com <zhangkun9038@dingtalk.com>
|
||
Date: Tue Aug 19 02:45:04 2025 +0800
|
||
|
||
加入risk_rate freqai属性
|
||
:
|
||
[24]+ Stopped git log
|
||
:^C
|
||
ubuntu@giana:~/freqtrade/freqtrade/templates$ cd ../../
|
||
ubuntu@giana:~/freqtrade$ cd tools/
|
||
ubuntu@giana:~/freqtrade/tools$ ls
|
||
analytic.py download.sh dryrunloop.timer exchanges.py freqtrade listpairs.sh mergetemp.sh result trades.sh
|
||
analytic.sh dryrunloop.service dryrun.sh exchanges.sh gitpush.sh live.sh output.log showtrades.sh tradestocsv.py
|
||
backtest.sh dryrunloop.sh '### Example Usage:' filter.py hyperopt.sh mergeme.sh plot.sh tools view_feather.py
|
||
ubuntu@giana:~/freqtrade/tools$ cat live.sh
|
||
#!/bin/bash
|
||
|
||
set -e # 出错立即停止
|
||
|
||
### 函数定义区 ###
|
||
|
||
# 定义 remove_existing_containers 函数
|
||
remove_existing_containers() {
|
||
echo "正在查找并移除所有名称符合 'freqtrade-dryrun-*' 的容器..."
|
||
|
||
# 查找所有名称以 freqtrade-dryrun- 开头的容器
|
||
CONTAINERS=$(docker ps -aq --filter "name=freqtrade-dryrun-*")
|
||
if [ -z "$CONTAINERS" ]; then
|
||
echo "未找到任何名称符合 'freqtrade-dryrun-*' 的容器。"
|
||
return
|
||
fi
|
||
|
||
echo "找到以下容器:"
|
||
docker ps -a --filter "name=freqtrade-dryrun-*"
|
||
|
||
echo "正在自动移除所有符合条件的容器..."
|
||
|
||
# 停止并移除容器
|
||
for CONTAINER in $CONTAINERS; do
|
||
echo "正在停止容器 $CONTAINER..."
|
||
docker stop "$CONTAINER" >/dev/null 2>&1
|
||
if [ $? -ne 0 ]; then
|
||
echo "警告:无法停止容器 $CONTAINER。"
|
||
continue
|
||
fi
|
||
|
||
echo "正在移除容器 $CONTAINER..."
|
||
docker rm "$CONTAINER" >/dev/null 2>&1
|
||
if [ $? -ne 0 ]; then
|
||
echo "警告:无法移除容器 $CONTAINER。"
|
||
continue
|
||
fi
|
||
|
||
echo "容器 $CONTAINER 已成功移除。"
|
||
done
|
||
|
||
echo "所有符合条件的容器已处理完毕。"
|
||
}
|
||
|
||
# 1. 从SQLite数据库获取当前开放交易的币对列表
|
||
get_open_trades_pairs() {
|
||
local db_path="$1"
|
||
local pairs=()
|
||
|
||
if [ ! -f "$db_path" ]; then
|
||
echo "⚠️ 交易数据库文件 $db_path 不存在,开放交易对列表为空" >&2
|
||
echo ""
|
||
return
|
||
fi
|
||
|
||
local query_result=$(sqlite3 -header -csv "$db_path" "SELECT id, pair, open_date, amount, open_rate, max_rate, close_profit, stake_amount FROM trades WHERE is_open = 1 ORDER BY open_date DESC LIMIT 10;")
|
||
pairs=($(echo "$query_result" | awk -F ',' 'NR > 1 {print $2}' | tr -d '"'))
|
||
|
||
echo "从数据库获取到开放交易对: ${pairs[*]}" >&2
|
||
echo "${pairs[@]}"
|
||
}
|
||
|
||
# 2. 从远程接口获取币对列表
|
||
get_remote_pairs() {
|
||
local remote_url="$1"
|
||
local pairs=()
|
||
|
||
if [ -z "$remote_url" ]; then
|
||
echo "⚠️ 未提供远程币对列表URL,远程币对列表为空" >&2
|
||
echo ""
|
||
return
|
||
fi
|
||
|
||
echo "正在从远程URL获取币对列表: $remote_url" >&2
|
||
local pairs_json=$(curl -s "$remote_url")
|
||
|
||
if [ $? -ne 0 ] || [ -z "$pairs_json" ]; then
|
||
echo "⚠️ 远程币对列表获取失败,使用空列表" >&2
|
||
echo ""
|
||
return
|
||
fi
|
||
|
||
local parsed_pairs=$(echo "$pairs_json" | python3 -c "
|
||
import sys, json
|
||
data = json.load(sys.stdin)
|
||
pairs = [pair.replace('-', '/') for pair in data.get('pairlist', [])]
|
||
print(' '.join(pairs) if pairs else '')
|
||
")
|
||
|
||
if [ -n "$parsed_pairs" ]; then
|
||
pairs=($parsed_pairs)
|
||
echo "从远程获取到币对: ${pairs[*]}" >&2
|
||
else
|
||
echo "⚠️ 远程币对列表解析为空" >&2
|
||
fi
|
||
|
||
echo "${pairs[@]}"
|
||
}
|
||
|
||
# 3. 合并并去重两个币对列表
|
||
merge_and_deduplicate_pairs() {
|
||
local -a db_pairs=($1)
|
||
local -a remote_pairs=($2)
|
||
local -a merged=()
|
||
|
||
merged=($(printf "%s\n" "${db_pairs[@]}" "${remote_pairs[@]}" | sort -u | tr '\n' ' '))
|
||
|
||
echo "合并去重后的币对列表: ${merged[*]}" >&2
|
||
echo "${merged[@]}"
|
||
}
|
||
|
||
# 4. 验证JSON文件格式是否有效
|
||
validate_json_file() {
|
||
local json_path="$1"
|
||
|
||
if ! command -v jq &>/dev/null; then
|
||
echo "❌ 未安装jq工具,请先安装jq" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if [ ! -f "$json_path" ]; then
|
||
echo "❌ JSON配置文件 $json_path 不存在" >&2
|
||
exit 1
|
||
fi
|
||
|
||
if ! jq . "$json_path" >/dev/null 2>&1; then
|
||
echo "❌ JSON文件格式错误:$json_path" >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# 5. 更新live.json中的pair_whitelist
|
||
update_live_json_pair_whitelist() {
|
||
local json_path="$1"
|
||
local -a pairlist=($2)
|
||
|
||
validate_json_file "$json_path"
|
||
|
||
local json_array=$(printf '%s\n' "${pairlist[@]}" | jq -R . | jq -s .)
|
||
|
||
echo "正在更新 $json_path 中的exchange.pair_whitelist..." >&2
|
||
jq --argjson pairs "$json_array" '.exchange.pair_whitelist = $pairs' "$json_path" >"$json_path.tmp" && mv "$json_path.tmp" "$json_path"
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo "✅ 成功更新 $json_path,新的pair_whitelist包含 ${#pairlist[@]} 个币对" >&2
|
||
else
|
||
echo "❌ 更新 $json_path 失败" >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
### 主逻辑区 ###
|
||
|
||
# 检查 .env 文件
|
||
if [ ! -f ".env" ]; then
|
||
echo "⚠️ 本地缺少 .env 文件,请创建并配置" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# 加载 .env 变量
|
||
export $(grep -v '^#' .env | xargs)
|
||
|
||
# 检查必要环境变量
|
||
if [ -z "$TEST_BRANCH" ] || [ -z "$DRYRUN_BRANCH" ]; then
|
||
echo "⚠️ .env 文件缺少 TEST_BRANCH 或 DRYRUN_BRANCH 配置" >&2
|
||
exit 1
|
||
fi
|
||
|
||
# 检查当前分支
|
||
current_branch=$(git rev-parse --abbrev-ref HEAD)
|
||
if [[ "$current_branch" != "$TEST_BRANCH" && "$current_branch" != "$DRYRUN_BRANCH" ]]; then
|
||
echo "⚠️ 错误:当前分支 '$current_branch' 不符合配置要求" >&2
|
||
exit 1
|
||
else
|
||
echo "✅ 当前分支 '$current_branch' 符合配置要求" >&2
|
||
fi
|
||
|
||
# dryrun分支清理
|
||
if [[ "$current_branch" == *"dryrun"* ]]; then
|
||
echo "当前为dryrun分支,执行git reset --hard清理工作区..." >&2
|
||
git reset --hard || {
|
||
echo "⚠️ git reset失败"
|
||
exit 1
|
||
}
|
||
echo "✅ Git工作区已清理" >&2
|
||
fi
|
||
|
||
# 设置默认远程币对列表URL,支持命令行参数覆盖
|
||
DEFAULT_PAIR_REMOTE_URL="http://pairlist.xl.home/api/pairlist?mute=true&count=30"
|
||
PAIR_REMOTE_LIST_URL="$DEFAULT_PAIR_REMOTE_URL"
|
||
|
||
# 解析命令行参数:如果提供则覆盖默认值
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--pairRemoteList=*)
|
||
PAIR_REMOTE_LIST_URL="${1#*=}"
|
||
shift
|
||
;;
|
||
--pairRemoteList)
|
||
if [[ -n "$2" && "$2" != -* ]]; then
|
||
PAIR_REMOTE_LIST_URL="$2"
|
||
shift 2
|
||
else
|
||
echo "错误:--pairRemoteList需要指定值" >&2
|
||
exit 1
|
||
fi
|
||
;;
|
||
*)
|
||
shift
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# 加载策略配置
|
||
STRATEGY_NAME=${STRATEGY_NAME:-TheForceV7}
|
||
CONFIG_FILE=${CONFIG_FILE:-basic.json}
|
||
PARAMS_NAME=$(echo "$STRATEGY_NAME" | tr '[:upper:]' '[:lower:]')
|
||
|
||
echo "使用策略: $STRATEGY_NAME" >&2
|
||
echo "使用配置: $CONFIG_FILE" >&2
|
||
echo "测试分支: $TEST_BRANCH" >&2
|
||
echo "远程币对列表URL: $PAIR_REMOTE_LIST_URL" >&2 # 显示当前使用的URL
|
||
|
||
### 核心:处理币对列表 ###
|
||
|
||
# 1. 获取数据库币对(使用绝对路径)
|
||
db_path="/home/ubuntu/freqtrade/user_data/tradesv3.sqlite"
|
||
db_pairs=$(get_open_trades_pairs "$db_path")
|
||
|
||
# 2. 获取远程币对
|
||
remote_pairs=$(get_remote_pairs "$PAIR_REMOTE_LIST_URL")
|
||
|
||
# 3. 合并去重
|
||
merged_pairs=$(merge_and_deduplicate_pairs "$db_pairs" "$remote_pairs")
|
||
|
||
# 4. 更新配置文件
|
||
update_live_json_pair_whitelist "../config_examples/live.json" "$merged_pairs"
|
||
|
||
### 启动容器 ###
|
||
|
||
GIT_COMMIT_SHORT=$(git rev-parse HEAD | cut -c 1-8)
|
||
CONTAINER_NAME="freqtrade-dryrun-${GIT_COMMIT_SHORT}"
|
||
|
||
echo "准备启动容器: $CONTAINER_NAME" >&2
|
||
|
||
# 移除已存在的同名容器
|
||
remove_existing_containers "$CONTAINER_NAME"
|
||
|
||
# 清理临时文件放置交叉感染
|
||
cd ../
|
||
source .venv/bin/activate
|
||
rm -rf user_data/models/*
|
||
rm -rf ./freqtrade/user_data/data/backtest_results/*
|
||
rm -fr ./user_data/dryrun_results/*
|
||
cd -
|
||
|
||
# 启动新容器
|
||
echo "启动容器: $CONTAINER_NAME" >&2
|
||
docker-compose run -d --rm \
|
||
--name "$CONTAINER_NAME" \
|
||
-p 8080:8080 \
|
||
freqtrade trade \
|
||
--logfile /freqtrade/user_data/logs/freqtrade.log \
|
||
--db-url sqlite:////freqtrade/user_data/tradesv3.sqlite \
|
||
--freqaimodel LightGBMRegressorMultiTarget \
|
||
--fee 0.0008 \
|
||
--config /freqtrade/config_examples/$CONFIG_FILE \
|
||
--config /freqtrade/templates/${PARAMS_NAME}.json \
|
||
--config /freqtrade/config_examples/live.json \
|
||
--strategy $STRATEGY_NAME \
|
||
--strategy-path /freqtrade/templates
|
||
|
||
if [ $? -eq 0 ]; then
|
||
echo "✅ 容器 $CONTAINER_NAME 启动完成" >&2
|
||
else
|
||
echo "❌ 容器启动失败" >&2
|
||
exit 1
|
||
fi
|
||
ubuntu@giana:~/freqtrade/tools$ |