增加 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:
"""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<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"):
"""初始化分析器
@ -262,12 +482,18 @@ class LLMSignalAnalyzer:
# 获取新闻数据
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)
# 调用 LLM
response = llm_service.chat([
{"role": "system", "content": self.SYSTEM_PROMPT},
{"role": "system", "content": system_prompt},
{"role": "user", "content": data_prompt}
], model_override=self.model_override)
@ -299,9 +525,31 @@ class LLMSignalAnalyzer:
return self._empty_result(symbol, str(e))
async def _get_news_context(self, symbol: str, symbols: List[str]) -> 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 参考"""

View File

@ -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:
"""