From f9d09fefcaafac1843a68aad591732a5131ff299 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 20 Feb 2026 15:20:27 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20brave=20=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/crypto_agent/llm_signal_analyzer.py | 260 +++++++++++++++++- backend/app/services/news_service.py | 117 +++++++- 2 files changed, 369 insertions(+), 8 deletions(-) diff --git a/backend/app/crypto_agent/llm_signal_analyzer.py b/backend/app/crypto_agent/llm_signal_analyzer.py index ec04475..ac3f173 100644 --- a/backend/app/crypto_agent/llm_signal_analyzer.py +++ b/backend/app/crypto_agent/llm_signal_analyzer.py @@ -14,8 +14,8 @@ from app.services.news_service import get_news_service class LLMSignalAnalyzer: """LLM 驱动的交易信号分析器""" - # 系统提示词 - 让 LLM 自主分析 - SYSTEM_PROMPT = """你是一位专业的加密货币交易员和技术分析师。你的任务是综合分析**K线数据、量价关系、技术指标和新闻舆情**,给出交易信号。 + # 加密货币专用系统提示词 + CRYPTO_SYSTEM_PROMPT = """你是一位专业的加密货币交易员和技术分析师。你的任务是综合分析**K线数据、量价关系、技术指标和新闻舆情**,给出交易信号。 ## 核心理念 加密货币市场波动大,每天都有交易机会。你的目标是: @@ -199,6 +199,226 @@ class LLMSignalAnalyzer: 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(相对强弱指标) +- RSI < 30:超卖区,关注反弹机会 +- RSI > 70:超买区,关注回落风险 +- RSI 背离:价格与 RSI 走势相反 = 重要反转信号 +- 股票市场中 RSI 极端值比加密货币更可靠 + +### MACD +- 金叉(DIF 上穿 DEA):做多信号 +- 死叉(DIF 下穿 DEA):做空信号 +- 零轴上方金叉:强势做多 +- 零轴下方金叉:弱势反弹 +- MACD 柱状图背离:重要反转信号 + +### 布林带 +- 触及下轨 + 企稳:反弹做多 +- 触及上轨 + 受阻:回落做空 +- 布林带收口:即将变盘 +- 布林带开口:趋势启动 + +### 均线系统(重要) +- 多头排列(MA5>MA10>MA20>MA50):上涨趋势 +- 空头排列(MA5 str: - """获取新闻上下文(暂时禁用)""" - # 暂时禁用新闻获取,只做技术面分析 - return "" + """获取新闻上下文""" + 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 参考""" diff --git a/backend/app/services/news_service.py b/backend/app/services/news_service.py index bd72c42..ccc8a63 100644 --- a/backend/app/services/news_service.py +++ b/backend/app/services/news_service.py @@ -1,5 +1,5 @@ """ -新闻舆情服务 - 获取加密货币相关新闻 +新闻舆情服务 - 获取加密货币和股票相关新闻 """ import re import html @@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET from typing import List, Dict, Any, Optional from datetime import datetime, timedelta from app.utils.logger import logger +from app.config import get_settings class NewsService: @@ -16,11 +17,15 @@ class NewsService: # 律动快讯 RSS BLOCKBEATS_RSS = "https://api.theblockbeats.news/v2/rss/newsflash" + # Brave Search API + BRAVE_SEARCH_API = "https://api.search.brave.com/res/v1/web/search" + def __init__(self): """初始化新闻服务""" - self._cache: List[Dict[str, Any]] = [] + self._cache: Dict[str, List[Dict[str, Any]]] = {'crypto': [], 'stock': {}} self._cache_time: Optional[datetime] = None self._cache_duration = timedelta(minutes=5) # 缓存5分钟 + self.settings = get_settings() logger.info("新闻舆情服务初始化完成") async def get_latest_news(self, limit: int = 20) -> List[Dict[str, Any]]: @@ -238,6 +243,114 @@ class NewsService: return filtered + async def search_stock_news(self, symbol: str, stock_name: str = '', + max_results: int = 10) -> List[Dict[str, Any]]: + """ + 使用 Brave Search API 搜索股票相关新闻 + + Args: + symbol: 股票代码(如 AAPL, 0700.HK) + stock_name: 股票中文名称(可选) + max_results: 最大结果数 + + Returns: + 新闻列表 + """ + api_key = self.settings.brave_api_key + if not api_key: + logger.warning("未配置 Brave API Key,跳过新闻搜索") + return [] + + # 检查缓存 + cache_key = f"{symbol}_{stock_name}" + if self._cache_time and cache_key in self._cache.get('stock', {}): + if datetime.now() - self._cache_time < self._cache_duration: + return self._cache['stock'][cache_key][:max_results] + + # 构建搜索查询 + # 根据股票类型构建不同的搜索词 + if symbol.endswith('.HK'): + # 港股 + if stock_name: + query = f"{stock_name} 港股 新闻 最新" + else: + query = f"{symbol.replace('.HK', '')} 港股 新闻 最新" + else: + # 美股 + if stock_name: + query = f"{stock_name} 股票 {symbol} news latest" + else: + query = f"{symbol} stock news latest" + + try: + headers = { + 'Accept': 'application/json', + 'Accept-Encoding': 'gzip', + 'X-Subscription-Token': api_key + } + + params = { + 'q': query, + 'count': max_results, + 'text_decorations': 'false', # 改为字符串 + 'search_lang': 'zh-hans', # Brave Search 使用 zh-hans 而非 zh-CN + # 'result_filter': 'news', # 免费计划不支持,移除此参数 + 'freshness': 'pd' # 过去24小时 + } + + async with aiohttp.ClientSession() as session: + async with session.get( + self.BRAVE_SEARCH_API, + headers=headers, + params=params, + timeout=10 + ) as response: + if response.status != 200: + logger.error(f"Brave Search API 请求失败: HTTP {response.status}") + return [] + + data = await response.json() + + # 解析搜索结果 + news_list = [] + web_results = data.get('web', {}).get('results', []) + + for item in web_results: + title = item.get('title', '') + url = item.get('url', '') + description = item.get('description', '') + + # 清理描述 + description = self._clean_html(description) + + news_list.append({ + 'title': title, + 'description': description[:500], + 'time': datetime.now(), # Brave Search 不返回精确时间 + 'time_str': datetime.now().strftime('%m-%d %H:%M'), + 'link': url, + 'source': 'Brave Search' + }) + + logger.info(f"Brave Search 搜索 {symbol} 获取到 {len(news_list)} 条新闻") + + # 更新缓存 + if 'stock' not in self._cache: + self._cache['stock'] = {} + self._cache['stock'][cache_key] = news_list + self._cache_time = datetime.now() + + return news_list[:max_results] + + except aiohttp.ClientError as e: + logger.error(f"Brave Search API 请求失败: {e}") + return [] + except Exception as e: + logger.error(f"搜索股票新闻失败: {e}") + import traceback + logger.debug(traceback.format_exc()) + return [] + def format_news_for_llm(self, news_list: List[Dict[str, Any]], max_items: int = 10) -> str: """