增加 brave 的支持

This commit is contained in:
aaron 2026-02-20 15:20:27 +08:00
parent df250a920b
commit f9d09fefca
2 changed files with 369 additions and 8 deletions

View File

@ -14,8 +14,8 @@ from app.services.news_service import get_news_service
class LLMSignalAnalyzer: class LLMSignalAnalyzer:
"""LLM 驱动的交易信号分析器""" """LLM 驱动的交易信号分析器"""
# 系统提示词 - 让 LLM 自主分析 # 加密货币专用系统提示词
SYSTEM_PROMPT = """你是一位专业的加密货币交易员和技术分析师。你的任务是综合分析**K线数据、量价关系、技术指标和新闻舆情**,给出交易信号。 CRYPTO_SYSTEM_PROMPT = """你是一位专业的加密货币交易员和技术分析师。你的任务是综合分析**K线数据、量价关系、技术指标和新闻舆情**,给出交易信号。
## 核心理念 ## 核心理念
加密货币市场波动大每天都有交易机会你的目标是 加密货币市场波动大每天都有交易机会你的目标是
@ -199,6 +199,226 @@ class LLMSignalAnalyzer:
7. entry_type 必须明确信号已触发用 market等待更好价位用 limit 7. entry_type 必须明确信号已触发用 market等待更好价位用 limit
8. **position_size 必须明确**根据信号质量和持仓情况给出 heavy/medium/light""" 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<MA10<MA20<MA50下跌趋势
- 价格回踩 MA20/MA50重要支撑位
- 价格反弹 MA20/MA50重要阻力位
- 均线金叉/死叉重要趋势信号
### 成交量分析
- **量价配合**价格上涨+放量或下跌+缩量是健康的
- **量价背离**价格上涨+缩量或下跌+放量要警惕
- **换手率**换手率过低说明关注度不够换手率过高可能是投机
## 四、多周期共振
- 日线 + 周线同向 = 中长线信号更可靠
- 日线 + 4小时同向 = 短线信号更可靠
- 多周期 RSI 同时超买/超卖 = 强反转信号
- 大周期决定方向小周期决定入场时机
## 五、股票市场特殊性
### 与加密货币的区别
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% 以上
## 重要原则
1. **量价优先** - 任何信号都必须有量能配合才可靠
2. **精选机会** - 股票不需要频繁交易等待高质量信号
3. **多周期确认** - 日线决定方向小周期决定入场
4. **结构止损** - 止损必须基于关键支撑/阻力位前低前高均线
5. **合理止盈** - 根据交易周期设置合理的止盈目标
6. **reason 字段必须包含量价分析**"放量突破+RSI=45量比1.8确认有效"
7. **entry_type 必须明确**信号已触发用 market等待更好价位用 limit
8. **position_size 必须明确**根据信号质量给出 heavy/medium/light"""
# 兼容旧代码,使用加密货币提示词作为默认值
SYSTEM_PROMPT = CRYPTO_SYSTEM_PROMPT
def __init__(self, agent_type: str = "crypto"): def __init__(self, agent_type: str = "crypto"):
"""初始化分析器 """初始化分析器
@ -262,12 +482,18 @@ class LLMSignalAnalyzer:
# 获取新闻数据 # 获取新闻数据
news_text = await self._get_news_context(symbol, symbols or [symbol]) news_text = await self._get_news_context(symbol, symbols or [symbol])
# 根据智能体类型选择提示词
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) data_prompt = self._build_data_prompt(symbol, data, news_text, position_info)
# 调用 LLM # 调用 LLM
response = llm_service.chat([ response = llm_service.chat([
{"role": "system", "content": self.SYSTEM_PROMPT}, {"role": "system", "content": system_prompt},
{"role": "user", "content": data_prompt} {"role": "user", "content": data_prompt}
], model_override=self.model_override) ], model_override=self.model_override)
@ -299,9 +525,31 @@ class LLMSignalAnalyzer:
return self._empty_result(symbol, str(e)) return self._empty_result(symbol, str(e))
async def _get_news_context(self, symbol: str, symbols: List[str]) -> str: async def _get_news_context(self, symbol: str, symbols: List[str]) -> str:
"""获取新闻上下文(暂时禁用)""" """获取新闻上下文"""
# 暂时禁用新闻获取,只做技术面分析 try:
return "" # 如果是股票类型,使用 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: def _format_position_info(self, symbol: str, position_info: Dict[str, Any]) -> str:
"""格式化持仓信息供 LLM 参考""" """格式化持仓信息供 LLM 参考"""

View File

@ -1,5 +1,5 @@
""" """
新闻舆情服务 - 获取加密货币相关新闻 新闻舆情服务 - 获取加密货币和股票相关新闻
""" """
import re import re
import html import html
@ -8,6 +8,7 @@ import xml.etree.ElementTree as ET
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
from datetime import datetime, timedelta from datetime import datetime, timedelta
from app.utils.logger import logger from app.utils.logger import logger
from app.config import get_settings
class NewsService: class NewsService:
@ -16,11 +17,15 @@ class NewsService:
# 律动快讯 RSS # 律动快讯 RSS
BLOCKBEATS_RSS = "https://api.theblockbeats.news/v2/rss/newsflash" 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): 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_time: Optional[datetime] = None
self._cache_duration = timedelta(minutes=5) # 缓存5分钟 self._cache_duration = timedelta(minutes=5) # 缓存5分钟
self.settings = get_settings()
logger.info("新闻舆情服务初始化完成") logger.info("新闻舆情服务初始化完成")
async def get_latest_news(self, limit: int = 20) -> List[Dict[str, Any]]: async def get_latest_news(self, limit: int = 20) -> List[Dict[str, Any]]:
@ -238,6 +243,114 @@ class NewsService:
return filtered 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]], def format_news_for_llm(self, news_list: List[Dict[str, Any]],
max_items: int = 10) -> str: max_items: int = 10) -> str:
""" """