支持 us stock

This commit is contained in:
aaron 2026-02-03 21:54:05 +08:00
parent 49adf5da6a
commit 29b2fad1d6
7 changed files with 768 additions and 11 deletions

View File

@ -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中填入对应的指数名称**
- **如果用户提到美股公司名称如苹果特斯拉微软或美股代码如AAPLTSLAMSFT归类为 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句话
## 💼 公司基本面
- 行业地位和竞争优势
- 估值水平分析PEPB是否合理
- 盈利能力和成长性
## 📈 技术面分析
- 当前趋势判断基于均线系统
- 关键支撑位和压力位
- 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()

View 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()

View 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()

View File

@ -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

View File

@ -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 */

View File

@ -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"

View File

@ -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 '';