From 65f039235eb5741b537ef38ee75466cb868dd0ab Mon Sep 17 00:00:00 2001 From: aaron <> Date: Tue, 24 Feb 2026 11:29:12 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E6=9E=B6=E6=9E=84=E5=88=87=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/crypto_agent/__init__.py | 3 +- backend/app/crypto_agent/crypto_agent.py | 9 +- .../app/crypto_agent/llm_signal_analyzer.py | 2090 ----------------- backend/app/crypto_agent/signal_analyzer.py | 1404 ----------- backend/app/services/yfinance_service.py | 5 +- .../app/stock_agent/market_signal_analyzer.py | 738 ++++++ backend/app/stock_agent/stock_agent.py | 64 +- backend/app/utils/signal_formatter.py | 226 ++ backend/test_crypto_agent.py | 24 +- scripts/test_stock.py | 43 +- 10 files changed, 1047 insertions(+), 3559 deletions(-) delete mode 100644 backend/app/crypto_agent/llm_signal_analyzer.py delete mode 100644 backend/app/crypto_agent/signal_analyzer.py create mode 100644 backend/app/stock_agent/market_signal_analyzer.py create mode 100644 backend/app/utils/signal_formatter.py diff --git a/backend/app/crypto_agent/__init__.py b/backend/app/crypto_agent/__init__.py index 30c8a09..be29744 100644 --- a/backend/app/crypto_agent/__init__.py +++ b/backend/app/crypto_agent/__init__.py @@ -2,7 +2,6 @@ 加密货币交易智能体模块 """ from app.crypto_agent.crypto_agent import CryptoAgent -from app.crypto_agent.signal_analyzer import SignalAnalyzer from app.crypto_agent.strategy import TrendFollowingStrategy -__all__ = ['CryptoAgent', 'SignalAnalyzer', 'TrendFollowingStrategy'] +__all__ = ['CryptoAgent', 'TrendFollowingStrategy'] diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index 5e0f5c4..4638397 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -38,7 +38,7 @@ class CryptoAgent: CryptoAgent._initialized = True self.settings = get_settings() - self.binance = bitget_service # 使用 Bitget 服务 + self.exchange = bitget_service # 交易所服务 self.feishu = get_feishu_service() self.telegram = get_telegram_service() @@ -46,9 +46,6 @@ class CryptoAgent: self.market_analyzer = MarketSignalAnalyzer() self.decision_maker = TradingDecisionMaker() - # 保留旧的 LLM 分析器用于兼容(可选) - # self.llm_analyzer = LLMSignalAnalyzer() - self.signal_db = get_signal_db_service() # 信号数据库服务 # 模拟交易服务(始终启用) @@ -406,7 +403,7 @@ class CryptoAgent: logger.info(f"{'─' * 50}") # 1. 获取多周期数据 - data = self.binance.get_multi_timeframe_data(symbol) + data = self.exchange.get_multi_timeframe_data(symbol) if not self._validate_data(data): logger.warning(f"⚠️ {symbol} 数据不完整,跳过分析") @@ -1483,7 +1480,7 @@ class CryptoAgent: async def analyze_once(self, symbol: str) -> Dict[str, Any]: """单次分析(用于测试或手动触发)""" - data = self.binance.get_multi_timeframe_data(symbol) + data = self.exchange.get_multi_timeframe_data(symbol) if not self._validate_data(data): return {'error': '数据不完整'} diff --git a/backend/app/crypto_agent/llm_signal_analyzer.py b/backend/app/crypto_agent/llm_signal_analyzer.py deleted file mode 100644 index 9db8c64..0000000 --- a/backend/app/crypto_agent/llm_signal_analyzer.py +++ /dev/null @@ -1,2090 +0,0 @@ -""" -LLM 驱动的信号分析器 - 让 LLM 自主分析市场数据并给出交易信号 -""" -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 -from app.services.news_service import get_news_service - - -class LLMSignalAnalyzer: - """LLM 驱动的交易信号分析器""" - - # 加密货币专用系统提示词 - CRYPTO_SYSTEM_PROMPT = """你是一位专业的加密货币交易员和技术分析师。你的任务是综合分析**K线数据、量价关系、技术指标和新闻舆情**,给出交易信号。 - -## 核心理念 -加密货币市场波动大,每天都有交易机会。你的目标是: -- **主动寻找机会**,而不是被动等待完美信号 -- 短线交易重点关注:超跌反弹、超涨回落、关键位突破 -- 中线交易重点关注:趋势回调、形态突破、多周期共振 - -## 一、量价分析(最重要) -量价关系是判断趋势真假的核心: - -### 1. 健康上涨信号 -- **放量上涨**:价格上涨 + 成交量放大(量比>1.5)= 上涨有效,可追多 -- **缩量回调**:上涨后回调 + 成交量萎缩(量比<0.7)= 回调健康,可低吸 - -### 2. 健康下跌信号 -- **放量下跌**:价格下跌 + 成交量放大 = 下跌有效,可追空 -- **缩量反弹**:下跌后反弹 + 成交量萎缩 = 反弹无力,可做空 - -### 3. 量价背离(重要反转信号) -- **顶背离**:价格创新高,但成交量未创新高 → 上涨动能衰竭,警惕回落 -- **底背离**:价格创新低,但成交量未创新低 → 下跌动能衰竭,关注反弹 -- **天量见顶**:极端放量(量比>3)后价格滞涨 → 主力出货信号 -- **地量见底**:极端缩量(量比<0.3)后价格企稳 → 抛压枯竭信号 - -### 4. 突破确认 -- **有效突破**:突破关键位 + 放量确认(量比>1.5)= 真突破 -- **假突破**:突破关键位 + 缩量 = 假突破,可能回落 - -## 二、K线形态分析 -### 反转形态 -- **锤子线/倒锤子**:下跌趋势中出现,下影线长 = 底部信号 -- **吞没形态**:大阳吞没前一根阴线 = 看涨;大阴吞没前一根阳线 = 看跌 -- **十字星**:在高位/低位出现 = 变盘信号 -- **早晨之星/黄昏之星**:三根K线组合的反转信号 - -### 持续形态 -- **三连阳/三连阴**:趋势延续信号 -- **旗形整理**:趋势中的健康回调 - -## 三、技术指标分析 -### RSI(相对强弱指标)- 使用 Wilder's Smoothing 标准算法 -**RSI 是最重要的超买超卖指标,请注意细节:** -- **RSI < 30**:超卖区,关注反弹机会 - - RSI 从 30 以下回升,交叉上穿 30:买入信号 - - RSI 底背离(价格新低但 RSI 未创新低):强买入信号 -- **RSI > 70**:超买区,关注回落风险 - - RSI 从 70 以上回落,交叉下穿 70:卖出信号 - - RSI 顶背离(价格新高但 RSI 未创新高):强卖出信号 -- **RSI 40-60**:震荡区,观望为主 -- **RSI 趋势**:RSI 自身的趋势变化比单一数值更重要 - -### MACD -- 金叉(DIF 上穿 DEA):做多信号 -- 死叉(DIF 下穿 DEA):做空信号 -- 零轴上方金叉:强势做多 -- 零轴下方死叉:强势做空 -- MACD 柱状图背离:重要反转信号 - -### 布林带 -- 触及下轨 + 企稳:反弹做多 -- 触及上轨 + 受阻:回落做空 -- 布林带收口:即将变盘 -- 布林带开口:趋势启动 - -### 均线系统(重要) -**均线系统是趋势判断的核心,请仔细分析:** -- **多头排列**(MA5 > MA10 > MA20 > MA50):强势上涨趋势,回调做多 -- **空头排列**(MA5 < MA10 < MA20 < MA50):强势下跌趋势,反弹做空 -- **价格与 MA 的关系**: - - 价格站稳 MA5/MA10 上方:短线上涨 - - 价格突破 MA20:中线转多 - - 价格跌破 MA20:中线转空 - - MA50 是中期趋势的分水岭 -- **均线金叉死叉**: - - MA5 上穿 MA10:短线买入信号 - - MA5 下穿 MA10:短线卖出信号 - - MA10 上穿 MA20:中线买入信号 - - MA10 下穿 MA20:中线卖出信号 - -## 四、新闻舆情分析 -结合最新市场新闻判断: -- **重大利好**:监管利好、机构入场、ETF 通过等 → 提高做多置信度 -- **重大利空**:监管打压、交易所暴雷、黑客攻击等 → 提高做空置信度 -- **市场情绪**:恐慌指数、社交媒体热度 -- **大户动向**:鲸鱼转账、交易所流入流出 - -## 五、多周期共振(关键分析框架) -**多周期共振是提高信号质量的核心方法:** - -### 周期层级关系 -- **4h(趋势层)**:决定中期大方向 -- **1h(主周期)**:主要交易周期 -- **15m(入场层)**:寻找入场时机 -- **5m(精确入场)**:确认最佳入场点 - -### 共振判断标准 -**强共振(A级信号)**: -- 所有周期趋势同向(如 4h多 + 1h多 + 15m多) -- 多周期 RSI 同时超买/超卖后出现背离 -- 多周期 MA 同时金叉/死叉 - -**中等共振(B级信号)**: -- 大周期(4h+1h)同向 -- 主周期(1h)技术指标明确 - -**弱共振(C级信号)**: -- 只有单一周期信号 -- 多周期方向不一致 - -### 实战策略 -- **顺势交易**:4h 和 1h 同向时,在 15m/5m 寻找入场点 -- **逆势谨慎**:只有 1h 信号但 4h 反向时,降低置信度 -- **突破交易**:多周期同时突破关键位,信号最强 - -## 六、入场方式 -- **market**:现价立即入场 - 信号已经触发,建议立即开仓 -- **limit**:挂单等待入场 - 等价格回调到更好位置再入场 - -## 输出格式 -请严格按照以下 JSON 格式输出: - -```json -{ - "analysis_summary": "简要描述当前市场状态(50字以内)", - "volume_analysis": "量价分析结论(30字以内)", - "news_sentiment": "positive/negative/neutral", - "news_impact": "新闻对市场的影响分析(30字以内)", - "signals": [ - { - "type": "short_term/medium_term/long_term", - "action": "buy/sell/wait", - "entry_type": "market/limit", - "confidence": 0-100, - "grade": "A/B/C/D", - "position_size": "heavy/medium/light", - "position_reason": "仓位建议理由(20字以内)", - "entry_price": 建议入场价, - "stop_loss": 止损价, - "take_profit": 止盈价, - "reason": "详细的入场理由(必须包含量价分析)", - "risk_warning": "风险提示" - } - ], - "key_levels": { - "support": [支撑位列表], - "resistance": [阻力位列表] - } -} -``` - -## 信号等级与置信度 -- **A级**(80-100):量价配合 + 多指标共振 + 多周期确认 -- **B级**(60-79):量价配合 + 主要指标确认 -- **C级**(40-59):有机会但量价不够理想 -- **D级**(<40):量价背离或信号矛盾 - -## 七、仓位管理(重要 - 全仓模式) -你需要根据信号质量和当前持仓情况,建议合适的仓位大小。 - -### 全仓模式说明 -- **全仓模式**:所有持仓共享账户保证金,总杠杆 = 总持仓价值 / 账户余额 -- **杠杆限制**:总杠杆永远不得超过 20 倍(硬性限制) -- **风险集中**:所有仓位共享风险,需要更加谨慎 - -### 仓位等级 -- **heavy**(重仓):机会极佳,建议使用较大仓位 -- **medium**(中仓):机会不错,建议使用中等仓位 -- **light**(轻仓):机会一般或风险较高,建议轻仓试探 - -### 仓位决策规则(必须参考持仓信息) -1. **A级信号**:可建议 heavy 或 medium -2. **B级信号**:建议 medium 或 light -3. **C级信号**:只能建议 light -4. **已有同向持仓时**:新仓位应降一级(避免过度集中) -5. **已有反向持仓时**:谨慎开仓,除非信号极强 -6. **市场波动剧烈时**:仓位应保守 - -### 安全底线(必须遵守) -- **总杠杆永远不得超过 20 倍**(这是硬性限制) -- 如果当前杠杆已经接近 20 倍,即使有好机会也必须降低仓位或跳过 -- 单一交易对持仓不宜过大(建议不超过总资金的 30%) -- 如果当前持仓已经较重,即使有好机会也要控制仓位 -- **必须参考当前持仓和挂单信息**:系统会提供账户余额、当前杠杆、可用杠杆空间等信息 - -## 八、止损止盈策略(重要更新) - -### 止损设置原则(结构化止损) -**不要使用固定百分比或固定ATR倍数!止损必须基于关键价位:** - -1. **做多止损**: - - 优先放在最近支撑位(前低)下方 0.3-0.5% - - 如果有 MA20 支撑,可放在 MA20 下方 0.5% - - 如果最近低点距离过近(<1%),则使用 ATR 1.2-1.5倍 - - 避免止损距离过大(>4%ATR) - -2. **做空止损**: - - 优先放在最近阻力位(前高)上方 0.3-0.5% - - 如果有 MA20 阻力,可放在 MA20 上方 0.5% - - 如果最近高点距离过近(<1%),则使用 ATR 1.2-1.5倍 - - 避免止损距离过大(>4%ATR) - -### 止盈设置(移动止盈策略) -**不设固定止盈位,让利润奔跑!** - -1. **take_profit 设置为保险价位**: - - 做多:入场价 + 15%(作为极端情况的保险止盈) - - 做空:入场价 - 15% - - 这个价位只是"保险",正常情况下不会触及 - -2. **实际止盈靠移动止损**: - - 系统会通过移动止损自动锁定利润 - - 盈利 2% 后开始移动止损,锁定部分利润 - - 盈利越多,止损跟随移动,确保吃到趋势 - -### 风险收益比 -- 虽然不设固定止盈,但仍要确保止损合理 -- 理想情况下,潜在风险(止损距离)应控制在 2-3% 以内 - -## 重要原则 -1. **量价优先** - 任何信号都必须有量能配合才可靠 -2. **积极但不冒进** - 有合理依据就给出信号,不要过于保守 -3. 每种类型最多输出一个信号 -4. 止损必须基于关键支撑/阻力位(前低前高、MA20),不要用固定百分比 -5. 止盈设置为保险价位(做多+15%,做空-15%),实际靠移动止损锁定利润 -6. reason 字段必须包含量价分析(如"放量突破+RSI=45,量比1.8确认有效") -7. entry_type 必须明确:信号已触发用 market,等待更好价位用 limit -8. **position_size 必须明确**:根据信号质量和持仓情况给出 heavy/medium/light""" - - # 股票专用系统提示词 - STOCK_SYSTEM_PROMPT = """你是一位专业的股票交易员和技术分析师。你的任务是综合分析**K线数据、量价关系、技术指标**,给出交易信号建议。 - -## 核心理念 -股票市场相对稳定,不需要每天都交易。你的目标是: -- **精选机会**,只在高质量信号时给出建议 -- 短线交易重点关注:突破回踩、趋势延续、箱体突破 -- 中线交易重点关注:趋势反转、业绩驱动、板块轮动 -- 长线交易重点关注:价值投资、成长股、红利股 - -## 一、量价分析(最重要) -量价关系是判断趋势真假的核心: - -### 1. 健康上涨信号 -- **放量上涨**:价格上涨 + 成交量放大(量比>1.5)= 上涨有效,可考虑买入 -- **缩量回调**:上涨后回调 + 成交量萎缩(量比<0.7)= 回调健康,可低吸 -- **温和放量**:温和放量上涨是最健康的上涨方式 - -### 2. 健康下跌信号 -- **放量下跌**:价格下跌 + 成交量放大 = 下跌有效,下跌趋势中不接飞刀 -- **缩量反弹**:下跌后反弹 + 成交量萎缩 = 反弹无力,反弹后可能继续下跌 -- **地量下跌**:成交量极度萎缩后价格企稳,可能见底 - -### 3. 量价背离(重要反转信号) -- **顶背离**:价格创新高,但成交量未创新高 → 上涨动能衰竭,警惕回落 -- **底背离**:价格创新低,但成交量未创新低 → 下跌动能衰竭,关注反弹 -- **高位天量**:高位放出巨量后价格滞涨 → 主力出货信号 -- **低位地量**:低位成交量极度萎缩 → 抛压枯竭信号 - -### 4. 突破确认 -- **有效突破**:突破关键位 + 放量确认(量比>1.3)+ 收盘站稳 = 真突破 -- **假突破**:突破关键位但缩量或无法站稳 = 假突破,可能回落 -- **回踩确认**:突破后回踩原压力位变成支撑位,是更好的买点 - -## 二、K线形态分析 -### 反转形态 -- **锤子线/倒锤子**:下跌趋势中出现,下影线长 = 底部信号 -- **吞没形态**:大阳吞没前一根阴线 = 看涨;大阴吞没前一根阳线 = 看跌 -- **十字星**:在高位/低位出现 = 变盘信号 -- **早晨之星/黄昏之星**:三根K线组合的反转信号 -- **头肩顶/头肩底**:重要的反转形态 - -### 持续形态 -- **上升三角形/下降三角形**:趋势延续信号 -- **旗形整理**:趋势中的健康回调 -- **箱体震荡**:震荡区间,突破后选择方向 - -## 三、技术指标分析 -### RSI(相对强弱指标)- 使用 Wilder's Smoothing 标准算法 -**RSI 是最重要的超买超卖指标,请注意细节:** -- **RSI < 30**:超卖区,关注反弹机会 - - RSI 从 30 以下回升,交叉上穿 30:买入信号 - - RSI 底背离(价格新低但 RSI 未创新低):强买入信号 -- **RSI > 70**:超买区,关注回落风险 - - RSI 从 70 以上回落,交叉下穿 70:卖出信号 - - RSI 顶背离(价格新高但 RSI 未创新高):强卖出信号 -- **RSI 40-60**:震荡区,观望为主 -- **RSI 趋势**:RSI 自身的趋势变化比单一数值更重要 -- 股票市场中 RSI 极端值比加密货币更可靠 - -### MACD -- 金叉(DIF 上穿 DEA):做多信号 -- 死叉(DIF 下穿 DEA):做空信号 -- 零轴上方金叉:强势做多 -- 零轴下方金叉:弱势反弹 -- MACD 柱状图背离:重要反转信号 - -### 布林带 -- 触及下轨 + 企稳:反弹做多 -- 触及上轨 + 受阻:回落做空 -- 布林带收口:即将变盘 -- 布林带开口:趋势启动 - -### 均线系统(重要) -**均线系统是趋势判断的核心,请仔细分析:** -- **多头排列**(MA5 > MA10 > MA20 > MA50):强势上涨趋势,回调做多 -- **空头排列**(MA5 < MA10 < MA20 < MA50):强势下跌趋势,反弹做空 -- **价格与 MA 的关系**: - - 价格站稳 MA5/MA10 上方:短线上涨 - - 价格突破 MA20:中线转多 - - 价格跌破 MA20:中线转空 - - MA20/MA50 是中期趋势的分水岭 -- **均线金叉死叉**: - - MA5 上穿 MA10:短线买入信号 - - MA5 下穿 MA10:短线卖出信号 - - MA10 上穿 MA20:中线买入信号 - - MA10 下穿 MA20:中线卖出信号 - -### 成交量分析 -- **量价配合**:价格上涨+放量或下跌+缩量是健康的 -- **量价背离**:价格上涨+缩量或下跌+放量要警惕 -- **换手率**:换手率过低说明关注度不够,换手率过高可能是投机 - -## 四、多周期共振(关键分析框架) -**多周期共振是提高信号质量的核心方法:** - -### 周期层级关系 -- **日线(趋势层)**:决定中长期大方向 -- **4h/1h(主周期)**:主要交易周期 -- **15m/5m(入场层)**:寻找最佳入场时机 - -### 共振判断标准 -**强共振(A级信号)**: -- 所有周期趋势同向(如日线多 + 4h多 + 1h多) -- 多周期 RSI 同时超买/超卖后出现背离 -- 多周期 MA 同时金叉/死叉 - -**中等共振(B级信号)**: -- 大周期(日线+4h)同向 -- 主周期技术指标明确 - -**弱共振(C级信号)**: -- 只有单一周期信号 -- 多周期方向不一致 - -### 实战策略 -- **顺势交易**:大周期和小周期同向时,信号最强 -- **逆势谨慎**:只有小周期信号但大周期反向时,降低置信度 -- **突破交易**:多周期同时突破关键位,信号最可靠 -- 大周期决定方向,小周期决定入场时机 - -## 五、股票市场特殊性 -### 与加密货币的区别 -1. **交易时间**:股票有固定交易时间,收盘后无法交易 -2. **波动性**:股票波动性通常低于加密货币 -3. **T+1规则**:部分市场(如A股)实行T+1,当天买入第二天才能卖出 -4. **涨跌停限制**:部分市场有涨跌停限制 -5. **分红送转**:股票有分红、送股等除权除息事件 - -### 港股特殊性 -- 无涨跌停限制 -- T+0交易(当天可买卖) -- 有港币兑换考虑 -- 受内地和美股双重影响 - -### 美股特殊性 -- 无涨跌停限制(但有熔断机制) -- T+0交易(当天可买卖) -- 有盘前盘后交易 -- 受财报季影响大 - -## 六、入场方式 -- **market**:现价立即入场 - 信号已经触发,建议立即开仓 -- **limit**:挂单等待入场 - 等价格回调到更好位置再入场 - -## 输出格式 -请严格按照以下 JSON 格式输出: - -```json -{ - "analysis_summary": "简要描述当前市场状态(50字以内)", - "volume_analysis": "量价分析结论(30字以内)", - "news_sentiment": "positive/negative/neutral", - "news_impact": "新闻对市场的影响分析(30字以内)", - "signals": [ - { - "type": "short_term/medium_term/long_term", - "action": "buy/sell/wait", - "entry_type": "market/limit", - "confidence": 0-100, - "grade": "A/B/C/D", - "position_size": "heavy/medium/light", - "position_reason": "仓位建议理由(20字以内)", - "entry_price": 建议入场价, - "stop_loss": 止损价, - "take_profit": 止盈价, - "reason": "详细的入场理由(必须包含量价分析)", - "risk_warning": "风险提示" - } - ], - "key_levels": { - "support": [支撑位列表], - "resistance": [阻力位列表] - } -} -``` - -## 信号等级与置信度 -- **A级**(80-100):量价配合 + 多指标共振 + 多周期确认 + 形态完美 -- **B级**(60-79):量价配合 + 主要指标确认 + 形态清晰 -- **C级**(40-59):有机会但量价不够理想或形态不完整 -- **D级**(<40):量价背离或信号矛盾 - -## 七、仓位管理(重要) -股票交易不需要频繁交易,建议精选机会。 - -### 仓位等级 -- **heavy**(重仓):机会极佳,建议使用较大仓位 -- **medium**(中仓):机会不错,建议使用中等仓位 -- **light**(轻仓):机会一般或风险较高,建议轻仓试探 - -### 仓位决策规则 -1. **A级信号**:可建议 heavy 或 medium -2. **B级信号**:建议 medium 或 light -3. **C级信号**:只能建议 light -4. **已在高位或低位**:即使有好机会也要控制仓位 -5. **市场整体环境**:大盘不好时要控制仓位 - -### 安全底线 -- 单一股票仓位不宜超过总资金的 30% -- 同一行业股票不宜过度集中 -- 保留现金储备应对市场变化 - -## 八、止损止盈策略 - -### 止损设置原则(结构化止损) -**止损必须基于关键价位,不要用固定百分比:** - -1. **做多止损**: - - 优先放在最近支撑位(前低)下方 2-3% - - 如果有 MA20/MA50 支撑,可放在均线下方 1-2% - - 如果最近低点距离过近(<3%),则使用 ATR 1.5-2倍 - - 技术位止损通常在 3-8% 之间 - -2. **做空止损**: - - 优先放在最近阻力位(前高)上方 2-3% - - 如果有 MA20/MA50 阻力,可放在均线上方 1-2% - - 如果最近高点距离过近(<3%),则使用 ATR 1.5-2倍 - -### 止盈设置 -**股票可以设置合理的止盈目标:** - -1. **短线止盈**: - - 突破类:目标 8-15% - - 反弹类:目标 10-20% - -2. **中线止盈**: - - 趋势类:目标 20-40% - - 可以分批止盈,保护利润 - -3. **长线止盈**: - - 价值投资:目标 50%+ - - 关注基本面变化 - -### 移动止盈 -- 盈利达到目标后,可以将止损移动到成本价以上 -- 盈利 15% 后,开始移动止盈锁定利润 -- 趋势强劲时,可以让利润奔跑 - -### 风险收益比 -- 理想的风险收益比应该在 1:3 以上 -- 即:潜在风险 3%,潜在收益 9% 以上 - -## 九、基本面分析(重要补充) -**对于股票交易,基本面分析是重要的参考维度:** - -### 估值指标分析 -- **PE(市盈率)**: - - PE < 15:估值偏低,价值投资机会 - - PE 15-25:估值合理 - - PE 25-40:估值偏高 - - PE > 40:估值过高,风险较大 -- **PB(市净率)**:PB < 1.5 通常表示被低估 -- **PEG(市盈率相对盈利增长比率)**:PEG < 1 表示被低估 - -### 盈利能力分析 -- **ROE(净资产收益率)**: - - ROE > 20%:优秀公司,盈利能力强 - - ROE 15-20%:良好 - - ROE 10-15%:一般 - - ROE < 10%:盈利能力较弱 -- **净利率**:净利率 > 20% 表示盈利质量高 -- **毛利率**:毛利率 > 40% 表示有竞争优势 - -### 成长性分析 -- **营收增长率**: - - > 30%:高成长 - - 20-30%:稳定成长 - - 10-20%:一般成长 - - < 10%:成长性不足 -- **盈利增长率**:与营收增长同步更健康 - -### 财务健康分析 -- **债务股本比**: - - < 1:财务健康 - - 1-2:可控范围 - - > 2:风险较高 -- **流动比率**:> 2 表示偿债能力强 - -### 基本面与技术面结合 -1. **基本面优秀 + 技术面突破** = 高质量做多机会(可提高置信度) -2. **基本面差 + 技术面破位** = 高质量做空机会(可提高置信度) -3. **基本面优秀 + 技术面回调** = 低吸机会(中线/长线) -4. **基本面差 + 技术面上涨** = 谨慎(可能是诱多) - -### 基本面评分参考 -- **80分以上(A级)**:基本面优秀,技术信号确认时可提高置信度 -- **60-80分(B级)**:基本面良好,可作为参考 -- **40-60分(C级)**:基本面一般,主要依赖技术分析 -- **40分以下(D级)**:基本面较差,降低信号置信度 - -## 重要原则 -1. **量价优先** - 任何信号都必须有量能配合才可靠 -2. **精选机会** - 股票不需要频繁交易,等待高质量信号 -3. **多周期确认** - 日线决定方向,小周期决定入场 -4. **结构止损** - 止损必须基于关键支撑/阻力位(前低前高、均线) -5. **合理止盈** - 根据交易周期设置合理的止盈目标 -6. **基本面参考** - 结合基本面评分和技术面综合判断,提高信号质量 -7. **reason 字段必须包含量价分析**(如"放量突破+RSI=45,量比1.8确认有效") -8. **entry_type 必须明确**:信号已触发用 market,等待更好价位用 limit -9. **position_size 必须明确**:根据信号质量给出 heavy/medium/light""" - - # 兼容旧代码,使用加密货币提示词作为默认值 - SYSTEM_PROMPT = CRYPTO_SYSTEM_PROMPT - - def __init__(self, agent_type: str = "crypto"): - """初始化分析器 - - Args: - agent_type: 智能体类型,支持 'crypto', 'stock', 'smart' - """ - from app.config import get_settings - from app.services.bitget_service import bitget_service - self.news_service = get_news_service() - self.binance_service = bitget_service # 使用 Bitget 服务 - settings = get_settings() - - # 根据智能体类型选择模型配置 - model_config_map = { - 'crypto': 'crypto_agent_model', - 'stock': 'stock_agent_model', - 'smart': 'smart_agent_model' - } - - config_key = model_config_map.get(agent_type, 'crypto_agent_model') - self.model_override = getattr(settings, config_key, None) - self.agent_type = agent_type - - agent_name_map = { - 'crypto': '加密货币', - 'stock': '股票', # 改为通用的"股票",具体市场类型会在分析时根据符号判断 - 'smart': '智能助手' - } - agent_name = agent_name_map.get(agent_type, '未知') - - logger.info(f"LLM 信号分析器初始化完成({agent_name},模型: {self.model_override or '默认'})") - - def _get_market_type(self, symbol: str) -> str: - """根据股票代码判断市场类型""" - if symbol.endswith('.HK'): - return '港股' - else: - return '美股' - - async def analyze(self, symbol: str, data: Dict[str, pd.DataFrame], - symbols: List[str] = None, - position_info: Dict[str, Any] = None, - fundamental_data: Dict[str, Any] = None, - fundamental_summary: str = "") -> Dict[str, Any]: - """ - 使用 LLM 分析市场数据 - - Args: - symbol: 交易对,如 'BTCUSDT' - data: 多周期K线数据 {'5m': df, '15m': df, '1h': df, '4h': df} - symbols: 所有监控的交易对(用于过滤相关新闻) - position_info: 当前持仓信息,用于仓位管理决策 - - account_balance: 账户余额 - - total_position_value: 总持仓价值 - - current_leverage: 当前杠杆倍数 - - positions: 各交易对持仓列表 - fundamental_data: 基本面数据(仅股票) - fundamental_summary: 基本面摘要文本(仅股票) - - Returns: - 分析结果 - """ - try: - # 获取市场类型 - market_type = self._get_market_type(symbol) if self.agent_type == 'stock' else '' - - # 获取新闻数据 - news_text = await self._get_news_context(symbol, symbols or [symbol]) - - # 获取合约市场数据(仅加密货币) - futures_data = None - if self.agent_type == 'crypto': - try: - futures_data = self.binance_service.get_futures_market_data(symbol) - if futures_data: - logger.info(f"{symbol} 资金费率: {futures_data.get('funding_rate', {}).get('funding_rate_percent', 0):.4f}% | " - f"情绪: {futures_data.get('market_sentiment', '')}") - except Exception as e: - logger.warning(f"获取 {symbol} 合约数据失败: {e}") - - # 根据智能体类型选择提示词 - if self.agent_type == 'stock': - system_prompt = self.STOCK_SYSTEM_PROMPT - else: - system_prompt = self.CRYPTO_SYSTEM_PROMPT - - # 构建数据提示 - data_prompt = self._build_data_prompt(symbol, data, news_text, position_info, futures_data, - fundamental_data, fundamental_summary) - - # 调用 LLM(使用异步方法避免阻塞事件循环) - response = await llm_service.achat([ - {"role": "system", "content": system_prompt}, - {"role": "user", "content": data_prompt} - ], model_override=self.model_override) - - if not response: - logger.warning(f"{symbol} LLM 分析无响应") - return self._empty_result(symbol, "LLM 无响应") - - # 解析响应 - result = self._parse_response(response) - result['symbol'] = symbol - result['timestamp'] = datetime.now().isoformat() - - # 记录日志 - signals = result.get('signals', []) - if signals: - for sig in signals: - logger.info(f"{symbol} [{market_type}][{sig['type']}] {sig['action']} " - f"置信度:{sig['confidence']}% 等级:{sig['grade']} " - f"原因:{sig['reason'][:50]}...") - else: - logger.info(f"{symbol} [{market_type}] 无交易信号 - {result.get('analysis_summary', '观望')}") - - return result - - except Exception as e: - logger.error(f"{symbol} LLM 分析出错: {e}") - import traceback - logger.error(traceback.format_exc()) - return self._empty_result(symbol, str(e)) - - async def _get_news_context(self, symbol: str, symbols: List[str]) -> str: - """获取新闻上下文""" - try: - # 如果是股票类型,使用 Brave Search 搜索新闻 - if self.agent_type == 'stock': - # 获取股票名称 - from app.stock_agent.stock_agent import STOCK_NAMES - stock_name = STOCK_NAMES.get(symbol, '') - - # 搜索股票新闻 - news_list = await self.news_service.search_stock_news(symbol, stock_name) - - if news_list: - return self.news_service.format_news_for_llm(news_list, max_items=5) - else: - return "" - else: - # 加密货币使用原有的 RSS 新闻 - news_list = await self.news_service.get_latest_news(limit=50) - filtered = self.news_service.filter_relevant_news( - news_list, symbols=symbols, hours=4 - ) - return self.news_service.format_news_for_llm(filtered, max_items=10) - except Exception as e: - logger.warning(f"获取新闻上下文失败: {e}") - return "" - - def _format_position_info(self, symbol: str, position_info: Dict[str, Any]) -> str: - """格式化持仓信息供 LLM 参考""" - lines = [] - - # 账户概况 - balance = position_info.get('account_balance', 0) - total_value = position_info.get('total_position_value', 0) - current_leverage = position_info.get('current_leverage', 0) - max_leverage = 20 # 最大杠杆限制 - - lines.append(f"- 账户余额: ${balance:,.2f}") - lines.append(f"- 总持仓价值: ${total_value:,.2f}") - lines.append(f"- 当前杠杆: {current_leverage:.1f}x / {max_leverage}x") - - # 可用杠杆空间 - available_leverage = max_leverage - current_leverage - if available_leverage > 0: - available_value = balance * available_leverage - lines.append(f"- 可开仓空间: ${available_value:,.2f} ({available_leverage:.1f}x)") - else: - lines.append("- ⚠️ 已达最大杠杆,不建议加仓") - - # 当前交易对持仓 - positions = position_info.get('positions', []) - symbol_positions = [p for p in positions if p.get('symbol') == symbol] - - if symbol_positions: - lines.append(f"\n**{symbol} 当前持仓**:") - for pos in symbol_positions: - side = "做多" if pos.get('side') == 'long' else "做空" - entry = pos.get('entry_price', 0) - pnl = pos.get('pnl_percent', 0) - lines.append(f" - {side} @ ${entry:,.2f} | 盈亏: {pnl:+.2f}%") - else: - lines.append(f"\n**{symbol}**: 无持仓") - - # 其他交易对持仓概况 - other_positions = [p for p in positions if p.get('symbol') != symbol and p.get('status') == 'open'] - if other_positions: - lines.append(f"\n**其他持仓**: {len(other_positions)} 个") - - return "\n".join(lines) - - def _format_fundamental_data(self, fundamental_data: Dict[str, Any]) -> str: - """格式化基本面数据供 LLM 参考""" - lines = [] - - # 基本信息 - company_name = fundamental_data.get('company_name', 'N/A') - sector = fundamental_data.get('sector', 'N/A') - industry = fundamental_data.get('industry', 'N/A') - market_cap = fundamental_data.get('market_cap', 0) - - lines.append(f"**公司**: {company_name}") - lines.append(f"**行业**: {sector} / {industry}") - if market_cap: - lines.append(f"**市值**: ${market_cap:,.0f}") - - # 基本面评分 - score_data = fundamental_data.get('score', {}) - total_score = score_data.get('total', 0) - rating = score_data.get('rating', 'N/A') - if total_score > 0: - lines.append(f"**基本面评分**: {total_score:.0f}/100 ({rating}级)") - - # 估值指标 - valuation = fundamental_data.get('valuation', {}) - if valuation.get('pe_ratio'): - pe = valuation['pe_ratio'] - pb = valuation.get('pb_ratio', 'N/A') - ps = valuation.get('ps_ratio', 'N/A') - lines.append(f"**估值**: PE={pe:.2f} | PB={pb} | PS={ps}") - - # 盈利能力 - profitability = fundamental_data.get('profitability', {}) - if profitability.get('return_on_equity'): - roe = profitability['return_on_equity'] - profit_margin = profitability.get('profit_margin') - gross_margin = profitability.get('gross_margin') - pm_str = f"{profit_margin:.1f}" if profit_margin is not None else "N/A" - gm_str = f"{gross_margin:.1f}" if gross_margin is not None else "N/A" - lines.append(f"**盈利**: ROE={roe:.2f}% | 净利率={pm_str}% | 毛利率={gm_str}%") - - # 成长性 - growth = fundamental_data.get('growth', {}) - revenue_growth = growth.get('revenue_growth') - earnings_growth = growth.get('earnings_growth') - if revenue_growth is not None or earnings_growth is not None: - rg_str = f"{revenue_growth:.1f}" if revenue_growth is not None else "N/A" - eg_str = f"{earnings_growth:.1f}" if earnings_growth is not None else "N/A" - lines.append(f"**成长**: 营收增长={rg_str}% | 盈利增长={eg_str}%") - - # 财务健康 - financial = fundamental_data.get('financial_health', {}) - if financial.get('debt_to_equity'): - debt_to_equity = financial['debt_to_equity'] - current_ratio = financial.get('current_ratio') - cr_str = f"{current_ratio:.2f}" if current_ratio is not None else "N/A" - lines.append(f"**财务**: 债务股本比={debt_to_equity:.2f} | 流动比率={cr_str}") - - # 分析师建议 - analyst = fundamental_data.get('analyst', {}) - if analyst.get('target_price'): - target_price = analyst['target_price'] - recommendation = analyst.get('recommendation', 'N/A') - lines.append(f"**分析师**: 目标价=${target_price:.2f} | 建议={recommendation}") - - return "\n".join(lines) - - def _build_data_prompt(self, symbol: str, data: Dict[str, pd.DataFrame], - news_text: str = "", position_info: Dict[str, Any] = None, - futures_data: Dict[str, Any] = None, - fundamental_data: Dict[str, Any] = None, - fundamental_summary: str = "") -> str: - """构建数据提示词""" - parts = [f"# {symbol} 市场数据分析\n"] - parts.append(f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n") - - # 当前价格 - current_price = 0 - if '5m' in data and not data['5m'].empty: - current_price = float(data['5m'].iloc[-1]['close']) - parts.append(f"**当前价格**: ${current_price:,.2f}\n") - - # === 新增:基本面数据(仅股票) === - if fundamental_data and self.agent_type == 'stock': - parts.append("\n## 基本面分析") - if fundamental_summary: - parts.append(fundamental_summary) - else: - parts.append(self._format_fundamental_data(fundamental_data)) - - # === 新增:合约市场数据 === - if futures_data and self.agent_type == 'crypto': - parts.append(self.binance_service.format_futures_data_for_llm(symbol, futures_data)) - - # === 新增:账户和持仓信息 === - if position_info: - parts.append("\n## 账户与持仓状态") - parts.append(self._format_position_info(symbol, position_info)) - - # === 新增:关键价位分析 === - key_levels = self._calculate_key_levels(data) - if key_levels: - parts.append("\n## 关键价位") - parts.append(key_levels) - - # === 新增:多周期共振分析 === - resonance = self._analyze_multi_timeframe_resonance(data) - if resonance: - parts.append("\n## 多周期共振") - parts.append(resonance) - - # === 新增:市场结构分析 === - structure = self._analyze_market_structure(data) - if structure: - parts.append("\n## 市场结构") - parts.append(structure) - - # === 新增:波动率分析 === - volatility = self._analyze_volatility(data) - if volatility: - parts.append("\n## 波动率分析") - parts.append(volatility) - - # 各周期数据 - for interval in ['4h', '1h', '15m', '5m']: - df = data.get(interval) - if df is None or df.empty: - continue - - parts.append(f"\n## {interval.upper()} 周期数据") - - # 最新指标(传入 df 以分析趋势变化) - latest = df.iloc[-1] - parts.append(self._format_indicators(latest, df)) - - # 最近 K 线数据 - parts.append(self._format_recent_klines(df, interval)) - - # 添加新闻数据 - if news_text and news_text != "暂无相关新闻": - parts.append(f"\n{news_text}") - - parts.append("\n---") - parts.append("请综合分析以上技术数据和新闻舆情,判断是否存在短线、中线或长线的交易机会。") - parts.append("如果没有明确的交易机会,signals 数组返回空即可。") - - return "\n".join(parts) - - def _calculate_key_levels(self, data: Dict[str, pd.DataFrame]) -> str: - """计算关键支撑阻力位""" - lines = [] - - # 使用 4h 数据计算关键价位 - df = data.get('4h') - if df is None or len(df) < 20: - return "" - - current_price = float(df.iloc[-1]['close']) - - # 1. 前高前低(最近 20 根 K 线) - recent = df.iloc[-20:] - recent_high = float(recent['high'].max()) - recent_low = float(recent['low'].min()) - - # 2. 整数关口 - round_levels = [] - base = int(current_price / 1000) * 1000 - for offset in [-2000, -1000, 0, 1000, 2000]: - level = base + offset - if level > 0: - round_levels.append(level) - - # 3. 斐波那契回撤位(基于最近的高低点) - fib_levels = [] - price_range = recent_high - recent_low - if price_range > 0: - fib_ratios = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1] - for ratio in fib_ratios: - fib_price = recent_low + price_range * ratio - fib_levels.append((ratio, fib_price)) - - # 构建输出 - lines.append(f"- 近期高点: ${recent_high:,.2f}") - lines.append(f"- 近期低点: ${recent_low:,.2f}") - - # 找出最近的支撑和阻力 - supports = [] - resistances = [] - - # 从斐波那契位找支撑阻力 - for ratio, price in fib_levels: - if price < current_price * 0.995: # 低于当前价 0.5% 以上 - supports.append(price) - elif price > current_price * 1.005: # 高于当前价 0.5% 以上 - resistances.append(price) - - # 添加整数关口 - for level in round_levels: - if level < current_price * 0.995: - supports.append(level) - elif level > current_price * 1.005: - resistances.append(level) - - # 排序并取最近的 - supports = sorted(set(supports), reverse=True)[:3] - resistances = sorted(set(resistances))[:3] - - if supports: - support_str = ", ".join([f"${s:,.0f}" for s in supports]) - lines.append(f"- 支撑位: {support_str}") - if resistances: - resistance_str = ", ".join([f"${r:,.0f}" for r in resistances]) - lines.append(f"- 阻力位: {resistance_str}") - - # 当前价格位置 - if recent_high > recent_low: - position = (current_price - recent_low) / (recent_high - recent_low) * 100 - if position > 80: - pos_text = "接近高点,注意回调风险" - elif position < 20: - pos_text = "接近低点,关注反弹机会" - else: - pos_text = f"处于区间 {position:.0f}% 位置" - lines.append(f"- 价格位置: {pos_text}") - - return "\n".join(lines) - - def _analyze_multi_timeframe_resonance(self, data: Dict[str, pd.DataFrame]) -> str: - """分析多周期共振""" - trends = {} - - for interval in ['4h', '1h', '15m', '5m']: - df = data.get(interval) - if df is None or len(df) < 10: - continue - - # 判断趋势方向 - ma5 = df['ma5'].iloc[-1] if 'ma5' in df.columns else None - ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns else None - close = df['close'].iloc[-1] - - if pd.notna(ma5) and pd.notna(ma20): - if close > ma5 > ma20: - trends[interval] = 'bullish' - elif close < ma5 < ma20: - trends[interval] = 'bearish' - else: - trends[interval] = 'neutral' - - if len(trends) < 2: - return "" - - lines = [] - - # 统计各方向数量 - bullish_count = sum(1 for t in trends.values() if t == 'bullish') - bearish_count = sum(1 for t in trends.values() if t == 'bearish') - total = len(trends) - - # 各周期趋势 - trend_map = {'bullish': '📈多', 'bearish': '📉空', 'neutral': '➡️震荡'} - trend_str = " | ".join([f"{k}: {trend_map.get(v, v)}" for k, v in trends.items()]) - lines.append(f"- 各周期趋势: {trend_str}") - - # 共振判断 - if bullish_count == total: - lines.append(f"- **强共振做多**: 所有周期均为多头排列") - elif bearish_count == total: - lines.append(f"- **强共振做空**: 所有周期均为空头排列") - elif bullish_count >= total * 0.75: - lines.append(f"- **偏多共振**: {bullish_count}/{total} 周期看多") - elif bearish_count >= total * 0.75: - lines.append(f"- **偏空共振**: {bearish_count}/{total} 周期看空") - else: - lines.append(f"- **无明显共振**: 多空分歧,建议观望") - - return "\n".join(lines) - - def _analyze_market_structure(self, data: Dict[str, pd.DataFrame]) -> str: - """分析市场结构(趋势、高低点)""" - df = data.get('1h') - if df is None or len(df) < 24: - return "" - - lines = [] - recent = df.iloc[-24:] # 最近 24 根 1h K 线 - - # 找出局部高低点 - highs = [] - lows = [] - - for i in range(2, len(recent) - 2): - # 局部高点:比前后两根都高 - if (recent.iloc[i]['high'] > recent.iloc[i-1]['high'] and - recent.iloc[i]['high'] > recent.iloc[i-2]['high'] and - recent.iloc[i]['high'] > recent.iloc[i+1]['high'] and - recent.iloc[i]['high'] > recent.iloc[i+2]['high']): - highs.append((i, float(recent.iloc[i]['high']))) - - # 局部低点:比前后两根都低 - if (recent.iloc[i]['low'] < recent.iloc[i-1]['low'] and - recent.iloc[i]['low'] < recent.iloc[i-2]['low'] and - recent.iloc[i]['low'] < recent.iloc[i+1]['low'] and - recent.iloc[i]['low'] < recent.iloc[i+2]['low']): - lows.append((i, float(recent.iloc[i]['low']))) - - # 判断趋势结构 - if len(highs) >= 2 and len(lows) >= 2: - # 检查高点是否越来越高 - higher_highs = all(highs[i][1] < highs[i+1][1] for i in range(len(highs)-1)) - lower_highs = all(highs[i][1] > highs[i+1][1] for i in range(len(highs)-1)) - - # 检查低点是否越来越高 - higher_lows = all(lows[i][1] < lows[i+1][1] for i in range(len(lows)-1)) - lower_lows = all(lows[i][1] > lows[i+1][1] for i in range(len(lows)-1)) - - if higher_highs and higher_lows: - lines.append("- **上升趋势**: 更高的高点(HH) + 更高的低点(HL)") - elif lower_highs and lower_lows: - lines.append("- **下降趋势**: 更低的高点(LH) + 更低的低点(LL)") - elif higher_lows and lower_highs: - lines.append("- **收敛三角形**: 高点下移 + 低点上移,即将突破") - elif lower_lows and higher_highs: - lines.append("- **扩散形态**: 波动加大,方向不明") - else: - lines.append("- **震荡结构**: 无明显趋势") - else: - lines.append("- **结构不明**: 高低点不足,难以判断") - - # 计算趋势强度 - if len(recent) >= 10: - price_change = (float(recent.iloc[-1]['close']) - float(recent.iloc[0]['close'])) / float(recent.iloc[0]['close']) * 100 - if abs(price_change) > 3: - direction = "上涨" if price_change > 0 else "下跌" - lines.append(f"- 24h 趋势: {direction} {abs(price_change):.1f}%") - - return "\n".join(lines) - - def _analyze_volatility(self, data: Dict[str, pd.DataFrame]) -> str: - """分析波动率变化""" - df = data.get('1h') - if df is None or len(df) < 24 or 'atr' not in df.columns: - return "" - - lines = [] - - # ATR 变化趋势 - recent_atr = df['atr'].iloc[-6:].mean() # 最近 6 根 - older_atr = df['atr'].iloc[-12:-6].mean() # 之前 6 根 - - 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) - - return "\n".join(parts) - - def _format_indicators(self, row: pd.Series, df: pd.DataFrame = None) -> str: - """格式化指标数据(含趋势变化分析)""" - lines = [] - - # 价格 - close = row.get('close', 0) - open_price = row.get('open', 0) - high = row.get('high', 0) - low = row.get('low', 0) - change = ((close - open_price) / open_price * 100) if open_price else 0 - lines.append(f"- K线: O={open_price:.2f} H={high:.2f} L={low:.2f} C={close:.2f} ({change:+.2f}%)") - - # 均线 - ma5 = row.get('ma5', 0) - ma10 = row.get('ma10', 0) - ma20 = row.get('ma20', 0) - ma50 = row.get('ma50', 0) - if pd.notna(ma20): - # 判断均线排列 - if pd.notna(ma5) and pd.notna(ma10): - if ma5 > ma10 > ma20: - ma_trend = "多头排列" - elif ma5 < ma10 < ma20: - ma_trend = "空头排列" - else: - ma_trend = "交织" - else: - ma_trend = "" - ma_str = f"- 均线: MA5={ma5:.2f}, MA10={ma10:.2f}, MA20={ma20:.2f}" - if pd.notna(ma50): - ma_str += f", MA50={ma50:.2f}" - if ma_trend: - ma_str += f" ({ma_trend})" - lines.append(ma_str) - - # RSI(含趋势分析) - rsi = row.get('rsi', 0) - if pd.notna(rsi): - rsi_status = "超卖" if rsi < 30 else ("超买" if rsi > 70 else "中性") - rsi_trend = self._analyze_indicator_trend(df, 'rsi', 6) if df is not None else "" - rsi_line = f"- RSI: {rsi:.1f} ({rsi_status})" - if rsi_trend: - rsi_line += f" {rsi_trend}" - lines.append(rsi_line) - - # MACD(含趋势分析) - macd = row.get('macd', 0) - macd_signal = row.get('macd_signal', 0) - macd_hist = row.get('macd_hist', 0) - if pd.notna(macd): - macd_status = "多头" if macd > macd_signal else "空头" - macd_trend = self._analyze_macd_trend(df) if df is not None else "" - macd_line = f"- MACD: DIF={macd:.4f}, DEA={macd_signal:.4f}, 柱={macd_hist:.4f} ({macd_status})" - if macd_trend: - macd_line += f" {macd_trend}" - lines.append(macd_line) - - # KDJ(含金叉死叉检测) - k = row.get('k', 0) - d = row.get('d', 0) - j = row.get('j', 0) - if pd.notna(k): - kdj_signal = self._detect_kdj_cross(df) if df is not None else "" - kdj_line = f"- KDJ: K={k:.1f}, D={d:.1f}, J={j:.1f}" - if kdj_signal: - kdj_line += f" {kdj_signal}" - lines.append(kdj_line) - - # 布林带(含位置分析) - bb_upper = row.get('bb_upper', 0) - bb_middle = row.get('bb_middle', 0) - bb_lower = row.get('bb_lower', 0) - if pd.notna(bb_upper): - # 判断价格在布林带中的位置 - if close >= bb_upper: - bb_pos = "触及上轨" - elif close <= bb_lower: - bb_pos = "触及下轨" - elif close > bb_middle: - bb_pos = "中轨上方" - else: - bb_pos = "中轨下方" - lines.append(f"- 布林带: 上={bb_upper:.2f}, 中={bb_middle:.2f}, 下={bb_lower:.2f} ({bb_pos})") - - # ATR - atr = row.get('atr', 0) - if pd.notna(atr): - lines.append(f"- ATR: {atr:.2f}") - - # 成交量 - volume = row.get('volume', 0) - volume_ratio = row.get('volume_ratio', 0) - if pd.notna(volume_ratio): - vol_status = "放量" if volume_ratio > 1.5 else ("缩量" if volume_ratio < 0.5 else "正常") - lines.append(f"- 成交量: {volume:.2f}, 量比={volume_ratio:.2f} ({vol_status})") - - return "\n".join(lines) - - def _analyze_indicator_trend(self, df: pd.DataFrame, indicator: str, lookback: int = 6) -> str: - """分析指标趋势变化""" - if df is None or len(df) < lookback: - return "" - - recent = df[indicator].iloc[-lookback:] - if recent.isna().any(): - return "" - - first_val = recent.iloc[0] - last_val = recent.iloc[-1] - change = last_val - first_val - - # RSI 特殊处理 - if indicator == 'rsi': - if first_val > 70 and last_val < 70: - return "[从超买回落]" - elif first_val < 30 and last_val > 30: - return "[从超卖反弹]" - elif change > 10: - return "[快速上升]" - elif change < -10: - return "[快速下降]" - - return "" - - def _analyze_macd_trend(self, df: pd.DataFrame, lookback: int = 6) -> str: - """分析 MACD 趋势""" - if df is None or len(df) < lookback: - return "" - - recent_hist = df['macd_hist'].iloc[-lookback:] - recent_macd = df['macd'].iloc[-lookback:] - recent_signal = df['macd_signal'].iloc[-lookback:] - - if recent_hist.isna().any(): - return "" - - # 检测金叉死叉 - for i in range(-3, 0): - if i - 1 >= -len(recent_macd): - prev_diff = recent_macd.iloc[i-1] - recent_signal.iloc[i-1] - curr_diff = recent_macd.iloc[i] - recent_signal.iloc[i] - if prev_diff < 0 and curr_diff > 0: - return "[刚刚金叉]" - elif prev_diff > 0 and curr_diff < 0: - return "[刚刚死叉]" - - # 检测柱状图趋势 - positive_count = sum(1 for x in recent_hist if x > 0) - hist_trend = recent_hist.iloc[-1] - recent_hist.iloc[-3] if len(recent_hist) >= 3 else 0 - - if positive_count == lookback and hist_trend > 0: - return "[红柱持续放大]" - elif positive_count == lookback and hist_trend < 0: - return "[红柱开始缩小]" - elif positive_count == 0 and hist_trend < 0: - return "[绿柱持续放大]" - elif positive_count == 0 and hist_trend > 0: - return "[绿柱开始缩小]" - - return "" - - def _detect_kdj_cross(self, df: pd.DataFrame, lookback: int = 3) -> str: - """检测 KDJ 金叉死叉""" - if df is None or len(df) < lookback: - return "" - - recent_k = df['k'].iloc[-lookback:] - recent_d = df['d'].iloc[-lookback:] - - if recent_k.isna().any() or recent_d.isna().any(): - return "" - - # 检测最近的交叉 - for i in range(-lookback + 1, 0): - prev_diff = recent_k.iloc[i-1] - recent_d.iloc[i-1] - curr_diff = recent_k.iloc[i] - recent_d.iloc[i] - - if prev_diff < 0 and curr_diff > 0: - # 金叉位置判断 - k_val = recent_k.iloc[i] - if k_val < 20: - return "[低位金叉,强买入信号]" - elif k_val < 50: - return "[中位金叉]" - else: - return "[高位金叉,谨慎]" - elif prev_diff > 0 and curr_diff < 0: - k_val = recent_k.iloc[i] - if k_val > 80: - return "[高位死叉,强卖出信号]" - elif k_val > 50: - return "[中位死叉]" - else: - return "[低位死叉,谨慎]" - - return "" - - def _format_recent_klines(self, df: pd.DataFrame, interval: str) -> str: - """格式化最近 K 线(含量价分析)""" - # 根据周期决定显示数量 - # 4h: 12根=2天, 1h: 24根=1天, 15m: 16根=4小时, 5m: 12根=1小时 - count = {'4h': 12, '1h': 24, '15m': 16, '5m': 12}.get(interval, 12) - - if len(df) < count: - count = len(df) - - lines = [f"\n最近 {count} 根 K 线(含量价数据):"] - lines.append("| 时间 | 开盘 | 最高 | 最低 | 收盘 | 涨跌 | 成交量 | 量比 | RSI |") - lines.append("|------|------|------|------|------|------|--------|------|-----|") - - for i in range(-count, 0): - row = df.iloc[i] - change = ((row['close'] - row['open']) / row['open'] * 100) if row['open'] else 0 - change_str = f"{change:+.2f}%" - time_str = row['open_time'].strftime('%m-%d %H:%M') if pd.notna(row.get('open_time')) else 'N/A' - rsi = row.get('rsi', 0) - rsi_str = f"{rsi:.0f}" if pd.notna(rsi) else "-" - - # 成交量和量比 - volume = row.get('volume', 0) - volume_ratio = row.get('volume_ratio', 1.0) - if pd.notna(volume) and volume > 0: - # 格式化成交量(大数字用K/M表示) - if volume >= 1000000: - vol_str = f"{volume/1000000:.1f}M" - elif volume >= 1000: - vol_str = f"{volume/1000:.1f}K" - else: - vol_str = f"{volume:.0f}" - else: - vol_str = "-" - - vol_ratio_str = f"{volume_ratio:.2f}" if pd.notna(volume_ratio) else "-" - - lines.append(f"| {time_str} | {row['open']:.2f} | {row['high']:.2f} | " - f"{row['low']:.2f} | {row['close']:.2f} | {change_str} | {vol_str} | {vol_ratio_str} | {rsi_str} |") - - # 添加量价分析提示 - lines.append(self._analyze_volume_price(df, count)) - - return "\n".join(lines) - - def _analyze_volume_price(self, df: pd.DataFrame, count: int) -> str: - """分析量价关系""" - if len(df) < count: - return "" - - recent = df.iloc[-count:] - analysis = [] - - # 计算价格趋势 - price_change = (recent.iloc[-1]['close'] - recent.iloc[0]['close']) / recent.iloc[0]['close'] * 100 - - # 计算成交量趋势 - vol_first_half = recent.iloc[:count//2]['volume'].mean() if 'volume' in recent.columns else 0 - vol_second_half = recent.iloc[count//2:]['volume'].mean() if 'volume' in recent.columns else 0 - - if vol_first_half > 0 and vol_second_half > 0: - vol_change = (vol_second_half - vol_first_half) / vol_first_half * 100 - - # 量价分析 - if price_change > 1: # 上涨 - if vol_change > 20: - analysis.append("📈 **量价分析**: 放量上涨,上涨有效") - elif vol_change < -20: - analysis.append("⚠️ **量价分析**: 缩量上涨,警惕回调") - else: - analysis.append("➡️ **量价分析**: 量能平稳上涨") - elif price_change < -1: # 下跌 - if vol_change > 20: - analysis.append("📉 **量价分析**: 放量下跌,下跌有效") - elif vol_change < -20: - analysis.append("💡 **量价分析**: 缩量下跌,关注企稳") - else: - analysis.append("➡️ **量价分析**: 量能平稳下跌") - else: # 横盘 - if vol_change < -30: - analysis.append("🔄 **量价分析**: 缩量整理,等待方向") - else: - analysis.append("🔄 **量价分析**: 横盘震荡") - - # 检测量价背离 - if len(df) >= 10: - recent_10 = df.iloc[-10:] - # 检查是否有新高/新低 - price_high_idx = recent_10['high'].idxmax() - price_low_idx = recent_10['low'].idxmin() - - if 'volume' in recent_10.columns: - # 顶背离检测 - if price_high_idx == recent_10.index[-1]: # 最新K线创新高 - prev_high_idx = recent_10['high'].iloc[:-1].idxmax() - if recent_10.loc[price_high_idx, 'volume'] < recent_10.loc[prev_high_idx, 'volume'] * 0.8: - analysis.append("🔴 **顶背离**: 价格新高但量能不足,警惕回落") - - # 底背离检测 - if price_low_idx == recent_10.index[-1]: # 最新K线创新低 - prev_low_idx = recent_10['low'].iloc[:-1].idxmin() - if recent_10.loc[price_low_idx, 'volume'] < recent_10.loc[prev_low_idx, 'volume'] * 0.8: - analysis.append("🟢 **底背离**: 价格新低但量能萎缩,关注反弹") - - return "\n" + "\n".join(analysis) if analysis else "" - - def _parse_response(self, response: str) -> Dict[str, Any]: - """解析 LLM 响应""" - result = { - 'raw_response': response, - 'analysis_summary': '', - 'signals': [], - 'key_levels': {'support': [], 'resistance': []} - } - - try: - json_str = None - - # 尝试多种方式提取 JSON - # 1. 尝试提取 ```json ... ``` 代码块 - json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response) - if json_match: - json_str = json_match.group(1).strip() - logger.debug(f"从 ```json 代码块提取 JSON,长度: {len(json_str)}") - else: - # 2. 尝试提取 ``` ... ``` 代码块(没有 json 标记) - code_match = re.search(r'```\s*([\s\S]*?)\s*```', response) - if code_match: - potential_json = code_match.group(1).strip() - # 检查是否像 JSON(以 { 开头) - if potential_json.startswith('{'): - json_str = potential_json - logger.debug(f"从 ``` 代码块提取 JSON,长度: {len(json_str)}") - - # 3. 如果还没找到,尝试直接找到 { ... } 结构 - if not json_str: - brace_match = re.search(r'\{[\s\S]*\}', response) - if brace_match: - json_str = brace_match.group(0).strip() - logger.debug(f"从花括号提取 JSON,长度: {len(json_str)}") - - # 4. 最后尝试直接解析整个响应 - if not json_str: - json_str = response.strip() - logger.debug(f"直接使用整个响应作为 JSON,长度: {len(json_str)}") - - # 清理 JSON 字符串中的问题字符 - json_str = self._clean_json_string(json_str) - - # 解析 JSON - parsed = json.loads(json_str) - - result['analysis_summary'] = parsed.get('analysis_summary', '') - result['signals'] = parsed.get('signals', []) - result['key_levels'] = parsed.get('key_levels', {'support': [], 'resistance': []}) - - # 验证和清理信号 - valid_signals = [] - for sig in result['signals']: - if self._validate_signal(sig): - valid_signals.append(sig) - result['signals'] = valid_signals - - logger.info(f"JSON 解析成功: {len(valid_signals)} 个有效信号") - - except json.JSONDecodeError as e: - logger.warning(f"LLM 响应不是有效 JSON: {e},尝试提取关键信息") - logger.debug(f"无法解析的 JSON 字符串: {json_str[:200] if json_str else response[:200]}...") - result['analysis_summary'] = self._extract_summary(response) - except Exception as e: - logger.error(f"解析响应时出错: {e}") - result['analysis_summary'] = self._extract_summary(response) - - return result - - def _validate_signal(self, signal: Dict[str, Any]) -> bool: - """验证信号是否有效""" - required_fields = ['type', 'action', 'confidence', 'grade', 'reason'] - for field in required_fields: - if field not in signal: - return False - - # 验证类型 - if signal['type'] not in ['short_term', 'medium_term', 'long_term']: - return False - - def _clean_json_string(self, json_str: str) -> str: - """清理 JSON 字符串中的问题字符""" - # 移除 BOM 标记 - json_str = json_str.strip('\ufeff') - - # 使用更健壮的方法:使用 json.JSONDecoder 的 raw_decode - # 但首先尝试简单的清理 - try: - # 方法1: 直接解析,如果成功就不需要清理 - json.loads(json_str) - return json_str - except json.JSONDecodeError: - pass - - # 方法2: 移除控制字符(保留换行、制表符等常见字符) - # 控制字符范围: 0x00-0x1F,除了 0x09(\t), 0x0A(\n), 0x0D(\r) - def remove_control_chars(s): - """移除字符串值中的控制字符""" - result = [] - in_string = False - escape = False - i = 0 - - while i < len(s): - char = s[i] - - if escape: - # 转义模式下,保留所有字符 - result.append(char) - escape = False - elif char == '\\': - # 开始转义 - result.append(char) - escape = True - elif char == '"' and (i == 0 or s[i-1] != '\\'): - # 字符串边界 - in_string = not in_string - result.append(char) - elif in_string: - # 在字符串内,检查是否为控制字符 - code = ord(char) - if code < 0x20 and code not in (0x09, 0x0A, 0x0D): - # 控制字符,跳过或替换 - if char == '\n': - result.append('\\n') - elif char == '\r': - result.append('\\r') - elif char == '\t': - result.append('\\t') - # 其他控制字符直接跳过 - else: - result.append(char) - else: - # 不在字符串内,保留所有字符(包括空格、换行等) - result.append(char) - - i += 1 - - return ''.join(result) - - json_str = remove_control_chars(json_str) - return json_str - - def _validate_signal(self, signal: Dict[str, Any]) -> bool: - """验证信号是否有效""" - required_fields = ['type', 'action', 'confidence', 'grade', 'reason'] - for field in required_fields: - if field not in signal: - return False - - # 验证类型 - if signal['type'] not in ['short_term', 'medium_term', 'long_term']: - return False - - # 验证动作 - if signal['action'] not in ['buy', 'sell', 'wait']: - return False - - # wait 动作不算有效信号 - if signal['action'] == 'wait': - return False - - # 验证置信度(必须 >= 60 才算有效信号,即 B 级及以上) - confidence = signal.get('confidence', 0) - if not isinstance(confidence, (int, float)) or confidence < 60: - return False - - # 验证入场类型(默认为 market) - entry_type = signal.get('entry_type', 'market') - if entry_type not in ['market', 'limit']: - signal['entry_type'] = 'market' # 默认现价入场 - - # 验证仓位大小(默认根据等级设置) - position_size = signal.get('position_size', '') - if position_size not in ['heavy', 'medium', 'light']: - # 根据信号等级设置默认仓位 - grade = signal.get('grade', 'C') - if grade == 'A': - signal['position_size'] = 'medium' # A级默认中仓 - elif grade == 'B': - signal['position_size'] = 'light' # B级默认轻仓 - else: - signal['position_size'] = 'light' # C级默认轻仓 - - return True - - def _extract_summary(self, text: str) -> str: - """从文本中提取摘要""" - text = text.strip() - if len(text) > 100: - return text[:100] + "..." - return text - - def _empty_result(self, symbol: str, reason: str = "") -> Dict[str, Any]: - """返回空结果""" - return { - 'symbol': symbol, - 'timestamp': datetime.now().isoformat(), - 'analysis_summary': reason or '无法分析', - 'signals': [], - 'key_levels': {'support': [], 'resistance': []}, - 'error': reason - } - - def get_best_signal(self, result: Dict[str, Any]) -> Optional[Dict[str, Any]]: - """ - 从分析结果中获取最佳信号 - - Args: - result: analyze() 的返回结果 - - Returns: - 最佳信号,如果没有则返回 None - """ - signals = result.get('signals', []) - if not signals: - return None - - # 按置信度排序 - sorted_signals = sorted(signals, key=lambda x: x.get('confidence', 0), reverse=True) - return sorted_signals[0] - - def format_signal_message(self, signal: Dict[str, Any], symbol: str) -> str: - """ - 格式化信号消息(用于 Telegram 通知) - - Args: - signal: 信号数据 - symbol: 交易对 - - Returns: - 格式化的消息文本 - """ - # 获取股票名称 - from app.stock_agent.stock_agent import STOCK_NAMES - stock_name = STOCK_NAMES.get(symbol, '') - - type_map = { - 'short_term': '短线', - 'medium_term': '中线', - 'long_term': '长线' - } - action_map = { - 'buy': '做多', - 'sell': '做空' - } - - signal_type = type_map.get(signal['type'], signal['type']) - action = action_map.get(signal['action'], signal['action']) - grade = signal.get('grade', 'C') - confidence = signal.get('confidence', 0) - entry_type = signal.get('entry_type', 'market') - - # 等级图标 - grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') - - # 方向图标 - action_icon = '🟢' if signal['action'] == 'buy' else '🔴' - - # 入场类型 - entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' - entry_type_icon = '⚡' if entry_type == 'market' else '⏳' - - # 仓位大小 - position_size = signal.get('position_size', 'light') - position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'} - position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱') - position_text = position_map.get(position_size, '轻仓') - - # 计算风险收益比 - entry = signal.get('entry_price', 0) - sl = signal.get('stop_loss', 0) - tp = signal.get('take_profit', 0) - sl_percent = ((sl - entry) / entry * 100) if entry else 0 - tp_percent = ((tp - entry) / entry * 100) if entry else 0 - - # 识别市场类型 - if self.agent_type == 'crypto': - market_tag = '[加密货币] ' - elif symbol.endswith('.HK'): - market_tag = '[港股] ' - else: - market_tag = '[美股] ' - - # 构建标题(带股票名称和市场类型) - symbol_display = f"{stock_name}({symbol})" if stock_name else symbol - - message = f"""📊 {market_tag}{symbol_display} {signal_type}信号 - -{action_icon} **方向**: {action} -{entry_type_icon} **入场**: {entry_type_text} -{position_icon} **仓位**: {position_text} -⭐ **等级**: {grade} {grade_icon} -📈 **置信度**: {confidence}% - -💰 **入场价**: ${entry:,.2f} -🛑 **止损价**: ${sl:,.2f} ({sl_percent:+.1f}%) -🎯 **止盈价**: ${tp:,.2f} ({tp_percent:+.1f}%) - -📝 **分析理由**: -{signal.get('reason', '无')} - -⚠️ **风险提示**: -{signal.get('risk_warning', '请注意风险控制')}""" - - return message - - def format_feishu_card(self, signal: Dict[str, Any], symbol: str) -> Dict[str, Any]: - """ - 格式化飞书卡片消息 - - Args: - signal: 信号数据 - symbol: 交易对 - - Returns: - 包含 title, content, color 的字典 - """ - # 获取股票名称 - from app.stock_agent.stock_agent import STOCK_NAMES - stock_name = STOCK_NAMES.get(symbol, '') - - type_map = { - 'short_term': '短线', - 'medium_term': '中线', - 'long_term': '长线' - } - action_map = { - 'buy': '做多', - 'sell': '做空' - } - - signal_type = type_map.get(signal['type'], signal['type']) - action = action_map.get(signal['action'], signal['action']) - grade = signal.get('grade', 'C') - confidence = signal.get('confidence', 0) - entry_type = signal.get('entry_type', 'market') - - # 等级图标 - grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') - - # 入场类型 - entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' - entry_type_icon = '⚡' if entry_type == 'market' else '⏳' - - # 仓位大小 - position_size = signal.get('position_size', 'light') - position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'} - position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱') - position_text = position_map.get(position_size, '轻仓') - - # 标题和颜色 - 区分加密货币/美股/港股 - is_market_order = entry_type == 'market' - market_badge = '【现价】' if is_market_order else '' - - # 识别市场类型 - if self.agent_type == 'crypto': - market_tag = '[加密货币] ' - elif symbol.endswith('.HK'): - market_tag = '[港股] ' - else: - market_tag = '[美股] ' - - # 构建带名称的股票显示 - symbol_display = f"{stock_name}({symbol})" if stock_name else symbol - - if signal['action'] == 'buy': - title = f"🟢 {market_tag}{symbol_display} {signal_type}做多信号 {market_badge}" - color = "green" - else: - title = f"🔴 {market_tag}{symbol_display} {signal_type}做空信号 {market_badge}" - color = "red" - - # 计算风险收益比 - entry = signal.get('entry_price', 0) - sl = signal.get('stop_loss', 0) - tp = signal.get('take_profit', 0) - sl_percent = ((sl - entry) / entry * 100) if entry else 0 - tp_percent = ((tp - entry) / entry * 100) if entry else 0 - - # 构建 Markdown 内容 - 现价时突出显示 - if is_market_order: - # 现价入场,重点突出价格 - content_parts = [ - f"**{signal_type}** | **{grade}**{grade_icon} | **{confidence}%** 置信度", - f"{entry_type_icon} **入场方式**: {entry_type_text}", - f"{position_icon} **建议仓位**: {position_text}", - "", - f"💰 **⭐ 现价入场 ⭐**", - f"**>>> ${entry:,.2f} <<<**", - "", - f"🛑 **止损**: ${sl:,.2f} ({sl_percent:+.1f}%)", - f"🎯 **止盈**: ${tp:,.2f} ({tp_percent:+.1f}%)", - "", - f"📝 **分析理由**:", - f"{signal.get('reason', '无')}", - "", - f"⚠️ **风险提示**:", - f"{signal.get('risk_warning', '请注意风险控制')}", - ] - else: - # 挂单,正常显示 - content_parts = [ - f"**{signal_type}** | **{grade}**{grade_icon} | **{confidence}%** 置信度", - f"{entry_type_icon} **入场方式**: {entry_type_text}", - f"{position_icon} **建议仓位**: {position_text}", - "", - f"💰 **入场**: ${entry:,.2f}", - f"🛑 **止损**: ${sl:,.2f} ({sl_percent:+.1f}%)", - f"🎯 **止盈**: ${tp:,.2f} ({tp_percent:+.1f}%)", - "", - f"📝 **分析理由**:", - f"{signal.get('reason', '无')}", - "", - f"⚠️ **风险提示**:", - f"{signal.get('risk_warning', '请注意风险控制')}", - ] - - return { - 'title': title, - 'content': '\n'.join(content_parts), - 'color': color - } - - # 持仓回顾分析的 System Prompt - POSITION_REVIEW_PROMPT = """你是一个专业的加密货币交易风险管理专家。你的任务是回顾现有持仓,根据最新市场行情决定是否需要调整。 - -## 你的职责 - -对于每个持仓,你需要分析: -1. 当前持仓状态(盈亏、持仓时间、风险敞口) -2. 最新市场行情(趋势、支撑阻力、技术指标) -3. 原有交易逻辑是否依然有效 -4. 是否需要调整止损止盈 -5. 是否需要平仓(部分或全部) - -## 决策类型 - -### 1. HOLD(保持) -- 适用场景:行情符合预期,趋势延续 -- 操作:不改变任何设置 - -### 2. ADJUST_SL_TP(调整止损止盈) -- 适用场景: - - **盈利状态**:趋势强劲,可以收紧止损锁定更多利润 - - **亏损状态**:支撑/阻力位变化,需要调整止损到更合理位置 - - **目标接近**:原止盈目标接近,但趋势仍强,可上移止盈 -- 操作:更新 stop_loss 和/或 take_profit - -### 3. PARTIAL_CLOSE(部分平仓) -- 适用场景: - - 盈利较大,但不确定性增加 - - 重要阻力位附近,锁定部分利润 - - 趋势有转弱迹象 -- 操作:平掉 close_percent 比例的仓位 - -### 4. FULL_CLOSE(全部平仓) -- 适用场景: - - **止损型**:趋势明确反转,止损信号出现 - - **止盈型**:目标达成,或出现更好的机会 - - **风险型**:重大利空/利好的不确定性 -- 操作:平掉全部仓位 - -## 调整原则 - -### 盈利状态(盈亏 > 0) -1. **收紧止损**:如果盈利 > 2%,可以将止损移至保本或盈利 1% 位置 -2. **部分止盈**:如果盈利 > 5% 且接近重要阻力位,可平掉 30-50% 仓位 -3. **继续持有**:如果趋势强劲,可以放宽止损让利润奔跑 - -### 亏损状态(盈亏 < 0) -1. **提前止损**:如果出现明确的反转信号,不要等止损触发 -2. **调整止损**:如果关键支撑/阻力位变化,更新止损位置 -3. **继续持有**:如果只是正常波动,原交易逻辑未变,继续持有 - -### 重要技术信号 -1. **趋势反转**:多周期共振转反、跌破/突破关键 MA -2. **量价背离**:价格新高但成交量萎缩 -3. **MACD 背离**:价格新高/新低但 MACD 未确认 -4. **RSI 极端**:RSI > 75 或 < 25 后掉头 - -## 输出格式 - -对于每个持仓,输出 JSON: -```json -{ - "order_id": "订单ID", - "action": "HOLD | ADJUST_SL_TP | PARTIAL_CLOSE | FULL_CLOSE", - "new_sl": 新止损价格(仅 ADJUST_SL_TP 时), - "new_tp": 新止盈价格(仅 ADJUST_SL_TP 时), - "close_percent": 平仓比例 0-100(仅 PARTIAL_CLOSE 时), - "reason": "调整原因(简明扼要,20字以内)" -} -``` - -## 重要原则 -1. **主动管理**:不要被动等待止损触发,主动识别风险 -2. **保护利润**:盈利状态下,优先考虑锁定利润 -3. **果断止损**:亏损状态下,如果趋势反转,果断离场 -4. **灵活调整**:根据最新行情,不局限于开仓时的判断 -5. **考虑成本**:频繁调整会增加交易成本,只在有明确信号时调整 -""" - - async def review_positions( - self, - symbol: str, - positions: List[Dict[str, Any]], - data: Dict[str, pd.DataFrame] - ) -> List[Dict[str, Any]]: - """ - 回顾并分析现有持仓,给出调整建议 - - Args: - symbol: 交易对 - positions: 持仓列表,每个持仓包含: - - order_id: 订单ID - - side: 'long' or 'short' - - entry_price: 开仓价格 - - current_price: 当前价格 - - stop_loss: 当前止损 - - take_profit: 当前止盈 - - quantity: 仓位数量 - - pnl_percent: 盈亏百分比 - - open_time: 开仓时间 - data: 多周期K线数据 - - Returns: - 调整建议列表 - """ - if not positions: - return [] - - try: - # 构建持仓分析提示 - prompt = self._build_position_review_prompt(symbol, positions, data) - - # 调用 LLM(使用异步方法避免阻塞事件循环) - response = await llm_service.achat([ - {"role": "system", "content": self.POSITION_REVIEW_PROMPT}, - {"role": "user", "content": prompt} - ], model_override=self.model_override) - - if not response: - logger.warning(f"{symbol} 持仓回顾 LLM 分析无响应") - return [] - - # 解析响应 - return self._parse_position_review_response(response) - - except Exception as e: - logger.error(f"持仓回顾分析失败: {e}", exc_info=True) - return [] - - def _build_position_review_prompt( - self, - symbol: str, - positions: List[Dict[str, Any]], - data: Dict[str, pd.DataFrame] - ) -> str: - """构建持仓分析提示""" - lines = [f"# {symbol} 持仓回顾分析", f"分析时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"] - lines.append("\n## 当前持仓") - - for idx, pos in enumerate(positions, 1): - side_text = "做多 📈" if pos['side'] == 'long' else "做空 📉" - pnl_text = f"+{pos['pnl_percent']:.1f}%" if pos['pnl_percent'] >= 0 else f"{pos['pnl_percent']:.1f}%" - pnl_emoji = "✅" if pos['pnl_percent'] >= 0 else "❌" - - lines.append(f"\n### 持仓 {idx}: {pos['order_id']}") - lines.append(f"- 方向: {side_text}") - lines.append(f"- 开仓价: ${pos['entry_price']:,.2f}") - lines.append(f"- 当前价: ${pos['current_price']:,.2f}") - lines.append(f"- 盈亏: {pnl_emoji} {pnl_text}") - lines.append(f"- 止损: ${pos['stop_loss']:,.2f}") - lines.append(f"- 止盈: ${pos['take_profit']:,.2f}") - lines.append(f"- 仓位: ${pos['quantity']:,.0f}") - - # 计算持仓时间 - if 'open_time' in pos: - open_time = pos['open_time'] - if isinstance(open_time, str): - open_time = datetime.fromisoformat(open_time) - duration = datetime.now() - open_time - hours = duration.total_seconds() / 3600 - lines.append(f"- 持仓时间: {hours:.1f} 小时") - - # 添加市场分析 - lines.append("\n## 最新市场分析") - - # 使用 1h 和 4h 数据分析 - for interval in ['4h', '1h']: - df = data.get(interval) - if df is None or len(df) < 20: - continue - - latest = df.iloc[-1] - prev = df.iloc[-2] - - lines.append(f"\n### {interval} 周期") - lines.append(f"- 当前价格: ${latest['close']:,.2f}") - lines.append(f"- 涨跌幅: {((latest['close'] - prev['close']) / prev['close'] * 100):+.2f}%") - - if 'ma5' in df.columns and pd.notna(latest['ma5']): - lines.append(f"- MA5: ${latest['ma5']:,.2f}") - if 'ma20' in df.columns and pd.notna(latest['ma20']): - lines.append(f"- MA20: ${latest['ma20']:,.2f}") - - if 'rsi' in df.columns and pd.notna(latest['rsi']): - rsi_val = latest['rsi'] - rsi_status = "超买" if rsi_val > 70 else "超卖" if rsi_val < 30 else "正常" - lines.append(f"- RSI: {rsi_val:.1f} ({rsi_status})") - - if 'macd' in df.columns and pd.notna(latest['macd']): - macd_trend = "多头" if latest['macd'] > 0 else "空头" - lines.append(f"- MACD: {latest['macd']:.4f} ({macd_trend})") - - # 添加趋势判断 - lines.append("\n## 请给出调整建议") - lines.append("对于每个持仓,请分析是否需要调整,并按 JSON 格式输出。") - - return "\n".join(lines) - - def _parse_position_review_response(self, response: str) -> List[Dict[str, Any]]: - """解析持仓回顾响应""" - try: - # 尝试提取 JSON 数组 - import json - import re - - # 查找 JSON 数组 - json_match = re.search(r'\[\s*\{.*?\}\s*\]', response, re.DOTALL) - if json_match: - json_str = json_match.group(0) - decisions = json.loads(json_str) - - # 验证每个决策的格式 - valid_decisions = [] - for decision in decisions: - if 'order_id' in decision and 'action' in decision: - valid_decisions.append(decision) - else: - logger.warning(f"无效的决策格式: {decision}") - - return valid_decisions - - # 如果找不到 JSON 数组,尝试解析单个对象 - json_match = re.search(r'\{[^{}]*"action"[^{}]*\}', response, re.DOTALL) - if json_match: - json_str = json_match.group(0) - decision = json.loads(json_str) - if 'order_id' in decision and 'action' in decision: - return [decision] - - logger.warning(f"无法解析持仓回顾响应: {response[:200]}") - return [] - - except json.JSONDecodeError as e: - logger.error(f"解析持仓回顾 JSON 失败: {e}") - return [] - except Exception as e: - logger.error(f"解析持仓回顾响应时出错: {e}") - return [] diff --git a/backend/app/crypto_agent/signal_analyzer.py b/backend/app/crypto_agent/signal_analyzer.py deleted file mode 100644 index 47ef1d3..0000000 --- a/backend/app/crypto_agent/signal_analyzer.py +++ /dev/null @@ -1,1404 +0,0 @@ -""" -信号分析器 - 多周期技术分析和 LLM 深度分析 -""" -import pandas as pd -from typing import Dict, Any, Optional, List -from app.utils.logger import logger -from app.services.llm_service import llm_service - - -class SignalAnalyzer: - """交易信号分析器 - 波段交易优化版""" - - # LLM 系统提示词 - 波段交易版 - CRYPTO_ANALYST_PROMPT = """你是一位经验丰富的加密货币波段交易员,专注于捕捉 1-7 天的中等波段行情。 - -## 交易风格 -- **波段交易**:持仓 1-7 天,不做超短线 -- **顺势回调**:在趋势中寻找回调入场机会 -- **风险控制**:单笔亏损不超过本金 2% - -## 多周期分析框架 -1. **4H 周期**:判断主趋势方向和强度 - - 趋势明确:价格在 MA20 同侧运行 3 根以上 K 线 - - 趋势强度:看 MACD 柱状图是否放大 - -2. **1H 周期**:确认趋势 + 寻找回调位置 - - 上涨趋势中:等待回调到 MA20 或前低支撑 - - 下跌趋势中:等待反弹到 MA20 或前高阻力 - -3. **15M 周期**:入场信号确认 - - 做多:RSI 从超卖回升 + MACD 金叉 + K 线企稳 - - 做空:RSI 从超买回落 + MACD 死叉 + K 线见顶 - -## 入场条件(波段做多) -1. 4H 趋势向上(价格 > MA20,MACD > 0 或底背离) -2. 1H 回调到支撑位(MA20 附近或前低) -3. 15M 出现止跌信号(RSI < 40 回升,或 MACD 金叉) -4. 止损明确(前低下方),风险收益比 >= 1:2 - -## 入场条件(波段做空) -1. 4H 趋势向下(价格 < MA20,MACD < 0 或顶背离) -2. 1H 反弹到阻力位(MA20 附近或前高) -3. 15M 出现见顶信号(RSI > 60 回落,或 MACD 死叉) -4. 止损明确(前高上方),风险收益比 >= 1:2 - -## 特殊情况处理 -- **极度超卖(RSI < 20)**:不追空,等待反弹做多机会 -- **极度超买(RSI > 80)**:不追多,等待回调做空机会 -- **震荡市**:观望,等待突破方向 - -## 输出格式(JSON) -```json -{ - "market_structure": { - "trend": "uptrend/downtrend/sideways", - "strength": "strong/moderate/weak", - "phase": "impulse/correction/reversal" - }, - "key_levels": { - "resistance": [阻力位1, 阻力位2], - "support": [支撑位1, 支撑位2] - }, - "signal": { - "quality": "A/B/C/D", - "action": "buy/sell/wait", - "confidence": 0-100, - "entry_zone": [入场区间下限, 入场区间上限], - "stop_loss": 止损价, - "targets": [目标1, 目标2], - "reason": "入场理由" - }, - "risk_warning": "风险提示" -} -``` - -信号质量说明: -- A级:趋势明确 + 回调到位 + 多重信号共振(置信度 80+) -- B级:趋势明确 + 信号较好(置信度 60-80) -- C级:有机会但需要更多确认(置信度 40-60) -- D级:不建议交易(置信度 < 40) - -重要:波段交易要有耐心,宁可错过也不要在不理想的位置入场。""" - - def __init__(self): - """初始化信号分析器""" - 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 - } - - # ==================== 短线信号分析 ==================== - - def _analyze_short_term_signal(self, m5_data: pd.DataFrame, m15_data: pd.DataFrame, - trend_direction: str) -> Dict[str, Any]: - """ - 分析短线交易信号(超跌反弹/超涨回落) - - 短线信号特点: - - 不依赖大趋势方向,主要看短周期超买超卖 - - 快进快出,持仓时间短 - - 置信度上限较低,建议轻仓 - - Args: - m5_data: 5分钟K线数据 - m15_data: 15分钟K线数据 - trend_direction: 大趋势方向(用于顺势加分) - - Returns: - { - 'action': 'buy' | 'sell' | 'hold', - 'confidence': 0-100, - 'reasons': [...], - 'signal_type': 'short_term' - } - """ - if len(m5_data) < 5 or len(m15_data) < 5: - return {'action': 'hold', 'confidence': 0, 'reasons': [], 'signal_type': 'short_term'} - - m5_latest = m5_data.iloc[-1] - m5_prev = m5_data.iloc[-2] - m15_latest = m15_data.iloc[-1] - m15_prev = m15_data.iloc[-2] - - buy_score = 0 - sell_score = 0 - buy_reasons = [] - sell_reasons = [] - - # === 15M RSI 超卖/超买 === - m15_rsi = m15_latest.get('rsi', 50) - if pd.notna(m15_rsi): - if m15_rsi < 25: - buy_reasons.append(f"15M RSI极度超卖({m15_rsi:.1f})") - buy_score += 3 - elif m15_rsi < 35: - buy_reasons.append(f"15M RSI超卖({m15_rsi:.1f})") - buy_score += 2 - elif m15_rsi > 75: - sell_reasons.append(f"15M RSI极度超买({m15_rsi:.1f})") - sell_score += 3 - elif m15_rsi > 65: - sell_reasons.append(f"15M RSI超买({m15_rsi:.1f})") - sell_score += 2 - - # === 5M RSI 反转确认 === - m5_rsi = m5_latest.get('rsi', 50) - m5_prev_rsi = m5_prev.get('rsi', 50) - if pd.notna(m5_rsi) and pd.notna(m5_prev_rsi): - # 超卖后回升 - if m5_rsi < 40 and m5_rsi > m5_prev_rsi and m5_prev_rsi < 35: - buy_reasons.append(f"5M RSI反转回升({m5_prev_rsi:.1f}→{m5_rsi:.1f})") - buy_score += 2 - # 超买后回落 - if m5_rsi > 60 and m5_rsi < m5_prev_rsi and m5_prev_rsi > 65: - sell_reasons.append(f"5M RSI反转回落({m5_prev_rsi:.1f}→{m5_rsi:.1f})") - sell_score += 2 - - # === 布林带触轨 === - if 'bb_lower' in m15_latest and pd.notna(m15_latest['bb_lower']): - if m15_latest['close'] <= m15_latest['bb_lower']: - buy_reasons.append("15M触及布林下轨") - buy_score += 2 - elif m15_latest['close'] >= m15_latest['bb_upper']: - sell_reasons.append("15M触及布林上轨") - sell_score += 2 - - # === KDJ 超卖/超买 === - m15_k = m15_latest.get('k', 50) - m15_d = m15_latest.get('d', 50) - if pd.notna(m15_k) and pd.notna(m15_d): - if m15_k < 20 and m15_d < 20: - buy_reasons.append(f"15M KDJ超卖区(K={m15_k:.1f})") - buy_score += 1.5 - elif m15_k > 80 and m15_d > 80: - sell_reasons.append(f"15M KDJ超买区(K={m15_k:.1f})") - sell_score += 1.5 - - # === 5M K线反转形态 === - if m5_latest['close'] > m5_latest['open'] and m5_prev['close'] < m5_prev['open']: - # 阴转阳 - if m5_rsi < 40: - buy_reasons.append("5M阴转阳反转") - buy_score += 1.5 - elif m5_latest['close'] < m5_latest['open'] and m5_prev['close'] > m5_prev['open']: - # 阳转阴 - if m5_rsi > 60: - sell_reasons.append("5M阳转阴反转") - sell_score += 1.5 - - # === 5M MACD 金叉/死叉 === - m5_macd = m5_latest.get('macd', 0) - m5_macd_signal = m5_latest.get('macd_signal', 0) - m5_prev_macd = m5_prev.get('macd', 0) - m5_prev_macd_signal = m5_prev.get('macd_signal', 0) - if pd.notna(m5_macd) and pd.notna(m5_prev_macd): - if m5_prev_macd <= m5_prev_macd_signal and m5_macd > m5_macd_signal: - buy_reasons.append("5M MACD金叉") - buy_score += 1.5 - elif m5_prev_macd >= m5_prev_macd_signal and m5_macd < m5_macd_signal: - sell_reasons.append("5M MACD死叉") - sell_score += 1.5 - - # === 顺势加分 === - if trend_direction == 'bullish' and buy_score > 0: - buy_score += 1 - buy_reasons.append("顺大势做多") - elif trend_direction == 'bearish' and sell_score > 0: - sell_score += 1 - sell_reasons.append("顺大势做空") - - # === 决策 === - action = 'hold' - confidence = 0 - reasons = [] - - # 短线信号阈值较低,但置信度上限也低 - if buy_score >= 4 and buy_score > sell_score: - action = 'buy' - confidence = min(35 + buy_score * 6, 65) # 短线最高65% - reasons = buy_reasons + ["📈 短线超跌反弹"] - elif sell_score >= 4 and sell_score > buy_score: - action = 'sell' - confidence = min(35 + sell_score * 6, 65) - reasons = sell_reasons + ["📉 短线超涨回落"] - - return { - 'action': action, - 'confidence': confidence, - 'reasons': reasons, - 'signal_type': 'short_term', - 'scores': {'buy': buy_score, 'sell': sell_score} - } - - # ==================== 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]: - """ - 分析趋势方向和强度(波段交易优化版) - - Args: - h1_data: 1小时K线数据(含技术指标) - h4_data: 4小时K线数据(含技术指标) - - Returns: - { - 'direction': 'bullish' | 'bearish' | 'neutral', - 'strength': 'strong' | 'moderate' | 'weak', - 'phase': 'impulse' | 'correction' | 'reversal', - 'h4_score': float, - 'h1_score': float - } - """ - if h1_data.empty or h4_data.empty: - return { - 'direction': 'neutral', - 'strength': 'weak', - 'phase': 'sideways', - 'h4_score': 0, - 'h1_score': 0 - } - - # 获取最新数据 - h1_latest = h1_data.iloc[-1] - h4_latest = h4_data.iloc[-1] - - # 计算各周期的趋势得分 - h4_score, h4_details = self._calculate_trend_score(h4_latest) - h1_score, h1_details = self._calculate_trend_score(h1_latest) - - # 判断趋势方向(4H 为主) - if h4_score > 0.3: - direction = 'bullish' - elif h4_score < -0.3: - direction = 'bearish' - else: - direction = 'neutral' - - # 判断趋势强度 - strength = self._assess_trend_strength(h4_data, h1_data) - - # 判断当前阶段(是主升/主跌还是回调) - phase = self._detect_market_phase(h4_data, h1_data, direction) - - # 检查极端情况 - h4_rsi = h4_latest.get('rsi', 50) - extreme_warning = "" - if pd.notna(h4_rsi): - if h4_rsi < 20: - extreme_warning = f" [RSI={h4_rsi:.1f} 极度超卖]" - phase = 'oversold' - elif h4_rsi > 80: - extreme_warning = f" [RSI={h4_rsi:.1f} 极度超买]" - phase = 'overbought' - - logger.info(f"趋势分析: 方向={direction}, 强度={strength}, 阶段={phase} | " - f"4H={h4_score:.2f}{h4_details}, 1H={h1_score:.2f}{h1_details}{extreme_warning}") - - return { - 'direction': direction, - 'strength': strength, - 'phase': phase, - 'h4_score': h4_score, - 'h1_score': h1_score - } - - def _assess_trend_strength(self, h4_data: pd.DataFrame, h1_data: pd.DataFrame) -> str: - """评估趋势强度""" - if len(h4_data) < 5: - return 'weak' - - h4_latest = h4_data.iloc[-1] - - # 检查 MACD 柱状图是否放大 - macd_hist = h4_data['macd_hist'].tail(5) - macd_expanding = False - if len(macd_hist) >= 3: - recent_abs = abs(macd_hist.iloc[-1]) - prev_abs = abs(macd_hist.iloc[-3]) - if recent_abs > prev_abs * 1.2: - macd_expanding = True - - # 检查价格是否持续在 MA20 同侧 - close_prices = h4_data['close'].tail(5) - ma20_values = h4_data['ma20'].tail(5) - consistent_side = True - if pd.notna(ma20_values.iloc[-1]): - above_ma = close_prices > ma20_values - consistent_side = above_ma.all() or (~above_ma).all() - - # 检查 RSI 是否在趋势区间 - rsi = h4_latest.get('rsi', 50) - rsi_trending = 40 < rsi < 60 # 中性区间表示趋势不强 - - if macd_expanding and consistent_side and not rsi_trending: - return 'strong' - elif consistent_side: - return 'moderate' - else: - return 'weak' - - def _detect_market_phase(self, h4_data: pd.DataFrame, h1_data: pd.DataFrame, - direction: str) -> str: - """检测市场阶段(主升/主跌 vs 回调)""" - if len(h1_data) < 10: - return 'unknown' - - h1_latest = h1_data.iloc[-1] - h4_latest = h4_data.iloc[-1] - - # 获取 1H 的短期趋势 - h1_ma5 = h1_latest.get('ma5', 0) - h1_ma20 = h1_latest.get('ma20', 0) - h1_close = h1_latest.get('close', 0) - - if not (pd.notna(h1_ma5) and pd.notna(h1_ma20)): - return 'unknown' - - # 判断 1H 是否在回调 - if direction == 'bullish': - # 上涨趋势中,1H 价格回落到 MA20 附近 = 回调 - if h1_close < h1_ma5 and h1_close > h1_ma20 * 0.98: - return 'correction' # 回调中,可能是入场机会 - elif h1_close > h1_ma5: - return 'impulse' # 主升浪 - elif direction == 'bearish': - # 下跌趋势中,1H 价格反弹到 MA20 附近 = 反弹 - if h1_close > h1_ma5 and h1_close < h1_ma20 * 1.02: - return 'correction' # 反弹中,可能是做空机会 - elif h1_close < h1_ma5: - return 'impulse' # 主跌浪 - - return 'sideways' - - def _calculate_trend_score(self, data: pd.Series) -> tuple: - """ - 计算单周期趋势得分 - - Args: - data: 包含技术指标的数据行 - - Returns: - (得分, 详情字符串) - """ - score = 0.0 - count = 0 - details = [] - - # 价格与均线关系 - if 'close' in data and 'ma20' in data and pd.notna(data['ma20']): - if data['close'] > data['ma20']: - score += 1 - details.append("价格>MA20") - else: - score -= 1 - details.append("价格 data['ma20']: - score += 1 - details.append("MA5>MA20") - else: - score -= 1 - details.append("MA5 data['macd_signal']: - score += 1 - details.append("MACD多") - else: - score -= 1 - details.append("MACD空") - count += 1 - - # RSI - if 'rsi' in data and pd.notna(data['rsi']): - if data['rsi'] > 50: - score += 0.5 - else: - score -= 0.5 - count += 0.5 - - final_score = score / count if count > 0 else 0 - detail_str = f"({','.join(details)})" if details else "" - return final_score, detail_str - - def analyze_entry_signal(self, m5_data: pd.DataFrame, m15_data: pd.DataFrame, - trend: Dict[str, Any], h1_data: pd.DataFrame = None) -> Dict[str, Any]: - """ - 分析 15M 进场信号(波段交易优化版 - 增强版) - - 新增功能: - - 成交量确认 - - K线形态识别 - - 支撑阻力位判断 - - 5M精确入场 - - Args: - m5_data: 5分钟K线数据(用于精确入场) - m15_data: 15分钟K线数据(主要入场周期) - trend: 趋势分析结果 - h1_data: 1小时K线数据(用于支撑阻力位计算,可选) - - Returns: - { - 'action': 'buy' | 'sell' | 'hold', - 'confidence': 0-100, - 'signal_grade': 'A' | 'B' | 'C' | 'D', - 'reasons': [...], - 'indicators': {...}, - 'patterns': {...}, - 'levels': {...}, - 'volume_analysis': {...} - } - """ - if m5_data.empty or m15_data.empty: - return {'action': 'hold', 'confidence': 0, 'signal_grade': 'D', - 'reasons': ['数据不足'], 'indicators': {}} - - # 兼容旧格式(如果 trend 是字符串) - if isinstance(trend, str): - trend_direction = trend - trend_phase = 'unknown' - trend_strength = 'moderate' - else: - trend_direction = trend.get('direction', 'neutral') - trend_phase = trend.get('phase', 'unknown') - 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'] - if rsi < 30: - buy_signals.append(f"RSI超卖({rsi:.1f})") - signal_weights['buy'] += 2 - elif rsi < 40 and len(m15_data) >= 2: - 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})") - signal_weights['buy'] += 1.5 - elif rsi > 70: - sell_signals.append(f"RSI超买({rsi:.1f})") - signal_weights['sell'] += 2 - elif rsi > 60 and len(m15_data) >= 2: - prev_rsi = m15_data.iloc[-2].get('rsi', 50) - if pd.notna(prev_rsi) and rsi < prev_rsi: - sell_signals.append(f"RSI回落({prev_rsi:.1f}→{rsi:.1f})") - signal_weights['sell'] += 1.5 - - # === MACD 信号 === - if len(m15_data) >= 2: - 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 - elif abs(m15_latest['macd_hist']) < abs(prev['macd_hist']) * 0.7: - if m15_latest['macd_hist'] > 0: - sell_signals.append("MACD动能减弱") - signal_weights['sell'] += 0.5 - else: - buy_signals.append("MACD动能减弱") - signal_weights['buy'] += 0.5 - - # === 布林带信号 === - if 'close' in m15_latest and 'bb_lower' in m15_latest and 'bb_upper' in m15_latest: - if pd.notna(m15_latest['bb_lower']) and pd.notna(m15_latest['bb_upper']): - bb_middle = m15_latest.get('bb_middle', (m15_latest['bb_upper'] + m15_latest['bb_lower']) / 2) - if m15_latest['close'] < m15_latest['bb_lower']: - buy_signals.append("触及布林下轨") - signal_weights['buy'] += 1.5 - 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: - buy_signals.append("突破布林中轨") - signal_weights['buy'] += 1 - elif prev_close > bb_middle and m15_latest['close'] < bb_middle: - sell_signals.append("跌破布林中轨") - signal_weights['sell'] += 1 - - # === KDJ 信号 === - if 'k' in m15_latest and 'd' in m15_latest and len(m15_data) >= 2: - prev = m15_data.iloc[-2] - if pd.notna(m15_latest['k']) and pd.notna(prev['k']): - if prev['k'] <= prev['d'] and m15_latest['k'] > m15_latest['d']: - if m15_latest['k'] < 30: - buy_signals.append("KDJ低位金叉") - signal_weights['buy'] += 1.5 - else: - buy_signals.append("KDJ金叉") - signal_weights['buy'] += 0.5 - elif prev['k'] >= prev['d'] and m15_latest['k'] < m15_latest['d']: - if m15_latest['k'] > 70: - sell_signals.append("KDJ高位死叉") - signal_weights['sell'] += 1.5 - else: - 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 = [] - signal_grade = 'D' - signal_type = 'swing' # 默认波段信号 - - # 波段交易核心逻辑:在回调中寻找入场机会 - 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 + ["📊 波段信号: 主升浪追多"] - signal_grade = 'B' if confidence >= 60 else 'C' - elif trend_phase == 'oversold' and signal_weights['buy'] >= 3: - # 极度超卖时允许抄底,但降低置信度并提示风险 - action = 'buy' - confidence = min(30 + signal_weights['buy'] * 8, 70) # 最高70% - reasons = buy_signals + ["📊 波段信号: 极度超卖抄底(高风险)", "建议轻仓试探"] - signal_grade = 'C' # 最高C级 - elif trend_phase == 'overbought': - reasons = ['极度超买,不宜追多'] - - 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})"] - signal_grade = 'A' if confidence >= 80 else ('B' if confidence >= 60 else 'C') - elif trend_phase == 'impulse' and signal_weights['sell'] >= 4: - action = 'sell' - confidence = min(30 + signal_weights['sell'] * 8, 80) - reasons = sell_signals + ["📊 波段信号: 主跌浪追空"] - signal_grade = 'B' if confidence >= 60 else 'C' - elif trend_phase == 'overbought' and signal_weights['sell'] >= 3: - # 极度超买时允许做空,但降低置信度并提示风险 - action = 'sell' - confidence = min(30 + signal_weights['sell'] * 8, 70) # 最高70% - reasons = sell_signals + ["📊 波段信号: 极度超买摸顶(高风险)", "建议轻仓试探"] - signal_grade = 'C' # 最高C级 - elif trend_phase == 'oversold': - reasons = ['极度超卖,不宜追空'] - - # ==================== 5.5 短线信号检测(中长线无信号时)==================== - # 如果中长线没有触发信号,检查短线超跌反弹/超涨回落机会 - if action == 'hold': - short_term = self._analyze_short_term_signal(m5_data, m15_data, trend_direction) - if short_term['action'] != 'hold' and short_term['confidence'] >= 50: - action = short_term['action'] - confidence = short_term['confidence'] - reasons = short_term['reasons'] - signal_type = 'short_term' - signal_grade = 'C' # 短线信号最高C级 - - # 如果还是没有信号 - if action == 'hold' and trend_direction == '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', 'volume_ratio']: - if col in m15_latest and pd.notna(m15_latest[col]): - indicators[col] = float(m15_latest[col]) - - # 记录详细日志(简化版,详细日志在 crypto_agent 中输出) - if action != 'hold': - type_text = "短线" if signal_type == 'short_term' else "波段" - logger.debug(f"信号详情: [{type_text}] {action} {confidence}% {signal_grade} | 买权重={signal_weights['buy']:.1f} 卖权重={signal_weights['sell']:.1f}") - - return { - 'action': action, - 'confidence': confidence, - 'signal_grade': signal_grade, - 'signal_type': signal_type, # 'swing' 波段 | 'short_term' 短线 - 'reasons': reasons, - 'indicators': indicators, - 'trend_info': { - '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], - symbol: str) -> Dict[str, Any]: - """ - 使用 LLM 进行深度分析 - - Args: - data: 多周期K线数据 - signal: 初步信号分析结果 - symbol: 交易对 - - Returns: - LLM 分析结果(结构化数据) - """ - try: - # 构建分析提示 - prompt = self._build_analysis_prompt(data, signal, symbol) - - # 调用 LLM(使用异步方法避免阻塞事件循环) - response = await llm_service.achat([ - {"role": "system", "content": self.CRYPTO_ANALYST_PROMPT}, - {"role": "user", "content": prompt} - ]) - - if response: - logger.info(f"LLM 分析完成: {symbol}") - # 解析 JSON 响应 - return self._parse_llm_response(response) - else: - return {"error": "LLM 分析暂时不可用", "raw": ""} - - except Exception as e: - logger.error(f"LLM 分析失败: {e}") - return {"error": str(e), "raw": ""} - - def _parse_llm_response(self, response: str) -> Dict[str, Any]: - """ - 解析 LLM 的 JSON 响应 - - Args: - response: LLM 原始响应 - - Returns: - 解析后的结构化数据 - """ - import json - import re - - result = { - "raw": response, - "parsed": None, - "summary": "" - } - - try: - # 尝试提取 JSON 块 - json_match = re.search(r'```json\s*([\s\S]*?)\s*```', response) - if json_match: - json_str = json_match.group(1) - else: - # 尝试直接解析整个响应 - json_str = response - - # 解析 JSON - parsed = json.loads(json_str) - result["parsed"] = parsed - - # 生成摘要 - if parsed: - recommendation = parsed.get("recommendation", {}) - action = recommendation.get("action", "wait") - confidence = recommendation.get("confidence", 0) - reason = recommendation.get("reason", "") - - if action == "wait": - result["summary"] = f"建议观望。{parsed.get('risk_warning', '')}" - else: - action_text = "做多" if action == "buy" else "做空" - result["summary"] = f"建议{action_text},置信度{confidence}%。{reason}" - - except json.JSONDecodeError: - # JSON 解析失败,提取关键信息 - logger.warning("LLM 响应不是有效 JSON,尝试提取关键信息") - result["summary"] = self._extract_summary_from_text(response) - - return result - - def _extract_summary_from_text(self, text: str) -> str: - """从非 JSON 文本中提取摘要""" - # 简单提取前 200 字符作为摘要 - text = text.strip() - if len(text) > 200: - return text[:200] + "..." - return text - - def _build_analysis_prompt(self, data: Dict[str, pd.DataFrame], signal: Dict[str, Any], - symbol: str) -> str: - """构建 LLM 分析提示 - 优化版""" - parts = [f"# {symbol} 技术分析数据\n"] - - # 当前价格 - current_price = float(data['5m'].iloc[-1]['close']) - parts.append(f"**当前价格**: ${current_price:,.2f}\n") - - # 添加各周期指标摘要 - for interval in ['4h', '1h', '15m']: - df = data.get(interval) - if df is None or df.empty: - continue - - latest = df.iloc[-1] - parts.append(f"\n## {interval.upper()} 周期指标") - - # 价格与均线关系 - close = latest.get('close', 0) - ma20 = latest.get('ma20', 0) - ma50 = latest.get('ma50', 0) - if pd.notna(ma20): - position = "上方" if close > ma20 else "下方" - parts.append(f"- 价格在 MA20 {position} (MA20={ma20:.2f})") - if pd.notna(ma50): - parts.append(f"- MA50: {ma50:.2f}") - - # RSI - rsi = latest.get('rsi', 0) - if pd.notna(rsi): - rsi_status = "超卖" if rsi < 30 else ("超买" if rsi > 70 else "中性") - parts.append(f"- RSI: {rsi:.1f} ({rsi_status})") - - # MACD - macd = latest.get('macd', 0) - macd_signal = latest.get('macd_signal', 0) - if pd.notna(macd) and pd.notna(macd_signal): - macd_status = "多头" if macd > macd_signal else "空头" - parts.append(f"- MACD: {macd:.2f}, Signal: {macd_signal:.2f} ({macd_status})") - - # 布林带 - bb_upper = latest.get('bb_upper', 0) - bb_lower = latest.get('bb_lower', 0) - if pd.notna(bb_upper) and pd.notna(bb_lower): - parts.append(f"- 布林带: 上轨={bb_upper:.2f}, 下轨={bb_lower:.2f}") - - # 添加最近 5 根 15M K 线(让 LLM 看形态) - parts.append("\n## 最近 5 根 15M K线") - parts.append("| 时间 | 开盘 | 最高 | 最低 | 收盘 | 涨跌 |") - parts.append("|------|------|------|------|------|------|") - - df_15m = data.get('15m') - if df_15m is not None and len(df_15m) >= 5: - for i in range(-5, 0): - row = df_15m.iloc[i] - change = ((row['close'] - row['open']) / row['open']) * 100 - change_str = f"+{change:.2f}%" if change >= 0 else f"{change:.2f}%" - time_str = row['open_time'].strftime('%H:%M') if pd.notna(row['open_time']) else 'N/A' - parts.append(f"| {time_str} | {row['open']:.2f} | {row['high']:.2f} | {row['low']:.2f} | {row['close']:.2f} | {change_str} |") - - # 计算关键价位 - parts.append("\n## 关键价位参考") - df_1h = data.get('1h') - if df_1h is not None and len(df_1h) >= 20: - recent_high = df_1h['high'].tail(20).max() - recent_low = df_1h['low'].tail(20).min() - parts.append(f"- 近期高点: ${recent_high:,.2f}") - parts.append(f"- 近期低点: ${recent_low:,.2f}") - - # 初步信号分析结果 - parts.append(f"\n## 规则引擎初步判断") - parts.append(f"- 趋势: {signal.get('trend', 'unknown')}") - parts.append(f"- 信号: {signal.get('action', 'hold')}") - parts.append(f"- 置信度: {signal.get('confidence', 0)}%") - parts.append(f"- 触发原因: {', '.join(signal.get('reasons', []))}") - - parts.append("\n---") - parts.append("请基于以上数据进行分析,严格按照 JSON 格式输出你的判断。") - - return "\n".join(parts) - - def calculate_stop_loss_take_profit(self, price: float, action: str, - atr: float, df: pd.DataFrame = None) -> Dict[str, float]: - """ - 计算止损止盈位置 - 结构化止损 + 移动止盈策略 - - 策略: - - 止损:基于关键支撑/阻力位(前高前低、均线) - - 止盈:不设固定止盈,通过移动止损锁定利润 - - Args: - price: 当前价格 - action: 'buy' 或 'sell' - atr: ATR 值 - df: K线数据(用于计算关键价位) - - Returns: - {'stop_loss': float, 'take_profit': float, 'method': str} - """ - if df is not None and len(df) >= 20: - # 使用结构化止损 - result = self._calculate_structured_stops(price, action, atr, df) - if result['stop_loss'] > 0: - return result - - # 回退到 ATR 止损 - if action == 'buy': - stop_loss = price - atr * 1.5 # 降低到 1.5 倍 ATR - # 止盈设置得很远,主要靠移动止损 - take_profit = price * 1.15 # 15% 作为一个"保险"止盈位 - method = 'atr' - elif action == 'sell': - stop_loss = price + atr * 1.5 - # 止盈设置得很远,主要靠移动止损 - take_profit = price * 0.85 # 15% 作为一个"保险"止盈位 - method = 'atr' - else: - return {'stop_loss': 0, 'take_profit': 0, 'method': 'none'} - - return { - 'stop_loss': round(stop_loss, 2), - 'take_profit': round(take_profit, 2), - 'method': method - } - - def _calculate_structured_stops(self, price: float, action: str, - atr: float, df: pd.DataFrame) -> Dict[str, float]: - """ - 基于关键价位计算结构化止损(无固定止盈,靠移动止损) - - 原则: - - 做多止损在前低/MA20下方 - - 做空止损在前高/MA20上方 - - 止损距离控制在 1.5-3% ATR 范围内 - - 止盈设置得很远,主要靠移动止损锁定利润 - """ - stop_loss = 0 - take_profit = 0 - - # 获取关键价位 - recent_data = df.tail(50) - - if action == 'buy': - # === 做多止损 === - # 1. 查找近期支撑位(前低) - recent_lows = recent_data['low'].values - # 找出低于当前价的低点 - valid_lows = [low for low in recent_lows if low < price * 0.99] - if valid_lows: - nearest_low = max(valid_lows) # 最近的低点 - # 止损放在前低下方 0.3% - sl_candidate = nearest_low * 0.997 - else: - sl_candidate = 0 - - # 2. 检查 MA20 作为支撑 - ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns else 0 - if pd.notna(ma20) and ma20 < price * 0.98: - sl_ma20 = ma20 * 0.995 # MA20 下方 0.5% - # 选择更保守的止损(更高的止损位) - if sl_candidate > 0: - sl_candidate = max(sl_candidate, sl_ma20) - else: - sl_candidate = sl_ma20 - - # 3. 验证止损距离合理性(1.5%-3% ATR) - if sl_candidate > 0: - sl_distance = (price - sl_candidate) / price * 100 - atr_percent = atr / price * 100 - - # 如果止损距离太小(< 1%ATR),使用 ATR 止损 - if sl_distance < atr_percent * 1.0: - sl_candidate = price - atr * 1.2 - # 如果止损距离过大(> 4%ATR),限制在合理范围 - elif sl_distance > atr_percent * 4: - sl_candidate = price - atr * 2.5 - - # 4. 回退到 ATR 止损 - if sl_candidate <= 0: - sl_candidate = price - atr * 1.5 - - stop_loss = sl_candidate - - # === 做多止盈(设置得很远,主要靠移动止损)=== - take_profit = price * 1.15 # 15% 作为保险止盈位 - - elif action == 'sell': - # === 做空止损 === - # 1. 查找近期阻力位(前高) - recent_highs = recent_data['high'].values - # 找出高于当前价的高点 - valid_highs = [high for high in recent_highs if high > price * 1.01] - if valid_highs: - nearest_high = min(valid_highs) # 最近的高点 - # 止损放在前高上方 0.3% - sl_candidate = nearest_high * 1.003 - else: - sl_candidate = 0 - - # 2. 检查 MA20 作为阻力 - ma20 = df['ma20'].iloc[-1] if 'ma20' in df.columns else 0 - if pd.notna(ma20) and ma20 > price * 1.02: - sl_ma20 = ma20 * 1.005 # MA20 上方 0.5% - # 选择更保守的止损(更低的止损位) - if sl_candidate > 0: - sl_candidate = min(sl_candidate, sl_ma20) - else: - sl_candidate = sl_ma20 - - # 3. 验证止损距离合理性 - if sl_candidate > 0: - sl_distance = (sl_candidate - price) / price * 100 - atr_percent = atr / price * 100 - - # 如果止损距离太小(< 1%ATR),使用 ATR 止损 - if sl_distance < atr_percent * 1.0: - sl_candidate = price + atr * 1.2 - # 如果止损距离过大(> 4%ATR),限制在合理范围 - elif sl_distance > atr_percent * 4: - sl_candidate = price + atr * 2.5 - - # 4. 回退到 ATR 止损 - if sl_candidate <= 0: - sl_candidate = price + atr * 1.5 - - stop_loss = sl_candidate - - # === 做空止盈(设置得很远,主要靠移动止损)=== - take_profit = price * 0.85 # 15% 作为保险止盈位 - - return { - 'stop_loss': round(stop_loss, 2), - 'take_profit': round(take_profit, 2), - 'method': 'structured' - } diff --git a/backend/app/services/yfinance_service.py b/backend/app/services/yfinance_service.py index 543dc23..c96fa00 100644 --- a/backend/app/services/yfinance_service.py +++ b/backend/app/services/yfinance_service.py @@ -83,9 +83,10 @@ class YFinanceService: 多时间周期数据字典 {'1d': df, '1h': df, ...} """ if timeframes is None: - # 默认时间周期配置 + # 默认时间周期配置 - 股票不需要太实时的数据 timeframes = { - '1d': ('1d', '3mo'), # 日级别,3个月 + '1w': ('1wk', '2y'), # 周级别,2年 + '1d': ('1d', '6mo'), # 日级别,6个月 '1h': ('1h', '1mo'), # 小时级别,1个月 } diff --git a/backend/app/stock_agent/market_signal_analyzer.py b/backend/app/stock_agent/market_signal_analyzer.py new file mode 100644 index 0000000..3af059f --- /dev/null +++ b/backend/app/stock_agent/market_signal_analyzer.py @@ -0,0 +1,738 @@ +""" +股票市场信号分析器 - 纯市场分析,不包含任何仓位信息 + +职责: +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': '信号分析失败' + } diff --git a/backend/app/stock_agent/stock_agent.py b/backend/app/stock_agent/stock_agent.py index 9cf45b9..6c3f668 100644 --- a/backend/app/stock_agent/stock_agent.py +++ b/backend/app/stock_agent/stock_agent.py @@ -1,5 +1,5 @@ """ -美股交易智能体 - 主控制器(LLM 驱动版) +美股交易智能体 - 主控制器(新架构版) 只进行市场分析和通知,不执行模拟交易 """ import asyncio @@ -14,7 +14,8 @@ from app.services.feishu_service import get_feishu_service from app.services.telegram_service import get_telegram_service from app.services.signal_database_service import get_signal_db_service from app.services.fundamental_service import get_fundamental_service -from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer +from app.services.news_service import get_news_service +from app.stock_agent.market_signal_analyzer import StockMarketSignalAnalyzer from app.utils.system_status import get_system_monitor, AgentStatus @@ -87,9 +88,10 @@ class StockAgent: self.yfinance = get_yfinance_service() self.feishu = get_feishu_service() self.telegram = get_telegram_service() - self.llm_analyzer = LLMSignalAnalyzer(agent_type="stock") # 指定使用 stock 模型配置 + self.market_analyzer = StockMarketSignalAnalyzer() # 使用新的市场信号分析器 self.signal_db = get_signal_db_service() # 信号数据库服务 self.fundamental = get_fundamental_service() # 基本面数据服务 + self.news = get_news_service() # 新闻服务 # 状态管理 self.last_signals: Dict[str, Dict[str, Any]] = {} @@ -421,32 +423,38 @@ class StockAgent: except Exception as e: logger.warning(f" ⚠️ 获取基本面数据失败: {e}") - # 5. LLM 分析 - logger.info(f"\n🤖 【LLM 分析中...】") - analysis = await self.llm_analyzer.analyze( + # 5. 获取新闻数据 + logger.info(f"\n📰 【新闻分析】") + news_data = None + try: + stock_name = STOCK_NAMES.get(symbol, '') + news_data = await self.news.search_stock_news(symbol, stock_name, max_results=5) + if news_data: + logger.info(f" 获取到 {len(news_data)} 条相关新闻") + else: + logger.info(f" 暂无相关新闻") + except Exception as e: + logger.warning(f" ⚠️ 获取新闻数据失败: {e}") + + # 6. 市场信号分析(使用新架构 - 技术面 + 基本面 + 新闻) + logger.info(f"\n🤖 【市场信号分析中...】") + market_signal = await self.market_analyzer.analyze( symbol, data, symbols=self.symbols, - position_info=None, # 美股不跟踪持仓 - fundamental_data=fundamental_data, # 传递基本面数据 - fundamental_summary=fundamental_summary # 传递基本面摘要 + fundamental_data=fundamental_data, + news_data=news_data ) # 输出分析摘要 - summary = analysis.get('analysis_summary', '无') + summary = market_signal.get('analysis_summary', '无') result['analysis_summary'] = summary logger.info(f" 市场状态: {summary}") - # 输出新闻情绪 - news_sentiment = analysis.get('news_sentiment', '') - news_impact = analysis.get('news_impact', '') - if news_sentiment: - sentiment_icon = {'positive': '📈', 'negative': '📉', 'neutral': '➖'}.get(news_sentiment, '') - logger.info(f" 新闻情绪: {sentiment_icon} {news_sentiment}") - if news_impact: - logger.info(f" 消息影响: {news_impact}") + # 输出新闻情绪(如果有) + # 注:新的分析器不包含新闻分析,可以跳过或从其他地方获取 # 输出关键价位 - levels = analysis.get('key_levels', {}) + levels = market_signal.get('key_levels', {}) if levels.get('support') or levels.get('resistance'): support_str = ', '.join([f"${s:,.2f}" for s in levels.get('support', [])[:2]]) resistance_str = ', '.join([f"${r:,.2f}" for r in levels.get('resistance', [])[:2]]) @@ -454,7 +462,7 @@ class StockAgent: logger.info(f" 阻力位: {resistance_str or '-'}") # 5. 处理信号 - signals = analysis.get('signals', []) + signals = market_signal.get('signals', []) result['signals'] = signals if not signals: @@ -556,8 +564,11 @@ class StockAgent: ): """发送信号通知""" try: - # 使用正确的方法格式化信号 - card = self.llm_analyzer.format_feishu_card(signal, symbol) + from app.utils.signal_formatter import get_signal_formatter + formatter = get_signal_formatter() + + # 使用格式化工具格式化信号 + card = formatter.format_feishu_card(signal, symbol, agent_type='stock') title = card['title'] content = card['content'] @@ -568,7 +579,7 @@ class StockAgent: await self.feishu.send_card(title, content, color) # 发送到 Telegram - await self.telegram.send_message(self.llm_analyzer.format_signal_message(signal, symbol)) + await self.telegram.send_message(formatter.format_signal_message(signal, symbol, agent_type='stock')) logger.info(f"✅ 信号通知已发送: {title}") @@ -610,12 +621,9 @@ class StockAgent: except Exception as e: logger.warning(f"获取基本面数据失败: {e}") - result = await self.llm_analyzer.analyze( + result = await self.market_analyzer.analyze( symbol, data, - symbols=self.symbols, - position_info=None, - fundamental_data=fundamental_data, - fundamental_summary=fundamental_summary + symbols=self.symbols ) return result diff --git a/backend/app/utils/signal_formatter.py b/backend/app/utils/signal_formatter.py new file mode 100644 index 0000000..ad69b29 --- /dev/null +++ b/backend/app/utils/signal_formatter.py @@ -0,0 +1,226 @@ +""" +信号格式化工具 + +用于格式化交易信号通知,支持: +- 飞书卡片格式 +- Telegram 文本格式 +- 支持加密货币、美股、港股 +""" +from typing import Dict, Any + + +class SignalFormatter: + """信号格式化工具""" + + @staticmethod + def format_signal_message(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto') -> str: + """ + 格式化信号消息(用于 Telegram 通知) + + Args: + signal: 信号数据 + symbol: 交易对 + agent_type: 智能体类型 (crypto/stock) + + Returns: + 格式化的消息文本 + """ + # 获取股票名称 + from app.stock_agent.stock_agent import STOCK_NAMES + stock_name = STOCK_NAMES.get(symbol, '') + + type_map = { + 'short_term': '短线', + 'medium_term': '中线', + 'long_term': '长线' + } + action_map = { + 'buy': '做多', + 'sell': '做空' + } + + # 兼容 timeframe 和 type 字段 + signal_type_key = 'timeframe' if 'timeframe' in signal else 'type' + signal_type = type_map.get(signal.get(signal_type_key), signal.get(signal_type_key)) + action = action_map.get(signal['action'], signal['action']) + grade = signal.get('grade', 'C') + confidence = signal.get('confidence', 0) + entry_type = signal.get('entry_type', 'market') + + # 等级图标 + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') + + # 方向图标 + action_icon = '🟢' if signal['action'] == 'buy' else '🔴' + + # 入场类型 + entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' + entry_type_icon = '⚡' if entry_type == 'market' else '⏳' + + # 仓位大小 + position_size = signal.get('position_size', 'light') + position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'} + position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱') + position_text = position_map.get(position_size, '轻仓') + + # 计算风险收益比 + entry = signal.get('entry_price') or signal.get('entry_zone', 0) + sl = signal.get('stop_loss', 0) + tp = signal.get('take_profit', 0) + sl_percent = ((sl - entry) / entry * 100) if entry else 0 + tp_percent = ((tp - entry) / entry * 100) if entry else 0 + + # 识别市场类型 + if agent_type == 'crypto': + market_tag = '[加密货币] ' + elif symbol.endswith('.HK'): + market_tag = '[港股] ' + else: + market_tag = '[美股] ' + + # 构建标题(带股票名称和市场类型) + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol + + message = f"""📊 {market_tag}{symbol_display} {signal_type}信号 + +{action_icon} **方向**: {action} +{entry_type_icon} **入场**: {entry_type_text} +{position_icon} **仓位**: {position_text} +⭐ **等级**: {grade} {grade_icon} +📈 **置信度**: {confidence}% + +💰 **入场价**: ${entry:,.2f} +🛑 **止损价**: ${sl:,.2f} ({sl_percent:+.1f}%) +🎯 **止盈价**: ${tp:,.2f} ({tp_percent:+.1f}%) + +📝 **分析理由**: +{signal.get('reason', '无')} + +⚠️ **风险提示**: +{signal.get('risk_warning', '请注意风险控制')}""" + + return message + + @staticmethod + def format_feishu_card(signal: Dict[str, Any], symbol: str, agent_type: str = 'crypto') -> Dict[str, Any]: + """ + 格式化飞书卡片消息 + + Args: + signal: 信号数据 + symbol: 交易对 + agent_type: 智能体类型 (crypto/stock) + + Returns: + 包含 title, content, color 的字典 + """ + # 获取股票名称 + from app.stock_agent.stock_agent import STOCK_NAMES + stock_name = STOCK_NAMES.get(symbol, '') + + type_map = { + 'short_term': '短线', + 'medium_term': '中线', + 'long_term': '长线' + } + action_map = { + 'buy': '做多', + 'sell': '做空' + } + + # 兼容 timeframe 和 type 字段 + signal_type_key = 'timeframe' if 'timeframe' in signal else 'type' + signal_type = type_map.get(signal.get(signal_type_key), signal.get(signal_type_key)) + action = action_map.get(signal['action'], signal['action']) + grade = signal.get('grade', 'C') + confidence = signal.get('confidence', 0) + entry_type = signal.get('entry_type', 'market') + + # 等级图标 + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') + + # 入场类型 + entry_type_text = '现价入场' if entry_type == 'market' else '挂单等待' + entry_type_icon = '⚡' if entry_type == 'market' else '⏳' + + # 仓位大小 + position_size = signal.get('position_size', 'light') + position_map = {'heavy': '重仓', 'medium': '中仓', 'light': '轻仓'} + position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱') + position_text = position_map.get(position_size, '轻仓') + + # 标题和颜色 - 区分加密货币/美股/港股 + is_market_order = entry_type == 'market' + market_badge = '【现价】' if is_market_order else '' + + # 识别市场类型 + if agent_type == 'crypto': + market_tag = '[加密货币] ' + elif symbol.endswith('.HK'): + market_tag = '[港股] ' + else: + market_tag = '[美股] ' + + # 构建带名称的股票显示 + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol + + if signal['action'] == 'buy': + title = f"🟢 {market_tag}{symbol_display} {signal_type}做多信号 {market_badge}" + color = "green" + else: + title = f"🔴 {market_tag}{symbol_display} {signal_type}做空信号 {market_badge}" + color = "red" + + # 计算风险收益比 + entry = signal.get('entry_price') or signal.get('entry_zone', 0) + sl = signal.get('stop_loss', 0) + tp = signal.get('take_profit', 0) + sl_percent = ((sl - entry) / entry * 100) if entry else 0 + tp_percent = ((tp - entry) / entry * 100) if entry else 0 + + # 构建内容 + content_lines = [ + f"{action_icon} **操作**: {action}", + f"{entry_type_icon} **入场方式**: {entry_type_text}", + f"{position_icon} **仓位**: {position_text} | 📈 信心度: **{confidence}%**", + f"⭐ **等级**: {grade} {grade_icon}", + f"", + f"💰 **入场价**: ${entry:,.2f}", + f"🛑 **止损价**: ${sl:,.2f} ({sl_percent:+.1f}%)", + f"🎯 **止盈价**: ${tp:,.2f} ({tp_percent:+.1f}%)", + f"", + f"📝 **分析理由**:", + f"{signal.get('reason', '无')}", + ] + + # 添加关键因素(如果有) + key_factors = signal.get('key_factors') + if key_factors and isinstance(key_factors, list): + content_lines.append("") + content_lines.append("**关键因素**:") + for factor in key_factors[:5]: + content_lines.append(f"- {factor}") + + # 添加风险提示(如果有) + risk_warning = signal.get('risk_warning') + if risk_warning: + content_lines.append("") + content_lines.append(f"⚠️ **风险提示**:") + content_lines.append(risk_warning) + + content = "\n".join(content_lines) + + return { + 'title': title, + 'content': content, + 'color': color + } + + +# 全局单例 +_signal_formatter = SignalFormatter() + + +def get_signal_formatter() -> SignalFormatter: + """获取信号格式化工具单例""" + return _signal_formatter diff --git a/backend/test_crypto_agent.py b/backend/test_crypto_agent.py index c42e58a..f076c30 100644 --- a/backend/test_crypto_agent.py +++ b/backend/test_crypto_agent.py @@ -50,30 +50,26 @@ async def test_binance_service(): async def test_signal_analyzer(): - """测试信号分析器""" + """测试信号分析器 - 使用新架构""" print("\n" + "=" * 50) - print("测试信号分析器") + print("测试市场信号分析器(新架构)") print("=" * 50) from app.services.binance_service import binance_service - from app.crypto_agent.signal_analyzer import SignalAnalyzer + from app.crypto_agent.market_signal_analyzer import MarketSignalAnalyzer - analyzer = SignalAnalyzer() + analyzer = MarketSignalAnalyzer() # 获取数据 data = binance_service.get_multi_timeframe_data('BTCUSDT') - # 测试趋势分析 - print("\n1. 分析趋势...") - trend = analyzer.analyze_trend(data['1h'], data['4h']) - print(f" 趋势: {trend}") + # 测试 LLM 分析 + print("\n1. LLM 市场分析...") + signal = await analyzer.analyze('BTCUSDT', data, symbols=['BTCUSDT', 'ETHUSDT']) - # 测试进场信号 - print("\n2. 分析进场信号...") - signal = analyzer.analyze_entry_signal(data['5m'], data['15m'], trend) - print(f" 动作: {signal['action']}") - print(f" 置信度: {signal['confidence']}%") - print(f" 原因: {', '.join(signal['reasons'])}") + print(f" 市场状态: {signal.get('market_state')}") + print(f" 趋势: {signal.get('trend')}") + print(f" 信号数量: {len(signal.get('signals', []))}") return True diff --git a/scripts/test_stock.py b/scripts/test_stock.py index 5677a46..7b8596e 100755 --- a/scripts/test_stock.py +++ b/scripts/test_stock.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -美股分析脚本(修复版) +美股分析脚本(新架构版) 用法: python3 scripts/test_stock.py AAPL @@ -20,7 +20,8 @@ from app.services.yfinance_service import get_yfinance_service from app.services.feishu_service import get_feishu_service from app.services.telegram_service import get_telegram_service from app.services.fundamental_service import get_fundamental_service -from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer +from app.services.news_service import get_news_service +from app.stock_agent.market_signal_analyzer import StockMarketSignalAnalyzer from app.config import get_settings from app.utils.logger import logger @@ -60,8 +61,9 @@ async def analyze(symbol: str, send_notification: bool = True): try: # 获取服务 yf_service = get_yfinance_service() - llm = LLMSignalAnalyzer(agent_type="stock") # 指定使用 stock 模型配置 + market_analyzer = StockMarketSignalAnalyzer() # 使用新的市场信号分析器 fundamental = get_fundamental_service() # 基本面服务 + news = get_news_service() # 新闻服务 feishu = get_feishu_service() telegram = get_telegram_service() @@ -162,19 +164,31 @@ async def analyze(symbol: str, send_notification: bool = True): except Exception as e: print(f" ⚠️ 获取基本面数据失败: {e}") - # LLM分析 - print(f"\n🤖 LLM分析中...\n") - analysis = await llm.analyze( + # 获取新闻数据 + print(f"\n📰 新闻分析...") + news_data = None + try: + stock_name = STOCK_NAMES.get(symbol, '') + news_data = await news.search_stock_news(symbol, stock_name, max_results=5) + if news_data: + print(f" 获取到 {len(news_data)} 条相关新闻") + else: + print(f" 暂无相关新闻") + except Exception as e: + print(f" ⚠️ 获取新闻数据失败: {e}") + + # 市场信号分析(使用新架构 - 技术面 + 基本面 + 新闻) + print(f"\n🤖 市场信号分析中...\n") + market_signal = await market_analyzer.analyze( symbol, data, symbols=[symbol], - position_info=None, fundamental_data=fundamental_data, - fundamental_summary=fundamental_summary + news_data=news_data ) # 输出结果 - summary = analysis.get('analysis_summary', '') - signals = analysis.get('signals', []) + summary = market_signal.get('analysis_summary', '') + signals = market_signal.get('signals', []) result['signals'] = signals print(f"市场状态: {summary}") @@ -215,8 +229,11 @@ async def analyze(symbol: str, send_notification: bool = True): break if best_signal: - # 使用正确的方法格式化通知 - card = llm.format_feishu_card(best_signal, symbol) + # 使用格式化工具格式化通知 + from app.utils.signal_formatter import get_signal_formatter + formatter = get_signal_formatter() + + card = formatter.format_feishu_card(best_signal, symbol, agent_type='stock') title = card['title'] content = card['content'] @@ -224,7 +241,7 @@ async def analyze(symbol: str, send_notification: bool = True): # 根据信号方向选择颜色 color = "green" if best_signal.get('action') == 'buy' else "red" await feishu.send_card(title, content, color) - await telegram.send_message(llm.format_signal_message(best_signal, symbol)) + await telegram.send_message(formatter.format_signal_message(best_signal, symbol, agent_type='stock')) print(f"\n📬 通知已发送:{title}") result['notified'] = True else: