This commit is contained in:
aaron 2026-02-05 12:35:20 +08:00
parent 946f1419f1
commit 24e8a5c1a2
4 changed files with 543 additions and 233 deletions

View File

@ -99,12 +99,14 @@ class SkillManager:
skill = self.get_skill(skill_name)
if not skill:
logger.error(f"❌ 技能不存在: {skill_name}")
return {
"success": False,
"error": f"技能不存在: {skill_name}"
}
if not skill.enabled:
logger.warning(f"⚠️ 技能已禁用: {skill_name}")
return {
"success": False,
"error": f"技能已禁用: {skill_name}"
@ -113,6 +115,7 @@ class SkillManager:
# 验证参数
valid, error = skill.validate_params(**kwargs)
if not valid:
logger.error(f"❌ 技能参数验证失败 {skill_name}: {error}")
return {
"success": False,
"error": error
@ -120,13 +123,15 @@ class SkillManager:
# 执行技能
try:
logger.info(f"🚀 开始执行技能: {skill_name}, 参数: {kwargs}")
result = await skill.execute(**kwargs)
logger.info(f"✅ 技能执行成功: {skill_name}")
return {
"success": True,
"data": result
}
except Exception as e:
logger.error(f"技能执行失败 {skill_name}: {e}")
logger.error(f"技能执行失败 {skill_name}: {e}")
return {
"success": False,
"error": str(e)
@ -249,13 +254,14 @@ class SkillManager:
# 按优先级顺序执行
for priority in sorted(priority_groups.keys()):
skill_group = priority_groups[priority]
logger.info(f"执行优先级 {priority} 的技能: {[s['name'] for s in skill_group]}")
logger.info(f"📋 执行优先级 {priority} 的技能: {[s['name'] for s in skill_group]}")
# 同一优先级的技能并行执行
tasks = []
for skill_info in skill_group:
params = skill_info['params'].copy()
params['stock_code'] = stock_code
logger.info(f" ➡️ 准备执行技能: {skill_info['name']}, 原因: {skill_info.get('reason', '未知')}")
task = self.execute_skill(skill_info['name'], **params)
tasks.append((skill_info['name'], task))
@ -265,14 +271,15 @@ class SkillManager:
# 处理结果
for (skill_name, _), result in zip(tasks, results):
if isinstance(result, Exception):
logger.error(f"技能执行失败: {skill_name}, {result}")
logger.error(f"❌ 技能执行异常: {skill_name}, {result}")
all_errors.append(f"{skill_name}: {str(result)}")
all_results[skill_name] = {'error': str(result)}
elif result.get('success'):
# 不在这里记录成功日志,因为 execute_skill 已经记录了
all_results[skill_name] = result.get('data', {})
else:
error_msg = result.get('error', '未知错误')
logger.error(f"技能执行失败: {skill_name}, {error_msg}")
logger.error(f"技能执行失败: {skill_name}, {error_msg}")
all_errors.append(f"{skill_name}: {error_msg}")
all_results[skill_name] = {'error': error_msg}

View File

@ -11,27 +11,31 @@ class SkillPlanner:
# 维度到技能的映射
DIMENSION_SKILL_MAP = {
'price_trend': {
'required': ['market_data'],
'required': ['market_data', 'brave_search'], # brave_search 必需
'optional': []
},
'technical': {
'required': ['market_data', 'technical_analysis'],
'required': ['market_data', 'technical_analysis', 'brave_search'], # brave_search 必需
'optional': ['visualization']
},
'fundamental': {
'required': ['fundamental'],
'required': ['fundamental', 'brave_search'], # brave_search 必需
'optional': []
},
'valuation': {
'required': ['advanced_data'],
'required': ['advanced_data', 'brave_search'], # brave_search 必需
'optional': []
},
'money_flow': {
'required': ['advanced_data'],
'required': ['advanced_data', 'brave_search'], # brave_search 必需
'optional': []
},
'risk': {
'required': ['technical_analysis', 'advanced_data'],
'required': ['technical_analysis', 'advanced_data', 'brave_search'], # brave_search 必需
'optional': []
},
'news': { # 新闻维度
'required': ['brave_search'],
'optional': []
}
}
@ -46,6 +50,7 @@ class SkillPlanner:
SKILL_PRIORITY = {
'market_data': 1, # 最高优先级
'fundamental': 1,
'brave_search': 1, # 新闻搜索也是高优先级
'technical_analysis': 2,
'advanced_data': 2,
'visualization': 3, # 最低优先级
@ -141,7 +146,8 @@ class SkillPlanner:
if enabled and dimension in self.DIMENSION_SKILL_MAP:
mapping = self.DIMENSION_SKILL_MAP[dimension]
skills.extend(mapping['required'])
# 可选技能稍后根据深度策略添加
# 默认也添加可选技能(特别是 brave_search
skills.extend(mapping['optional'])
return skills
@ -217,6 +223,37 @@ class SkillPlanner:
params['data_types'] = data_types
elif skill_name == 'brave_search':
# 构建搜索查询
target = intent.get('target', {})
stock_name = target.get('stock_name', '')
stock_code = target.get('stock_code', '')
dimensions = intent.get('dimensions', {})
# 根据维度构建搜索关键词
search_keywords = []
if stock_name:
search_keywords.append(stock_name)
elif stock_code:
search_keywords.append(stock_code)
# 添加维度相关关键词
if dimensions.get('fundamental'):
search_keywords.append('财报 业绩')
if dimensions.get('news'):
search_keywords.append('最新消息')
if dimensions.get('risk'):
search_keywords.append('风险 预警')
# 如果没有特定维度,搜索一般新闻
if not any(dimensions.values()):
search_keywords.append('最新动态')
params['query'] = ' '.join(search_keywords)
params['search_type'] = 'news' # 默认搜索新闻
params['count'] = 5
params['freshness'] = 'pw' # 过去一周
return params
def _get_skill_reason(self, skill_name: str, intent: Dict[str, Any]) -> str:
@ -253,4 +290,14 @@ class SkillPlanner:
elif skill_name == 'visualization':
reasons.append('生成K线图表')
elif skill_name == 'brave_search':
if dimensions.get('news'):
reasons.append('用户关注最新新闻')
elif dimensions.get('fundamental'):
reasons.append('搜索公司最新动态和财报信息')
elif dimensions.get('risk'):
reasons.append('搜索风险预警信息')
else:
reasons.append('获取最新市场资讯和舆情')
return ', '.join(reasons) if reasons else '提供分析数据'

View File

@ -16,6 +16,7 @@ from app.skills.fundamental import FundamentalSkill
from app.skills.visualization import VisualizationSkill
from app.skills.advanced_data import AdvancedDataSkill
from app.skills.us_stock_skill import USStockSkill
from app.skills.brave_search import BraveSearchSkill
from app.services.llm_service import llm_service
from app.services.tushare_service import tushare_service
from app.utils.logger import logger
@ -60,7 +61,8 @@ class SmartStockAgent:
skill_manager.register(VisualizationSkill())
skill_manager.register(AdvancedDataSkill())
skill_manager.register(USStockSkill())
logger.info("技能注册完成Tushare Pro高级数据 + 美股支持)")
skill_manager.register(BraveSearchSkill())
logger.info("技能注册完成Tushare Pro高级数据 + 美股支持 + Brave搜索")
async def process_message(
self,
@ -112,7 +114,7 @@ class SmartStockAgent:
message: str
) -> Dict[str, Any]:
"""
全面分析整合多个数据源 + LLM深度分析
全面分析使用 skill_planner 智能规划技能 + LLM深度分析
Args:
stock_code: 股票代码
@ -126,46 +128,48 @@ class SmartStockAgent:
display_name = stock_name or stock_code
# 1. 并行获取所有数据
try:
# 获取实时行情
quote_result = await skill_manager.execute_skill(
"market_data",
stock_code=stock_code,
data_type="quote"
# 1. 使用 QuestionAnalyzer 分析问题意图
intent = await self.question_analyzer.analyze_question(
question=message,
context=[],
session_id=""
)
# 获取技术指标
technical_result = await skill_manager.execute_skill(
"technical_analysis",
stock_code=stock_code,
indicators=["ma", "macd", "rsi", "kdj"]
)
# 确保 intent 包含股票信息
if 'target' not in intent:
intent['target'] = {}
intent['target']['stock_code'] = stock_code
intent['target']['stock_name'] = stock_name
# 获取基本面
fundamental_result = await skill_manager.execute_skill(
"fundamental",
logger.info(f"问题意图分析: dimensions={intent.get('dimensions')}")
# 2. 使用 SkillPlanner 规划技能(包括 brave_search
plan = self.skill_planner.plan_skills(intent)
logger.info(f"技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}")
# 3. 执行技能规划
execution_results = await skill_manager.execute_plan(
plan=plan,
stock_code=stock_code
)
# 获取高级数据Tushare Pro 5000+积分)
advanced_result = await skill_manager.execute_skill(
"advanced_data",
stock_code=stock_code,
data_type="all"
)
if execution_results['errors']:
logger.warning(f"技能执行有错误: {execution_results['errors']}")
# 整合数据
# 4. 整合数据(兼容旧格式)
results = execution_results['results']
all_data = {
"stock_code": stock_code,
"stock_name": display_name,
"quote": quote_result.get("data") if quote_result.get("success") else None,
"technical": technical_result.get("data") if technical_result.get("success") else None,
"fundamental": fundamental_result.get("data") if fundamental_result.get("success") else None,
"advanced": advanced_result.get("data") if advanced_result.get("success") else None
"quote": results.get("market_data"),
"technical": results.get("technical_analysis"),
"fundamental": results.get("fundamental"),
"advanced": results.get("advanced_data"),
"news": results.get("brave_search") # 新增:新闻数据
}
# 2. 使用LLM进行深度分析
# 5. 使用LLM进行深度分析
if self.use_llm:
analysis = await self._llm_comprehensive_analysis(all_data, message)
else:
@ -175,12 +179,15 @@ class SmartStockAgent:
"message": analysis,
"metadata": {
"type": "comprehensive",
"data": all_data
"data": all_data,
"plan": plan
}
}
except Exception as e:
logger.error(f"全面分析失败: {e}")
import traceback
logger.error(traceback.format_exc())
return {
"message": f"分析{display_name}时出错:{str(e)}",
"metadata": {"type": "error"}
@ -256,6 +263,31 @@ class SmartStockAgent:
else:
advanced_summary = "\n【高级财务数据】\n暂无高级数据\n"
# 格式化新闻数据
news_section = ""
news_data = data.get('news', {})
if news_data and not news_data.get('error'):
results = news_data.get('results', [])
if results:
news_section = "\n【最新新闻和舆情】来源Brave Search\n"
for i, item in enumerate(results[:5], 1):
title = item.get('title', '无标题')
description = item.get('description', '')
source = item.get('source', '')
published = item.get('published', '')
news_section += f"{i}. {title}\n"
if description:
news_section += f" 摘要:{description[:100]}...\n"
if source:
news_section += f" 来源:{source}\n"
if published:
news_section += f" 发布时间:{published}\n"
news_section += "\n"
else:
news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n"
else:
news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n"
# 构建详细的分析提示
prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。
@ -275,7 +307,7 @@ class SmartStockAgent:
数据来源Tushare Pro API
{json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'}
{advanced_summary}
{news_section}
请按以下结构进行分析并在每个部分明确标注数据来源和时效性
## 一、基本面分析
@ -304,12 +336,13 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
**支撑与压力**
关键支撑位和压力位的具体价格区间
## 三、市场情绪分析
## 三、市场情绪和新闻分析
分段分析市场情绪
- 第一段资金流向分析主力资金大单资金流入/流出情况
- 第二段融资融券情况如有
- 第三段当前市场情绪乐观/谨慎/悲观及原因
- 第四段短期可能的催化因素
- 第三段**基于最新新闻分析市场情绪和舆情识别可能影响股价的重要事件**
- 第四段当前市场情绪乐观/谨慎/悲观及原因
- 第五段短期可能的催化因素
## 四、投资建议
基于技术面分析给出具体的操作建议和点位
@ -349,6 +382,7 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
- 资金流向Tushare Pro主力资金大单资金
- 融资融券Tushare Pro如有
- 公告数据Tushare Pro重大公告
- 新闻舆情Brave Search最新市场资讯
写作要求
1. 语言简洁专业避免过度修饰和比喻
@ -1438,7 +1472,7 @@ MA60{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
async def _handle_us_stock(self, keyword: str, message: str) -> Dict[str, Any]:
"""
处理美股查询
处理美股查询使用 skill_planner
Args:
keyword: 股票关键词
@ -1453,30 +1487,75 @@ MA60{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
logger.info(f"处理美股查询: {keyword} -> {symbol}")
try:
# 调用美股分析技能
result = await skill_manager.execute_skill(
"us_stock_analysis",
symbol=symbol,
analysis_type="comprehensive"
# 1. 使用 QuestionAnalyzer 分析问题意图
intent = await self.question_analyzer.analyze_question(
question=message,
context=[],
session_id=""
)
if not result.get("success"):
# 确保 intent 包含股票信息
if 'target' not in intent:
intent['target'] = {}
intent['target']['stock_code'] = symbol
intent['target']['stock_name'] = keyword
intent['target']['market'] = '美股'
logger.info(f"美股问题意图分析: dimensions={intent.get('dimensions')}")
# 2. 使用 SkillPlanner 规划技能(包括 brave_search
plan = self.skill_planner.plan_skills(intent)
# 3. 将 us_stock_analysis 添加到技能列表(如果不存在)
skill_names = [s['name'] for s in plan['skills']]
if 'us_stock_analysis' not in skill_names:
plan['skills'].insert(0, {
'name': 'us_stock_analysis',
'params': {'symbol': symbol, 'analysis_type': 'comprehensive'},
'priority': 1,
'required': True,
'reason': '获取美股基础数据'
})
logger.info(f"美股技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}")
# 4. 执行技能规划
execution_results = await skill_manager.execute_plan(
plan=plan,
stock_code=symbol
)
if execution_results['errors']:
logger.warning(f"美股技能执行有错误: {execution_results['errors']}")
# 5. 检查 us_stock_analysis 是否成功
us_stock_data = execution_results['results'].get('us_stock_analysis')
if not us_stock_data or 'error' in us_stock_data:
return {
"message": f"抱歉,未找到美股 {symbol}。请确认股票代码是否正确。\n\n提示:美股代码通常为大写字母,如 AAPL苹果、TSLA特斯拉、MSFT微软等。",
"metadata": {"type": "error"}
}
# 使用LLM分析美股数据
# 6. 整合数据
all_data = {
"symbol": symbol,
"name": keyword,
**us_stock_data,
"news": execution_results['results'].get("brave_search") # 新增:新闻数据
}
# 7. 使用LLM分析美股数据
if self.use_llm:
analysis = await self._llm_us_stock_analysis(result["data"], message)
analysis = await self._llm_us_stock_analysis(all_data, message)
else:
analysis = self._format_us_stock_data(result["data"])
analysis = self._format_us_stock_data(all_data)
return {
"message": analysis,
"metadata": {
"type": "us_stock_analysis",
"data": result["data"]
"data": all_data,
"plan": plan
}
}
@ -1509,10 +1588,35 @@ MA60{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
week_52_low = data.get("52_week_low", 0)
technical = data.get("technical_indicators", {})
description = data.get("description", "")
news_data = data.get("news", {}) # 新增:获取新闻数据
# 格式化市值
market_cap_str = f"${market_cap / 1e9:.2f}B" if market_cap > 1e9 else f"${market_cap / 1e6:.2f}M"
# 格式化新闻数据
news_section = ""
if news_data and not news_data.get('error'):
results = news_data.get('results', [])
if results:
news_section = "\n【最新新闻和舆情】来源Brave Search\n"
for i, item in enumerate(results[:5], 1):
title = item.get('title', '无标题')
description = item.get('description', '')
source = item.get('source', '')
published = item.get('published', '')
news_section += f"{i}. {title}\n"
if description:
news_section += f" 摘要:{description[:100]}...\n"
if source:
news_section += f" 来源:{source}\n"
if published:
news_section += f" 发布时间:{published}\n"
news_section += "\n"
else:
news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n"
else:
news_section = "\n【最新新闻和舆情】\n暂无相关新闻\n"
# 构建分析提示
prompt = f"""你是一位专业的美股分析师。请基于以下数据对 {name} ({symbol}) 进行全面分析。
@ -1545,7 +1649,7 @@ MA20{f"${technical.get('ma20'):.2f}" if technical.get('ma20') else '计算中
MA60{f"${technical.get('ma60'):.2f}" if technical.get('ma60') else '计算中'}
RSI{f"{technical.get('rsi'):.2f}" if technical.get('rsi') else '计算中'}
MACD{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中'}
{news_section}
用户问题{user_message}
请提供专业的分析报告包括
@ -1563,6 +1667,11 @@ MACD{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中'
- 关键支撑位和压力位
- RSI和MACD信号解读
## 📰 市场情绪和新闻分析
- 基于最新新闻分析市场情绪和舆情
- 识别可能影响股价的重要事件或消息
- 评估新闻对短期和中期走势的影响
## 💡 投资建议
- 短期操作建议1-2
- 中期投资价值1-3个月
@ -1572,16 +1681,17 @@ MACD{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中'
1. 语言专业但易懂避免过度修饰
2. 分析客观理性基于数据和事实
3. 每个部分独立成段段落间用空行分隔
4. 控制在500-600
5. **不要在报告中添加日期标题直接开始分析内容**
6. 最后声明"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。"
4. **充分利用新闻数据进行市场情绪分析**
5. 控制在600-800
6. **不要在报告中添加日期标题直接开始分析内容**
7. 最后声明"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。"
"""
try:
analysis = await self._call_llm_async(
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=2000
max_tokens=2500 # 增加 token 数量以容纳新闻分析
)
if analysis:
@ -1732,7 +1842,7 @@ RSI{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'}
yield chunk
async def _handle_a_stock_stream(self, stock_keyword: str, message: str):
"""流式处理A股分析"""
"""流式处理A股分析(使用 skill_planner"""
# 使用LLM进行智能匹配
stock_info = await self._match_stock_with_llm(stock_keyword)
@ -1744,23 +1854,50 @@ RSI{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'}
stock_name = stock_info['name']
is_index = stock_info['is_index']
# 获取数据(非流式)
try:
quote_result = await skill_manager.execute_skill("market_data", stock_code=stock_code, data_type="quote")
technical_result = await skill_manager.execute_skill("technical_analysis", stock_code=stock_code, indicators=["ma", "macd", "rsi", "kdj"])
fundamental_result = await skill_manager.execute_skill("fundamental", stock_code=stock_code)
advanced_result = await skill_manager.execute_skill("advanced_data", stock_code=stock_code, data_type="all")
logger.info(f"[流式] A股分析: {stock_name}({stock_code})")
try:
# 1. 使用 QuestionAnalyzer 分析问题意图
intent = await self.question_analyzer.analyze_question(
question=message,
context=[],
session_id=""
)
# 确保 intent 包含股票信息
if 'target' not in intent:
intent['target'] = {}
intent['target']['stock_code'] = stock_code
intent['target']['stock_name'] = stock_name
logger.info(f"[流式] A股问题意图分析: dimensions={intent.get('dimensions')}")
# 2. 使用 SkillPlanner 规划技能(包括 brave_search
plan = self.skill_planner.plan_skills(intent)
logger.info(f"[流式] A股技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}")
# 3. 执行技能规划
execution_results = await skill_manager.execute_plan(
plan=plan,
stock_code=stock_code
)
if execution_results['errors']:
logger.warning(f"[流式] A股技能执行有错误: {execution_results['errors']}")
# 4. 整合数据(兼容旧格式)
results = execution_results['results']
all_data = {
"stock_code": stock_code,
"stock_name": stock_name,
"quote": quote_result.get("data") if quote_result.get("success") else None,
"technical": technical_result.get("data") if technical_result.get("success") else None,
"fundamental": fundamental_result.get("data") if fundamental_result.get("success") else None,
"advanced": advanced_result.get("data") if advanced_result.get("success") else None
"quote": results.get("market_data"),
"technical": results.get("technical_analysis"),
"fundamental": results.get("fundamental"),
"advanced": results.get("advanced_data"),
"news": results.get("brave_search") # 新增:新闻数据
}
# 使用LLM流式分析
# 5. 使用LLM流式分析
if self.use_llm:
async for chunk in self._llm_comprehensive_analysis_stream(all_data, message, is_index):
yield chunk
@ -1769,42 +1906,282 @@ RSI{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'}
except Exception as e:
logger.error(f"A股分析失败: {e}")
import traceback
logger.error(traceback.format_exc())
yield f"分析{stock_name}时出错:{str(e)}"
async def _handle_us_stock_stream(self, keyword: str, message: str):
"""流式处理美股分析(智能模式"""
"""流式处理美股分析(使用 skill_planner"""
symbol = self._get_us_stock_symbol(keyword)
logger.info(f"[智能模式-流式] 美股查询: {keyword} -> {symbol}")
try:
result = await skill_manager.execute_skill("us_stock_analysis", symbol=symbol, analysis_type="comprehensive")
# 1. 使用 QuestionAnalyzer 分析问题意图
intent = await self.question_analyzer.analyze_question(
question=message,
context=[],
session_id=""
)
if not result.get("success"):
# 确保 intent 包含股票信息
if 'target' not in intent:
intent['target'] = {}
intent['target']['stock_code'] = symbol
intent['target']['stock_name'] = keyword
intent['target']['market'] = '美股'
logger.info(f"[流式] 美股问题意图分析: dimensions={intent.get('dimensions')}")
# 2. 使用 SkillPlanner 规划技能(包括 brave_search
plan = self.skill_planner.plan_skills(intent)
# 3. 将 us_stock_analysis 添加到技能列表(如果不存在)
skill_names = [s['name'] for s in plan['skills']]
if 'us_stock_analysis' not in skill_names:
plan['skills'].insert(0, {
'name': 'us_stock_analysis',
'params': {'symbol': symbol, 'analysis_type': 'comprehensive'},
'priority': 1,
'required': True,
'reason': '获取美股基础数据'
})
logger.info(f"[流式] 美股技能规划完成: {[s['name'] for s in plan['skills']]}, 策略: {plan['execution_strategy']}")
# 4. 执行技能规划
execution_results = await skill_manager.execute_plan(
plan=plan,
stock_code=symbol
)
if execution_results['errors']:
logger.warning(f"[流式] 美股技能执行有错误: {execution_results['errors']}")
# 5. 检查 us_stock_analysis 是否成功
us_stock_data = execution_results['results'].get('us_stock_analysis')
if not us_stock_data or 'error' in us_stock_data:
yield f"抱歉,未找到美股 {symbol}。请确认股票代码是否正确。"
return
# 使用智能模式的动态prompt生成
# 6. 整合数据
all_data = {
"symbol": symbol,
"name": keyword,
**us_stock_data,
"news": execution_results['results'].get("brave_search") # 新增:新闻数据
}
# 7. 使用智能模式的动态prompt生成
if self.use_llm:
# 构建美股数据的动态prompt
us_data = result["data"]
prompt = self._build_us_stock_dynamic_prompt(us_data, symbol, message)
prompt = self._build_us_stock_dynamic_prompt(all_data, symbol, message)
# 流式生成
stream = llm_service.chat_stream(
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=2000
max_tokens=2500 # 增加 token 数量以容纳新闻分析
)
for chunk in stream:
yield chunk
else:
yield self._format_us_stock_data(result["data"])
yield self._format_us_stock_data(all_data)
except Exception as e:
logger.error(f"美股查询失败: {e}")
import traceback
logger.error(traceback.format_exc())
yield f"查询美股 {symbol} 时出错:{str(e)}"
def _format_news_section(self, news_data: Dict[str, Any]) -> str:
"""
格式化新闻数据为统一格式
Args:
news_data: 新闻数据字典
Returns:
格式化后的新闻文本
"""
if not news_data or news_data.get('error'):
return "\n**最新新闻和舆情**: 暂无相关新闻\n"
results = news_data.get('results', [])
if not results:
return "\n**最新新闻和舆情**: 暂无相关新闻\n"
news_section = "\n**最新新闻和舆情**来源Brave Search:\n"
for i, item in enumerate(results[:5], 1):
title = item.get('title', '无标题')
description = item.get('description', '')
source = item.get('source', '')
published = item.get('published', '')
news_section += f"{i}. {title}\n"
if description:
news_section += f" 摘要:{description[:100]}...\n"
if source:
news_section += f" 来源:{source}\n"
if published:
news_section += f" 发布时间:{published}\n"
news_section += "\n"
return news_section
def _build_unified_analysis_prompt(
self,
market: str,
stock_info: Dict[str, Any],
data: Dict[str, Any],
user_message: str
) -> str:
"""
统一的分析提示词构建函数自然语言风格
Args:
market: 市场类型'A股' '美股'
stock_info: 股票基本信息 {'code': ..., 'name': ...}
data: 市场数据行情技术基本面新闻等
user_message: 用户问题
Returns:
构建好的prompt字符串
"""
from datetime import datetime
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
stock_code = stock_info.get('code', '')
stock_name = stock_info.get('name', '')
# 格式化新闻数据(统一格式)
news_section = self._format_news_section(data.get('news', {}))
if market == '美股':
# 美股数据格式
current_price = data.get('current_price', 0)
change = data.get('change', 0)
change_percent = data.get('change_percent', 0)
volume = data.get('volume', 0)
market_cap = data.get('market_cap', 0)
pe_ratio = data.get('pe_ratio', 0)
pb_ratio = data.get('pb_ratio', 0)
technical = data.get('technical_indicators', {})
ma5 = technical.get('ma5', 0)
ma10 = technical.get('ma10', 0)
ma20 = technical.get('ma20', 0)
rsi = technical.get('rsi', 0)
macd = technical.get('macd', 0)
market_data_section = f"""
**行情数据**:
- 最新价: ${current_price:.2f}
- 涨跌: ${change:+.2f} ({change_percent:+.2f}%)
- 成交量: {volume:,.0f}
- 市值: ${market_cap:,.0f}
- 市盈率(PE): {pe_ratio:.2f}
- 市净率(PB): {pb_ratio:.2f}
**技术指标**:
- 均线: MA5=${ma5:.2f}, MA10=${ma10:.2f}, MA20=${ma20:.2f}
- RSI: {rsi:.2f}
- MACD: {macd:.4f}
"""
analyst_role = "美股分析师"
risk_disclaimer = "以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。"
else: # A股
# A股数据格式
quote = data.get('quote', {})
technical = data.get('technical', {})
fundamental = data.get('fundamental', {})
advanced = data.get('advanced', {})
# 获取交易日期
quote_date = quote.get('trade_date', '未知') if quote else '未知'
# 构建行情数据部分
if quote:
market_data_section = f"""
**行情数据**截止{quote_date}:
- 最新价: ¥{quote.get('close', 0):.2f}
- 涨跌幅: {quote.get('pct_chg', 0):+.2f}%
- 成交量: {quote.get('vol', 0):,.0f}
- 成交额: {quote.get('amount', 0):,.0f}千元
- 换手率: {quote.get('turnover_rate', 0):.2f}%
"""
else:
market_data_section = "\n**行情数据**: 数据获取失败\n"
# 添加技术指标
if technical:
market_data_section += f"""
**技术指标**截止{quote_date}:
- 均线: MA5=¥{technical.get('ma5', 0):.2f}, MA10=¥{technical.get('ma10', 0):.2f}, MA20=¥{technical.get('ma20', 0):.2f}
- RSI: {technical.get('rsi', 0):.2f}
- MACD: {technical.get('macd', 0):.4f}
"""
# 添加基本面数据
if fundamental:
market_data_section += f"""
**基本面数据**:
- 市盈率(PE): {fundamental.get('pe', 0):.2f}
- 市净率(PB): {fundamental.get('pb', 0):.2f}
- 总市值: {fundamental.get('total_mv', 0):,.0f}亿元
- ROE: {fundamental.get('roe', 0):.2f}%
"""
# 添加高级数据(资金流向、融资融券等)
if advanced:
if advanced.get('money_flow'):
money_flow = advanced['money_flow'][0] if advanced['money_flow'] else {}
market_data_section += f"""
**资金流向**:
- 主力净流入: {money_flow.get('net_mf_amount', 0):,.0f}万元
- 大单净流入: {money_flow.get('buy_lg_amount', 0) - money_flow.get('sell_lg_amount', 0):,.0f}万元
"""
if advanced.get('margin'):
margin = advanced['margin'][0] if advanced['margin'] else {}
market_data_section += f"""
**融资融券**:
- 融资余额: {margin.get('rzye', 0):,.0f}
- 融券余额: {margin.get('rqye', 0):,.0f}
"""
analyst_role = "A股分析师"
risk_disclaimer = "以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
# 构建统一的prompt
prompt = f"""你是一位专业的{analyst_role}。请根据以下数据分析【{stock_name}({stock_code})】。
**用户问题**: {user_message}
**重要最新新闻和舆情必须分析**
{news_section}
## 数据信息
{market_data_section}
## 分析要求
请根据用户的问题提供自然有针对性的分析不要使用固定格式而是像专业分析师一样用自然的语言回答用户的问题
**重要必须在分析中包含对上面提供的最新新闻的分析评估新闻对股价的影响**
分析时请注意
- 如果用户关注价格走势重点分析价格和趋势并结合新闻分析市场情绪
- 如果用户关注技术指标重点分析技术面并结合新闻判断短期走势
- 如果用户关注基本面重点分析公司情况和估值并结合新闻评估投资价值
- **无论用户问什么都要在分析中提及最新新闻对股价的潜在影响**
请直接开始分析不要添加日期标题控制在600-800最后声明"{risk_disclaimer}"
"""
return prompt
def _build_us_stock_dynamic_prompt(
self,
data: Dict[str, Any],
@ -1812,7 +2189,7 @@ RSI{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'}
user_message: str
) -> str:
"""
为美股构建动态prompt
为美股构建动态prompt调用统一函数
Args:
data: 美股数据
@ -1822,165 +2199,42 @@ RSI{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'}
Returns:
prompt字符串
"""
# 提取数据
name = data.get('name', symbol)
current_price = data.get('current_price', 0)
change = data.get('change', 0)
change_percent = data.get('change_percent', 0)
volume = data.get('volume', 0)
market_cap = data.get('market_cap', 0)
pe_ratio = data.get('pe_ratio', 0)
stock_info = {
'code': symbol,
'name': data.get('name', symbol)
}
# 技术指标
technical = data.get('technical_indicators', {})
ma5 = technical.get('ma5', 0)
ma10 = technical.get('ma10', 0)
ma20 = technical.get('ma20', 0)
rsi = technical.get('rsi', 0)
macd = technical.get('macd', 0)
prompt = f"""你是一个专业的美股分析师。请根据以下数据分析【{name}({symbol})】。
**用户问题**: {user_message}
## 数据信息
**行情数据**:
- 最新价: ${current_price:.2f}
- 涨跌: ${change:+.2f} ({change_percent:+.2f}%)
- 成交量: {volume:,.0f}
- 市值: ${market_cap:,.0f}
- 市盈率(PE): {pe_ratio:.2f}
**技术指标**:
- 均线: MA5=${ma5:.2f}, MA10=${ma10:.2f}, MA20=${ma20:.2f}
- RSI: {rsi:.2f}
- MACD: {macd:.4f}
## 分析要求
请根据用户的问题提供自然有针对性的分析不要使用固定格式而是像专业分析师一样用自然的语言回答用户的问题
- 如果用户关注价格走势重点分析价格和趋势
- 如果用户关注技术指标重点分析技术面
- 如果用户关注基本面重点分析公司情况和估值
请直接开始分析不要添加日期标题最后声明"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。"
"""
return prompt
return self._build_unified_analysis_prompt(
market='美股',
stock_info=stock_info,
data=data,
user_message=user_message
)
async def _llm_comprehensive_analysis_stream(self, data: Dict[str, Any], user_message: str, is_index: bool = False):
"""使用LLM流式进行综合分析"""
from datetime import datetime
import json
"""使用LLM流式进行综合分析调用统一函数"""
stock_info = {
'code': data.get('stock_code', ''),
'name': data.get('stock_name', '')
}
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
# 使用统一的prompt构建函数
prompt = self._build_unified_analysis_prompt(
market='A股',
stock_info=stock_info,
data=data,
user_message=user_message
)
# 获取行情数据的交易日期
quote_date = "未知"
if data.get('quote') and data['quote'].get('trade_date'):
quote_date = data['quote']['trade_date']
# 构建高级数据摘要
advanced_summary = ""
if data.get('advanced'):
advanced_data = data['advanced']
advanced_summary = "\n【高级财务数据】Tushare Pro 5000+积分)\n"
# 财务指标
if advanced_data.get('financial'):
financial = advanced_data['financial']
if financial.get('indicators'):
indicators = financial['indicators'].get('indicators', {})
advanced_summary += f"财务指标(截止:{financial['indicators'].get('end_date', '未知')}\n"
advanced_summary += f" ROE: {indicators.get('roe', 'N/A')}%\n"
advanced_summary += f" ROA: {indicators.get('roa', 'N/A')}%\n"
advanced_summary += f" 毛利率: {indicators.get('gross_margin', 'N/A')}%\n"
advanced_summary += f" 资产负债率: {indicators.get('debt_to_assets', 'N/A')}%\n"
advanced_summary += f" 流动比率: {indicators.get('current_ratio', 'N/A')}\n\n"
# 估值数据
if advanced_data.get('valuation'):
valuation = advanced_data['valuation']
advanced_summary += f"估值指标:\n"
advanced_summary += f" PE(市盈率): {valuation.get('pe', 'N/A')}\n"
advanced_summary += f" PB(市净率): {valuation.get('pb', 'N/A')}\n"
advanced_summary += f" PS(市销率): {valuation.get('ps', 'N/A')}\n"
advanced_summary += f" 总市值: {valuation.get('total_mv', 'N/A')}万元\n"
advanced_summary += f" 流通市值: {valuation.get('circ_mv', 'N/A')}万元\n"
advanced_summary += f" 换手率: {valuation.get('turnover_rate', 'N/A')}%\n\n"
# 资金流向(最近一天)
if advanced_data.get('money_flow') and len(advanced_data['money_flow']) > 0:
latest_flow = advanced_data['money_flow'][0]
advanced_summary += f"资金流向({latest_flow.get('trade_date', '最近')}\n"
advanced_summary += f" 主力净流入: {latest_flow.get('net_mf_amount', 'N/A')}万元\n"
advanced_summary += f" 超大单净流入: {latest_flow.get('buy_elg_amount', 0) - latest_flow.get('sell_elg_amount', 0):.2f}万元\n"
advanced_summary += f" 大单净流入: {latest_flow.get('buy_lg_amount', 0) - latest_flow.get('sell_lg_amount', 0):.2f}万元\n\n"
# 融资融券
if advanced_data.get('margin') and len(advanced_data['margin']) > 0:
latest_margin = advanced_data['margin'][0]
advanced_summary += f"融资融券({latest_margin.get('trade_date', '最近')}\n"
advanced_summary += f" 融资余额: {latest_margin.get('rzye', 'N/A')}\n"
advanced_summary += f" 融券余额: {latest_margin.get('rqye', 'N/A')}\n\n"
else:
advanced_summary = "\n【高级财务数据】\n暂无高级数据\n"
# 构建详细的分析提示
prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。
用户问题{user_message}
实时行情数据
数据来源Tushare Pro API
交易日期{quote_date}
{json.dumps(data.get('quote'), ensure_ascii=False, indent=2) if data.get('quote') else '数据获取失败'}
技术指标数据
数据来源Tushare Pro API基于历史K线数据计算
计算截止日期{quote_date}
{json.dumps(data.get('technical'), ensure_ascii=False, indent=2) if data.get('technical') else '数据获取失败'}
基本面数据
数据来源Tushare Pro API
{json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'}
{advanced_summary}
请按以下结构进行分析
## 一、基本面分析
分段说明公司情况每个要点独立成段
## 二、技术面分析(数据截止:{quote_date}
使用清晰的分段结构每个技术指标独立成段
## 三、市场情绪分析
分段分析市场情绪和资金流向
## 四、投资建议
基于分析给出具体的操作建议和点位
写作要求
1. 语言简洁专业但易懂
2. 分析客观理性基于数据
3. 每个分析点独立成段
4. 控制在600-800
5. 最后声明"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
"""
# 流式调用LLM同步生成器使用线程避免阻塞
# 流式调用LLM
import asyncio
stream = llm_service.chat_stream(
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=2000
max_tokens=2500
)
# 在线程中迭代同步生成器,避免阻塞事件循环
loop = asyncio.get_event_loop()
for chunk in stream:
# 每次yield后让出控制权
await asyncio.sleep(0)
@ -2078,7 +2332,7 @@ MACD{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中'
stream = llm_service.chat_stream(
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=2000
max_tokens=2500 # 增加 token 数量以容纳新闻分析
)
# 在线程中迭代同步生成器,避免阻塞事件循环

View File

@ -55,9 +55,11 @@ if os.path.exists(frontend_path):
@app.get("/")
async def root():
"""根路径,重定向到登录页"""
from fastapi.responses import RedirectResponse
return RedirectResponse(url="/static/login.html")
"""根路径,返回主应用页面"""
index_path = os.path.join(frontend_path, "index.html")
if os.path.exists(index_path):
return FileResponse(index_path)
return {"message": "页面不存在"}
@app.get("/app")
async def app_page():