myTestFreqAI/tools/live_old.sh
zhangkun9038@dingtalk.com c948802ede 合并远程币对
2026-01-04 11:21:57 +08:00

2075 lines
93 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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}] 尝试将趋势得分写入 Rediskey={cache_key}, value={trend_score:.2f}")
try:
redis_client.setex(cache_key, 86400, trend_score)
logger.info(f"[{pair}] ✅ 成功将趋势得分存储到 Rediskey={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$