From 49adf5da6a622c2e14b4dfa0794da2a16c4f39ff Mon Sep 17 00:00:00 2001 From: aaron <> Date: Tue, 3 Feb 2026 21:16:36 +0800 Subject: [PATCH] update --- backend/app/agent/smart_agent.py | 578 +++++++++++++---- backend/app/api/llm.py | 95 +++ backend/app/config.py | 3 +- backend/app/main.py | 3 +- backend/app/services/llm_service.py | 174 +---- backend/app/services/multi_llm_service.py | 210 +++++++ .../app/services/tushare_advanced_service.py | 595 ++++++++++++++++++ backend/app/skills/advanced_data.py | 125 ++++ backend/requirements.txt | 1 + frontend/css/style.css | 38 ++ frontend/index.html | 24 +- frontend/js/app.js | 49 +- 12 files changed, 1619 insertions(+), 276 deletions(-) create mode 100644 backend/app/api/llm.py create mode 100644 backend/app/services/multi_llm_service.py create mode 100644 backend/app/services/tushare_advanced_service.py create mode 100644 backend/app/skills/advanced_data.py diff --git a/backend/app/agent/smart_agent.py b/backend/app/agent/smart_agent.py index b8edcd5..c5b72ef 100644 --- a/backend/app/agent/smart_agent.py +++ b/backend/app/agent/smart_agent.py @@ -11,7 +11,7 @@ from app.skills.market_data import MarketDataSkill from app.skills.technical_analysis import TechnicalAnalysisSkill from app.skills.fundamental import FundamentalSkill 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.tushare_service import tushare_service 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 if self.use_llm: - logger.info("Smart Agent初始化完成(LLM深度集成模式 + Brave搜索)") + logger.info("Smart Agent初始化完成(LLM深度集成模式 + Tushare Pro高级数据)") else: logger.warning("Smart Agent初始化完成(规则模式,建议配置LLM)") @@ -42,8 +42,8 @@ class SmartStockAgent: skill_manager.register(TechnicalAnalysisSkill()) skill_manager.register(FundamentalSkill()) skill_manager.register(VisualizationSkill()) - skill_manager.register(BraveSearchSkill()) - logger.info("技能注册完成(包含Brave搜索)") + skill_manager.register(AdvancedDataSkill()) + logger.info("技能注册完成(Tushare Pro高级数据)") async def process_message( self, @@ -68,12 +68,24 @@ class SmartStockAgent: self.context_manager.add_message(session_id, "user", message) # 第一步:使用LLM理解问题意图 - intent_analysis = await self._analyze_question_intent(message) + intent_analysis = await self._analyze_question_intent(message, session_id) if not intent_analysis: + # 不直接说"无法理解",而是引导用户 response = { - "message": "抱歉,我无法理解您的问题。请重新描述您的需求。", - "metadata": {"type": "error"} + "message": """我是您的金融智能助手,可以帮您: + +📊 **股票分析** - 分析个股走势、技术指标、基本面 +📈 **市场观察** - 解读大盘走势、行业热点 +📚 **知识问答** - 解答金融投资相关问题 + +您可以这样问我: +• "分析一下贵州茅台" +• "现在A股市场怎么样" +• "什么是MACD指标" + +请告诉我您想了解什么?""", + "metadata": {"type": "guide"} } self.context_manager.add_message(session_id, "assistant", response["message"]) return response @@ -90,10 +102,21 @@ class SmartStockAgent: elif question_type == 'knowledge': # 金融知识问答 response = await self._handle_knowledge_question(intent_analysis, message) + elif question_type == 'general_chat': + # 一般对话,引导用户 + response = await self._handle_general_chat(intent_analysis, message) else: + # 未知类型,智能引导 response = { - "message": "抱歉,我暂时无法处理这类问题。", - "metadata": {"type": "error"} + "message": f"""我理解您想了解:{intent_analysis.get('description', '相关信息')} + +作为金融智能助手,我擅长: +• 📊 分析具体股票(如"分析比亚迪") +• 📈 解读市场走势(如"现在大盘怎么样") +• 📚 解答金融知识(如"什么是市盈率") + +能否更具体地告诉我您想了解什么?""", + "metadata": {"type": "guide"} } # 保存助手响应 @@ -171,14 +194,11 @@ class SmartStockAgent: stock_code=stock_code ) - # 获取最新新闻(Brave搜索) - search_query = f"{display_name} {stock_code} 股票 最新消息" - news_result = await skill_manager.execute_skill( - "brave_search", - query=search_query, - search_type="news", - count=5, - freshness="pw" # 过去一周 + # 获取高级数据(Tushare Pro 5000+积分) + advanced_result = await skill_manager.execute_skill( + "advanced_data", + stock_code=stock_code, + data_type="all" ) # 整合数据 @@ -188,7 +208,7 @@ class SmartStockAgent: "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, - "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进行深度分析 @@ -228,22 +248,59 @@ class SmartStockAgent: if data.get('quote') and data['quote'].get('trade_date'): quote_date = data['quote']['trade_date'] - # 构建新闻摘要 - news_summary = "" - news_source_info = "" - if data.get('news'): - news_summary = "\n【消息面分析】\n" - news_summary += f"数据来源:Brave Search API\n" - news_summary += f"搜索时间:{current_time}\n" - news_summary += f"新闻范围:过去一周内相关新闻\n\n" - for idx, news_item in enumerate(data['news'][: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('description', '无描述')}\n" - news_summary += f" 发布时间: {news_item.get('published', '未知')}\n\n" - news_source_info = "(消息来源:Brave搜索引擎,数据可能存在延迟)" + # 构建高级数据摘要 + 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" + + # 重大公告 + 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: - news_summary = "\n【消息面分析】\n暂无最新新闻数据\n" + advanced_summary = "\n【高级财务数据】\n暂无高级数据\n" # 构建详细的分析提示 prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。 @@ -263,15 +320,17 @@ class SmartStockAgent: 【基本面数据】 数据来源:Tushare Pro API {json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'} -{news_summary} +{advanced_summary} 请按以下结构进行分析,并在每个部分明确标注数据来源和时效性: ## 一、基本面分析 分段说明公司情况,每个要点独立成段: - 第一段:公司主营业务和行业地位 -- 第二段:所属行业发展前景 -- 第三段:如果有新闻,简要分析对公司的影响{news_source_info} +- 第二段:财务健康度分析(基于ROE、资产负债率、流动比率等财务指标) +- 第三段:估值水平分析(PE、PB、PS是否合理) +- 第四段:所属行业发展前景 +- 第五段:如果有公告,简要分析对公司的影响 ## 二、技术面分析(数据截止:{quote_date}) 使用清晰的分段结构,每个技术指标独立成段: @@ -293,9 +352,10 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 ## 三、市场情绪分析 分段分析市场情绪: -- 第一段:当前市场情绪(乐观/谨慎/悲观)及原因 -- 第二段:如果有新闻,分析是利好还是利空 -- 第三段:短期可能的催化因素 +- 第一段:资金流向分析(主力资金、大单资金流入/流出情况) +- 第二段:融资融券情况(如有) +- 第三段:当前市场情绪(乐观/谨慎/悲观)及原因 +- 第四段:短期可能的催化因素 ## 四、投资建议 基于技术面分析,给出具体的操作建议和点位: @@ -330,7 +390,11 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 **数据说明** - 行情数据来源:Tushare Pro(截止{quote_date}) - 技术指标:基于历史K线数据计算(截止{quote_date}) -- 新闻数据:Brave搜索(搜索时间{current_time},范围:过去一周) +- 财务数据:Tushare Pro 5000+积分接口(利润表、资产负债表、财务指标) +- 估值数据:Tushare Pro(PE、PB、PS、市值等) +- 资金流向:Tushare Pro(主力资金、大单资金) +- 融资融券:Tushare Pro(如有) +- 公告数据:Tushare Pro(重大公告) 写作要求: 1. 语言简洁专业,避免过度修饰和比喻 @@ -338,17 +402,17 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 3. **重要:每个分析点必须独立成段,段落之间用空行分隔** 4. **技术面分析部分,每个指标必须使用加粗标题(**标题**)并独立成段** 5. 分析要客观理性,基于数据而非情绪 -3. 分析要客观理性,基于数据而非情绪 -4. 结论要明确,不要模棱两可 -5. 控制在500-600字 -6. 最后必须声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" +6. 充分利用提供的财务数据、估值数据、资金流向等高级数据进行分析 +7. 结论要明确,不要模棱两可 +8. 控制在800-1000字(由于数据更丰富,可以写得更详细) +9. 最后必须声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" """ try: analysis = llm_service.chat( messages=[{"role": "user", "content": prompt}], temperature=0.7, - max_tokens=2000 + max_tokens=3000 # 增加到3000,因为分析更详细了 ) if analysis: @@ -517,6 +581,189 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 logger.error(f"LLM单一分析失败: {e}") 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: """基于规则的分析(LLM不可用时的备选方案)""" parts = [f"【{data['stock_name']}({data['stock_code']}) - 综合分析】\n"] @@ -710,16 +957,17 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 "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: message: 用户消息 + session_id: 会话ID Returns: 意图分析结果: { - 'type': 'stock_specific' | 'macro_finance' | 'knowledge', + 'type': 'stock_specific' | 'macro_finance' | 'knowledge' | 'general_chat', 'description': '问题描述', 'keywords': ['关键词列表'], 'stock_names': ['股票名称'] (如果是stock_specific类型) @@ -729,27 +977,51 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 logger.warning("LLM未配置,无法分析意图") 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** - 针对特定股票的问题 - 例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标" +1. **stock_specific** - 针对特定股票或指数的问题 + 例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标"、"帮我看看这只股票" + **重要**:指数查询也属于此类,例如:"上证指数怎么样"、"分析大盘"、"A股指数走势"、"深证成指" -2. **macro_finance** - 宏观金融问题(不针对特定股票) - 例如:"现在A股市场怎么样"、"最近有什么投资机会"、"如何看待当前经济形势" +2. **macro_finance** - 宏观金融/市场问题(不针对特定股票或指数) + 例如:"最近有什么投资机会"、"现在适合买股票吗"、"市场情绪如何" 3. **knowledge** - 金融知识问答 - 例如:"什么是MACD"、"如何看K线图"、"价值投资是什么" + 例如:"什么是MACD"、"如何看K线图"、"价值投资是什么"、"市盈率怎么算" + +4. **general_chat** - 一般对话/问候/不明确的问题 + 例如:"你好"、"在吗"、"你能做什么"、"帮我"(没有具体说明) + +**重要提示**: +- 如果用户问题不明确,但可能与金融相关,优先归类为 general_chat,以便引导用户 +- 如果用户提到"这只股票"、"它"等代词,查看对话历史判断是否指特定股票 +- **如果用户提到"大盘"、"上证"、"深证"、"A股指数"等,归类为 stock_specific,并在stock_names中填入对应的指数名称** +- 对于模糊的问题,不要强行归类,使用 general_chat 类型 请以JSON格式返回分析结果: {{ "type": "问题类型", - "description": "问题的简要描述", + "description": "问题的简要描述(用一句话概括用户想了解什么)", "keywords": ["关键词1", "关键词2"], - "stock_names": ["股票名称"] (仅当type为stock_specific时) + "stock_names": ["股票名称或指数名称"] (仅当type为stock_specific时,如果有的话) }} 只返回JSON,不要有任何其他内容。""" @@ -797,38 +1069,72 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 intent_analysis: Dict[str, Any], message: str ) -> Dict[str, Any]: - """处理针对特定股票的问题""" + """处理针对特定股票或指数的问题""" stock_names = intent_analysis.get('stock_names', []) if not stock_names: return { - "message": "抱歉,我没有识别到您提到的股票。请提供更明确的股票代码或名称。", + "message": "抱歉,我没有识别到您提到的股票或指数。请提供更明确的股票代码、名称或指数名称。", "metadata": {"type": "error"} } - # 提取第一个股票(暂时只处理单只股票) + # 提取第一个股票或指数 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 { - "message": f"抱歉,未找到股票\"{stock_keyword}\"。请确认股票名称或代码是否正确。", - "metadata": {"type": "error"} - } + # 检查是否是指数查询 + stock_code = None + stock_name = None + is_index = False - stock = search_results[0] - stock_code = stock['symbol'] - stock_name = stock['name'] + for key, code in index_mapping.items(): + if key in stock_keyword or stock_keyword in key: + 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) 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: return await self._single_query(stock_code, stock_name, message) @@ -843,57 +1149,34 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 logger.info(f"处理宏观问题: {description}") - # 使用Brave搜索获取最新信息 - search_query = f"A股市场 {' '.join(keywords)} 最新分析" - try: - news_result = await skill_manager.execute_skill( - "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进行分析 + # 使用LLM进行分析(基于Tushare数据和公开信息) prompt = f"""你是一位专业的金融分析师。用户询问了宏观金融问题。 用户问题:{message} 问题分析:{description} 关键词:{', '.join(keywords)} -{news_summary} -请基于当前市场情况和最新动态,给出专业的分析和建议: +请基于你的金融知识和市场经验,给出专业且易懂的分析: ## 市场现状分析 -- 当前市场整体情况 -- 主要影响因素 +简要说明当前市场整体情况和主要影响因素(分段说明,每个要点独立成段) ## 趋势判断 -- 短期趋势 -- 中长期展望 +- 短期趋势(1-2周) +- 中期展望(1-3个月) ## 投资建议 -- 投资策略建议 -- 风险提示 +- 具体的投资策略建议 +- 需要关注的风险点 写作要求: -1. 语言简洁专业,避免过度修饰 -2. 分析要客观理性,基于事实 -3. 控制在400-500字 -4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" +1. 语言简洁专业但易懂,避免过度修饰 +2. 分析要客观理性,基于事实和数据 +3. 每个分析点独立成段,段落之间用空行分隔 +4. 控制在400-500字 +5. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。" """ analysis = llm_service.chat( @@ -911,9 +1194,22 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 except Exception as e: logger.error(f"宏观问题处理失败: {e}") + # 降级方案:提供友好的引导 return { - "message": "抱歉,暂时无法获取相关信息。请稍后再试。", - "metadata": {"type": "error"} + "message": f"""我理解您想了解:{description} + +目前我可以帮您: +• 📊 分析具体股票的走势和投资价值 +• 📈 查看大盘指数(如上证指数、深证成指) +• 📚 解答金融投资相关问题 + +您可以更具体地问我,比如: +• "现在上证指数怎么样" +• "分析一下创业板的走势" +• "最近哪些行业比较热门" + +请告诉我您想了解什么?""", + "metadata": {"type": "guide"} } async def _handle_knowledge_question( @@ -972,6 +1268,70 @@ DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信 "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() diff --git a/backend/app/api/llm.py b/backend/app/api/llm.py new file mode 100644 index 0000000..fada3a5 --- /dev/null +++ b/backend/app/api/llm.py @@ -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)) diff --git a/backend/app/config.py b/backend/app/config.py index da202e9..4dd5595 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -51,8 +51,9 @@ class Settings(BaseSettings): # Tushare配置 tushare_token: str = "" - # 智谱AI配置 + # LLM配置 zhipuai_api_key: str = "" + deepseek_api_key: str = "" # 数据库配置 database_url: str = "sqlite:///./stock_agent.db" diff --git a/backend/app/main.py b/backend/app/main.py index 4de4af7..cfd9b8f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -7,7 +7,7 @@ from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse from app.config import get_settings from app.utils.logger import logger -from app.api import chat, stock, skills +from app.api import chat, stock, skills, llm import os # 创建FastAPI应用 @@ -31,6 +31,7 @@ app.add_middleware( app.include_router(chat.router, prefix="/api/chat", tags=["对话"]) app.include_router(stock.router, prefix="/api/stock", 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") diff --git a/backend/app/services/llm_service.py b/backend/app/services/llm_service.py index f9993c4..35da9fb 100644 --- a/backend/app/services/llm_service.py +++ b/backend/app/services/llm_service.py @@ -1,63 +1,23 @@ """ -LLM服务 - 智谱AI GLM-4集成 +LLM服务 - 兼容层,使用多模型服务 """ 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 -try: - from zhipuai import ZhipuAI - ZHIPUAI_AVAILABLE = True -except ImportError: - ZHIPUAI_AVAILABLE = False - logger.warning("zhipuai包未安装,LLM功能将不可用") - class LLMService: - """LLM服务类""" + """LLM服务类(兼容层)""" def __init__(self): """初始化LLM服务""" - settings = get_settings() - - 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 + self.multi_service = multi_llm_service + self.client = multi_llm_service.current_model # 兼容性 def chat( self, messages: List[Dict[str, str]], - model: str = "glm-4", + model: str = None, temperature: float = 0.7, max_tokens: int = 2000 ) -> Optional[str]: @@ -65,127 +25,23 @@ class LLMService: 调用LLM进行对话 Args: - messages: 消息列表 [{"role": "user", "content": "..."}] - model: 模型名称 + messages: 消息列表 + model: 模型名称(忽略,使用当前选择的模型) temperature: 温度参数 max_tokens: 最大token数 Returns: LLM响应文本 """ - if not self.client: - logger.error("LLM客户端未初始化") - return None - - 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 + return self.multi_service.chat( + messages=messages, + temperature=temperature, + max_tokens=max_tokens + ) def analyze_intent(self, user_message: str) -> Dict[str, Any]: - """ - 使用LLM分析用户意图 - - 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 "分析生成失败" + """使用LLM分析用户意图""" + return self.multi_service.analyze_intent(user_message) # 创建全局实例 diff --git a/backend/app/services/multi_llm_service.py b/backend/app/services/multi_llm_service.py new file mode 100644 index 0000000..eba4c58 --- /dev/null +++ b/backend/app/services/multi_llm_service.py @@ -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() diff --git a/backend/app/services/tushare_advanced_service.py b/backend/app/services/tushare_advanced_service.py new file mode 100644 index 0000000..1fc29d9 --- /dev/null +++ b/backend/app/services/tushare_advanced_service.py @@ -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]]: + """ + 获取财务指标数据(ROE、ROA、毛利率等) + + 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]]: + """ + 获取每日指标(PE、PB、PS、市值、换手率等) + + 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() diff --git a/backend/app/skills/advanced_data.py b/backend/app/skills/advanced_data.py new file mode 100644 index 0000000..1ad4a02 --- /dev/null +++ b/backend/app/skills/advanced_data.py @@ -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: 估值数据(PE、PB、PS、市值等) + - 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) + } diff --git a/backend/requirements.txt b/backend/requirements.txt index 28e0024..4642b61 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -3,6 +3,7 @@ uvicorn[standard]==0.27.0 langchain==0.1.0 langchain-community==0.0.20 zhipuai==2.0.1 +openai>=1.0.0 tushare==1.3.8 sqlalchemy==2.0.25 pydantic==2.5.3 diff --git a/frontend/css/style.css b/frontend/css/style.css index 14c98c3..b9652cf 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -76,6 +76,44 @@ html, body { 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 { display: flex; align-items: center; diff --git a/frontend/index.html b/frontend/index.html index 5e74776..67617ba 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -26,11 +26,25 @@ - 龙哥的 AI 金融智能体 + AI金融智能体 -