支持 us stock
This commit is contained in:
parent
49adf5da6a
commit
29b2fad1d6
@ -12,6 +12,7 @@ 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.advanced_data import AdvancedDataSkill
|
from app.skills.advanced_data import AdvancedDataSkill
|
||||||
|
from app.skills.us_stock_skill import USStockSkill
|
||||||
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
|
||||||
@ -43,7 +44,8 @@ class SmartStockAgent:
|
|||||||
skill_manager.register(FundamentalSkill())
|
skill_manager.register(FundamentalSkill())
|
||||||
skill_manager.register(VisualizationSkill())
|
skill_manager.register(VisualizationSkill())
|
||||||
skill_manager.register(AdvancedDataSkill())
|
skill_manager.register(AdvancedDataSkill())
|
||||||
logger.info("技能注册完成(Tushare Pro高级数据)")
|
skill_manager.register(USStockSkill())
|
||||||
|
logger.info("技能注册完成(Tushare Pro高级数据 + 美股支持)")
|
||||||
|
|
||||||
async def process_message(
|
async def process_message(
|
||||||
self,
|
self,
|
||||||
@ -1000,6 +1002,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
|||||||
1. **stock_specific** - 针对特定股票或指数的问题
|
1. **stock_specific** - 针对特定股票或指数的问题
|
||||||
例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标"、"帮我看看这只股票"
|
例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标"、"帮我看看这只股票"
|
||||||
**重要**:指数查询也属于此类,例如:"上证指数怎么样"、"分析大盘"、"A股指数走势"、"深证成指"
|
**重要**:指数查询也属于此类,例如:"上证指数怎么样"、"分析大盘"、"A股指数走势"、"深证成指"
|
||||||
|
**美股支持**:美股查询也属于此类,例如:"苹果股票怎么样"、"AAPL分析"、"特斯拉走势"、"TSLA技术指标"
|
||||||
|
|
||||||
2. **macro_finance** - 宏观金融/市场问题(不针对特定股票或指数)
|
2. **macro_finance** - 宏观金融/市场问题(不针对特定股票或指数)
|
||||||
例如:"最近有什么投资机会"、"现在适合买股票吗"、"市场情绪如何"
|
例如:"最近有什么投资机会"、"现在适合买股票吗"、"市场情绪如何"
|
||||||
@ -1014,6 +1017,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
|||||||
- 如果用户问题不明确,但可能与金融相关,优先归类为 general_chat,以便引导用户
|
- 如果用户问题不明确,但可能与金融相关,优先归类为 general_chat,以便引导用户
|
||||||
- 如果用户提到"这只股票"、"它"等代词,查看对话历史判断是否指特定股票
|
- 如果用户提到"这只股票"、"它"等代词,查看对话历史判断是否指特定股票
|
||||||
- **如果用户提到"大盘"、"上证"、"深证"、"A股指数"等,归类为 stock_specific,并在stock_names中填入对应的指数名称**
|
- **如果用户提到"大盘"、"上证"、"深证"、"A股指数"等,归类为 stock_specific,并在stock_names中填入对应的指数名称**
|
||||||
|
- **如果用户提到美股公司名称(如苹果、特斯拉、微软)或美股代码(如AAPL、TSLA、MSFT),归类为 stock_specific,并在stock_names中填入对应的股票名称或代码**
|
||||||
- 对于模糊的问题,不要强行归类,使用 general_chat 类型
|
- 对于模糊的问题,不要强行归类,使用 general_chat 类型
|
||||||
|
|
||||||
请以JSON格式返回分析结果:
|
请以JSON格式返回分析结果:
|
||||||
@ -1021,7 +1025,8 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
|||||||
"type": "问题类型",
|
"type": "问题类型",
|
||||||
"description": "问题的简要描述(用一句话概括用户想了解什么)",
|
"description": "问题的简要描述(用一句话概括用户想了解什么)",
|
||||||
"keywords": ["关键词1", "关键词2"],
|
"keywords": ["关键词1", "关键词2"],
|
||||||
"stock_names": ["股票名称或指数名称"] (仅当type为stock_specific时,如果有的话)
|
"stock_names": ["股票名称或指数名称或美股代码"] (仅当type为stock_specific时,如果有的话),
|
||||||
|
"market": "A股" 或 "美股" (仅当type为stock_specific时,根据股票类型判断)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
只返回JSON,不要有任何其他内容。"""
|
只返回JSON,不要有任何其他内容。"""
|
||||||
@ -1071,6 +1076,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
|||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""处理针对特定股票或指数的问题"""
|
"""处理针对特定股票或指数的问题"""
|
||||||
stock_names = intent_analysis.get('stock_names', [])
|
stock_names = intent_analysis.get('stock_names', [])
|
||||||
|
market = intent_analysis.get('market', 'A股') # 默认A股
|
||||||
|
|
||||||
if not stock_names:
|
if not stock_names:
|
||||||
return {
|
return {
|
||||||
@ -1081,6 +1087,14 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
|||||||
# 提取第一个股票或指数
|
# 提取第一个股票或指数
|
||||||
stock_keyword = stock_names[0]
|
stock_keyword = stock_names[0]
|
||||||
|
|
||||||
|
# 检测是否为美股
|
||||||
|
is_us_stock = self._is_us_stock(stock_keyword, market)
|
||||||
|
|
||||||
|
if is_us_stock:
|
||||||
|
# 处理美股
|
||||||
|
return await self._handle_us_stock(stock_keyword, message)
|
||||||
|
|
||||||
|
# 处理A股和指数
|
||||||
# 指数映射表
|
# 指数映射表
|
||||||
index_mapping = {
|
index_mapping = {
|
||||||
"上证指数": "000001.SH",
|
"上证指数": "000001.SH",
|
||||||
@ -1332,6 +1346,285 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
|||||||
"metadata": {"type": "chat"}
|
"metadata": {"type": "chat"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def _is_us_stock(self, keyword: str, market: str) -> bool:
|
||||||
|
"""
|
||||||
|
判断是否为美股
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword: 股票关键词
|
||||||
|
market: 市场类型(从LLM意图分析中获取)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否为美股
|
||||||
|
"""
|
||||||
|
# 如果LLM已经判断为美股
|
||||||
|
if market == "美股":
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查是否为全大写字母(美股代码特征)
|
||||||
|
if keyword.isupper() and keyword.isalpha() and len(keyword) <= 5:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 常见美股公司名称映射
|
||||||
|
us_stock_names = {
|
||||||
|
"苹果": "AAPL",
|
||||||
|
"特斯拉": "TSLA",
|
||||||
|
"微软": "MSFT",
|
||||||
|
"谷歌": "GOOGL",
|
||||||
|
"亚马逊": "AMZN",
|
||||||
|
"Meta": "META",
|
||||||
|
"脸书": "META",
|
||||||
|
"英伟达": "NVDA",
|
||||||
|
"奈飞": "NFLX",
|
||||||
|
"网飞": "NFLX",
|
||||||
|
"迪士尼": "DIS",
|
||||||
|
"可口可乐": "KO",
|
||||||
|
"麦当劳": "MCD",
|
||||||
|
"星巴克": "SBUX",
|
||||||
|
"耐克": "NKE",
|
||||||
|
"波音": "BA",
|
||||||
|
"英特尔": "INTC",
|
||||||
|
"AMD": "AMD",
|
||||||
|
"高通": "QCOM",
|
||||||
|
"推特": "TWTR",
|
||||||
|
"优步": "UBER",
|
||||||
|
"Uber": "UBER",
|
||||||
|
"Airbnb": "ABNB",
|
||||||
|
"爱彼迎": "ABNB",
|
||||||
|
}
|
||||||
|
|
||||||
|
if keyword in us_stock_names:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _get_us_stock_symbol(self, keyword: str) -> str:
|
||||||
|
"""
|
||||||
|
获取美股代码
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword: 股票关键词
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
美股代码
|
||||||
|
"""
|
||||||
|
# 如果已经是代码格式,直接返回
|
||||||
|
if keyword.isupper() and keyword.isalpha():
|
||||||
|
return keyword
|
||||||
|
|
||||||
|
# 中文名称映射
|
||||||
|
us_stock_names = {
|
||||||
|
"苹果": "AAPL",
|
||||||
|
"特斯拉": "TSLA",
|
||||||
|
"微软": "MSFT",
|
||||||
|
"谷歌": "GOOGL",
|
||||||
|
"亚马逊": "AMZN",
|
||||||
|
"Meta": "META",
|
||||||
|
"脸书": "META",
|
||||||
|
"英伟达": "NVDA",
|
||||||
|
"奈飞": "NFLX",
|
||||||
|
"网飞": "NFLX",
|
||||||
|
"迪士尼": "DIS",
|
||||||
|
"可口可乐": "KO",
|
||||||
|
"麦当劳": "MCD",
|
||||||
|
"星巴克": "SBUX",
|
||||||
|
"耐克": "NKE",
|
||||||
|
"波音": "BA",
|
||||||
|
"英特尔": "INTC",
|
||||||
|
"AMD": "AMD",
|
||||||
|
"高通": "QCOM",
|
||||||
|
"推特": "TWTR",
|
||||||
|
"优步": "UBER",
|
||||||
|
"Uber": "UBER",
|
||||||
|
"Airbnb": "ABNB",
|
||||||
|
"爱彼迎": "ABNB",
|
||||||
|
}
|
||||||
|
|
||||||
|
return us_stock_names.get(keyword, keyword.upper())
|
||||||
|
|
||||||
|
async def _handle_us_stock(self, keyword: str, message: str) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
处理美股查询
|
||||||
|
|
||||||
|
Args:
|
||||||
|
keyword: 股票关键词
|
||||||
|
message: 用户消息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
分析结果
|
||||||
|
"""
|
||||||
|
# 获取美股代码
|
||||||
|
symbol = self._get_us_stock_symbol(keyword)
|
||||||
|
|
||||||
|
logger.info(f"处理美股查询: {keyword} -> {symbol}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 调用美股分析技能
|
||||||
|
result = await skill_manager.execute_skill(
|
||||||
|
"us_stock_analysis",
|
||||||
|
symbol=symbol,
|
||||||
|
analysis_type="comprehensive"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not result.get("success"):
|
||||||
|
return {
|
||||||
|
"message": f"抱歉,未找到美股 {symbol}。请确认股票代码是否正确。\n\n提示:美股代码通常为大写字母,如 AAPL(苹果)、TSLA(特斯拉)、MSFT(微软)等。",
|
||||||
|
"metadata": {"type": "error"}
|
||||||
|
}
|
||||||
|
|
||||||
|
# 使用LLM分析美股数据
|
||||||
|
if self.use_llm:
|
||||||
|
analysis = await self._llm_us_stock_analysis(result["data"], message)
|
||||||
|
else:
|
||||||
|
analysis = self._format_us_stock_data(result["data"])
|
||||||
|
|
||||||
|
return {
|
||||||
|
"message": analysis,
|
||||||
|
"metadata": {
|
||||||
|
"type": "us_stock_analysis",
|
||||||
|
"data": result["data"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"美股查询失败: {e}")
|
||||||
|
return {
|
||||||
|
"message": f"查询美股 {symbol} 时出错:{str(e)}",
|
||||||
|
"metadata": {"type": "error"}
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _llm_us_stock_analysis(self, data: Dict[str, Any], user_message: str) -> str:
|
||||||
|
"""使用LLM分析美股数据"""
|
||||||
|
from datetime import datetime
|
||||||
|
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
# 提取关键数据
|
||||||
|
symbol = data.get("symbol", "")
|
||||||
|
name = data.get("name", "")
|
||||||
|
sector = data.get("sector", "")
|
||||||
|
industry = data.get("industry", "")
|
||||||
|
current_price = data.get("current_price", 0)
|
||||||
|
change = data.get("change", 0)
|
||||||
|
change_pct = data.get("change_percent", 0)
|
||||||
|
volume = data.get("volume", 0)
|
||||||
|
market_cap = data.get("market_cap", 0)
|
||||||
|
pe_ratio = data.get("pe_ratio", 0)
|
||||||
|
pb_ratio = data.get("pb_ratio", 0)
|
||||||
|
dividend_yield = data.get("dividend_yield", 0)
|
||||||
|
week_52_high = data.get("52_week_high", 0)
|
||||||
|
week_52_low = data.get("52_week_low", 0)
|
||||||
|
technical = data.get("technical_indicators", {})
|
||||||
|
description = data.get("description", "")
|
||||||
|
|
||||||
|
# 格式化市值
|
||||||
|
market_cap_str = f"${market_cap / 1e9:.2f}B" if market_cap > 1e9 else f"${market_cap / 1e6:.2f}M"
|
||||||
|
|
||||||
|
# 构建分析提示
|
||||||
|
prompt = f"""你是一位专业的美股分析师。请基于以下数据对 {name} ({symbol}) 进行全面分析。
|
||||||
|
|
||||||
|
【基本信息】
|
||||||
|
股票代码:{symbol}
|
||||||
|
公司名称:{name}
|
||||||
|
所属行业:{sector} - {industry}
|
||||||
|
公司简介:{description[:300] if description else '暂无'}
|
||||||
|
|
||||||
|
【实时行情】(数据时间:{current_time})
|
||||||
|
当前价格:${current_price:.2f}
|
||||||
|
涨跌额:${change:.2f}
|
||||||
|
涨跌幅:{change_pct:.2f}%
|
||||||
|
成交量:{volume:,}
|
||||||
|
市值:{market_cap_str}
|
||||||
|
|
||||||
|
【估值指标】
|
||||||
|
市盈率(PE):{f"{pe_ratio:.2f}" if pe_ratio else '暂无'}
|
||||||
|
市净率(PB):{f"{pb_ratio:.2f}" if pb_ratio else '暂无'}
|
||||||
|
股息率:{f"{dividend_yield * 100:.2f}%" if dividend_yield else '暂无'}
|
||||||
|
52周最高:${week_52_high:.2f}
|
||||||
|
52周最低:${week_52_low:.2f}
|
||||||
|
|
||||||
|
【技术指标】
|
||||||
|
MA5:{f"${technical.get('ma5'):.2f}" if technical.get('ma5') else '计算中'}
|
||||||
|
MA10:{f"${technical.get('ma10'):.2f}" if technical.get('ma10') else '计算中'}
|
||||||
|
MA20:{f"${technical.get('ma20'):.2f}" if technical.get('ma20') else '计算中'}
|
||||||
|
MA60:{f"${technical.get('ma60'):.2f}" if technical.get('ma60') else '计算中'}
|
||||||
|
RSI:{f"{technical.get('rsi'):.2f}" if technical.get('rsi') else '计算中'}
|
||||||
|
MACD:{f"{technical.get('macd'):.4f}" if technical.get('macd') else '计算中'}
|
||||||
|
|
||||||
|
用户问题:{user_message}
|
||||||
|
|
||||||
|
请提供专业的分析报告,包括:
|
||||||
|
|
||||||
|
## 📊 行情概览
|
||||||
|
简要总结当前股价表现和市场表现(2-3句话)
|
||||||
|
|
||||||
|
## 💼 公司基本面
|
||||||
|
- 行业地位和竞争优势
|
||||||
|
- 估值水平分析(PE、PB是否合理)
|
||||||
|
- 盈利能力和成长性
|
||||||
|
|
||||||
|
## 📈 技术面分析
|
||||||
|
- 当前趋势判断(基于均线系统)
|
||||||
|
- 关键支撑位和压力位
|
||||||
|
- RSI和MACD信号解读
|
||||||
|
|
||||||
|
## 💡 投资建议
|
||||||
|
- 短期操作建议(1-2周)
|
||||||
|
- 中期投资价值(1-3个月)
|
||||||
|
- 风险提示
|
||||||
|
|
||||||
|
写作要求:
|
||||||
|
1. 语言专业但易懂,避免过度修饰
|
||||||
|
2. 分析客观理性,基于数据和事实
|
||||||
|
3. 每个部分独立成段,段落间用空行分隔
|
||||||
|
4. 控制在500-600字
|
||||||
|
5. 最后声明:"以上分析仅供参考,不构成投资建议。美股投资有风险,请谨慎决策。"
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
analysis = llm_service.chat(
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
temperature=0.7,
|
||||||
|
max_tokens=2000
|
||||||
|
)
|
||||||
|
|
||||||
|
if analysis:
|
||||||
|
return f"【美股分析】{name} ({symbol})\n\n{analysis}"
|
||||||
|
else:
|
||||||
|
return self._format_us_stock_data(data)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"LLM美股分析失败: {e}")
|
||||||
|
return self._format_us_stock_data(data)
|
||||||
|
|
||||||
|
def _format_us_stock_data(self, data: Dict[str, Any]) -> str:
|
||||||
|
"""格式化美股数据(降级方案)"""
|
||||||
|
symbol = data.get("symbol", "")
|
||||||
|
name = data.get("name", "")
|
||||||
|
current_price = data.get("current_price", 0)
|
||||||
|
change = data.get("change", 0)
|
||||||
|
change_pct = data.get("change_percent", 0)
|
||||||
|
market_cap = data.get("market_cap", 0)
|
||||||
|
pe_ratio = data.get("pe_ratio", 0)
|
||||||
|
technical = data.get("technical_indicators", {})
|
||||||
|
|
||||||
|
market_cap_str = f"${market_cap / 1e9:.2f}B" if market_cap > 1e9 else f"${market_cap / 1e6:.2f}M"
|
||||||
|
|
||||||
|
change_emoji = "📈" if change >= 0 else "📉"
|
||||||
|
|
||||||
|
return f"""【美股行情】{name} ({symbol})
|
||||||
|
|
||||||
|
{change_emoji} 当前价格:${current_price:.2f}
|
||||||
|
涨跌:${change:.2f} ({change_pct:+.2f}%)
|
||||||
|
市值:{market_cap_str}
|
||||||
|
市盈率:{pe_ratio:.2f if pe_ratio else '暂无'}
|
||||||
|
|
||||||
|
【技术指标】
|
||||||
|
MA5:${technical.get('ma5', 0):.2f if technical.get('ma5') else '计算中'}
|
||||||
|
MA20:${technical.get('ma20', 0):.2f if technical.get('ma20') else '计算中'}
|
||||||
|
RSI:{technical.get('rsi', 0):.2f if technical.get('rsi') else '计算中'}
|
||||||
|
|
||||||
|
以上数据仅供参考,不构成投资建议。"""
|
||||||
|
|
||||||
|
|
||||||
# 创建全局实例
|
# 创建全局实例
|
||||||
smart_agent = SmartStockAgent()
|
smart_agent = SmartStockAgent()
|
||||||
|
|||||||
282
backend/app/services/us_stock_service.py
Normal file
282
backend/app/services/us_stock_service.py
Normal file
@ -0,0 +1,282 @@
|
|||||||
|
"""
|
||||||
|
美股数据服务 - 使用 yfinance 获取美股数据
|
||||||
|
"""
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
import yfinance as yf
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import pandas as pd
|
||||||
|
from app.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class USStockService:
|
||||||
|
"""美股数据服务类"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
"""初始化美股数据服务"""
|
||||||
|
self.cache = {} # 简单的内存缓存
|
||||||
|
|
||||||
|
def get_stock_info(self, symbol: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取美股基本信息
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 股票代码(如 AAPL, TSLA)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
股票基本信息字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stock = yf.Ticker(symbol)
|
||||||
|
info = stock.info
|
||||||
|
|
||||||
|
if not info or 'symbol' not in info:
|
||||||
|
logger.warning(f"未找到股票: {symbol}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 提取关键信息
|
||||||
|
result = {
|
||||||
|
"symbol": symbol,
|
||||||
|
"name": info.get("longName", info.get("shortName", symbol)),
|
||||||
|
"sector": info.get("sector", "未知"),
|
||||||
|
"industry": info.get("industry", "未知"),
|
||||||
|
"market_cap": info.get("marketCap", 0),
|
||||||
|
"current_price": info.get("currentPrice", info.get("regularMarketPrice", 0)),
|
||||||
|
"previous_close": info.get("previousClose", 0),
|
||||||
|
"open": info.get("open", 0),
|
||||||
|
"day_high": info.get("dayHigh", 0),
|
||||||
|
"day_low": info.get("dayLow", 0),
|
||||||
|
"volume": info.get("volume", 0),
|
||||||
|
"avg_volume": info.get("averageVolume", 0),
|
||||||
|
"pe_ratio": info.get("trailingPE", 0),
|
||||||
|
"forward_pe": info.get("forwardPE", 0),
|
||||||
|
"pb_ratio": info.get("priceToBook", 0),
|
||||||
|
"dividend_yield": info.get("dividendYield", 0),
|
||||||
|
"52_week_high": info.get("fiftyTwoWeekHigh", 0),
|
||||||
|
"52_week_low": info.get("fiftyTwoWeekLow", 0),
|
||||||
|
"50_day_avg": info.get("fiftyDayAverage", 0),
|
||||||
|
"200_day_avg": info.get("twoHundredDayAverage", 0),
|
||||||
|
"beta": info.get("beta", 0),
|
||||||
|
"eps": info.get("trailingEps", 0),
|
||||||
|
"description": info.get("longBusinessSummary", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"获取美股信息成功: {symbol}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取美股信息失败 {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_historical_data(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
period: str = "1mo",
|
||||||
|
interval: str = "1d"
|
||||||
|
) -> Optional[pd.DataFrame]:
|
||||||
|
"""
|
||||||
|
获取美股历史K线数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 股票代码
|
||||||
|
period: 时间周期 (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
|
||||||
|
interval: K线间隔 (1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
包含OHLCV数据的DataFrame
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stock = yf.Ticker(symbol)
|
||||||
|
hist = stock.history(period=period, interval=interval)
|
||||||
|
|
||||||
|
if hist.empty:
|
||||||
|
logger.warning(f"未找到历史数据: {symbol}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.info(f"获取美股历史数据成功: {symbol}, 周期: {period}")
|
||||||
|
return hist
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取美股历史数据失败 {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_financial_data(self, symbol: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取美股财务数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 股票代码
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
财务数据字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
stock = yf.Ticker(symbol)
|
||||||
|
|
||||||
|
# 获取财务报表
|
||||||
|
financials = stock.financials
|
||||||
|
balance_sheet = stock.balance_sheet
|
||||||
|
cashflow = stock.cashflow
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"symbol": symbol,
|
||||||
|
"income_statement": financials.to_dict() if not financials.empty else {},
|
||||||
|
"balance_sheet": balance_sheet.to_dict() if not balance_sheet.empty else {},
|
||||||
|
"cash_flow": cashflow.to_dict() if not cashflow.empty else {},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 获取关键财务指标
|
||||||
|
info = stock.info
|
||||||
|
result["key_metrics"] = {
|
||||||
|
"revenue": info.get("totalRevenue", 0),
|
||||||
|
"gross_profit": info.get("grossProfits", 0),
|
||||||
|
"ebitda": info.get("ebitda", 0),
|
||||||
|
"net_income": info.get("netIncomeToCommon", 0),
|
||||||
|
"total_assets": info.get("totalAssets", 0),
|
||||||
|
"total_debt": info.get("totalDebt", 0),
|
||||||
|
"total_cash": info.get("totalCash", 0),
|
||||||
|
"operating_cash_flow": info.get("operatingCashflow", 0),
|
||||||
|
"free_cash_flow": info.get("freeCashflow", 0),
|
||||||
|
"roe": info.get("returnOnEquity", 0),
|
||||||
|
"roa": info.get("returnOnAssets", 0),
|
||||||
|
"profit_margin": info.get("profitMargins", 0),
|
||||||
|
"operating_margin": info.get("operatingMargins", 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"获取美股财务数据成功: {symbol}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取美股财务数据失败 {symbol}: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def calculate_technical_indicators(self, hist: pd.DataFrame) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
计算技术指标
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hist: 历史数据DataFrame
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
技术指标字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if hist.empty or len(hist) < 20:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
close = hist['Close']
|
||||||
|
|
||||||
|
# 计算移动平均线
|
||||||
|
ma5 = close.rolling(window=5).mean().iloc[-1] if len(close) >= 5 else None
|
||||||
|
ma10 = close.rolling(window=10).mean().iloc[-1] if len(close) >= 10 else None
|
||||||
|
ma20 = close.rolling(window=20).mean().iloc[-1] if len(close) >= 20 else None
|
||||||
|
ma60 = close.rolling(window=60).mean().iloc[-1] if len(close) >= 60 else None
|
||||||
|
|
||||||
|
# 计算RSI
|
||||||
|
delta = close.diff()
|
||||||
|
gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
|
||||||
|
loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
|
||||||
|
rs = gain / loss
|
||||||
|
rsi = 100 - (100 / (1 + rs))
|
||||||
|
rsi_value = rsi.iloc[-1] if len(rsi) >= 14 else None
|
||||||
|
|
||||||
|
# 计算MACD
|
||||||
|
exp1 = close.ewm(span=12, adjust=False).mean()
|
||||||
|
exp2 = close.ewm(span=26, adjust=False).mean()
|
||||||
|
macd = exp1 - exp2
|
||||||
|
signal = macd.ewm(span=9, adjust=False).mean()
|
||||||
|
macd_value = macd.iloc[-1] if len(macd) >= 26 else None
|
||||||
|
signal_value = signal.iloc[-1] if len(signal) >= 26 else None
|
||||||
|
|
||||||
|
# 计算布林带
|
||||||
|
bb_middle = close.rolling(window=20).mean()
|
||||||
|
bb_std = close.rolling(window=20).std()
|
||||||
|
bb_upper = bb_middle + (bb_std * 2)
|
||||||
|
bb_lower = bb_middle - (bb_std * 2)
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"ma5": float(ma5) if ma5 and not pd.isna(ma5) else None,
|
||||||
|
"ma10": float(ma10) if ma10 and not pd.isna(ma10) else None,
|
||||||
|
"ma20": float(ma20) if ma20 and not pd.isna(ma20) else None,
|
||||||
|
"ma60": float(ma60) if ma60 and not pd.isna(ma60) else None,
|
||||||
|
"rsi": float(rsi_value) if rsi_value and not pd.isna(rsi_value) else None,
|
||||||
|
"macd": float(macd_value) if macd_value and not pd.isna(macd_value) else None,
|
||||||
|
"macd_signal": float(signal_value) if signal_value and not pd.isna(signal_value) else None,
|
||||||
|
"bb_upper": float(bb_upper.iloc[-1]) if len(bb_upper) >= 20 and not pd.isna(bb_upper.iloc[-1]) else None,
|
||||||
|
"bb_middle": float(bb_middle.iloc[-1]) if len(bb_middle) >= 20 and not pd.isna(bb_middle.iloc[-1]) else None,
|
||||||
|
"bb_lower": float(bb_lower.iloc[-1]) if len(bb_lower) >= 20 and not pd.isna(bb_lower.iloc[-1]) else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"计算技术指标失败: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_comprehensive_analysis(self, symbol: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
获取美股综合分析数据
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 股票代码
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
综合分析数据字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# 获取基本信息
|
||||||
|
info = self.get_stock_info(symbol)
|
||||||
|
if not info:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 获取历史数据
|
||||||
|
hist = self.get_historical_data(symbol, period="6mo", interval="1d")
|
||||||
|
if hist is None or hist.empty:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "无法获取历史数据"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 计算技术指标
|
||||||
|
technical = self.calculate_technical_indicators(hist)
|
||||||
|
|
||||||
|
# 获取最近的价格数据
|
||||||
|
latest = hist.iloc[-1]
|
||||||
|
prev = hist.iloc[-2] if len(hist) > 1 else latest
|
||||||
|
|
||||||
|
# 计算涨跌幅
|
||||||
|
change = latest['Close'] - prev['Close']
|
||||||
|
change_pct = (change / prev['Close'] * 100) if prev['Close'] != 0 else 0
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"success": True,
|
||||||
|
"symbol": symbol,
|
||||||
|
"name": info["name"],
|
||||||
|
"sector": info["sector"],
|
||||||
|
"industry": info["industry"],
|
||||||
|
"current_price": float(latest['Close']),
|
||||||
|
"change": float(change),
|
||||||
|
"change_percent": float(change_pct),
|
||||||
|
"volume": int(latest['Volume']),
|
||||||
|
"market_cap": info["market_cap"],
|
||||||
|
"pe_ratio": info["pe_ratio"],
|
||||||
|
"pb_ratio": info["pb_ratio"],
|
||||||
|
"dividend_yield": info["dividend_yield"],
|
||||||
|
"52_week_high": info["52_week_high"],
|
||||||
|
"52_week_low": info["52_week_low"],
|
||||||
|
"technical_indicators": technical,
|
||||||
|
"description": info["description"][:500] if info["description"] else "",
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"获取美股综合分析成功: {symbol}")
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取美股综合分析失败 {symbol}: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 创建全局实例
|
||||||
|
us_stock_service = USStockService()
|
||||||
118
backend/app/skills/us_stock_skill.py
Normal file
118
backend/app/skills/us_stock_skill.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
"""
|
||||||
|
美股分析技能
|
||||||
|
"""
|
||||||
|
from typing import Dict, Any
|
||||||
|
from app.skills.base import BaseSkill, SkillParameter
|
||||||
|
from app.services.us_stock_service import us_stock_service
|
||||||
|
from app.utils.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class USStockSkill(BaseSkill):
|
||||||
|
"""美股分析技能"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.name = "us_stock_analysis"
|
||||||
|
self.description = "分析美股(如 AAPL, TSLA, MSFT 等),获取实时行情、技术指标、基本面数据"
|
||||||
|
self.parameters = [
|
||||||
|
SkillParameter(
|
||||||
|
name="symbol",
|
||||||
|
type="string",
|
||||||
|
description="美股代码(如 AAPL, TSLA, MSFT)",
|
||||||
|
required=True
|
||||||
|
),
|
||||||
|
SkillParameter(
|
||||||
|
name="analysis_type",
|
||||||
|
type="string",
|
||||||
|
description="分析类型:basic(基本信息)、technical(技术分析)、fundamental(基本面)、comprehensive(综合分析)",
|
||||||
|
required=False,
|
||||||
|
default="comprehensive"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
执行美股分析
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 美股代码
|
||||||
|
analysis_type: 分析类型
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
分析结果字典
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
symbol = kwargs.get("symbol", "").upper()
|
||||||
|
analysis_type = kwargs.get("analysis_type", "comprehensive")
|
||||||
|
|
||||||
|
if not symbol:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "请提供美股代码"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(f"开始分析美股: {symbol}, 类型: {analysis_type}")
|
||||||
|
|
||||||
|
if analysis_type == "basic":
|
||||||
|
# 基本信息
|
||||||
|
info = us_stock_service.get_stock_info(symbol)
|
||||||
|
if not info:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"未找到股票 {symbol}"
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": info
|
||||||
|
}
|
||||||
|
|
||||||
|
elif analysis_type == "technical":
|
||||||
|
# 技术分析
|
||||||
|
hist = us_stock_service.get_historical_data(symbol, period="6mo")
|
||||||
|
if hist is None or hist.empty:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "无法获取历史数据"
|
||||||
|
}
|
||||||
|
|
||||||
|
technical = us_stock_service.calculate_technical_indicators(hist)
|
||||||
|
latest = hist.iloc[-1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": {
|
||||||
|
"symbol": symbol,
|
||||||
|
"current_price": float(latest['Close']),
|
||||||
|
"volume": int(latest['Volume']),
|
||||||
|
"technical_indicators": technical
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
elif analysis_type == "fundamental":
|
||||||
|
# 基本面分析
|
||||||
|
financial = us_stock_service.get_financial_data(symbol)
|
||||||
|
if not financial:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": "无法获取财务数据"
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"data": financial
|
||||||
|
}
|
||||||
|
|
||||||
|
else:
|
||||||
|
# 综合分析(默认)
|
||||||
|
result = us_stock_service.get_comprehensive_analysis(symbol)
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"美股分析失败: {e}")
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": str(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 创建全局实例
|
||||||
|
us_stock_skill = USStockSkill()
|
||||||
@ -10,8 +10,9 @@ pydantic==2.5.3
|
|||||||
pydantic-settings==2.1.0
|
pydantic-settings==2.1.0
|
||||||
python-dotenv==1.0.0
|
python-dotenv==1.0.0
|
||||||
slowapi==0.1.9
|
slowapi==0.1.9
|
||||||
websockets==12.0
|
websockets>=13.0
|
||||||
pandas>=2.2.0
|
pandas>=2.2.0
|
||||||
numpy>=1.26.0
|
numpy>=1.26.0
|
||||||
python-multipart==0.0.6
|
python-multipart==0.0.6
|
||||||
aiohttp==3.9.1
|
aiohttp==3.9.1
|
||||||
|
yfinance>=0.2.36
|
||||||
|
|||||||
@ -153,11 +153,11 @@ html, body {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 40px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome-icon {
|
.welcome-icon {
|
||||||
margin-bottom: 32px;
|
margin-bottom: 20px;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -169,14 +169,56 @@ html, body {
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
margin-bottom: 12px;
|
margin-bottom: 8px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.welcome p {
|
.welcome-subtitle {
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 0.5px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.guide-section {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 700px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-queries {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-btn {
|
||||||
|
padding: 10px 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 6px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.example-btn:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
border-color: var(--accent);
|
||||||
|
color: var(--accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-footer p {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
letter-spacing: 0.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Messages */
|
/* Messages */
|
||||||
|
|||||||
@ -53,12 +53,27 @@
|
|||||||
<!-- Welcome Screen -->
|
<!-- Welcome Screen -->
|
||||||
<div v-if="messages.length === 0" class="welcome">
|
<div v-if="messages.length === 0" class="welcome">
|
||||||
<div class="welcome-icon">
|
<div class="welcome-icon">
|
||||||
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
<svg width="60" height="60" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
||||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<h1>开始对话</h1>
|
<h1>AI 金融智能体</h1>
|
||||||
<p>输入股票代码或名称,获取实时分析</p>
|
<p class="welcome-subtitle">支持 A股 + 美股双市场分析</p>
|
||||||
|
|
||||||
|
<div class="guide-section">
|
||||||
|
<div class="example-queries">
|
||||||
|
<button class="example-btn" @click="sendExample('分析贵州茅台')">分析贵州茅台</button>
|
||||||
|
<button class="example-btn" @click="sendExample('比亚迪怎么样')">比亚迪怎么样</button>
|
||||||
|
<button class="example-btn" @click="sendExample('上证指数走势')">上证指数走势</button>
|
||||||
|
<button class="example-btn" @click="sendExample('分析特斯拉')">分析特斯拉</button>
|
||||||
|
<button class="example-btn" @click="sendExample('苹果股票')">苹果股票</button>
|
||||||
|
<button class="example-btn" @click="sendExample('NVDA基本面')">NVDA基本面</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="welcome-footer">
|
||||||
|
<p>💬 输入股票名称或代码开始分析</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Messages -->
|
||||||
@ -116,7 +131,7 @@
|
|||||||
<textarea
|
<textarea
|
||||||
v-model="userInput"
|
v-model="userInput"
|
||||||
@keydown.enter.exact.prevent="sendMessage"
|
@keydown.enter.exact.prevent="sendMessage"
|
||||||
placeholder="输入消息..."
|
placeholder="输入股票名称或代码..."
|
||||||
rows="1"
|
rows="1"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
ref="textarea"
|
ref="textarea"
|
||||||
|
|||||||
@ -94,6 +94,12 @@ createApp({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
sendExample(exampleText) {
|
||||||
|
// Set the example text to input and send
|
||||||
|
this.userInput = exampleText;
|
||||||
|
this.sendMessage();
|
||||||
|
},
|
||||||
|
|
||||||
renderMarkdown(content) {
|
renderMarkdown(content) {
|
||||||
if (!content) return '';
|
if (!content) return '';
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user