增加 brave 的支持
This commit is contained in:
parent
df250a920b
commit
f9d09fefca
@ -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 参考"""
|
||||
|
||||
@ -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:
|
||||
"""
|
||||
|
||||
Loading…
Reference in New Issue
Block a user