From 29b2fad1d6602b6ac780bf8e657d2a3f336323c1 Mon Sep 17 00:00:00 2001 From: aaron <> Date: Tue, 3 Feb 2026 21:54:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=94=AF=E6=8C=81=20us=20stock?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/agent/smart_agent.py | 297 ++++++++++++++++++++++- backend/app/services/us_stock_service.py | 282 +++++++++++++++++++++ backend/app/skills/us_stock_skill.py | 118 +++++++++ backend/requirements.txt | 3 +- frontend/css/style.css | 50 +++- frontend/index.html | 23 +- frontend/js/app.js | 6 + 7 files changed, 768 insertions(+), 11 deletions(-) create mode 100644 backend/app/services/us_stock_service.py create mode 100644 backend/app/skills/us_stock_skill.py diff --git a/backend/app/agent/smart_agent.py b/backend/app/agent/smart_agent.py index c5b72ef..53c4ad6 100644 --- a/backend/app/agent/smart_agent.py +++ b/backend/app/agent/smart_agent.py @@ -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() diff --git a/backend/app/services/us_stock_service.py b/backend/app/services/us_stock_service.py new file mode 100644 index 0000000..082c44f --- /dev/null +++ b/backend/app/services/us_stock_service.py @@ -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() diff --git a/backend/app/skills/us_stock_skill.py b/backend/app/skills/us_stock_skill.py new file mode 100644 index 0000000..e8696f7 --- /dev/null +++ b/backend/app/skills/us_stock_skill.py @@ -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() diff --git a/backend/requirements.txt b/backend/requirements.txt index 4642b61..db0f982 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -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 diff --git a/frontend/css/style.css b/frontend/css/style.css index b9652cf..e402cf2 100644 --- a/frontend/css/style.css +++ b/frontend/css/style.css @@ -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 */ diff --git a/frontend/index.html b/frontend/index.html index 67617ba..5670bd0 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -53,12 +53,27 @@
输入股票代码或名称,获取实时分析
+支持 A股 + 美股双市场分析
+ +