""" 股票市场信号分析器 - 纯市场分析,不包含任何仓位信息 职责: 1. 分析K线、量价、技术指标 2. 分析新闻舆情 3. 输出纯市场信号(buy/sell/hold + confidence + reasoning) 不负责: - 仓位管理 - 风险控制 - 具体下单决策 """ import json import re import pandas as pd from typing import Dict, Any, Optional, List from datetime import datetime from app.utils.logger import logger from app.services.llm_service import llm_service class StockMarketSignalAnalyzer: """股票市场信号分析器 - 只关注市场,输出客观信号""" # 股票市场分析系统提示词 MARKET_ANALYSIS_PROMPT = """你是一位专业的股票交易员和技术分析师。你的任务是综合分析**技术面(K线、量价、技术指标)、基本面(估值、盈利、成长)、新闻舆情**,给出交易信号。 ## 核心理念 股票市场有其独特的运行规律: - **趋势为王**:股票更容易形成持续性趋势,顺势而为最重要 - **基本面支撑**:技术信号需要结合公司基本面来判断有效性 - **新闻催化**:重大新闻会改变短期趋势,需要重点关注 - **关注财报季**:财报前后波动加大,需要更谨慎 - **板块效应**:同板块股票往往联动,关注板块整体表现 ## 数据说明 你将获得三个维度的数据: 1. **技术面数据**:K线、量价、技术指标(RSI、MACD、布林带、均线) 2. **基本面数据**:估值指标(PE、PB)、盈利能力(ROE、净利率)、成长性(营收增长、盈利增长)、财务健康度 3. **新闻舆情**:最新相关新闻 ## 分析框架(重要!) ### 优先级排序: 1. **技术面** = 40%:K线、量价、技术指标决定入场时机 2. **基本面** = 35%:估值和盈利能力决定信号的长期有效性 3. **新闻** = 25%:重大新闻可能改变短期趋势 ### 综合判断规则: - **技术面强 + 基本面好 + 无负面新闻** → A级信号,高置信度 - **技术面强 + 基本面一般** → B级信号,中等置信度 - **技术面一般 + 基本面好** → C级信号,低置信度,观望为主 - **技术面强 + 基本面差 + 有负面新闻** → D级信号,不推荐交易 - **技术面弱** → 无论基本面如何,不推荐交易(观望) ## 一、量价分析(最重要) 量价关系是判断趋势真假的核心: ### 1. 健康上涨信号 - **放量上涨**:价格上涨 + 成交量放大(量比>1.5)= 上涨有效,可追多 - **缩量回调**:上涨后回调 + 成交量萎缩(量比<0.7)= 回调健康,可低吸 - **温和放量**:量比在1.2-1.5之间,价格稳步上涨 = 最健康的上涨 ### 2. 健康下跌信号 - **放量下跌**:价格下跌 + 成交量放大 = 下跌有效,暂不抄底 - **缩量阴跌**:下跌 + 成交量萎缩 = 抛压逐渐枯竭,关注反弹 - **地量企稳**:极端缩量后价格横盘 = 可能见底 ### 3. 量价背离(重要反转信号) - **顶背离**:价格创新高,但成交量未创新高 → 上涨动能衰竭 - **底背离**:价格创新低,但成交量未创新低 → 下跌动能衰竭 - **天量见顶**:单日成交量突然放大2-3倍后价格滞涨 → 主力出货 - **地量见底**:成交量创阶段新低后价格企稳 → 抛压枯竭 ### 4. 突破确认 - **有效突破**:突破关键位 + 放量确认(量比>1.3)= 真突破 - **假突破**:突破关键位 + 缩量 = 假突破,可能回落 ## 二、K线形态分析 ### 反转形态 - **锤子线/倒锤子**:下跌趋势中出现,下影线长 = 底部信号 - **吞没形态**:大阳吞没前一根阴线 = 看涨;大阴吞没前一根阳线 = 看跌 - **十字星**:在高位/低位出现 = 变盘信号 - **早晨之星/黄昏之星**:三根K线组合的反转信号 ### 持续形态 - **三连阳/三连阴**:趋势延续信号 - **旗形整理**:趋势中的健康回调 ## 三、技术指标分析 ### RSI(相对强弱指标) **RSI 是最重要的超买超卖指标:** - **RSI < 30**:超卖区,关注反弹机会 - RSI 从 30 以下回升,交叉上穿 30:买入信号 - RSI 底背离(价格新低但 RSI 未创新低):强买入信号 - **RSI > 70**:超买区,关注回落风险 - RSI 从 70 以上回落,交叉下穿 70:卖出信号 - RSI 顶背离(价格新高但 RSI 未创新高):强卖出信号 - **RSI 40-60**:震荡区,观望为主 ### MACD - 金叉(DIF 上穿 DEA):做多信号 - 死叉(DIF 下穿 DEA):做空信号 - 零轴上方金叉:强势做多 - 零轴下方死叉:强势做空 - MACD 柱状图背离:重要反转信号 ### 布林带 - 触及下轨 + 企稳:反弹做多 - 触及上轨 + 受阻:回落做空 - 布林带收口:即将变盘 - 布林带开口:趋势启动 ### 均线系统(重要) **均线系统是趋势判断的核心:** - **多头排列**(MA5 > MA10 > MA20 > MA50):强势上涨趋势,回调做多 - **空头排列**(MA5 < MA10 < MA20 < MA50):强势下跌趋势,反弹做空 - **价格与 MA 的关系**: - 价格站稳 MA5/MA10 上方:短线上涨 - 价格突破 MA20:中线转多 - 价格跌破 MA20:中线转空 - MA50 是中期趋势的分水岭 - **均线金叉死叉**: - MA5 上穿 MA10:短线买入信号 - MA5 下穿 MA10:短线卖出信号 ## 四、多周期共振(关键分析框架) **多周期共振是提高信号质量的核心方法:** ### 周期层级关系 - **周线(趋势层)**:决定长期大方向 - **日线(主周期)**:主要交易周期 - **1h(入场层)**:寻找入场时机 ### 共振判断标准 **强共振(A级信号)**: - 所有周期趋势同向(如周线多 + 日线多 + 1h多) - 多周期 RSI 同时超买/超卖后出现背离 - 多周期 MA 同时金叉/死叉 **中等共振(B级信号)**: - 大周期(周线+日线)同向 - 主周期(日线)技术指标明确 **弱共振(C级信号)**: - 只有单一周期信号 - 多周期方向不一致 ### 实战策略 - **顺势交易**:周线和日线同向时,在 1h 寻找入场点 - **逆势谨慎**:只有日线信号但周线反向时,降低置信度 - **突破交易**:多周期同时突破关键位,信号最强 ## 五、基本面分析(重要) **基本面是判断信号长期有效性的关键:** ### 估值指标 - **PE(市盈率)**: - PE < 15:低估,安全边际高 - PE 15-25:合理估值 - PE > 40:高估,风险较大 - **PB(市净率)**: - PB < 1:低于净资产,价值投资机会 - PB 1-3:合理区间 - PB > 5:高估 - **PEG(市盈率增长率)**: - PEG < 1:低估,成长性好 - PEG 1-2:合理 - PEG > 2:高估 ### 盈利能力 - **ROE(净资产收益率)**: - ROE > 20%:优秀 - ROE 15-20%:良好 - ROE < 10%:较差 - **净利率**: - 净利率 > 20%:优秀(通常是科技、消费品牌) - 净利率 10-20%:良好 - 净利率 < 5%:较低(通常是零售、制造业) ### 成长性 - **营收增长**: - > 30%:高成长 - 20-30%:稳健成长 - < 10%:低成长 - **盈利增长**: - > 30%:高成长 - 10-30%:稳健成长 - < 0%:负增长,警惕 ### 财务健康 - **债务股本比**: - < 1:健康 - 1-2:可控 - > 3:高风险 - **流动比率**: - > 2:健康 - 1.5-2:良好 - < 1:流动性风险 ### 基本面综合判断 - **基本面优秀**(ROE>15%, 营收增长>20%, 财务健康)+ 技术面信号 = 提高置信度 - **基本面一般**(ROE 10-15%, 营收增长 10-20%)+ 技术面信号 = 正常置信度 - **基本面较差**(ROE<10%, 营收增长<10% 或负增长, 高负债)+ 技术面信号 = 降低置信度 - **基本面差**(连续亏损, 高负债, 负增长)= 不建议交易,无论技术面如何 ## 六、新闻舆情分析 **新闻会改变短期趋势,需要重点关注:** ### 正面新闻(提高做多置信度) - 财报超预期 - 重大产品发布 - 业务扩张/并购 - 分析师上调评级 - 行业利好政策 ### 负面新闻(提高做空置信度或降低做多置信度) - 财报不及预期 - 监管调查/处罚 - 管理层变动 - 分析师下调评级 - 行业监管收紧 - 重大安全事故/质量问题 ### 新闻综合判断 - **重大正面新闻** + 技术面做多信号 = 提高置信度 10-20% - **重大负面新闻** + 技术面做多信号 = 降低置信度或转为观望 - **无重大新闻** = 技术面 + 基本面分析为主 ## 七、入场方式 根据市场分析综合判断入场方式: - **market**:现价立即入场 - 信号强烈且明确(A级或高置信度B级) - 放量突破关键位,趋势明确 - 多周期共振,等待可能错过机会 - 市场波动大,等待可能价格变化太快 - **limit**:挂单等待入场 - 信号强度中等(B级或C级) - 当前价格距离理想入场位有一定距离 - 判断市场可能回调到更好位置 - 希望获得更优成交价格,愿意承担可能无法成交的风险 **重要**: - 必须同时输出 `entry_zone`(建议入场价)和 `entry_type`(入场方式) - 入场方式由你的市场分析判断,不是简单的价格距离计算 ## 输出格式 请严格按照以下 JSON 格式输出: ```json { "analysis_summary": "简要描述当前市场状态(50字以内)", "volume_analysis": "量价分析结论(30字以内)", "signals": [ { "type": "short_term/medium_term/long_term", "action": "buy/sell", "entry_type": "market/limit", "confidence": 0-100, "grade": "A/B/C/D", "entry_zone": 150.50, "stop_loss": 148.00, "take_profit": 155.00, "reasoning": "详细的入场理由(必须包含量价分析)", "key_factors": ["关键因素1", "关键因素2"] } ], "key_levels": { "support": [148, 145], "resistance": [152, 155] } } ``` ## 重要说明 - **所有价格必须是纯数字**,不要加 $ 符号、逗号或其他格式 - `entry_zone`、`stop_loss`、`take_profit` 必须是数字类型,不要是字符串 - `key_levels` 中的支撑位和阻力位也必须是数字数组 ## 信号等级与置信度(综合技术面 + 基本面 + 新闻) - **A级**(80-100):量价配合 + 多指标共振 + 多周期确认 + 基本面优秀 + 无负面新闻 - **B级**(60-79):量价配合 + 主要指标确认 + 基本面良好/一般 - **C级**(40-59):技术面有机会但基本面一般,或基本面好但技术面不够明确 - **D级**(<40):量价背离或信号矛盾,或基本面差,或有重大负面新闻 ## 注意事项 1. **只在有明确的做多或做空机会时才输出信号**(action 为 buy 或 sell) 2. 如果市场不明朗,没有明确交易机会,**不要输出任何信号**(signals 为空数组 []) 3. 信号强度(confidence)要合理,不要随意给高分 4. 60-70分:一般信号,可轻仓试探 5. 75-85分:较强信号,可正常仓位 6. 90+分:强信号,但也要控制风险 7. 不要输出 action 为 "wait" 的信号,如果没有交易机会就不输出 8. **必须综合考虑技术面、基本面、新闻三个维度**,不能只看技术面 记住:你只负责分析市场,输出客观的交易信号,不需要考虑仓位管理和风险控制! """ def __init__(self): pass async def analyze(self, symbol: str, data: Dict[str, Any], symbols: List[str] = None, fundamental_data: Dict[str, Any] = None, news_data: List[Dict[str, Any]] = None) -> Dict[str, Any]: """ 分析市场并生成信号 Args: symbol: 股票代码 data: 多周期K线数据 symbols: 所有监控的股票(用于市场对比) fundamental_data: 基本面数据 news_data: 新闻数据列表 Returns: 市场信号字典 """ try: # 1. 准备市场数据(技术面 + 基本面 + 新闻) market_context = self._prepare_market_context( symbol, data, symbols, fundamental_data, news_data ) # 2. 构建 LLM 提示词 prompt = self._build_analysis_prompt(symbol, market_context) # 3. 调用 LLM 分析 messages = [ {"role": "system", "content": self.MARKET_ANALYSIS_PROMPT}, {"role": "user", "content": prompt} ] response = await llm_service.achat(messages) # 4. 解析结果 result = self._parse_llm_response(response, symbol) return result except Exception as e: logger.error(f"市场信号分析失败: {e}") import traceback logger.debug(traceback.format_exc()) return self._get_empty_signal(symbol) def _prepare_market_context(self, symbol: str, data: Dict, symbols: List[str] = None, fundamental_data: Dict[str, Any] = None, news_data: List[Dict[str, Any]] = None) -> str: """准备市场上下文信息(技术面 + 基本面 + 新闻)""" context_parts = [] # 当前价格和24h变化(使用日线数据) df_1d = data.get('1d') if df_1d is None or len(df_1d) == 0: df_1h = data.get('1h') # 备用:使用1h数据 if df_1d is None or len(df_1d) == 0: return "" # 没有数据就返回空 current_price = float(df_1d.iloc[-1]['close']) price_change_24h = self._calculate_price_change_24h(df_1d) context_parts.append(f"当前价格: ${current_price:,.2f} ({price_change_24h})") # 多周期数据 for tf_name, df in data.items(): if df is None or len(df) == 0: continue latest = df.iloc[-1] context_parts.append(f"\n## {tf_name} 数据") context_parts.append(f"开: {latest['open']}, 高: {latest['high']}, 低: {latest['low']}, 收: {latest['close']}") context_parts.append(f"成交量: {latest.get('volume', 'N/A')}") # 技术指标 if 'rsi' in df.columns: rsi = df['rsi'].iloc[-1] context_parts.append(f"RSI: {rsi:.2f}") if 'macd' in df.columns: macd = df['macd'].iloc[-1] signal = df['macd_signal'].iloc[-1] context_parts.append(f"MACD: {macd:.4f}, 信号线: {signal:.4f}") if 'bb_upper' in df.columns: bb_upper = df['bb_upper'].iloc[-1] bb_lower = df['bb_lower'].iloc[-1] context_parts.append(f"布林带: 上轨 {bb_upper:.2f}, 下轨 {bb_lower:.2f}") # 均线系统(使用日线数据) context_parts.append(f"\n## 均线系统") df_1d = data.get('1d') if df_1d is not None and len(df_1d) > 0: latest = df_1d.iloc[-1] context_parts.append(f"MA5: {latest.get('ma5', 'N/A')}") context_parts.append(f"MA10: {latest.get('ma10', 'N/A')}") context_parts.append(f"MA20: {latest.get('ma20', 'N/A')}") context_parts.append(f"MA50: {latest.get('ma50', 'N/A')}") # 判断均线排列 ma5 = latest.get('ma5', 0) ma10 = latest.get('ma10', 0) ma20 = latest.get('ma20', 0) ma50 = latest.get('ma50', 0) if all([ma5, ma10, ma20, ma50]): if ma5 > ma10 > ma20 > ma50: context_parts.append("均线排列: 多头排列 📈") elif ma5 < ma10 < ma20 < ma50: context_parts.append("均线排列: 空头排列 📉") else: context_parts.append("均线排列: 交织,方向不明") # 量比分析(使用日线数据) df_1d = data.get('1d') if df_1d is not None and len(df_1d) >= 20: vol_latest = df_1d['volume'].iloc[-1] vol_ma20 = df_1d['volume'].iloc[-20:-1].mean() volume_ratio = vol_latest / vol_ma20 if vol_ma20 > 0 else 1 context_parts.append(f"\n## 量价分析") context_parts.append(f"最新成交量: {vol_latest:.0f}") context_parts.append(f"20周期均量: {vol_ma20:.0f}") context_parts.append(f"量比: {volume_ratio:.2f}") if volume_ratio > 1.5: context_parts.append("量价状态: 放量 📊") elif volume_ratio < 0.7: context_parts.append("量价状态: 缩量 📉") else: context_parts.append("量价状态: 平量 ➖") # 波动率分析 volatility_analysis = self._analyze_volatility(data) if volatility_analysis: context_parts.append(f"\n## 波动率分析") context_parts.append(volatility_analysis) # 基本面分析 if fundamental_data: context_parts.append(f"\n## 基本面分析") context_parts.append(self._format_fundamental_data(fundamental_data)) # 新闻舆情 if news_data: context_parts.append(f"\n## 最新新闻") context_parts.append(self._format_news_data(news_data)) return "\n".join(context_parts) def _build_analysis_prompt(self, symbol: str, market_context: str) -> str: """构建分析提示词""" return f"""请分析 {symbol} 的市场情况: {market_context} 请根据以上数据,给出你的市场判断和交易信号。 """ def _parse_llm_response(self, response: str, symbol: str) -> Dict[str, Any]: """解析 LLM 响应""" try: # 尝试提取 JSON json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response) if json_match: json_str = json_match.group(1) else: json_match = re.search(r'\{[\s\S]*\}', response) if json_match: json_str = json_match.group(0) else: raise ValueError("无法找到 JSON 响应") # 清理 JSON 字符串 json_str = self._clean_json_string(json_str) result = json.loads(json_str) # 清理价格字段 - 转换为 float result = self._clean_price_fields(result) # 添加元数据 result['symbol'] = symbol result['timestamp'] = datetime.now().isoformat() result['raw_response'] = response # 兼容处理:确保 signals 中的字段与旧格式一致 if 'signals' in result: for sig in result['signals']: if 'type' in sig: if sig['type'] in ['short_term', 'medium_term', 'long_term']: sig['timeframe'] = sig.pop('type') elif sig['type'] in ['buy', 'sell', 'wait']: sig['action'] = sig.pop('type') if 'action' not in sig and 'timeframe' in sig: sig['action'] = 'wait' if 'grade' not in sig: confidence = sig.get('confidence', 0) if confidence >= 80: sig['grade'] = 'A' elif confidence >= 60: sig['grade'] = 'B' elif confidence >= 40: sig['grade'] = 'C' else: sig['grade'] = 'D' # 从信号中推断 market_state 和 trend if 'signals' in result and result['signals']: best_signal = max(result['signals'], key=lambda s: s.get('confidence', 0)) action = best_signal.get('action', 'wait') confidence = best_signal.get('confidence', 0) if confidence >= 70: if action == 'buy': result['market_state'] = '强势上涨' elif action == 'sell': result['market_state'] = '强势下跌' else: result['market_state'] = '震荡整理' else: result['market_state'] = '震荡整理' if action == 'buy': result['trend'] = 'up' elif action == 'sell': result['trend'] = 'down' else: result['trend'] = 'sideways' else: result['market_state'] = '无明确信号' result['trend'] = 'sideways' logger.info(f"✅ 市场信号分析完成: {symbol}") return result except Exception as e: logger.warning(f"解析 LLM 响应失败: {e}") logger.warning(f"原始响应: {response[:1000]}...") return self._get_empty_signal(symbol) def _clean_json_string(self, json_str: str) -> str: """清理 JSON 字符串,移除可能导致解析错误的内容""" import re json_str = re.sub(r'//.*?(?=\n|$)', '', json_str) json_str = re.sub(r'/\*[\s\S]*?\*/', '', json_str) json_str = re.sub(r',\s*([}\]])', r'\1', json_str) return json_str def _clean_price_fields(self, data: Dict[str, Any]) -> Dict[str, Any]: """清理价格字段,转换为 float""" def clean_price(price_value): if price_value is None: return None if isinstance(price_value, (int, float)): return float(price_value) if isinstance(price_value, str): cleaned = price_value.replace('$', '').replace(',', '').strip() if cleaned: try: return float(cleaned) except ValueError: return None return None if 'key_levels' in data and data['key_levels']: key_levels = data['key_levels'] if 'support' in key_levels: data['key_levels']['support'] = [clean_price(s) for s in key_levels['support']] if 'resistance' in key_levels: data['key_levels']['resistance'] = [clean_price(r) for r in key_levels['resistance']] if 'signals' in data: for sig in data['signals']: price_fields = ['entry_zone', 'stop_loss', 'take_profit'] for field in price_fields: if field in sig: sig[field] = clean_price(sig[field]) return data def _calculate_price_change_24h(self, df) -> str: """计算24小时涨跌幅""" try: if df is None or len(df) < 24: return "N/A" current_price = float(df['close'].iloc[-1]) price_24h_ago = float(df['close'].iloc[-24]) change = ((current_price - price_24h_ago) / price_24h_ago) * 100 sign = "+" if change >= 0 else "" return f"{sign}{change:.2f}%" except Exception as e: logger.debug(f"计算24h涨跌失败: {e}") return "N/A" def _analyze_volatility(self, data: Dict[str, pd.DataFrame]) -> str: """分析波动率变化(使用日线数据)""" df = data.get('1d') if df is None or len(df) < 24 or 'atr' not in df.columns: return "" lines = [] recent_atr = df['atr'].iloc[-6:].mean() older_atr = df['atr'].iloc[-12:-6].mean() if pd.isna(recent_atr) or pd.isna(older_atr) or older_atr == 0: return "" atr_change = (recent_atr - older_atr) / older_atr * 100 current_atr = float(df['atr'].iloc[-1]) current_price = float(df['close'].iloc[-1]) atr_percent = current_atr / current_price * 100 lines.append(f"当前 ATR: ${current_atr:.2f} ({atr_percent:.2f}%)") if atr_change > 20: lines.append(f"**波动率扩张**: ATR 上升 {atr_change:.0f}%,趋势可能启动") elif atr_change < -20: lines.append(f"**波动率收缩**: ATR 下降 {abs(atr_change):.0f}%,可能即将突破") else: lines.append(f"波动率稳定: ATR 变化 {atr_change:+.0f}%") if 'bb_upper' in df.columns and 'bb_lower' in df.columns: bb_width = (float(df['bb_upper'].iloc[-1]) - float(df['bb_lower'].iloc[-1])) / current_price * 100 bb_width_prev = (float(df['bb_upper'].iloc[-6]) - float(df['bb_lower'].iloc[-6])) / float(df['close'].iloc[-6]) * 100 if bb_width < bb_width_prev * 0.8: lines.append(f"**布林带收口**: 宽度 {bb_width:.1f}%,变盘信号") elif bb_width > bb_width_prev * 1.2: lines.append(f"**布林带开口**: 宽度 {bb_width:.1f}%,趋势延续") return "\n".join(lines) def _format_fundamental_data(self, data: Dict[str, Any]) -> str: """格式化基本面数据""" if not data: return "暂无基本面数据" lines = [] # 基本信息 company_name = data.get('company_name', 'N/A') sector = data.get('sector', 'N/A') lines.append(f"公司: {company_name}") lines.append(f"行业: {sector}") # 估值指标 val = data.get('valuation', {}) if val.get('pe_ratio'): pe = val['pe_ratio'] pb = val.get('pb_ratio') ps = val.get('ps_ratio') peg = val.get('peg_ratio') pb_str = f"{pb:.2f}" if pb is not None else "N/A" ps_str = f"{ps:.2f}" if ps is not None else "N/A" peg_str = f"{peg:.2f}" if peg is not None else "N/A" lines.append(f"估值: PE={pe:.2f} | PB={pb_str} | PS={ps_str} | PEG={peg_str}") # 盈利能力 prof = data.get('profitability', {}) if prof.get('return_on_equity'): roe = prof['return_on_equity'] pm = prof.get('profit_margin') gm = prof.get('gross_margin') pm_str = f"{pm:.1f}" if pm is not None else "N/A" gm_str = f"{gm:.1f}" if gm is not None else "N/A" lines.append(f"盈利: ROE={roe:.2f}% | 净利率={pm_str}% | 毛利率={gm_str}%") # 成长性 growth = data.get('growth', {}) rg = growth.get('revenue_growth') eg = growth.get('earnings_growth') if rg is not None or eg is not None: rg_str = f"{rg:.1f}" if rg is not None else "N/A" eg_str = f"{eg:.1f}" if eg is not None else "N/A" lines.append(f"成长: 营收增长={rg_str}% | 盈利增长={eg_str}%") # 财务健康 fin = data.get('financial_health', {}) if fin.get('debt_to_equity'): de = fin['debt_to_equity'] cr = fin.get('current_ratio') cr_str = f"{cr:.2f}" if cr is not None else "N/A" lines.append(f"财务: 债务股本比={de:.2f} | 流动比率={cr_str}") # 分析师建议 analyst = data.get('analyst', {}) if analyst.get('target_price'): tp = analyst['target_price'] rec = analyst.get('recommendation', 'N/A') lines.append(f"分析师: 目标价=${tp:.2f} | 评级={rec}") # 基本面评分 score = data.get('score', {}) if score.get('total'): lines.append(f"基本面评分: {score['total']:.0f}/100 ({score.get('rating', 'N/A')}级)") return "\n".join(lines) def _format_news_data(self, news_list: List[Dict[str, Any]]) -> str: """格式化新闻数据""" if not news_list: return "暂无相关新闻" lines = [] for i, news in enumerate(news_list[:5], 1): # 最多5条 title = news.get('title', '') desc = news.get('description', '')[:150] # 限制描述长度 source = news.get('source', '') time_str = news.get('time_str', '') lines.append(f"{i}. [{time_str}] {title}") if desc: lines.append(f" {desc}") if source: lines.append(f" 来源: {source}") lines.append("") return "\n".join(lines) def _get_empty_signal(self, symbol: str) -> Dict[str, Any]: """返回空信号""" return { 'symbol': symbol, 'analysis_summary': 'unknown', 'volume_analysis': '分析失败', 'market_state': '分析失败', 'trend': 'sideways', 'signals': [], 'key_levels': {}, 'timestamp': datetime.now().isoformat(), 'error': '信号分析失败' }