支持 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.visualization import VisualizationSkill
|
||||
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.tushare_service import tushare_service
|
||||
from app.utils.logger import logger
|
||||
@ -43,7 +44,8 @@ class SmartStockAgent:
|
||||
skill_manager.register(FundamentalSkill())
|
||||
skill_manager.register(VisualizationSkill())
|
||||
skill_manager.register(AdvancedDataSkill())
|
||||
logger.info("技能注册完成(Tushare Pro高级数据)")
|
||||
skill_manager.register(USStockSkill())
|
||||
logger.info("技能注册完成(Tushare Pro高级数据 + 美股支持)")
|
||||
|
||||
async def process_message(
|
||||
self,
|
||||
@ -1000,6 +1002,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
||||
1. **stock_specific** - 针对特定股票或指数的问题
|
||||
例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标"、"帮我看看这只股票"
|
||||
**重要**:指数查询也属于此类,例如:"上证指数怎么样"、"分析大盘"、"A股指数走势"、"深证成指"
|
||||
**美股支持**:美股查询也属于此类,例如:"苹果股票怎么样"、"AAPL分析"、"特斯拉走势"、"TSLA技术指标"
|
||||
|
||||
2. **macro_finance** - 宏观金融/市场问题(不针对特定股票或指数)
|
||||
例如:"最近有什么投资机会"、"现在适合买股票吗"、"市场情绪如何"
|
||||
@ -1014,6 +1017,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
||||
- 如果用户问题不明确,但可能与金融相关,优先归类为 general_chat,以便引导用户
|
||||
- 如果用户提到"这只股票"、"它"等代词,查看对话历史判断是否指特定股票
|
||||
- **如果用户提到"大盘"、"上证"、"深证"、"A股指数"等,归类为 stock_specific,并在stock_names中填入对应的指数名称**
|
||||
- **如果用户提到美股公司名称(如苹果、特斯拉、微软)或美股代码(如AAPL、TSLA、MSFT),归类为 stock_specific,并在stock_names中填入对应的股票名称或代码**
|
||||
- 对于模糊的问题,不要强行归类,使用 general_chat 类型
|
||||
|
||||
请以JSON格式返回分析结果:
|
||||
@ -1021,7 +1025,8 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
||||
"type": "问题类型",
|
||||
"description": "问题的简要描述(用一句话概括用户想了解什么)",
|
||||
"keywords": ["关键词1", "关键词2"],
|
||||
"stock_names": ["股票名称或指数名称"] (仅当type为stock_specific时,如果有的话)
|
||||
"stock_names": ["股票名称或指数名称或美股代码"] (仅当type为stock_specific时,如果有的话),
|
||||
"market": "A股" 或 "美股" (仅当type为stock_specific时,根据股票类型判断)
|
||||
}}
|
||||
|
||||
只返回JSON,不要有任何其他内容。"""
|
||||
@ -1071,6 +1076,7 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
||||
) -> Dict[str, Any]:
|
||||
"""处理针对特定股票或指数的问题"""
|
||||
stock_names = intent_analysis.get('stock_names', [])
|
||||
market = intent_analysis.get('market', 'A股') # 默认A股
|
||||
|
||||
if not stock_names:
|
||||
return {
|
||||
@ -1081,6 +1087,14 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
||||
# 提取第一个股票或指数
|
||||
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 = {
|
||||
"上证指数": "000001.SH",
|
||||
@ -1332,6 +1346,285 @@ MA60:{f"{ma['ma60']:.2f}" if ma['ma60'] else '计算中'}
|
||||
"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()
|
||||
|
||||
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
|
||||
python-dotenv==1.0.0
|
||||
slowapi==0.1.9
|
||||
websockets==12.0
|
||||
websockets>=13.0
|
||||
pandas>=2.2.0
|
||||
numpy>=1.26.0
|
||||
python-multipart==0.0.6
|
||||
aiohttp==3.9.1
|
||||
yfinance>=0.2.36
|
||||
|
||||
@ -153,11 +153,11 @@ html, body {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 20px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
@ -169,14 +169,56 @@ html, body {
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 8px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.welcome p {
|
||||
.welcome-subtitle {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
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 */
|
||||
|
||||
@ -53,12 +53,27 @@
|
||||
<!-- Welcome Screen -->
|
||||
<div v-if="messages.length === 0" class="welcome">
|
||||
<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"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>开始对话</h1>
|
||||
<p>输入股票代码或名称,获取实时分析</p>
|
||||
<h1>AI 金融智能体</h1>
|
||||
<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>
|
||||
|
||||
<!-- Messages -->
|
||||
@ -116,7 +131,7 @@
|
||||
<textarea
|
||||
v-model="userInput"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
placeholder="输入消息..."
|
||||
placeholder="输入股票名称或代码..."
|
||||
rows="1"
|
||||
:disabled="loading"
|
||||
ref="textarea"
|
||||
|
||||
@ -94,6 +94,12 @@ createApp({
|
||||
}
|
||||
},
|
||||
|
||||
sendExample(exampleText) {
|
||||
// Set the example text to input and send
|
||||
this.userInput = exampleText;
|
||||
this.sendMessage();
|
||||
},
|
||||
|
||||
renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user