From 9166e4a9877afa81103889a577ad15564d213ce6 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 13 Mar 2026 21:24:31 +0800 Subject: [PATCH] update --- .../crypto_agent/market_signal_analyzer.py | 186 +++++++++++++++++- backend/app/news_agent/analyzer.py | 57 ++++++ backend/app/services/multi_llm_service.py | 24 ++- 3 files changed, 258 insertions(+), 9 deletions(-) diff --git a/backend/app/crypto_agent/market_signal_analyzer.py b/backend/app/crypto_agent/market_signal_analyzer.py index 70fcea2..b593727 100644 --- a/backend/app/crypto_agent/market_signal_analyzer.py +++ b/backend/app/crypto_agent/market_signal_analyzer.py @@ -24,10 +24,78 @@ from app.services.news_service import get_news_service class MarketSignalAnalyzer: """市场信号分析器 - 只关注市场,输出客观信号""" - # 纯市场分析系统提示词(日内交易优化版 + 多级别反转检测) - MARKET_ANALYSIS_PROMPT = """你是一位专业的加密货币**日内交易员**和技术分析师。你的任务是综合分析**趋势方向、K线数据、量价关系、技术指标和新闻舆情**,给出**适合日内快进快出**的交易信号。 + # 纯市场分析系统提示词(日内交易优化版 + 多级别反转检测 + 市场状态动态调整) + MARKET_ANALYSIS_PROMPT = """你是一位专业的加密货币**智能交易员**和技术分析师。你的任务是综合分析**趋势方向、市场状态、K线数据、量价关系、技术指标和新闻舆情**,给出**适合当前市场状态**的交易信号。 -## 🎯 日内交易核心定位 + ## 🎯 核心策略:根据市场状态动态调整(最重要!) + + ### 📊 第一步:判断市场状态 + + **市场状态分类**: + - **震荡市(Range-bound)**:价格在一定区间内波动,无明确方向 + - **趋势市(Trending)**:价格有明确方向(上涨/下跌),持续突破/跌破 + + **判断标准**: + 1. **1h EMA 趋势判断**: + - EMA5/10/20 多头/空头排列 → 趋势市 + - EMA 纠缠,无明确方向 → 震荡市 + + 2. **30m 波动率判断(ATR)**: + - ATR 收缩(较前期下降 > 20%) → 震荡市 + - ATR 扩张(较前期上升 > 20%) → 趋势市启动 + + 3. **15m 价格动量判断**: + - 价格在区间内波动(±2%) → 震荡市 + - 价格持续创新高/新低 → 趋势市 + + ### 📈 第二步:根据市场状态选择策略 + + #### 策略A:震荡市策略 + **特点**:无明确方向,价格在区间内波动 + **核心操作**:**5分钟级别高抛低吸,快进快出** + + **震荡市入场时机**: + - ✅ 回调到下沿支撑 → 5m 反弹信号 → 做多(目标 1-2%) + - ✅ 反弹到上沿压力 → 5m 下跌信号 → 做空(目标 1-2%) + - ✅ RSI 40-60 震荡,超卖区做多,超买区做空 + - ✅ 盈亏比 ≥ 1:1.2(震荡市目标小,盈亏比可适当放宽) + - ✅ 持仓时间:30分钟-2小时(快速进出) + + **震荡市严禁**: + - ❌ 追涨杀跌(价格突破后不要追,通常会回落) + - ❌ 期待大行情(震荡市无大趋势,不要贪婪) + - ❌ 持仓过久(区间内快速进出) + + #### 策略B:趋势市策略 + **特点**:价格有明确方向,持续突破/跌破 + **核心操作**:**跟随趋势,等待回调/反弹入场,持仓更长** + + **趋势市入场时机**: + - ✅ 趋势回调/反弹到 EMA20/支撑压力位 → 顺势入场 + - ✅ 放量突破/跌破关键位 → 等待回踩确认后入场 + - ✅ 盈亏比 ≥ 1:1.5(趋势市目标更大,要求更严格) + - ✅ 持仓时间:2-6小时(跟随趋势) + + **趋势市严禁**: + - ❌ **逆势做超短线**(趋势明确时不要反向操作!) + - ❌ 频繁进出(趋势要持仓,不要被小波动洗出) + - ❌ 小波动就止盈(让利润奔跑) + + ### 🔄 策略切换规则(关键!) + + **震荡 → 趋势**: + - 1h EMA 多头/空头排列形成 + - 放量突破/跌破震荡区间 + - ATR 显著扩张(> 20%) + - **立即切换到趋势市策略,停止超短线反向操作** + + **趋势 → 震荡**: + - 趋势减弱,EMA 开始纠缠 + - 波动率收缩(ATR 下降 > 20%) + - 价格不再创新高/新低 + - **切换到震荡市策略,准备高抛低吸** + + ## 🎯 日内交易核心定位 **日内交易 = 快进快出 + 盈亏比第一 + 严控风险** - 目标:2-3% 快速获利,不是波段行情 - 时限:单笔持仓不超过 4 小时 @@ -544,6 +612,7 @@ class MarketSignalAnalyzer: ```json { + "market_state": "ranging/trending", "trend_direction": "uptrend/downtrend/neutral", "trend_strength": "strong/medium/weak", "analysis_summary": "简要描述当前市场状态(50字以内)", @@ -571,6 +640,31 @@ class MarketSignalAnalyzer: } ``` +## 重要说明 - market_state 字段(新增) +- **market_state**:**必须明确判断当前是震荡市还是趋势市** + - `ranging`(震荡市):价格在区间内波动,无明确方向,使用5分钟高抛低吸策略 + - `trending`(趋势市):价格有明确方向,跟随趋势,等待回调/反弹入场 + +**判断标准**: +1. **1h EMA 纠缠** → ranging +2. **30m ATR 收缩 > 20%** → ranging +3. **1h EMA 多头/空头排列** → trending +4. **价格持续创新高/新低** → trending + +**策略差异**: +- **震荡市(ranging)**: + - 5分钟级别操作 + - 支撑位做多,压力位做空 + - 目标 1-2%,快速进出 + - 盈亏比 ≥ 1:1.2 + +- **趋势市(trending)**: + - 30分钟/1小时级别操作 + - 等待回调/反弹到 EMA20 顺势入场 + - 目标 3-5%,跟随趋势 + - 盈亏比 ≥ 1:1.5 + - **严禁逆势做超短线** + ## 重要说明 - `entry_price`:建议入场价格(单一值) - `entry_type`:入场方式 - `market`(现价立即入场)或 `limit`(挂单等待) @@ -887,10 +981,11 @@ class MarketSignalAnalyzer: return "新闻获取失败" def _analyze_trend_position(self, data: Dict[str, pd.DataFrame]) -> str: - """分析趋势位置和日内交易机会(使用 EMA)""" + """分析趋势位置和日内交易机会(使用 EMA)+ 市场状态判断(震荡/趋势)""" try: df_30m = data.get('30m') df_15m = data.get('15m') + df_1h = data.get('1h') if df_30m is None or len(df_30m) < 50: return "" @@ -906,6 +1001,61 @@ class MarketSignalAnalyzer: if not all([ema5_30m, ema10_30m, ema20_30m]): return "" + # ========== 新增:市场状态判断(震荡 vs 趋势) ========== + market_state = "unknown" + market_state_reason = [] + + # 1h EMA 趋势判断 + if df_1h is not None and len(df_1h) >= 20: + latest_1h = df_1h.iloc[-1] + ema5_1h = latest_1h.get('ma5') + ema10_1h = latest_1h.get('ma10') + ema20_1h = latest_1h.get('ma20') + + if ema5_1h and ema10_1h and ema20_1h: + # 1h EMA 多头/空头排列 → 趋势市 + if ema5_1h > ema10_1h > ema20_1h: + market_state = "trending" + market_state_reason.append("1h EMA 多头排列") + elif ema5_1h < ema10_1h < ema20_1h: + market_state = "trending" + market_state_reason.append("1h EMA 空头排列") + else: + market_state = "ranging" + market_state_reason.append("1h EMA 纠缠") + + # 波动率判断(ATR 变化) + if df_30m is not None and len(df_30m) >= 24 and 'atr' in df_30m.columns: + recent_atr = df_30m['atr'].iloc[-6:].mean() # 最近3小时 + older_atr = df_30m['atr'].iloc[-12:-6].mean() # 之前3小时 + + if pd.notna(recent_atr) and pd.notna(older_atr) and older_atr > 0: + atr_change = (recent_atr - older_atr) / older_atr * 100 + + if atr_change > 20: + if market_state != "trending": + market_state = "trending" + market_state_reason.append(f"ATR 扩张 {atr_change:.0f}%") + elif atr_change < -20: + if market_state != "ranging": + market_state = "ranging" + market_state_reason.append(f"ATR 收缩 {abs(atr_change):.0f}%") + + # 价格动量判断(15m) + if df_15m is not None and len(df_15m) >= 20: + recent_high = df_15m['high'].iloc[-20:].max() + recent_low = df_15m['low'].iloc[-20:].min() + price_range = (recent_high - recent_low) / current_price * 100 + + if price_range < 2.5: # 15分钟内波动小于2.5% → 震荡 + if market_state != "trending": + market_state = "ranging" + market_state_reason.append(f"15m 波动 {price_range:.1f}% 较小") + elif price_range > 4: # 15分钟内波动大于4% → 趋势 + if market_state != "ranging": + market_state = "trending" + market_state_reason.append(f"15m 波动 {price_range:.1f}% 较大") + # 判断日内趋势(30m EMA 为主) if ema5_30m > ema10_30m > ema20_30m: intraday_trend = "上升" @@ -917,7 +1067,33 @@ class MarketSignalAnalyzer: intraday_trend = "震荡" intraday_emoji = "➖" - analysis = [f"日内趋势(30m EMA): {intraday_emoji} {intraday_trend}"] + # 构建市场状态分析 + analysis_parts = [] + + # 市场状态显示(新增) + if market_state == "trending": + state_emoji = "📊" + state_text = f"{state_emoji} **市场状态: 趋势市**" + analysis_parts.append(state_text) + analysis_parts.append(f" 判断依据: {', '.join(market_state_reason)}") + analysis_parts.append(f" 策略: 跟随趋势,等待回调/反弹到 EMA20 顺势入场") + analysis_parts.append(f" 目标: 3-5%,盈亏比 ≥ 1:1.5") + analysis_parts.append(f" 严禁: 逆势做超短线") + elif market_state == "ranging": + state_emoji = "🔄" + state_text = f"{state_emoji} **市场状态: 震荡市**" + analysis_parts.append(state_text) + analysis_parts.append(f" 判断依据: {', '.join(market_state_reason)}") + analysis_parts.append(f" 策略: 5分钟级别高抛低吸,支撑位多、压力位空") + analysis_parts.append(f" 目标: 1-2%,盈亏比 ≥ 1:1.2") + analysis_parts.append(f" 严禁: 追涨杀跌") + else: + analysis_parts.append(f"⚠️ 市场状态: 不明确,观望为主") + + analysis_parts.append(f"") + analysis_parts.append(f"日内趋势(30m EMA): {intraday_emoji} {intraday_trend}") + + analysis = analysis_parts # 检查15分钟级别入场时机 if df_15m is not None and len(df_15m) >= 20: diff --git a/backend/app/news_agent/analyzer.py b/backend/app/news_agent/analyzer.py index 3186d7e..541dab5 100644 --- a/backend/app/news_agent/analyzer.py +++ b/backend/app/news_agent/analyzer.py @@ -32,6 +32,10 @@ class NewsAnalyzer: self.batch_size = 10 # 每次最多分析 10 条新闻(只传标题,可以增加数量) self.max_retries = 2 + # 余额错误通知冷却时间(秒) + self._balance_error_cooldown = 3600 # 1小时内只通知一次 + self._balance_error_last_notified = None + def _build_analysis_prompt(self, news_item: NewsItem) -> str: """构建单条新闻的分析提示词""" @@ -315,6 +319,13 @@ class NewsAnalyzer: except Exception as e: logger.warning(f"分析失败 (尝试 {attempt + 1}/{self.max_retries}): {e}") + # 检查是否是余额不足错误 (402) + error_str = str(e) + error_code = str(e).split('Error code: ')[1].split(' -')[0] if 'Error code:' in error_str else '' + if error_code == '402' or ('402' in error_str and 'insufficient balance' in error_str.lower()): + await self._notify_balance_error(e) + break # 余额不足不再重试 + logger.error(f"新闻分析失败,已达最大重试次数: {news_item.title[:50]}") return None @@ -369,11 +380,57 @@ class NewsAnalyzer: results.extend([None] * len(batch)) except Exception as e: + error_str = str(e) + error_code = str(e).split('Error code: ')[1].split(' -')[0] if 'Error code:' in error_str else '' + logger.error(f"批量分析失败: {e}") + + # 检查是否是余额不足错误 (402) + if error_code == '402' or ('402' in error_str and 'insufficient balance' in error_str.lower()): + await self._notify_balance_error(e) + results.extend([None] * len(batch)) return results + async def _notify_balance_error(self, error: Exception): + """ + 发送余额不足的飞书通知 + + Args: + error: 异常对象 + """ + # 检查冷却时间 + now = datetime.now() + if self._balance_error_last_notified: + time_since_last = (now - self._balance_error_last_notified).total_seconds() + if time_since_last < self._balance_error_cooldown: + logger.info(f"余额错误通知冷却中,剩余 {int(self._balance_error_cooldown - time_since_last)} 秒") + return + + # 发送通知 + try: + from app.services.feishu_service import get_feishu_service + feishu = get_feishu_service() + + message = f"""🚨 **新闻分析 LLM API 余额不足警告** + +**服务商**: DeepSeek +**错误类型**: 余额不足 (Insufficient Balance) +**错误信息**: {str(error)[:200]} +**时间**: {now.strftime('%Y-%m-%d %H:%M:%S')} + +⚠️ 请及时充值,否则新闻智能体将无法正常工作""" + + await feishu.send_text(message) + logger.warning("已发送 DeepSeek 余额不足飞书通知") + + # 记录通知时间 + self._balance_error_last_notified = now + + except Exception as e: + logger.error(f"发送余额不足通知失败: {e}") + def calculate_priority(self, analysis: Dict[str, Any], quality_score: float = 0.5) -> float: """ 根据分析结果计算优先级 diff --git a/backend/app/services/multi_llm_service.py b/backend/app/services/multi_llm_service.py index 343ab59..8dd1758 100644 --- a/backend/app/services/multi_llm_service.py +++ b/backend/app/services/multi_llm_service.py @@ -152,7 +152,7 @@ class MultiLLMService: async def _notify_balance_error(self, provider: str, error: Exception): """ - 发送余额不足的Telegram通知 + 发送余额不足的通知(Telegram + 飞书) Args: provider: LLM提供商 @@ -170,14 +170,17 @@ class MultiLLMService: # 发送通知 try: from app.services.telegram_service import get_telegram_service + from app.services.feishu_service import get_feishu_service telegram = get_telegram_service() + feishu = get_feishu_service() provider_name = { 'zhipu': '智谱AI (GLM-4)', 'deepseek': 'DeepSeek' }.get(provider, provider) - message = f"""🚨 LLM API 余额不足警告 + # Telegram 通知 + telegram_message = f"""🚨 LLM API 余额不足警告 ━━━━━━━━━━━━━━━━━━━━ @@ -189,8 +192,21 @@ class MultiLLMService: 请及时充值,否则智能体将无法正常工作""" - await telegram.send_message(message, parse_mode="HTML") - logger.warning(f"已发送 {provider} 余额不足Telegram通知") + await telegram.send_message(telegram_message, parse_mode="HTML") + logger.info(f"已发送 {provider} 余额不足 Telegram 通知") + + # 飞书通知 + feishu_message = f"""🚨 **LLM API 余额不足警告** + +**服务商**: {provider_name} +**错误类型**: 余额不足 (Insufficient Balance) +**错误信息**: {str(error)[:200]} +**时间**: {now.strftime('%Y-%m-%d %H:%M:%S')} + +⚠️ 请及时充值,否则智能体将无法正常工作""" + + await feishu.send_text(feishu_message) + logger.info(f"已发送 {provider} 余额不足飞书通知") # 记录通知时间 self._balance_error_notified[provider] = now