update
This commit is contained in:
parent
af799e8ee3
commit
49adf5da6a
@ -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}
|
- 第三段:估值水平分析(PE、PB、PS是否合理)
|
||||||
|
- 第四段:所属行业发展前景
|
||||||
|
- 第五段:如果有公告,简要分析对公司的影响
|
||||||
|
|
||||||
## 二、技术面分析(数据截止:{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 Pro(PE、PB、PS、市值等)
|
||||||
|
- 资金流向: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
95
backend/app/api/llm.py
Normal 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))
|
||||||
@ -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"
|
||||||
|
|||||||
@ -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")
|
||||||
|
|||||||
@ -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 "分析生成失败"
|
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
# 创建全局实例
|
||||||
|
|||||||
210
backend/app/services/multi_llm_service.py
Normal file
210
backend/app/services/multi_llm_service.py
Normal 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()
|
||||||
595
backend/app/services/tushare_advanced_service.py
Normal file
595
backend/app/services/tushare_advanced_service.py
Normal 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]]:
|
||||||
|
"""
|
||||||
|
获取财务指标数据(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()
|
||||||
125
backend/app/skills/advanced_data.py
Normal file
125
backend/app/skills/advanced_data.py
Normal 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: 估值数据(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)
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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('切换失败');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user