This commit is contained in:
aaron 2026-02-03 21:16:36 +08:00
parent af799e8ee3
commit 49adf5da6a
12 changed files with 1619 additions and 276 deletions

View File

@ -11,7 +11,7 @@ from app.skills.market_data import MarketDataSkill
from app.skills.technical_analysis import TechnicalAnalysisSkill from app.skills.technical_analysis import TechnicalAnalysisSkill
from app.skills.fundamental import FundamentalSkill from app.skills.fundamental import FundamentalSkill
from app.skills.visualization import VisualizationSkill from app.skills.visualization import VisualizationSkill
from app.skills.brave_search import BraveSearchSkill from app.skills.advanced_data import AdvancedDataSkill
from app.services.llm_service import llm_service from app.services.llm_service import llm_service
from app.services.tushare_service import tushare_service from app.services.tushare_service import tushare_service
from app.utils.logger import logger from app.utils.logger import logger
@ -32,7 +32,7 @@ class SmartStockAgent:
self.use_llm = bool(self.settings.zhipuai_api_key) and llm_service.client is not None self.use_llm = bool(self.settings.zhipuai_api_key) and llm_service.client is not None
if self.use_llm: if self.use_llm:
logger.info("Smart Agent初始化完成LLM深度集成模式 + Brave搜索") logger.info("Smart Agent初始化完成LLM深度集成模式 + Tushare Pro高级数据")
else: else:
logger.warning("Smart Agent初始化完成规则模式建议配置LLM") logger.warning("Smart Agent初始化完成规则模式建议配置LLM")
@ -42,8 +42,8 @@ class SmartStockAgent:
skill_manager.register(TechnicalAnalysisSkill()) skill_manager.register(TechnicalAnalysisSkill())
skill_manager.register(FundamentalSkill()) skill_manager.register(FundamentalSkill())
skill_manager.register(VisualizationSkill()) skill_manager.register(VisualizationSkill())
skill_manager.register(BraveSearchSkill()) skill_manager.register(AdvancedDataSkill())
logger.info("技能注册完成(包含Brave搜索") logger.info("技能注册完成(Tushare Pro高级数据")
async def process_message( async def process_message(
self, self,
@ -68,12 +68,24 @@ class SmartStockAgent:
self.context_manager.add_message(session_id, "user", message) self.context_manager.add_message(session_id, "user", message)
# 第一步使用LLM理解问题意图 # 第一步使用LLM理解问题意图
intent_analysis = await self._analyze_question_intent(message) intent_analysis = await self._analyze_question_intent(message, session_id)
if not intent_analysis: if not intent_analysis:
# 不直接说"无法理解",而是引导用户
response = { response = {
"message": "抱歉,我无法理解您的问题。请重新描述您的需求。", "message": """我是您的金融智能助手,可以帮您:
"metadata": {"type": "error"}
📊 **股票分析** - 分析个股走势技术指标基本面
📈 **市场观察** - 解读大盘走势行业热点
📚 **知识问答** - 解答金融投资相关问题
您可以这样问我
"分析一下贵州茅台"
"现在A股市场怎么样"
"什么是MACD指标"
请告诉我您想了解什么""",
"metadata": {"type": "guide"}
} }
self.context_manager.add_message(session_id, "assistant", response["message"]) self.context_manager.add_message(session_id, "assistant", response["message"])
return response return response
@ -90,10 +102,21 @@ class SmartStockAgent:
elif question_type == 'knowledge': elif question_type == 'knowledge':
# 金融知识问答 # 金融知识问答
response = await self._handle_knowledge_question(intent_analysis, message) response = await self._handle_knowledge_question(intent_analysis, message)
elif question_type == 'general_chat':
# 一般对话,引导用户
response = await self._handle_general_chat(intent_analysis, message)
else: else:
# 未知类型,智能引导
response = { response = {
"message": "抱歉,我暂时无法处理这类问题。", "message": f"""我理解您想了解:{intent_analysis.get('description', '相关信息')}
"metadata": {"type": "error"}
作为金融智能助手我擅长
📊 分析具体股票"分析比亚迪"
📈 解读市场走势"现在大盘怎么样"
📚 解答金融知识"什么是市盈率"
能否更具体地告诉我您想了解什么""",
"metadata": {"type": "guide"}
} }
# 保存助手响应 # 保存助手响应
@ -171,14 +194,11 @@ class SmartStockAgent:
stock_code=stock_code stock_code=stock_code
) )
# 获取最新新闻Brave搜索 # 获取高级数据Tushare Pro 5000+积分)
search_query = f"{display_name} {stock_code} 股票 最新消息" advanced_result = await skill_manager.execute_skill(
news_result = await skill_manager.execute_skill( "advanced_data",
"brave_search", stock_code=stock_code,
query=search_query, data_type="all"
search_type="news",
count=5,
freshness="pw" # 过去一周
) )
# 整合数据 # 整合数据
@ -188,7 +208,7 @@ class SmartStockAgent:
"quote": quote_result.get("data") if quote_result.get("success") else None, "quote": quote_result.get("data") if quote_result.get("success") else None,
"technical": technical_result.get("data") if technical_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, "fundamental": fundamental_result.get("data") if fundamental_result.get("success") else None,
"news": news_result.get("results") if news_result and not news_result.get("error") else None "advanced": advanced_result.get("data") if advanced_result.get("success") else None
} }
# 2. 使用LLM进行深度分析 # 2. 使用LLM进行深度分析
@ -228,22 +248,59 @@ class SmartStockAgent:
if data.get('quote') and data['quote'].get('trade_date'): if data.get('quote') and data['quote'].get('trade_date'):
quote_date = data['quote']['trade_date'] quote_date = data['quote']['trade_date']
# 构建新闻摘要 # 构建高级数据摘要
news_summary = "" advanced_summary = ""
news_source_info = "" if data.get('advanced'):
if data.get('news'): advanced_data = data['advanced']
news_summary = "\n【消息面分析】\n" advanced_summary = "\n【高级财务数据】Tushare Pro 5000+积分)\n"
news_summary += f"数据来源Brave Search API\n"
news_summary += f"搜索时间:{current_time}\n" # 财务指标
news_summary += f"新闻范围:过去一周内相关新闻\n\n" if advanced_data.get('financial'):
for idx, news_item in enumerate(data['news'][:5], 1): financial = advanced_data['financial']
news_summary += f"{idx}. {news_item.get('title', '无标题')}\n" if financial.get('indicators'):
news_summary += f" 来源: {news_item.get('source', '未知')}\n" indicators = financial['indicators'].get('indicators', {})
news_summary += f" 摘要: {news_item.get('description', '无描述')}\n" advanced_summary += f"财务指标(截止:{financial['indicators'].get('end_date', '未知')}\n"
news_summary += f" 发布时间: {news_item.get('published', '未知')}\n\n" advanced_summary += f" ROE: {indicators.get('roe', 'N/A')}%\n"
news_source_info = "消息来源Brave搜索引擎数据可能存在延迟" 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"
# 重大公告
if advanced_data.get('announcements') and len(advanced_data['announcements']) > 0:
advanced_summary += "最近公告:\n"
for ann in advanced_data['announcements'][:3]:
advanced_summary += f" · {ann.get('title', '无标题')} ({ann.get('ann_date', '')})\n"
advanced_summary += "\n"
else: else:
news_summary = "\n【消息面分析】\n暂无最新新闻数据\n" advanced_summary = "\n【高级财务数据】\n暂无高级数据\n"
# 构建详细的分析提示 # 构建详细的分析提示
prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。 prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。
@ -263,15 +320,17 @@ class SmartStockAgent:
基本面数据 基本面数据
数据来源Tushare Pro API 数据来源Tushare Pro API
{json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'} {json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'}
{news_summary} {advanced_summary}
请按以下结构进行分析并在每个部分明确标注数据来源和时效性 请按以下结构进行分析并在每个部分明确标注数据来源和时效性
## 一、基本面分析 ## 一、基本面分析
分段说明公司情况每个要点独立成段 分段说明公司情况每个要点独立成段
- 第一段公司主营业务和行业地位 - 第一段公司主营业务和行业地位
- 第二段所属行业发展前景 - 第二段财务健康度分析基于ROE资产负债率流动比率等财务指标
- 第三段如果有新闻简要分析对公司的影响{news_source_info} - 第三段估值水平分析PEPBPS是否合理
- 第四段所属行业发展前景
- 第五段如果有公告简要分析对公司的影响
## 二、技术面分析(数据截止:{quote_date} ## 二、技术面分析(数据截止:{quote_date}
使用清晰的分段结构每个技术指标独立成段 使用清晰的分段结构每个技术指标独立成段
@ -293,9 +352,10 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
## 三、市场情绪分析 ## 三、市场情绪分析
分段分析市场情绪 分段分析市场情绪
- 第一段当前市场情绪乐观/谨慎/悲观及原因 - 第一段资金流向分析主力资金大单资金流入/流出情况
- 第二段如果有新闻分析是利好还是利空 - 第二段融资融券情况如有
- 第三段短期可能的催化因素 - 第三段当前市场情绪乐观/谨慎/悲观及原因
- 第四段短期可能的催化因素
## 四、投资建议 ## 四、投资建议
基于技术面分析给出具体的操作建议和点位 基于技术面分析给出具体的操作建议和点位
@ -330,7 +390,11 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
**数据说明** **数据说明**
- 行情数据来源Tushare Pro截止{quote_date} - 行情数据来源Tushare Pro截止{quote_date}
- 技术指标基于历史K线数据计算截止{quote_date} - 技术指标基于历史K线数据计算截止{quote_date}
- 新闻数据Brave搜索搜索时间{current_time}范围过去一周 - 财务数据Tushare Pro 5000+积分接口利润表资产负债表财务指标
- 估值数据Tushare ProPEPBPS市值等
- 资金流向Tushare Pro主力资金大单资金
- 融资融券Tushare Pro如有
- 公告数据Tushare Pro重大公告
写作要求 写作要求
1. 语言简洁专业避免过度修饰和比喻 1. 语言简洁专业避免过度修饰和比喻
@ -338,17 +402,17 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
3. **重要每个分析点必须独立成段段落之间用空行分隔** 3. **重要每个分析点必须独立成段段落之间用空行分隔**
4. **技术面分析部分每个指标必须使用加粗标题**标题**并独立成段** 4. **技术面分析部分每个指标必须使用加粗标题**标题**并独立成段**
5. 分析要客观理性基于数据而非情绪 5. 分析要客观理性基于数据而非情绪
3. 分析要客观理性基于数据而非情绪 6. 充分利用提供的财务数据估值数据资金流向等高级数据进行分析
4. 结论要明确不要模棱两可 7. 结论要明确不要模棱两可
5. 控制在500-600 8. 控制在800-1000由于数据更丰富可以写得更详细
6. 最后必须声明"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" 9. 最后必须声明"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
""" """
try: try:
analysis = llm_service.chat( analysis = llm_service.chat(
messages=[{"role": "user", "content": prompt}], messages=[{"role": "user", "content": prompt}],
temperature=0.7, temperature=0.7,
max_tokens=2000 max_tokens=3000 # 增加到3000因为分析更详细了
) )
if analysis: if analysis:
@ -517,6 +581,189 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
logger.error(f"LLM单一分析失败: {e}") logger.error(f"LLM单一分析失败: {e}")
return self._format_response(intent, result, stock_code, stock_name) return self._format_response(intent, result, stock_code, stock_name)
async def _comprehensive_index_analysis(
self,
index_code: str,
index_name: str,
message: str
) -> Dict[str, Any]:
"""
指数全面分析使用Tushare获取指数数据 + LLM深度分析
Args:
index_code: 指数代码如000001.SH
index_name: 指数名称
message: 用户消息
Returns:
综合分析结果
"""
logger.info(f"执行指数全面分析: {index_code}")
try:
# 从tushare_advanced_service获取指数数据
from app.services.tushare_advanced_service import tushare_advanced_service
# 获取指数日线数据
index_data = tushare_advanced_service.get_index_daily(
ts_code=index_code,
start_date=None, # 默认180天
end_date=None
)
if not index_data or len(index_data) == 0:
return {
"message": f"抱歉,未能获取{index_name}的数据。",
"metadata": {"type": "error"}
}
# 获取最新数据
latest = index_data[-1]
# 计算技术指标(简单版)
closes = [d['close'] for d in index_data]
# 计算均线
ma5 = sum(closes[-5:]) / 5 if len(closes) >= 5 else None
ma10 = sum(closes[-10:]) / 10 if len(closes) >= 10 else None
ma20 = sum(closes[-20:]) / 20 if len(closes) >= 20 else None
ma60 = sum(closes[-60:]) / 60 if len(closes) >= 60 else None
# 整合数据
all_data = {
"index_code": index_code,
"index_name": index_name,
"latest": latest,
"ma": {
"ma5": ma5,
"ma10": ma10,
"ma20": ma20,
"ma60": ma60
},
"history_days": len(index_data)
}
# 使用LLM进行深度分析
if self.use_llm:
analysis = await self._llm_index_analysis(all_data, message)
else:
analysis = f"{index_name}】最新数据\n\n" \
f"日期:{latest['trade_date']}\n" \
f"收盘:{latest['close']:.2f}\n" \
f"涨跌幅:{latest['pct_chg']:+.2f}%\n" \
f"成交量:{latest['vol']:.0f}"
return {
"message": analysis,
"metadata": {
"type": "index_analysis",
"data": all_data
}
}
except Exception as e:
logger.error(f"指数分析失败: {e}")
import traceback
logger.error(traceback.format_exc())
return {
"message": f"分析{index_name}时出错:{str(e)}",
"metadata": {"type": "error"}
}
async def _llm_index_analysis(
self,
data: Dict[str, Any],
user_message: str
) -> str:
"""使用LLM进行指数深度分析"""
latest = data['latest']
ma = data['ma']
prompt = f"""你是一位专业的股票分析师。请对{data['index_name']}({data['index_code']})进行全面分析,用简洁专业但易懂的语言回答。
用户问题{user_message}
指数最新数据
数据来源Tushare Pro API
交易日期{latest['trade_date']}
收盘点位{latest['close']:.2f}
开盘点位{latest['open']:.2f}
最高点位{latest['high']:.2f}
最低点位{latest['low']:.2f}
涨跌幅{latest['pct_chg']:+.2f}%
涨跌点数{latest['change']:+.2f}
成交量{latest['vol']:.0f}
成交额{latest['amount']:.0f}千元
技术指标
MA5{f"{ma['ma5']:.2f}" if ma['ma5'] else '计算中'}
MA10{f"{ma['ma10']:.2f}" if ma['ma10'] else '计算中'}
MA20{f"{ma['ma20']:.2f}" if ma['ma20'] else '计算中'}
MA60{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
当前点位{latest['close']:.2f}
请按以下结构进行分析
## 一、市场现状
- 当前点位分析相对历史位置
- 当日表现涨跌幅成交量
## 二、技术面分析
**均线系统**
分析当前点位与各均线的关系判断趋势多头/空头/震荡
**支撑与压力**
基于近期走势给出关键支撑位和压力位
## 三、市场情绪
- 当前市场情绪乐观/谨慎/悲观
- 成交量分析
## 四、投资建议
**短期1-2**
- 趋势判断
- 关键点位支撑位压力位
- 操作建议
**中期1-3个月**
- 趋势展望
- 策略建议
**风险提示**
主要风险点和注意事项
## 五、总结
用一句话概括核心观点
---
**数据说明**
- 数据来源Tushare Pro截止{latest['trade_date']}
- 分析周期基于近{data['history_days']}个交易日数据
写作要求
1. 语言简洁专业但易懂
2. 每个分析点独立成段
3. 控制在600-800
4. 最后声明"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
"""
try:
analysis = llm_service.chat(
messages=[{"role": "user", "content": prompt}],
temperature=0.7,
max_tokens=2500
)
if analysis:
return f"{data['index_name']}({data['index_code']}) - AI深度分析】\n\n{analysis}"
else:
return f"{data['index_name']}】分析生成失败"
except Exception as e:
logger.error(f"LLM指数分析失败: {e}")
return f"{data['index_name']}】分析生成失败"
def _rule_based_analysis(self, data: Dict[str, Any]) -> str: def _rule_based_analysis(self, data: Dict[str, Any]) -> str:
"""基于规则的分析LLM不可用时的备选方案""" """基于规则的分析LLM不可用时的备选方案"""
parts = [f"{data['stock_name']}({data['stock_code']}) - 综合分析】\n"] parts = [f"{data['stock_name']}({data['stock_code']}) - 综合分析】\n"]
@ -710,16 +957,17 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
"metadata": {"type": "data", "data": data} "metadata": {"type": "data", "data": data}
} }
async def _analyze_question_intent(self, message: str) -> Optional[Dict[str, Any]]: async def _analyze_question_intent(self, message: str, session_id: str) -> Optional[Dict[str, Any]]:
""" """
使用LLM分析问题意图 使用LLM分析问题意图支持上下文
Args: Args:
message: 用户消息 message: 用户消息
session_id: 会话ID
Returns: Returns:
意图分析结果: { 意图分析结果: {
'type': 'stock_specific' | 'macro_finance' | 'knowledge', 'type': 'stock_specific' | 'macro_finance' | 'knowledge' | 'general_chat',
'description': '问题描述', 'description': '问题描述',
'keywords': ['关键词列表'], 'keywords': ['关键词列表'],
'stock_names': ['股票名称'] (如果是stock_specific类型) 'stock_names': ['股票名称'] (如果是stock_specific类型)
@ -729,27 +977,51 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
logger.warning("LLM未配置无法分析意图") logger.warning("LLM未配置无法分析意图")
return None return None
prompt = f"""分析用户的金融问题,判断问题类型和关键信息。 # 获取历史对话上下文
history = self.context_manager.get_context(session_id)
context_str = ""
if history:
context_str = "\n\n【对话历史】\n"
# 只取最近4条消息
for msg in history[-4:]:
role = "用户" if msg["role"] == "user" else "助手"
content = msg['content'][:100] # 限制长度
context_str += f"{role}: {content}\n"
用户问题{message} prompt = f"""你是一个专业的金融智能助手。分析用户的问题,理解用户意图。
{context_str}
当前问题
用户: {message}
请分析这个问题属于以下哪一类 请分析这个问题属于以下哪一类
1. **stock_specific** - 针对特定股票的问题 1. **stock_specific** - 针对特定股票或指数的问题
例如"贵州茅台怎么样""分析一下比亚迪""600519的技术指标" 例如"贵州茅台怎么样""分析一下比亚迪""600519的技术指标""帮我看看这只股票"
**重要**指数查询也属于此类例如"上证指数怎么样""分析大盘""A股指数走势""深证成指"
2. **macro_finance** - 宏观金融问题不针对特定股票 2. **macro_finance** - 宏观金融/市场问题不针对特定股票或指数
例如"现在A股市场怎么样""最近有什么投资机会""如何看待当前经济形势" 例如"最近有什么投资机会""现在适合买股票吗""市场情绪如何"
3. **knowledge** - 金融知识问答 3. **knowledge** - 金融知识问答
例如"什么是MACD""如何看K线图""价值投资是什么" 例如"什么是MACD""如何看K线图""价值投资是什么""市盈率怎么算"
4. **general_chat** - 一般对话/问候/不明确的问题
例如"你好""在吗""你能做什么""帮我"没有具体说明
**重要提示**
- 如果用户问题不明确但可能与金融相关优先归类为 general_chat以便引导用户
- 如果用户提到"这只股票"""等代词查看对话历史判断是否指特定股票
- **如果用户提到"大盘""上证""深证""A股指数"归类为 stock_specific并在stock_names中填入对应的指数名称**
- 对于模糊的问题不要强行归类使用 general_chat 类型
请以JSON格式返回分析结果 请以JSON格式返回分析结果
{{ {{
"type": "问题类型", "type": "问题类型",
"description": "问题的简要描述", "description": "问题的简要描述(用一句话概括用户想了解什么)",
"keywords": ["关键词1", "关键词2"], "keywords": ["关键词1", "关键词2"],
"stock_names": ["股票名称"] (仅当type为stock_specific时) "stock_names": ["股票名称或指数名称"] (仅当type为stock_specific时如果有的话)
}} }}
只返回JSON不要有任何其他内容""" 只返回JSON不要有任何其他内容"""
@ -797,38 +1069,72 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
intent_analysis: Dict[str, Any], intent_analysis: Dict[str, Any],
message: str message: str
) -> Dict[str, Any]: ) -> Dict[str, Any]:
"""处理针对特定股票的问题""" """处理针对特定股票或指数的问题"""
stock_names = intent_analysis.get('stock_names', []) stock_names = intent_analysis.get('stock_names', [])
if not stock_names: if not stock_names:
return { return {
"message": "抱歉,我没有识别到您提到的股票。请提供更明确的股票代码或名称。", "message": "抱歉,我没有识别到您提到的股票或指数。请提供更明确的股票代码、名称指数名称。",
"metadata": {"type": "error"} "metadata": {"type": "error"}
} }
# 提取第一个股票(暂时只处理单只股票) # 提取第一个股票或指数
stock_keyword = stock_names[0] stock_keyword = stock_names[0]
# 使用Tushare搜索股票 # 指数映射表
search_results = tushare_service.search_stock(stock_keyword) index_mapping = {
"上证指数": "000001.SH",
"上证": "000001.SH",
"大盘": "000001.SH",
"沪指": "000001.SH",
"深证成指": "399001.SZ",
"深证": "399001.SZ",
"深指": "399001.SZ",
"创业板指": "399006.SZ",
"创业板": "399006.SZ",
"科创50": "000688.SH",
"沪深300": "000300.SH",
"中证500": "000905.SH",
"A股": "000001.SH" # 默认用上证指数代表A股
}
if not search_results: # 检查是否是指数查询
return { stock_code = None
"message": f"抱歉,未找到股票\"{stock_keyword}\"。请确认股票名称或代码是否正确。", stock_name = None
"metadata": {"type": "error"} is_index = False
}
stock = search_results[0] for key, code in index_mapping.items():
stock_code = stock['symbol'] if key in stock_keyword or stock_keyword in key:
stock_name = stock['name'] stock_code = code
stock_name = key if key in stock_keyword else stock_keyword
is_index = True
logger.info(f"识别为指数查询: {stock_name} -> {stock_code}")
break
logger.info(f"处理股票问题: {stock_name}({stock_code})") # 如果不是指数使用Tushare搜索股票
if not is_index:
search_results = tushare_service.search_stock(stock_keyword)
if not search_results:
return {
"message": f"抱歉,未找到股票\"{stock_keyword}\"。请确认股票名称或代码是否正确。",
"metadata": {"type": "error"}
}
stock = search_results[0]
stock_code = stock['symbol']
stock_name = stock['name']
logger.info(f"处理{'指数' if is_index else '股票'}问题: {stock_name}({stock_code})")
# 判断是否需要全面分析 # 判断是否需要全面分析
is_comprehensive = self._is_comprehensive_analysis(message) is_comprehensive = self._is_comprehensive_analysis(message)
if is_comprehensive: if is_comprehensive:
return await self._comprehensive_analysis(stock_code, stock_name, message) if is_index:
return await self._comprehensive_index_analysis(stock_code, stock_name, message)
else:
return await self._comprehensive_analysis(stock_code, stock_name, message)
else: else:
return await self._single_query(stock_code, stock_name, message) return await self._single_query(stock_code, stock_name, message)
@ -843,57 +1149,34 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
logger.info(f"处理宏观问题: {description}") logger.info(f"处理宏观问题: {description}")
# 使用Brave搜索获取最新信息
search_query = f"A股市场 {' '.join(keywords)} 最新分析"
try: try:
news_result = await skill_manager.execute_skill( # 使用LLM进行分析基于Tushare数据和公开信息
"brave_search",
query=search_query,
search_type="news",
count=5,
freshness="pw"
)
# 构建新闻摘要
news_summary = ""
if news_result and not news_result.get("error"):
results = news_result.get("results", [])
if results:
news_summary = "\n【最新市场动态】\n"
for idx, news_item in enumerate(results[:5], 1):
news_summary += f"{idx}. {news_item.get('title', '无标题')}\n"
news_summary += f" 来源: {news_item.get('source', '未知')}\n"
news_summary += f" 时间: {news_item.get('published', '未知')}\n\n"
# 使用LLM进行分析
prompt = f"""你是一位专业的金融分析师。用户询问了宏观金融问题。 prompt = f"""你是一位专业的金融分析师。用户询问了宏观金融问题。
用户问题{message} 用户问题{message}
问题分析{description} 问题分析{description}
关键词{', '.join(keywords)} 关键词{', '.join(keywords)}
{news_summary}
请基于当前市场情况和最新动态给出专业的分析和建议 请基于你的金融知识和市场经验给出专业且易懂的分析
## 市场现状分析 ## 市场现状分析
- 当前市场整体情况 简要说明当前市场整体情况和主要影响因素分段说明每个要点独立成段
- 主要影响因素
## 趋势判断 ## 趋势判断
- 短期趋势 - 短期趋势1-2
- 期展望 - 中期展望1-3个月
## 投资建议 ## 投资建议
- 投资策略建议 - 具体的投资策略建议
- 风险提示 - 需要关注的风险点
写作要求 写作要求
1. 语言简洁专业避免过度修饰 1. 语言简洁专业但易懂避免过度修饰
2. 分析要客观理性基于事实 2. 分析要客观理性基于事实和数据
3. 控制在400-500 3. 每个分析点独立成段段落之间用空行分隔
4. 最后声明"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" 4. 控制在400-500
5. 最后声明"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
""" """
analysis = llm_service.chat( analysis = llm_service.chat(
@ -911,9 +1194,22 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
except Exception as e: except Exception as e:
logger.error(f"宏观问题处理失败: {e}") logger.error(f"宏观问题处理失败: {e}")
# 降级方案:提供友好的引导
return { return {
"message": "抱歉,暂时无法获取相关信息。请稍后再试。", "message": f"""我理解您想了解:{description}
"metadata": {"type": "error"}
目前我可以帮您
📊 分析具体股票的走势和投资价值
📈 查看大盘指数如上证指数深证成指
📚 解答金融投资相关问题
您可以更具体地问我比如
"现在上证指数怎么样"
"分析一下创业板的走势"
"最近哪些行业比较热门"
请告诉我您想了解什么""",
"metadata": {"type": "guide"}
} }
async def _handle_knowledge_question( async def _handle_knowledge_question(
@ -972,6 +1268,70 @@ DIF和DEA的位置关系MACD柱状图变化判断动能强弱和买卖信
"metadata": {"type": "error"} "metadata": {"type": "error"}
} }
async def _handle_general_chat(
self,
intent_analysis: Dict[str, Any],
message: str
) -> Dict[str, Any]:
"""处理一般对话,智能引导用户"""
description = intent_analysis.get('description', '')
logger.info(f"处理一般对话: {description}")
# 使用LLM生成友好的引导回复
prompt = f"""你是一位专业且友好的金融智能助手。用户发来了一条消息,但不够具体。
用户消息{message}
问题分析{description}
你的任务是
1. 如果是问候"你好""在吗"友好回应并介绍你的能力
2. 如果问题不明确"帮我""看看"礼貌地询问用户想了解什么
3. 如果可能与金融相关但不具体给出具体的提问示例引导用户
你可以提供的服务
📊 股票分析 - 分析个股走势技术指标基本面
📈 市场观察 - 解读大盘走势行业热点投资机会
📚 知识问答 - 解答金融投资相关问题
回复要求
1. 语气友好专业但不生硬
2. 简洁明了不要太长150字以内
3. 给出2-3个具体的提问示例
4. 不要使用"抱歉""无法理解"等负面词汇
5. 用emoji让回复更友好但不要过度使用
直接返回回复内容不要有其他格式"""
try:
reply = llm_service.chat(
messages=[{"role": "user", "content": prompt}],
temperature=0.8,
max_tokens=400
)
if reply:
return {
"message": reply,
"metadata": {"type": "chat"}
}
except Exception as e:
logger.error(f"一般对话处理失败: {e}")
# 降级方案
return {
"message": """您好!我是您的金融智能助手 👋
我可以帮您
📊 分析具体股票"分析比亚迪"
📈 解读市场走势"现在大盘怎么样"
📚 解答金融知识"什么是市盈率"
请告诉我您想了解什么""",
"metadata": {"type": "chat"}
}
# 创建全局实例 # 创建全局实例
smart_agent = SmartStockAgent() smart_agent = SmartStockAgent()

95
backend/app/api/llm.py Normal file
View File

@ -0,0 +1,95 @@
"""
LLM模型管理API
"""
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Dict, Any, Optional
from app.services.multi_llm_service import multi_llm_service
from app.utils.logger import logger
router = APIRouter(prefix="/api/llm", tags=["llm"])
class ModelSwitchRequest(BaseModel):
"""模型切换请求"""
provider: str # 'zhipu' 或 'deepseek'
@router.get("/models")
async def get_available_models() -> Dict[str, Any]:
"""
获取所有可用的模型列表
Returns:
{
"models": [...],
"current": {...}
}
"""
try:
models = multi_llm_service.get_available_models()
current = multi_llm_service.get_current_model_info()
return {
"success": True,
"models": models,
"current": current
}
except Exception as e:
logger.error(f"获取模型列表失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.post("/switch")
async def switch_model(request: ModelSwitchRequest) -> Dict[str, Any]:
"""
切换当前使用的模型
Args:
request: 包含provider的请求
Returns:
切换结果
"""
try:
success = multi_llm_service.switch_model(request.provider)
if success:
current = multi_llm_service.get_current_model_info()
return {
"success": True,
"message": f"已切换到 {current['name']}",
"current": current
}
else:
raise HTTPException(status_code=400, detail="模型不可用")
except Exception as e:
logger.error(f"切换模型失败: {e}")
raise HTTPException(status_code=500, detail=str(e))
@router.get("/current")
async def get_current_model() -> Dict[str, Any]:
"""
获取当前使用的模型信息
Returns:
当前模型信息
"""
try:
current = multi_llm_service.get_current_model_info()
if current:
return {
"success": True,
"current": current
}
else:
return {
"success": False,
"message": "没有可用的模型"
}
except Exception as e:
logger.error(f"获取当前模型失败: {e}")
raise HTTPException(status_code=500, detail=str(e))

View File

@ -51,8 +51,9 @@ class Settings(BaseSettings):
# Tushare配置 # Tushare配置
tushare_token: str = "" tushare_token: str = ""
# 智谱AI配置 # LLM配置
zhipuai_api_key: str = "" zhipuai_api_key: str = ""
deepseek_api_key: str = ""
# 数据库配置 # 数据库配置
database_url: str = "sqlite:///./stock_agent.db" database_url: str = "sqlite:///./stock_agent.db"

View File

@ -7,7 +7,7 @@ from fastapi.staticfiles import StaticFiles
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from app.config import get_settings from app.config import get_settings
from app.utils.logger import logger from app.utils.logger import logger
from app.api import chat, stock, skills from app.api import chat, stock, skills, llm
import os import os
# 创建FastAPI应用 # 创建FastAPI应用
@ -31,6 +31,7 @@ app.add_middleware(
app.include_router(chat.router, prefix="/api/chat", tags=["对话"]) app.include_router(chat.router, prefix="/api/chat", tags=["对话"])
app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"]) app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"])
app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"]) app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"])
app.include_router(llm.router, tags=["LLM模型"])
# 挂载静态文件 # 挂载静态文件
frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend") frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")

View File

@ -1,63 +1,23 @@
""" """
LLM服务 - 智谱AI GLM-4集成 LLM服务 - 兼容层使用多模型服务
""" """
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any
from app.config import get_settings from app.services.multi_llm_service import multi_llm_service
from app.utils.logger import logger from app.utils.logger import logger
try:
from zhipuai import ZhipuAI
ZHIPUAI_AVAILABLE = True
except ImportError:
ZHIPUAI_AVAILABLE = False
logger.warning("zhipuai包未安装LLM功能将不可用")
class LLMService: class LLMService:
"""LLM服务类""" """LLM服务类兼容层"""
def __init__(self): def __init__(self):
"""初始化LLM服务""" """初始化LLM服务"""
settings = get_settings() self.multi_service = multi_llm_service
self.client = multi_llm_service.current_model # 兼容性
if not ZHIPUAI_AVAILABLE:
logger.warning("智谱AI SDK未安装")
self.client = None
return
if not settings.zhipuai_api_key:
logger.warning("智谱AI API Key未配置")
self.client = None
return
# 验证API Key格式
api_key = settings.zhipuai_api_key.strip()
if not api_key or len(api_key) < 10:
logger.error(f"智谱AI API Key格式无效长度: {len(api_key)}")
self.client = None
return
# 检查API Key是否包含必要的分隔符
if '.' not in api_key:
logger.error("智谱AI API Key格式无效缺少'.'分隔符")
logger.error(f"当前API Key前10个字符: {api_key[:10]}...")
self.client = None
return
try:
logger.info(f"正在初始化智谱AI客户端API Key前10位: {api_key[:10]}...")
self.client = ZhipuAI(api_key=api_key)
logger.info("智谱AI LLM服务初始化成功")
except Exception as e:
logger.error(f"智谱AI初始化失败: {type(e).__name__}: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
self.client = None
def chat( def chat(
self, self,
messages: List[Dict[str, str]], messages: List[Dict[str, str]],
model: str = "glm-4", model: str = None,
temperature: float = 0.7, temperature: float = 0.7,
max_tokens: int = 2000 max_tokens: int = 2000
) -> Optional[str]: ) -> Optional[str]:
@ -65,127 +25,23 @@ class LLMService:
调用LLM进行对话 调用LLM进行对话
Args: Args:
messages: 消息列表 [{"role": "user", "content": "..."}] messages: 消息列表
model: 模型名称 model: 模型名称忽略使用当前选择的模型
temperature: 温度参数 temperature: 温度参数
max_tokens: 最大token数 max_tokens: 最大token数
Returns: Returns:
LLM响应文本 LLM响应文本
""" """
if not self.client: return self.multi_service.chat(
logger.error("LLM客户端未初始化") messages=messages,
return None temperature=temperature,
max_tokens=max_tokens
try: )
logger.info(f"调用LLM: model={model}, messages={len(messages)}")
response = self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
max_tokens=max_tokens
)
if response.choices:
content = response.choices[0].message.content
logger.info(f"LLM响应成功长度: {len(content) if content else 0}")
return content
else:
logger.warning("LLM响应中没有choices")
return None
except Exception as e:
logger.error(f"LLM调用失败: {type(e).__name__}: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
return None
def analyze_intent(self, user_message: str) -> Dict[str, Any]: def analyze_intent(self, user_message: str) -> Dict[str, Any]:
""" """使用LLM分析用户意图"""
使用LLM分析用户意图 return self.multi_service.analyze_intent(user_message)
Args:
user_message: 用户消息
Returns:
意图分析结果
"""
if not self.client:
return {"type": "unknown", "confidence": 0}
prompt = f"""你是一个股票分析助手的意图识别模块。请分析用户的查询意图。
用户消息{user_message}
请识别以下意图类型之一
1. market_data - 查询实时行情价格
2. technical_analysis - 技术分析技术指标
3. fundamental - 基本面信息公司信息
4. visualization - K线图图表
5. unknown - 无法识别
请以JSON格式返回
{{
"type": "意图类型",
"confidence": 0.0-1.0,
"stock_name": "提取的股票名称(如果有)"
}}
"""
try:
response = self.chat([{"role": "user", "content": prompt}], temperature=0.3)
if response:
import json
# 尝试解析JSON
result = json.loads(response)
return result
except Exception as e:
logger.error(f"意图分析失败: {e}")
return {"type": "unknown", "confidence": 0}
def generate_analysis_summary(
self,
stock_code: str,
stock_name: str,
data: Dict[str, Any]
) -> str:
"""
使用LLM生成分析总结
Args:
stock_code: 股票代码
stock_name: 股票名称
data: 分析数据
Returns:
分析总结文本
"""
if not self.client:
return "LLM服务不可用无法生成智能分析"
prompt = f"""你是一个专业的股票分析师。请根据以下数据对{stock_name}({stock_code})进行分析总结。
数据
{data}
请提供
1. 当前状态评估
2. 技术指标解读
3. 投资建议仅供参考
注意
- 使用专业但易懂的语言
- 控制在200字以内
- 必须声明"仅供参考,不构成投资建议"
"""
try:
response = self.chat([{"role": "user", "content": prompt}], temperature=0.7)
return response or "分析生成失败"
except Exception as e:
logger.error(f"分析总结生成失败: {e}")
return "分析生成失败"
# 创建全局实例 # 创建全局实例

View File

@ -0,0 +1,210 @@
"""
多模型LLM服务 - 支持智谱AI和DeepSeek
"""
from typing import Optional, List, Dict, Any
from app.config import get_settings
from app.utils.logger import logger
# 智谱AI
try:
from zhipuai import ZhipuAI
ZHIPUAI_AVAILABLE = True
except ImportError:
ZHIPUAI_AVAILABLE = False
logger.warning("zhipuai包未安装")
# DeepSeek (使用OpenAI兼容接口)
try:
from openai import OpenAI
OPENAI_AVAILABLE = True
except ImportError:
OPENAI_AVAILABLE = False
logger.warning("openai包未安装")
class MultiLLMService:
"""多模型LLM服务类"""
def __init__(self):
"""初始化多模型LLM服务"""
settings = get_settings()
self.clients = {}
self.current_model = None
self.model_info = {}
# 初始化智谱AI
if ZHIPUAI_AVAILABLE and settings.zhipuai_api_key:
try:
api_key = settings.zhipuai_api_key.strip()
if '.' in api_key and len(api_key) > 10:
self.clients['zhipu'] = ZhipuAI(api_key=api_key)
self.model_info['zhipu'] = {
'name': '智谱AI GLM-4',
'model_id': 'glm-4',
'provider': 'zhipu',
'available': True
}
logger.info("智谱AI初始化成功")
except Exception as e:
logger.error(f"智谱AI初始化失败: {e}")
# 初始化DeepSeek
if OPENAI_AVAILABLE and settings.deepseek_api_key:
try:
self.clients['deepseek'] = OpenAI(
api_key=settings.deepseek_api_key,
base_url="https://api.deepseek.com"
)
self.model_info['deepseek'] = {
'name': 'DeepSeek Chat',
'model_id': 'deepseek-chat',
'provider': 'deepseek',
'available': True
}
logger.info("DeepSeek初始化成功")
except Exception as e:
logger.error(f"DeepSeek初始化失败: {e}")
# 设置默认模型优先DeepSeek因为更便宜
if 'deepseek' in self.clients:
self.current_model = 'deepseek'
elif 'zhipu' in self.clients:
self.current_model = 'zhipu'
if self.current_model:
logger.info(f"当前使用模型: {self.model_info[self.current_model]['name']}")
else:
logger.warning("没有可用的LLM模型")
def get_available_models(self) -> List[Dict[str, Any]]:
"""获取所有可用的模型列表"""
return [info for info in self.model_info.values() if info['available']]
def get_current_model_info(self) -> Optional[Dict[str, Any]]:
"""获取当前使用的模型信息"""
if self.current_model:
return self.model_info[self.current_model]
return None
def switch_model(self, provider: str) -> bool:
"""
切换模型
Args:
provider: 模型提供商 ('zhipu' 'deepseek')
Returns:
是否切换成功
"""
if provider in self.clients:
self.current_model = provider
logger.info(f"切换到模型: {self.model_info[provider]['name']}")
return True
else:
logger.error(f"模型不可用: {provider}")
return False
def chat(
self,
messages: List[Dict[str, str]],
temperature: float = 0.7,
max_tokens: int = 2000,
model_override: Optional[str] = None
) -> Optional[str]:
"""
调用LLM进行对话
Args:
messages: 消息列表
temperature: 温度参数
max_tokens: 最大token数
model_override: 临时覆盖使用的模型
Returns:
LLM响应文本
"""
provider = model_override or self.current_model
if not provider or provider not in self.clients:
logger.error("没有可用的LLM客户端")
return None
try:
client = self.clients[provider]
model_id = self.model_info[provider]['model_id']
logger.info(f"调用LLM: provider={provider}, model={model_id}, messages={len(messages)}")
if provider == 'zhipu':
# 智谱AI调用
response = client.chat.completions.create(
model=model_id,
messages=messages,
temperature=temperature,
max_tokens=max_tokens
)
elif provider == 'deepseek':
# DeepSeek调用OpenAI兼容
response = client.chat.completions.create(
model=model_id,
messages=messages,
temperature=temperature,
max_tokens=max_tokens
)
else:
logger.error(f"未知的模型提供商: {provider}")
return None
if response.choices:
content = response.choices[0].message.content
logger.info(f"LLM响应成功长度: {len(content) if content else 0}")
return content
else:
logger.warning("LLM响应中没有choices")
return None
except Exception as e:
logger.error(f"LLM调用失败: {type(e).__name__}: {e}")
import traceback
logger.error(f"详细错误: {traceback.format_exc()}")
return None
def analyze_intent(self, user_message: str) -> Dict[str, Any]:
"""使用LLM分析用户意图"""
if not self.current_model:
return {"type": "unknown", "confidence": 0}
prompt = f"""你是一个股票分析助手的意图识别模块。请分析用户的查询意图。
用户消息{user_message}
请识别以下意图类型之一
1. market_data - 查询实时行情价格
2. technical_analysis - 技术分析技术指标
3. fundamental - 基本面信息公司信息
4. visualization - K线图图表
5. unknown - 无法识别
请以JSON格式返回
{{
"type": "意图类型",
"confidence": 0.0-1.0,
"stock_name": "提取的股票名称(如果有)"
}}
"""
try:
response = self.chat([{"role": "user", "content": prompt}], temperature=0.3)
if response:
import json
result = json.loads(response)
return result
except Exception as e:
logger.error(f"意图分析失败: {e}")
return {"type": "unknown", "confidence": 0}
# 创建全局实例
multi_llm_service = MultiLLMService()

View File

@ -0,0 +1,595 @@
"""
Tushare高级数据服务
充分利用5000+积分获取财务数据资金流向新闻公告等
"""
import tushare as ts
import pandas as pd
from typing import Optional, List, Dict, Any
from datetime import datetime, timedelta
from app.config import get_settings
from app.utils.logger import logger
from app.utils.validators import normalize_stock_code
class TushareAdvancedService:
"""Tushare高级数据服务类需要5000+积分)"""
def __init__(self):
"""初始化Tushare服务"""
settings = get_settings()
if not settings.tushare_token:
logger.warning("Tushare token未配置")
self.pro = None
else:
ts.set_token(settings.tushare_token)
self.pro = ts.pro_api()
logger.info("Tushare高级服务初始化成功")
# ==================== 财务数据 ====================
def get_income_statement(
self,
stock_code: str,
period: str = None,
start_date: str = None,
end_date: str = None
) -> Optional[Dict[str, Any]]:
"""
获取利润表数据
Args:
stock_code: 股票代码
period: 报告期YYYYMMDD如20231231
start_date: 开始日期
end_date: 结束日期
Returns:
利润表数据
"""
if not self.pro:
logger.error("Tushare服务未初始化")
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
# 默认获取最近4个季度的数据
if not period and not start_date:
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=400)).strftime('%Y%m%d')
df = self.pro.income(
ts_code=ts_code,
period=period,
start_date=start_date,
end_date=end_date,
fields='ts_code,ann_date,f_ann_date,end_date,report_type,comp_type,'
'total_revenue,revenue,operating_profit,total_profit,n_income,'
'n_income_attr_p,basic_eps,diluted_eps'
)
if df.empty:
logger.warning(f"未找到利润表数据: {ts_code}")
return None
# 转换为字典列表,按日期降序
df = df.sort_values('end_date', ascending=False)
return {
'ts_code': ts_code,
'data': df.to_dict('records')
}
except Exception as e:
logger.error(f"获取利润表失败: {e}")
return None
def get_balance_sheet(
self,
stock_code: str,
period: str = None,
start_date: str = None,
end_date: str = None
) -> Optional[Dict[str, Any]]:
"""
获取资产负债表数据
Args:
stock_code: 股票代码
period: 报告期
start_date: 开始日期
end_date: 结束日期
Returns:
资产负债表数据
"""
if not self.pro:
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
if not period and not start_date:
end_date = datetime.now().strftime('%Y%m%d')
start_date = (datetime.now() - timedelta(days=400)).strftime('%Y%m%d')
df = self.pro.balancesheet(
ts_code=ts_code,
period=period,
start_date=start_date,
end_date=end_date,
fields='ts_code,ann_date,f_ann_date,end_date,report_type,'
'total_assets,total_liab,total_hldr_eqy_exc_min_int,'
'total_cur_assets,total_cur_liab,money_cap'
)
if df.empty:
return None
df = df.sort_values('end_date', ascending=False)
return {
'ts_code': ts_code,
'data': df.to_dict('records')
}
except Exception as e:
logger.error(f"获取资产负债表失败: {e}")
return None
def get_financial_indicators(
self,
stock_code: str,
period: str = None
) -> Optional[Dict[str, Any]]:
"""
获取财务指标数据ROEROA毛利率等
Args:
stock_code: 股票代码
period: 报告期
Returns:
财务指标数据
"""
if not self.pro:
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
# 获取最近一期的财务指标
df = self.pro.fina_indicator(
ts_code=ts_code,
period=period,
fields='ts_code,end_date,eps,dt_eps,total_revenue_ps,revenue_ps,'
'capital_rese_ps,undist_profit_ps,extra_item,profit_dedt,'
'gross_margin,current_ratio,quick_ratio,roe,roe_waa,'
'roe_dt,roa,npta,roic,debt_to_assets,assets_to_eqt'
)
if df.empty:
return None
# 取最新一期
latest = df.sort_values('end_date', ascending=False).iloc[0]
return {
'ts_code': ts_code,
'end_date': latest['end_date'],
'indicators': latest.to_dict()
}
except Exception as e:
logger.error(f"获取财务指标失败: {e}")
return None
# ==================== 估值数据 ====================
def get_daily_basic(
self,
stock_code: str,
trade_date: str = None
) -> Optional[Dict[str, Any]]:
"""
获取每日指标PEPBPS市值换手率等
Args:
stock_code: 股票代码
trade_date: 交易日期YYYYMMDD
Returns:
每日指标数据
"""
if not self.pro:
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
if not trade_date:
trade_date = datetime.now().strftime('%Y%m%d')
df = self.pro.daily_basic(
ts_code=ts_code,
trade_date=trade_date,
fields='ts_code,trade_date,close,turnover_rate,turnover_rate_f,'
'volume_ratio,pe,pe_ttm,pb,ps,ps_ttm,'
'dv_ratio,dv_ttm,total_share,float_share,free_share,'
'total_mv,circ_mv'
)
if df.empty:
return None
return {
'ts_code': ts_code,
'data': df.iloc[0].to_dict()
}
except Exception as e:
logger.error(f"获取每日指标失败: {e}")
return None
# ==================== 资金流向 ====================
def get_money_flow(
self,
stock_code: str,
start_date: str = None,
end_date: str = None
) -> Optional[Dict[str, Any]]:
"""
获取资金流向数据
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
Returns:
资金流向数据
"""
if not self.pro:
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
if not start_date:
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y%m%d')
if not end_date:
end_date = datetime.now().strftime('%Y%m%d')
df = self.pro.moneyflow(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
fields='ts_code,trade_date,buy_sm_vol,buy_sm_amount,'
'sell_sm_vol,sell_sm_amount,buy_md_vol,buy_md_amount,'
'sell_md_vol,sell_md_amount,buy_lg_vol,buy_lg_amount,'
'sell_lg_vol,sell_lg_amount,buy_elg_vol,buy_elg_amount,'
'sell_elg_vol,sell_elg_amount,net_mf_vol,net_mf_amount'
)
if df.empty:
return None
df = df.sort_values('trade_date', ascending=False)
return {
'ts_code': ts_code,
'data': df.to_dict('records')
}
except Exception as e:
logger.error(f"获取资金流向失败: {e}")
return None
# ==================== 新闻公告 ====================
def get_news(
self,
stock_code: str = None,
start_date: str = None,
end_date: str = None,
src: str = None
) -> Optional[List[Dict[str, Any]]]:
"""
获取新闻资讯
Args:
stock_code: 股票代码可选
start_date: 开始日期
end_date: 结束日期
src: 新闻来源
Returns:
新闻列表
"""
if not self.pro:
return None
try:
ts_code = None
if stock_code:
ts_code = normalize_stock_code(stock_code)
if not start_date:
start_date = (datetime.now() - timedelta(days=7)).strftime('%Y%m%d')
if not end_date:
end_date = datetime.now().strftime('%Y%m%d')
# 使用news接口需要5000积分
df = self.pro.news(
src=src,
start_date=start_date,
end_date=end_date,
fields='datetime,content,title,channels,score'
)
if df.empty:
return None
# 如果指定了股票代码,过滤相关新闻
if ts_code:
# 简单的关键词过滤
stock_info = self.pro.stock_basic(ts_code=ts_code, fields='name,symbol')
if not stock_info.empty:
name = stock_info.iloc[0]['name']
symbol = stock_info.iloc[0]['symbol']
df = df[
df['title'].str.contains(name, na=False) |
df['content'].str.contains(name, na=False) |
df['title'].str.contains(symbol, na=False)
]
df = df.sort_values('datetime', ascending=False)
return df.head(10).to_dict('records')
except Exception as e:
logger.error(f"获取新闻失败: {e}")
return None
def get_major_news(
self,
stock_code: str,
start_date: str = None,
end_date: str = None
) -> Optional[List[Dict[str, Any]]]:
"""
获取重大公告
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
Returns:
公告列表
"""
if not self.pro:
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
if not start_date:
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y%m%d')
if not end_date:
end_date = datetime.now().strftime('%Y%m%d')
# 获取公告数据
df = self.pro.anns(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
fields='ts_code,ann_date,title,ann_type'
)
if df.empty:
return None
df = df.sort_values('ann_date', ascending=False)
return df.head(20).to_dict('records')
except Exception as e:
logger.error(f"获取公告失败: {e}")
return None
# ==================== 市场特色数据 ====================
def get_margin_detail(
self,
stock_code: str,
start_date: str = None,
end_date: str = None
) -> Optional[Dict[str, Any]]:
"""
获取融资融券详情
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
Returns:
融资融券数据
"""
if not self.pro:
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
if not start_date:
start_date = (datetime.now() - timedelta(days=30)).strftime('%Y%m%d')
if not end_date:
end_date = datetime.now().strftime('%Y%m%d')
df = self.pro.margin_detail(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
fields='ts_code,trade_date,rzye,rqye,rzmre,rqyl,'
'rzche,rqchl,rqmcl,rzrqye'
)
if df.empty:
return None
df = df.sort_values('trade_date', ascending=False)
return {
'ts_code': ts_code,
'data': df.to_dict('records')
}
except Exception as e:
logger.error(f"获取融资融券失败: {e}")
return None
def get_block_trade(
self,
stock_code: str,
start_date: str = None,
end_date: str = None
) -> Optional[List[Dict[str, Any]]]:
"""
获取大宗交易数据
Args:
stock_code: 股票代码
start_date: 开始日期
end_date: 结束日期
Returns:
大宗交易列表
"""
if not self.pro:
return None
try:
ts_code = normalize_stock_code(stock_code)
if not ts_code:
return None
if not start_date:
start_date = (datetime.now() - timedelta(days=90)).strftime('%Y%m%d')
if not end_date:
end_date = datetime.now().strftime('%Y%m%d')
df = self.pro.block_trade(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
fields='ts_code,trade_date,price,vol,amount,buyer,seller'
)
if df.empty:
return None
df = df.sort_values('trade_date', ascending=False)
return df.to_dict('records')
except Exception as e:
logger.error(f"获取大宗交易失败: {e}")
return None
def get_top_list(
self,
trade_date: str = None
) -> Optional[Dict[str, Any]]:
"""
获取龙虎榜数据
Args:
trade_date: 交易日期
Returns:
龙虎榜数据
"""
if not self.pro:
return None
try:
if not trade_date:
trade_date = datetime.now().strftime('%Y%m%d')
df = self.pro.top_list(
trade_date=trade_date,
fields='trade_date,ts_code,name,close,pct_change,turnover_rate,'
'amount,l_sell,l_buy,l_amount,net_amount,net_rate,'
'amount_rate,float_values,reason'
)
if df.empty:
return None
return {
'trade_date': trade_date,
'data': df.to_dict('records')
}
except Exception as e:
logger.error(f"获取龙虎榜失败: {e}")
return None
# ==================== 指数数据 ====================
def get_index_daily(
self,
ts_code: str,
start_date: str = None,
end_date: str = None
) -> Optional[List[Dict[str, Any]]]:
"""
获取指数日线行情
Args:
ts_code: 指数代码如000001.SH=上证指数
start_date: 开始日期
end_date: 结束日期
Returns:
指数行情数据
"""
if not self.pro:
return None
try:
if not start_date:
start_date = (datetime.now() - timedelta(days=180)).strftime('%Y%m%d')
if not end_date:
end_date = datetime.now().strftime('%Y%m%d')
df = self.pro.index_daily(
ts_code=ts_code,
start_date=start_date,
end_date=end_date,
fields='ts_code,trade_date,close,open,high,low,pre_close,'
'change,pct_chg,vol,amount'
)
if df.empty:
return None
df = df.sort_values('trade_date')
return df.to_dict('records')
except Exception as e:
logger.error(f"获取指数数据失败: {e}")
return None
# 创建全局实例
tushare_advanced_service = TushareAdvancedService()

View File

@ -0,0 +1,125 @@
"""
高级数据技能
封装Tushare Pro高级数据接口需要5000+积分
"""
from typing import Dict, Any
from app.skills.base import BaseSkill, SkillParameter
from app.services.tushare_advanced_service import tushare_advanced_service
from app.utils.logger import logger
class AdvancedDataSkill(BaseSkill):
"""高级数据技能"""
def __init__(self):
super().__init__()
self.name = "advanced_data"
self.description = "获取高级财务数据、估值数据、资金流向等Tushare Pro 5000+积分)"
self.parameters = [
SkillParameter(
name="stock_code",
type="string",
description="股票代码",
required=True
),
SkillParameter(
name="data_type",
type="string",
description="数据类型financial财务、valuation估值、money_flow资金流向、all全部",
required=False,
default="all"
)
]
async def execute(self, **kwargs) -> Dict[str, Any]:
"""
执行高级数据查询
支持的数据类型:
- financial: 财务数据利润表资产负债表财务指标
- valuation: 估值数据PEPBPS市值等
- money_flow: 资金流向
- margin: 融资融券
- block_trade: 大宗交易
- announcement: 重大公告
"""
stock_code = kwargs.get('stock_code')
data_type = kwargs.get('data_type', 'all') # 默认获取所有数据
if not stock_code:
return {
"success": False,
"error": "缺少股票代码"
}
try:
result = {
"success": True,
"data": {}
}
# 财务数据(利润表、资产负债表、财务指标)
if data_type in ['financial', 'all']:
financial_data = {}
# 获取财务指标(最重要)
indicators = tushare_advanced_service.get_financial_indicators(stock_code)
if indicators:
financial_data['indicators'] = indicators
# 获取利润表(最近一期)
income = tushare_advanced_service.get_income_statement(stock_code)
if income and income.get('data'):
financial_data['income'] = income['data'][0] if income['data'] else None
# 获取资产负债表(最近一期)
balance = tushare_advanced_service.get_balance_sheet(stock_code)
if balance and balance.get('data'):
financial_data['balance'] = balance['data'][0] if balance['data'] else None
if financial_data:
result['data']['financial'] = financial_data
# 估值数据
if data_type in ['valuation', 'all']:
valuation = tushare_advanced_service.get_daily_basic(stock_code)
if valuation:
result['data']['valuation'] = valuation.get('data')
# 资金流向
if data_type in ['money_flow', 'all']:
money_flow = tushare_advanced_service.get_money_flow(stock_code)
if money_flow:
# 只取最近5天的数据
result['data']['money_flow'] = money_flow.get('data', [])[:5]
# 融资融券
if data_type in ['margin', 'all']:
margin = tushare_advanced_service.get_margin_detail(stock_code)
if margin:
# 只取最近5天的数据
result['data']['margin'] = margin.get('data', [])[:5]
# 大宗交易
if data_type in ['block_trade', 'all']:
block_trade = tushare_advanced_service.get_block_trade(stock_code)
if block_trade:
# 只取最近10条
result['data']['block_trade'] = block_trade[:10]
# 重大公告
if data_type in ['announcement', 'all']:
announcements = tushare_advanced_service.get_major_news(stock_code)
if announcements:
# 只取最近10条
result['data']['announcements'] = announcements[:10]
logger.info(f"获取高级数据成功: {stock_code}, 类型: {data_type}")
return result
except Exception as e:
logger.error(f"获取高级数据失败: {e}")
return {
"success": False,
"error": str(e)
}

View File

@ -3,6 +3,7 @@ uvicorn[standard]==0.27.0
langchain==0.1.0 langchain==0.1.0
langchain-community==0.0.20 langchain-community==0.0.20
zhipuai==2.0.1 zhipuai==2.0.1
openai>=1.0.0
tushare==1.3.8 tushare==1.3.8
sqlalchemy==2.0.25 sqlalchemy==2.0.25
pydantic==2.5.3 pydantic==2.5.3

View File

@ -76,6 +76,44 @@ html, body {
color: var(--accent); color: var(--accent);
} }
.header-right {
display: flex;
align-items: center;
gap: 16px;
}
/* Model Selector */
.model-selector {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: var(--bg-secondary);
border: 1px solid var(--border-bright);
border-radius: 2px;
}
.model-selector svg {
color: var(--accent);
flex-shrink: 0;
}
.model-select {
background: transparent;
border: none;
outline: none;
color: var(--text-primary);
font-size: 13px;
font-family: inherit;
cursor: pointer;
padding: 0;
}
.model-select option {
background: var(--bg-secondary);
color: var(--text-primary);
}
.status { .status {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -26,11 +26,25 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none"> <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/> <path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/>
</svg> </svg>
<span>龙哥的 AI 金融智能体</span> <span>AI金融智能体</span>
</div> </div>
<div class="status"> <div class="header-right">
<div class="status-dot"></div> <!-- Model Selector -->
<span>在线</span> <div class="model-selector" v-if="currentModel">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"/>
<path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24"/>
</svg>
<select v-model="selectedModel" @change="switchModel" class="model-select">
<option v-for="model in availableModels" :key="model.provider" :value="model.provider">
{{ model.name }}
</option>
</select>
</div>
<div class="status">
<div class="status-dot"></div>
<span>在线</span>
</div>
</div> </div>
</header> </header>
@ -122,7 +136,7 @@
<!-- Author Info --> <!-- Author Info -->
<div class="author-info"> <div class="author-info">
<span class="author-label">点击联系作者</span> <span class="author-label">联系龙哥</span>
<span class="author-divider">|</span> <span class="author-divider">|</span>
<span class="author-contact" @click="copyWechat" title="点击复制微信号">微信aaronlzhou</span> <span class="author-contact" @click="copyWechat" title="点击复制微信号">微信aaronlzhou</span>
</div> </div>

View File

@ -10,12 +10,16 @@ createApp({
sessionId: null, sessionId: null,
charts: {}, charts: {},
showImageModal: false, showImageModal: false,
modalImageUrl: '' modalImageUrl: '',
availableModels: [],
currentModel: null,
selectedModel: null
}; };
}, },
mounted() { mounted() {
this.sessionId = this.generateSessionId(); this.sessionId = this.generateSessionId();
this.autoResizeTextarea(); this.autoResizeTextarea();
this.loadModels();
}, },
methods: { methods: {
async sendMessage() { async sendMessage() {
@ -359,6 +363,49 @@ createApp({
document.body.removeChild(notification); document.body.removeChild(notification);
} }
}, 2000); }, 2000);
},
async loadModels() {
try {
const response = await fetch('/api/llm/models');
const data = await response.json();
if (data.success) {
this.availableModels = data.models;
this.currentModel = data.current;
this.selectedModel = data.current ? data.current.provider : null;
}
} catch (error) {
console.error('加载模型列表失败:', error);
}
},
async switchModel() {
if (!this.selectedModel) return;
try {
const response = await fetch('/api/llm/switch', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
provider: this.selectedModel
})
});
const data = await response.json();
if (data.success) {
this.currentModel = data.current;
this.showNotification(`已切换到 ${data.current.name}`);
} else {
this.showNotification('切换失败');
}
} catch (error) {
console.error('切换模型失败:', error);
this.showNotification('切换失败');
}
} }
}, },