diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 2312b3c..9138ed2 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -59,7 +59,15 @@ class CryptoAgent: async def run(self): """主运行循环 - 在5的倍数分钟执行""" self.running = True - logger.info("加密货币智能体开始运行...") + + # 启动横幅 + logger.info("\n" + "=" * 60) + logger.info("🚀 加密货币交易信号智能体") + logger.info("=" * 60) + logger.info(f" 监控交易对: {', '.join(self.symbols)}") + logger.info(f" 运行模式: 每5分钟整点执行 (:00, :05, :10, ...)") + logger.info(f" LLM阈值: {self.llm_threshold * 100:.0f}%") + logger.info("=" * 60 + "\n") # 发送启动通知 await self.feishu.send_text( @@ -74,21 +82,27 @@ class CryptoAgent: wait_seconds = self._get_seconds_until_next_5min() if wait_seconds > 0: next_run = datetime.now() + timedelta(seconds=wait_seconds) - logger.info(f"等待 {wait_seconds} 秒,下次运行时间: {next_run.strftime('%H:%M:%S')}") + logger.info(f"⏳ 等待 {wait_seconds} 秒,下次运行: {next_run.strftime('%H:%M:%S')}") await asyncio.sleep(wait_seconds) # 执行分析 run_time = datetime.now() - logger.info(f"=== 开始分析 [{run_time.strftime('%H:%M:%S')}] ===") + logger.info("\n" + "=" * 60) + logger.info(f"⏰ 定时任务执行 [{run_time.strftime('%Y-%m-%d %H:%M:%S')}]") + logger.info("=" * 60) for symbol in self.symbols: await self.analyze_symbol(symbol) + logger.info("\n" + "─" * 60) + logger.info(f"✅ 本轮分析完成,共分析 {len(self.symbols)} 个交易对") + logger.info("─" * 60 + "\n") + # 等待几秒确保不会在同一分钟内重复执行 await asyncio.sleep(2) except Exception as e: - logger.error(f"分析循环出错: {e}") + logger.error(f"❌ 分析循环出错: {e}") await asyncio.sleep(10) # 出错后等待10秒再继续 def stop(self): @@ -104,18 +118,36 @@ class CryptoAgent: symbol: 交易对,如 'BTCUSDT' """ try: - logger.info(f"开始分析 {symbol}...") + # 分隔线 + logger.info(f"\n{'─' * 50}") + logger.info(f"📊 {symbol} 分析开始") + logger.info(f"{'─' * 50}") # 1. 获取多周期数据 data = self.binance.get_multi_timeframe_data(symbol) if not self._validate_data(data): - logger.warning(f"{symbol} 数据不完整,跳过分析") + logger.warning(f"⚠️ {symbol} 数据不完整,跳过分析") return + # 获取当前价格 + current_price = float(data['5m'].iloc[-1]['close']) + price_change_24h = self._calculate_price_change(data['1h']) + logger.info(f"💰 当前价格: ${current_price:,.2f} ({price_change_24h})") + # 2. 分析趋势(1H + 4H)- 返回详细趋势信息 + logger.info(f"\n📈 【趋势分析】") trend = self.analyzer.analyze_trend(data['1h'], data['4h']) trend_direction = trend.get('direction', 'neutral') if isinstance(trend, dict) else trend + trend_strength = trend.get('strength', 'unknown') if isinstance(trend, dict) else 'unknown' + trend_phase = trend.get('phase', 'unknown') if isinstance(trend, dict) else 'unknown' + + # 趋势方向图标 + trend_icon = {'bullish': '🟢 看涨', 'bearish': '🔴 看跌', 'neutral': '⚪ 震荡'}.get(trend_direction, '❓') + phase_text = {'impulse': '主升/主跌浪', 'correction': '回调/反弹', 'oversold': '极度超卖', + 'overbought': '极度超买', 'sideways': '横盘'}.get(trend_phase, trend_phase) + + logger.info(f" 方向: {trend_icon} | 强度: {trend_strength} | 阶段: {phase_text}") # 3. 检查趋势变化 last_direction = self.last_trends.get(symbol, {}) @@ -126,16 +158,54 @@ class CryptoAgent: self.last_trends[symbol] = trend - # 4. 分析进场信号(15M 为主,5M 辅助) - signal = self.analyzer.analyze_entry_signal(data['5m'], data['15m'], trend) + # 4. 分析进场信号(15M 为主,5M 辅助,传入 1H 数据用于支撑阻力位) + logger.info(f"\n🎯 【信号分析】") + signal = self.analyzer.analyze_entry_signal(data['5m'], data['15m'], trend, data['1h']) signal['symbol'] = symbol signal['trend'] = trend_direction signal['trend_info'] = trend if isinstance(trend, dict) else {'direction': trend} - signal['price'] = float(data['5m'].iloc[-1]['close']) + signal['price'] = current_price signal['timestamp'] = datetime.now() + # 输出信号详情 + action_icon = {'buy': '🟢 买入', 'sell': '🔴 卖出', 'hold': '⏸️ 观望'}.get(signal['action'], '❓') + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(signal.get('signal_grade', 'D'), '') + + logger.info(f" 信号: {action_icon} | 置信度: {signal['confidence']}% | 等级: {signal.get('signal_grade', 'D')} {grade_icon}") + + # 输出触发原因 + if signal.get('reasons'): + logger.info(f" 原因: {', '.join(signal['reasons'][:5])}") # 最多显示5个原因 + + # 输出权重详情 + weights = signal.get('signal_weights', {}) + if weights: + logger.info(f" 权重: 买入={weights.get('buy', 0):.1f} | 卖出={weights.get('sell', 0):.1f}") + + # 输出K线形态 + patterns = signal.get('patterns', {}) + if patterns.get('bullish_patterns') or patterns.get('bearish_patterns'): + all_patterns = patterns.get('bullish_patterns', []) + patterns.get('bearish_patterns', []) + logger.info(f" 形态: {', '.join(all_patterns)}") + + # 输出成交量分析 + vol = signal.get('volume_analysis', {}) + if vol: + vol_icon = {'high': '📈', 'low': '📉', 'normal': '➖'}.get(vol.get('volume_signal', 'normal'), '') + confirm_text = '✓ 确认' if vol.get('volume_confirms') else '✗ 未确认' + logger.info(f" 成交量: {vol_icon} {vol.get('volume_signal', 'normal')} | {confirm_text}") + + # 输出支撑阻力位 + levels = signal.get('levels', {}) + if levels.get('nearest_support') or levels.get('nearest_resistance'): + support = f"${levels['nearest_support']:,.2f}" if levels.get('nearest_support') else '-' + resistance = f"${levels['nearest_resistance']:,.2f}" if levels.get('nearest_resistance') else '-' + logger.info(f" 关键位: 支撑={support} | 阻力={resistance}") + # 5. 检查是否需要发送信号 if self._should_send_signal(symbol, signal): + logger.info(f"\n📤 【发送通知】") + # 6. 计算止损止盈 atr = float(data['15m'].iloc[-1].get('atr', 0)) if atr > 0: @@ -143,9 +213,11 @@ class CryptoAgent: signal['price'], signal['action'], atr ) signal.update(sl_tp) + logger.info(f" 止损: ${signal['stop_loss']:,.2f} | 止盈: ${signal['take_profit']:,.2f}") # 7. LLM 深度分析(置信度超过阈值时) if signal['confidence'] >= self.llm_threshold * 100: + logger.info(f" 🤖 触发 LLM 深度分析...") llm_result = await self.analyzer.llm_analyze(data, signal, symbol) # 处理 LLM 分析结果 @@ -158,6 +230,7 @@ class CryptoAgent: if recommendation.get('action') == 'wait': signal['confidence'] = min(signal['confidence'], 40) signal['llm_analysis'] = llm_result.get('summary', 'LLM 建议观望') + logger.info(f" 🤖 LLM 建议: 观望") else: # 使用 LLM 的止损止盈建议 if recommendation.get('stop_loss'): @@ -167,6 +240,7 @@ class CryptoAgent: elif recommendation.get('take_profit'): signal['take_profit'] = recommendation['take_profit'] signal['llm_analysis'] = llm_result.get('summary', '') + logger.info(f" 🤖 LLM 建议: {recommendation.get('action', 'N/A')}") else: signal['llm_analysis'] = llm_result.get('summary', llm_result.get('raw', '')[:200]) @@ -178,10 +252,34 @@ class CryptoAgent: self.last_signals[symbol] = signal self.signal_cooldown[symbol] = datetime.now() - logger.info(f"{symbol} 发送{signal['action']}信号,置信度: {signal['confidence']}%") + action_text = '买入' if signal['action'] == 'buy' else '卖出' + logger.info(f" ✅ 已发送 {action_text} 信号通知") + else: + logger.info(f" ⏸️ 置信度不足({signal['confidence']}%),不发送通知") + else: + # 输出为什么不发送 + if signal['action'] == 'hold': + logger.info(f"\n⏸️ 结论: 观望,无交易机会") + elif signal['confidence'] < 50: + logger.info(f"\n⏸️ 结论: 置信度不足({signal['confidence']}%),继续观望") + else: + logger.info(f"\n⏸️ 结论: 信号冷却中,跳过") except Exception as e: - logger.error(f"分析 {symbol} 出错: {e}") + logger.error(f"❌ 分析 {symbol} 出错: {e}") + import traceback + logger.error(traceback.format_exc()) + + def _calculate_price_change(self, h1_data: pd.DataFrame) -> str: + """计算24小时价格变化""" + if len(h1_data) < 24: + return "N/A" + price_now = h1_data.iloc[-1]['close'] + price_24h_ago = h1_data.iloc[-24]['close'] + change = ((price_now - price_24h_ago) / price_24h_ago) * 100 + if change >= 0: + return f"+{change:.2f}%" + return f"{change:.2f}%" def _validate_data(self, data: Dict[str, pd.DataFrame]) -> bool: """验证数据完整性""" diff --git a/backend/app/crypto_agent/signal_analyzer.py b/backend/app/crypto_agent/signal_analyzer.py index d3ec3c7..40521c4 100644 --- a/backend/app/crypto_agent/signal_analyzer.py +++ b/backend/app/crypto_agent/signal_analyzer.py @@ -85,6 +85,376 @@ class SignalAnalyzer: """初始化信号分析器""" logger.info("信号分析器初始化完成") + # ==================== K线形态识别 ==================== + + def _detect_candlestick_patterns(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + 识别 K 线形态 + + Args: + df: K线数据(至少需要3根K线) + + Returns: + { + 'bullish_patterns': [...], # 看涨形态 + 'bearish_patterns': [...], # 看跌形态 + 'pattern_weight': float # 形态权重 + } + """ + if len(df) < 3: + return {'bullish_patterns': [], 'bearish_patterns': [], 'pattern_weight': 0} + + bullish = [] + bearish = [] + weight = 0 + + # 获取最近3根K线 + curr = df.iloc[-1] + prev = df.iloc[-2] + prev2 = df.iloc[-3] + + curr_body = curr['close'] - curr['open'] + curr_body_abs = abs(curr_body) + curr_range = curr['high'] - curr['low'] + prev_body = prev['close'] - prev['open'] + prev_body_abs = abs(prev_body) + + # 避免除零 + if curr_range == 0: + curr_range = 0.0001 + + # === 锤子线 / 倒锤子线 === + upper_shadow = curr['high'] - max(curr['open'], curr['close']) + lower_shadow = min(curr['open'], curr['close']) - curr['low'] + + # 锤子线:下影线长,实体小,出现在下跌后 + if lower_shadow > curr_body_abs * 2 and upper_shadow < curr_body_abs * 0.5: + if prev_body < 0: # 前一根是阴线 + bullish.append("锤子线") + weight += 1.5 + + # 倒锤子线:上影线长,实体小,出现在下跌后 + if upper_shadow > curr_body_abs * 2 and lower_shadow < curr_body_abs * 0.5: + if prev_body < 0: + bullish.append("倒锤子线") + weight += 1 + + # 上吊线:锤子线形态但出现在上涨后 + if lower_shadow > curr_body_abs * 2 and upper_shadow < curr_body_abs * 0.5: + if prev_body > 0: + bearish.append("上吊线") + weight -= 1.5 + + # === 吞没形态 === + # 看涨吞没:阳线实体完全包住前一根阴线 + if curr_body > 0 and prev_body < 0: + if curr['open'] <= prev['close'] and curr['close'] >= prev['open']: + if curr_body_abs > prev_body_abs * 1.2: + bullish.append("看涨吞没") + weight += 2 + + # 看跌吞没:阴线实体完全包住前一根阳线 + if curr_body < 0 and prev_body > 0: + if curr['open'] >= prev['close'] and curr['close'] <= prev['open']: + if curr_body_abs > prev_body_abs * 1.2: + bearish.append("看跌吞没") + weight -= 2 + + # === 十字星 === + if curr_body_abs < curr_range * 0.1: # 实体很小 + if upper_shadow > curr_range * 0.3 and lower_shadow > curr_range * 0.3: + # 十字星本身是中性的,需要结合前一根K线判断 + if prev_body > 0: + bearish.append("十字星(上涨后)") + weight -= 1 + elif prev_body < 0: + bullish.append("十字星(下跌后)") + weight += 1 + + # === 早晨之星 / 黄昏之星 (3根K线形态) === + prev2_body = prev2['close'] - prev2['open'] + prev_range = prev['high'] - prev['low'] if prev['high'] != prev['low'] else 0.0001 + + # 早晨之星:大阴线 + 小实体(星) + 大阳线 + if prev2_body < 0 and abs(prev2_body) > prev_range * 0.5: # 第一根大阴线 + if abs(prev_body) < prev_range * 0.3: # 第二根小实体 + if curr_body > 0 and curr_body_abs > curr_range * 0.5: # 第三根大阳线 + if curr['close'] > (prev2['open'] + prev2['close']) / 2: + bullish.append("早晨之星") + weight += 2.5 + + # 黄昏之星:大阳线 + 小实体(星) + 大阴线 + if prev2_body > 0 and prev2_body > prev_range * 0.5: + if abs(prev_body) < prev_range * 0.3: + if curr_body < 0 and curr_body_abs > curr_range * 0.5: + if curr['close'] < (prev2['open'] + prev2['close']) / 2: + bearish.append("黄昏之星") + weight -= 2.5 + + return { + 'bullish_patterns': bullish, + 'bearish_patterns': bearish, + 'pattern_weight': weight + } + + # ==================== 支撑阻力位计算 ==================== + + def _calculate_support_resistance(self, df: pd.DataFrame, current_price: float) -> Dict[str, Any]: + """ + 计算支撑位和阻力位 + + Args: + df: K线数据(建议使用1H或4H数据) + current_price: 当前价格 + + Returns: + { + 'supports': [支撑位1, 支撑位2], + 'resistances': [阻力位1, 阻力位2], + 'nearest_support': float, + 'nearest_resistance': float, + 'at_support': bool, + 'at_resistance': bool + } + """ + if len(df) < 20: + return { + 'supports': [], 'resistances': [], + 'nearest_support': 0, 'nearest_resistance': 0, + 'at_support': False, 'at_resistance': False + } + + # 方法1:使用近期高低点 + highs = df['high'].tail(50).values + lows = df['low'].tail(50).values + + # 找局部高点和低点 + local_highs = [] + local_lows = [] + + for i in range(2, len(highs) - 2): + # 局部高点 + if highs[i] > highs[i-1] and highs[i] > highs[i-2] and \ + highs[i] > highs[i+1] and highs[i] > highs[i+2]: + local_highs.append(highs[i]) + # 局部低点 + if lows[i] < lows[i-1] and lows[i] < lows[i-2] and \ + lows[i] < lows[i+1] and lows[i] < lows[i+2]: + local_lows.append(lows[i]) + + # 方法2:使用均线作为动态支撑阻力 + ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns and pd.notna(df['ma20'].iloc[-1]) else 0 + ma50 = df['ma50'].iloc[-1] if 'ma50' in df.columns and pd.notna(df['ma50'].iloc[-1]) else 0 + + # 方法3:布林带 + bb_upper = df['bb_upper'].iloc[-1] if 'bb_upper' in df.columns and pd.notna(df['bb_upper'].iloc[-1]) else 0 + bb_lower = df['bb_lower'].iloc[-1] if 'bb_lower' in df.columns and pd.notna(df['bb_lower'].iloc[-1]) else 0 + + # 合并所有支撑位(低于当前价格) + all_supports = [] + if local_lows: + all_supports.extend([l for l in local_lows if l < current_price]) + if ma20 and ma20 < current_price: + all_supports.append(ma20) + if ma50 and ma50 < current_price: + all_supports.append(ma50) + if bb_lower and bb_lower < current_price: + all_supports.append(bb_lower) + + # 合并所有阻力位(高于当前价格) + all_resistances = [] + if local_highs: + all_resistances.extend([h for h in local_highs if h > current_price]) + if ma20 and ma20 > current_price: + all_resistances.append(ma20) + if ma50 and ma50 > current_price: + all_resistances.append(ma50) + if bb_upper and bb_upper > current_price: + all_resistances.append(bb_upper) + + # 排序并去重(合并相近的价位) + supports = sorted(set(all_supports), reverse=True)[:3] # 最近的3个支撑 + resistances = sorted(set(all_resistances))[:3] # 最近的3个阻力 + + # 找最近的支撑和阻力 + nearest_support = supports[0] if supports else 0 + nearest_resistance = resistances[0] if resistances else 0 + + # 判断是否在支撑/阻力位附近(1%范围内) + at_support = nearest_support > 0 and abs(current_price - nearest_support) / current_price < 0.01 + at_resistance = nearest_resistance > 0 and abs(current_price - nearest_resistance) / current_price < 0.01 + + return { + 'supports': supports, + 'resistances': resistances, + 'nearest_support': nearest_support, + 'nearest_resistance': nearest_resistance, + 'at_support': at_support, + 'at_resistance': at_resistance + } + + # ==================== 成交量分析 ==================== + + def _analyze_volume(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + 分析成交量 + + Args: + df: K线数据(需要包含 volume, volume_ma20, volume_ratio) + + Returns: + { + 'volume_signal': 'high' | 'normal' | 'low', + 'volume_trend': 'increasing' | 'decreasing' | 'stable', + 'volume_confirms': bool, # 成交量是否确认价格走势 + 'volume_weight': float + } + """ + if len(df) < 5 or 'volume' not in df.columns: + return { + 'volume_signal': 'normal', + 'volume_trend': 'stable', + 'volume_confirms': False, + 'volume_weight': 0 + } + + latest = df.iloc[-1] + prev = df.iloc[-2] + + # 量比判断 + volume_ratio = latest.get('volume_ratio', 1) + if pd.isna(volume_ratio): + volume_ratio = 1 + + if volume_ratio > 2: + volume_signal = 'high' + elif volume_ratio < 0.5: + volume_signal = 'low' + else: + volume_signal = 'normal' + + # 成交量趋势(最近5根K线) + recent_volumes = df['volume'].tail(5) + if len(recent_volumes) >= 5: + first_half = recent_volumes.iloc[:2].mean() + second_half = recent_volumes.iloc[-2:].mean() + if second_half > first_half * 1.3: + volume_trend = 'increasing' + elif second_half < first_half * 0.7: + volume_trend = 'decreasing' + else: + volume_trend = 'stable' + else: + volume_trend = 'stable' + + # 判断成交量是否确认价格走势 + price_up = latest['close'] > prev['close'] + volume_up = latest['volume'] > prev['volume'] + + # 价涨量增 或 价跌量缩 = 确认 + volume_confirms = (price_up and volume_up) or (not price_up and not volume_up) + + # 计算权重 + weight = 0 + if volume_signal == 'high' and volume_confirms: + weight = 1.5 + elif volume_signal == 'high' and not volume_confirms: + weight = -0.5 # 放量但不确认,可能是假突破 + elif volume_signal == 'low': + weight = -0.5 # 缩量,信号可靠性降低 + + return { + 'volume_signal': volume_signal, + 'volume_trend': volume_trend, + 'volume_confirms': volume_confirms, + 'volume_weight': weight + } + + # ==================== 5M 精确入场 ==================== + + def _analyze_5m_entry(self, m5_data: pd.DataFrame, action: str) -> Dict[str, Any]: + """ + 使用 5M 数据寻找精确入场点 + + Args: + m5_data: 5分钟K线数据 + action: 'buy' 或 'sell' + + Returns: + { + 'entry_confirmed': bool, + 'entry_reasons': [...], + 'entry_weight': float + } + """ + if len(m5_data) < 5: + return {'entry_confirmed': False, 'entry_reasons': [], 'entry_weight': 0} + + latest = m5_data.iloc[-1] + prev = m5_data.iloc[-2] + reasons = [] + weight = 0 + + # 获取指标 + rsi = latest.get('rsi', 50) + prev_rsi = prev.get('rsi', 50) + macd = latest.get('macd', 0) + macd_signal = latest.get('macd_signal', 0) + prev_macd = prev.get('macd', 0) + prev_macd_signal = prev.get('macd_signal', 0) + + if action == 'buy': + # 5M RSI 从超卖回升 + if pd.notna(rsi) and pd.notna(prev_rsi): + if rsi < 40 and rsi > prev_rsi: + reasons.append("5M RSI回升") + weight += 1 + if rsi < 30: + reasons.append("5M RSI超卖") + weight += 0.5 + + # 5M MACD 金叉 + if pd.notna(macd) and pd.notna(prev_macd): + if prev_macd <= prev_macd_signal and macd > macd_signal: + reasons.append("5M MACD金叉") + weight += 1.5 + + # 5M K线企稳(阳线) + if latest['close'] > latest['open']: + if prev['close'] < prev['open']: # 前一根是阴线 + reasons.append("5M阳线反转") + weight += 1 + + elif action == 'sell': + # 5M RSI 从超买回落 + if pd.notna(rsi) and pd.notna(prev_rsi): + if rsi > 60 and rsi < prev_rsi: + reasons.append("5M RSI回落") + weight += 1 + if rsi > 70: + reasons.append("5M RSI超买") + weight += 0.5 + + # 5M MACD 死叉 + if pd.notna(macd) and pd.notna(prev_macd): + if prev_macd >= prev_macd_signal and macd < macd_signal: + reasons.append("5M MACD死叉") + weight += 1.5 + + # 5M K线见顶(阴线) + if latest['close'] < latest['open']: + if prev['close'] > prev['open']: + reasons.append("5M阴线反转") + weight += 1 + + entry_confirmed = weight >= 2 + + return { + 'entry_confirmed': entry_confirmed, + 'entry_reasons': reasons, + 'entry_weight': weight + } + def analyze_trend(self, h1_data: pd.DataFrame, h4_data: pd.DataFrame) -> Dict[str, Any]: """ 分析趋势方向和强度(波段交易优化版) @@ -280,14 +650,21 @@ class SignalAnalyzer: return final_score, detail_str def analyze_entry_signal(self, m5_data: pd.DataFrame, m15_data: pd.DataFrame, - trend: Dict[str, Any]) -> Dict[str, Any]: + trend: Dict[str, Any], h1_data: pd.DataFrame = None) -> Dict[str, Any]: """ - 分析 15M 进场信号(波段交易优化版) + 分析 15M 进场信号(波段交易优化版 - 增强版) + + 新增功能: + - 成交量确认 + - K线形态识别 + - 支撑阻力位判断 + - 5M精确入场 Args: m5_data: 5分钟K线数据(用于精确入场) m15_data: 15分钟K线数据(主要入场周期) trend: 趋势分析结果 + h1_data: 1小时K线数据(用于支撑阻力位计算,可选) Returns: { @@ -295,7 +672,10 @@ class SignalAnalyzer: 'confidence': 0-100, 'signal_grade': 'A' | 'B' | 'C' | 'D', 'reasons': [...], - 'indicators': {...} + 'indicators': {...}, + 'patterns': {...}, + 'levels': {...}, + 'volume_analysis': {...} } """ if m5_data.empty or m15_data.empty: @@ -313,12 +693,15 @@ class SignalAnalyzer: trend_strength = trend.get('strength', 'moderate') m15_latest = m15_data.iloc[-1] + current_price = float(m15_latest['close']) # 收集信号 buy_signals = [] sell_signals = [] signal_weights = {'buy': 0, 'sell': 0} + # ==================== 1. 传统技术指标信号 ==================== + # === RSI 信号 === if 'rsi' in m15_latest and pd.notna(m15_latest['rsi']): rsi = m15_latest['rsi'] @@ -326,7 +709,6 @@ class SignalAnalyzer: buy_signals.append(f"RSI超卖({rsi:.1f})") signal_weights['buy'] += 2 elif rsi < 40 and len(m15_data) >= 2: - # RSI 从低位回升 prev_rsi = m15_data.iloc[-2].get('rsi', 50) if pd.notna(prev_rsi) and rsi > prev_rsi: buy_signals.append(f"RSI回升({prev_rsi:.1f}→{rsi:.1f})") @@ -345,15 +727,12 @@ class SignalAnalyzer: prev = m15_data.iloc[-2] if 'macd' in m15_latest and 'macd_signal' in m15_latest: if pd.notna(m15_latest['macd']) and pd.notna(prev['macd']): - # 金叉 if prev['macd'] <= prev['macd_signal'] and m15_latest['macd'] > m15_latest['macd_signal']: buy_signals.append("MACD金叉") signal_weights['buy'] += 2 - # 死叉 elif prev['macd'] >= prev['macd_signal'] and m15_latest['macd'] < m15_latest['macd_signal']: sell_signals.append("MACD死叉") signal_weights['sell'] += 2 - # MACD 柱状图缩小(趋势减弱) elif abs(m15_latest['macd_hist']) < abs(prev['macd_hist']) * 0.7: if m15_latest['macd_hist'] > 0: sell_signals.append("MACD动能减弱") @@ -372,7 +751,6 @@ class SignalAnalyzer: elif m15_latest['close'] > m15_latest['bb_upper']: sell_signals.append("触及布林上轨") signal_weights['sell'] += 1.5 - # 突破中轨 elif len(m15_data) >= 2: prev_close = m15_data.iloc[-2]['close'] if prev_close < bb_middle and m15_latest['close'] > bb_middle: @@ -401,7 +779,47 @@ class SignalAnalyzer: sell_signals.append("KDJ死叉") signal_weights['sell'] += 0.5 - # === 根据趋势和阶段决定动作 === + # ==================== 2. K线形态识别 ==================== + patterns = self._detect_candlestick_patterns(m15_data) + if patterns['bullish_patterns']: + buy_signals.extend(patterns['bullish_patterns']) + signal_weights['buy'] += patterns['pattern_weight'] + if patterns['bearish_patterns']: + sell_signals.extend(patterns['bearish_patterns']) + signal_weights['sell'] += abs(patterns['pattern_weight']) + + # ==================== 3. 成交量分析 ==================== + volume_analysis = self._analyze_volume(m15_data) + if volume_analysis['volume_signal'] == 'high': + if volume_analysis['volume_confirms']: + # 放量确认 + if signal_weights['buy'] > signal_weights['sell']: + buy_signals.append(f"放量确认(量比{m15_latest.get('volume_ratio', 1):.1f})") + signal_weights['buy'] += volume_analysis['volume_weight'] + else: + sell_signals.append(f"放量确认(量比{m15_latest.get('volume_ratio', 1):.1f})") + signal_weights['sell'] += volume_analysis['volume_weight'] + else: + # 放量不确认,可能是假信号 + if signal_weights['buy'] > signal_weights['sell']: + buy_signals.append("放量但不确认(警惕)") + signal_weights['buy'] += volume_analysis['volume_weight'] # 负权重 + else: + sell_signals.append("放量但不确认(警惕)") + signal_weights['sell'] += volume_analysis['volume_weight'] + + # ==================== 4. 支撑阻力位分析 ==================== + levels = {} + if h1_data is not None and not h1_data.empty: + levels = self._calculate_support_resistance(h1_data, current_price) + if levels.get('at_support') and trend_direction == 'bullish': + buy_signals.append(f"触及支撑位({levels['nearest_support']:.2f})") + signal_weights['buy'] += 1.5 + if levels.get('at_resistance') and trend_direction == 'bearish': + sell_signals.append(f"触及阻力位({levels['nearest_resistance']:.2f})") + signal_weights['sell'] += 1.5 + + # ==================== 5. 根据趋势和阶段决定动作 ==================== action = 'hold' confidence = 0 reasons = [] @@ -410,13 +828,11 @@ class SignalAnalyzer: # 波段交易核心逻辑:在回调中寻找入场机会 if trend_direction == 'bullish': if trend_phase == 'correction' and signal_weights['buy'] >= 3: - # 上涨趋势 + 回调 + 买入信号 = 最佳做多机会 action = 'buy' confidence = min(40 + signal_weights['buy'] * 10, 95) reasons = buy_signals + [f"上涨趋势回调({trend_strength})"] signal_grade = 'A' if confidence >= 80 else ('B' if confidence >= 60 else 'C') elif trend_phase == 'impulse' and signal_weights['buy'] >= 4: - # 主升浪中追多需要更强信号 action = 'buy' confidence = min(30 + signal_weights['buy'] * 8, 80) reasons = buy_signals + ["主升浪追多"] @@ -426,7 +842,6 @@ class SignalAnalyzer: elif trend_direction == 'bearish': if trend_phase == 'correction' and signal_weights['sell'] >= 3: - # 下跌趋势 + 反弹 + 卖出信号 = 最佳做空机会 action = 'sell' confidence = min(40 + signal_weights['sell'] * 10, 95) reasons = sell_signals + [f"下跌趋势反弹({trend_strength})"] @@ -440,18 +855,36 @@ class SignalAnalyzer: reasons = ['极端行情,等待企稳'] else: # neutral - # 震荡市不交易 reasons = ['趋势不明确,观望'] + # ==================== 6. 5M 精确入场确认 ==================== + if action != 'hold' and not m5_data.empty: + entry_5m = self._analyze_5m_entry(m5_data, action) + if entry_5m['entry_confirmed']: + confidence = min(confidence + 10, 95) + reasons.extend(entry_5m['entry_reasons']) + if signal_grade == 'B': + signal_grade = 'A' + elif signal_grade == 'C': + signal_grade = 'B' + elif entry_5m['entry_weight'] < 1: + # 5M 没有确认,降低置信度 + confidence = max(confidence - 10, 0) + reasons.append("5M未确认入场") + if not reasons: reasons = ['信号不足,继续观望'] # 收集指标数据 indicators = {} - for col in ['rsi', 'macd', 'macd_signal', 'macd_hist', 'k', 'd', 'j', 'close', 'ma20']: + for col in ['rsi', 'macd', 'macd_signal', 'macd_hist', 'k', 'd', 'j', 'close', 'ma20', 'volume_ratio']: if col in m15_latest and pd.notna(m15_latest[col]): indicators[col] = float(m15_latest[col]) + # 记录详细日志(简化版,详细日志在 crypto_agent 中输出) + if action != 'hold': + logger.debug(f"信号详情: {action} {confidence}% {signal_grade} | 买权重={signal_weights['buy']:.1f} 卖权重={signal_weights['sell']:.1f}") + return { 'action': action, 'confidence': confidence, @@ -462,7 +895,11 @@ class SignalAnalyzer: 'direction': trend_direction, 'phase': trend_phase, 'strength': trend_strength - } + }, + 'patterns': patterns, + 'volume_analysis': volume_analysis, + 'levels': levels, + 'signal_weights': signal_weights } async def llm_analyze(self, data: Dict[str, pd.DataFrame], signal: Dict[str, Any], diff --git a/backend/app/services/binance_service.py b/backend/app/services/binance_service.py index 0dbf3dc..4870444 100644 --- a/backend/app/services/binance_service.py +++ b/backend/app/services/binance_service.py @@ -143,6 +143,11 @@ class BinanceService: # ATR df['atr'] = self._calculate_atr(df['high'], df['low'], df['close']) + # 成交量均线 + df['volume_ma5'] = self._calculate_ma(df['volume'], 5) + df['volume_ma20'] = self._calculate_ma(df['volume'], 20) + df['volume_ratio'] = df['volume'] / df['volume_ma20'] # 量比 + return df @staticmethod