From b61b9a0f0bb26107226baca0ce0e12238e17665a Mon Sep 17 00:00:00 2001 From: aaron <> Date: Thu, 19 Feb 2026 21:20:20 +0800 Subject: [PATCH] update --- .env.example | 12 + backend/app/api/stocks.py | 131 +++++++++ backend/app/config.py | 6 + backend/app/main.py | 28 +- backend/app/services/yfinance_service.py | 246 ++++++++++++++++ backend/app/stock_agent/__init__.py | 6 + backend/app/stock_agent/stock_agent.py | 356 +++++++++++++++++++++++ scripts/README.md | 134 +++++++++ scripts/STOCK_USAGE.md | 198 +++++++++++++ scripts/analyze_stock.py | 213 ++++++++++++++ scripts/analyze_stock_simple.py | 100 +++++++ scripts/stock.sh | 27 ++ scripts/test_stock.py | 161 ++++++++++ 13 files changed, 1616 insertions(+), 2 deletions(-) create mode 100644 backend/app/api/stocks.py create mode 100644 backend/app/services/yfinance_service.py create mode 100644 backend/app/stock_agent/__init__.py create mode 100644 backend/app/stock_agent/stock_agent.py create mode 100644 scripts/README.md create mode 100644 scripts/STOCK_USAGE.md create mode 100755 scripts/analyze_stock.py create mode 100644 scripts/analyze_stock_simple.py create mode 100755 scripts/stock.sh create mode 100755 scripts/test_stock.py diff --git a/.env.example b/.env.example index 200dd12..cf050af 100644 --- a/.env.example +++ b/.env.example @@ -145,3 +145,15 @@ PAPER_TRADING_POSITION_C=200 # 可选值: zhipu, deepseek SMART_AGENT_MODEL=deepseek CRYPTO_AGENT_MODEL=deepseek +STOCK_AGENT_MODEL=deepseek + +# ---------------------------------------------------------------------------- +# 美股智能体配置 +# ---------------------------------------------------------------------------- +# 监控的股票代码(逗号分隔) +# 配置策略:科技龙头30% + AI/半导体成长30% + 生物医疗20% + 新能源10% + 金融10% +STOCK_SYMBOLS=AAPL,MSFT,GOOGL,META,AMZN,NVDA,AMD,AVGO,ARM,PLTR,SNOW,LLY,NVO,VRTX,TSLA,ENPH,V,MA,HD,COST +# 分析间隔(秒,美股交易时间内每小时分析一次=3600秒) +STOCK_ANALYSIS_INTERVAL=3600 +# 触发 LLM 分析的置信度阈值(0-1) +STOCK_LLM_THRESHOLD=0.60 diff --git a/backend/app/api/stocks.py b/backend/app/api/stocks.py new file mode 100644 index 0000000..7b79b94 --- /dev/null +++ b/backend/app/api/stocks.py @@ -0,0 +1,131 @@ +""" +美股相关 API 路由 +""" +from fastapi import APIRouter, HTTPException +from typing import Dict, Any, List +from app.utils.logger import logger +from app.config import get_settings + +router = APIRouter() + +# 全局变量,用于访问智能体实例 +_stock_agent_instance = None + + +def set_stock_agent(agent): + """设置智能体实例(由 main.py 调用)""" + global _stock_agent_instance + _stock_agent_instance = agent + + +@router.get("/status") +async def get_stock_status() -> Dict[str, Any]: + """ + 获取美股智能体状态 + + Returns: + 智能体状态信息 + """ + try: + if _stock_agent_instance is None: + return { + "enabled": False, + "message": "美股智能体未启用" + } + + status = _stock_agent_instance.get_status() + status["enabled"] = True + return status + + except Exception as e: + logger.error(f"获取美股状态失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/symbols") +async def get_stock_symbols() -> Dict[str, Any]: + """ + 获取当前监控的股票列表 + + Returns: + 股票列表 + """ + try: + settings = get_settings() + symbols = settings.stock_symbols.split(',') if settings.stock_symbols else [] + + return { + "symbols": symbols, + "count": len(symbols) + } + + except Exception as e: + logger.error(f"获取股票列表失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/analyze/{symbol}") +async def analyze_stock(symbol: str) -> Dict[str, Any]: + """ + 手动触发分析指定股票 + + Args: + symbol: 股票代码,如 'AAPL' + + Returns: + 分析结果 + """ + try: + if _stock_agent_instance is None: + raise HTTPException(status_code=400, detail="美股智能体未启用") + + # 执行单次分析 + result = await _stock_agent_instance.analyze_once(symbol) + + if "error" in result: + raise HTTPException(status_code=400, detail=result["error"]) + + return { + "success": True, + "symbol": symbol, + "result": result + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"分析 {symbol} 失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/quote/{symbol}") +async def get_stock_quote(symbol: str) -> Dict[str, Any]: + """ + 获取股票实时行情 + + Args: + symbol: 股票代码 + + Returns: + 行情数据 + """ + try: + from app.services.yfinance_service import get_yfinance_service + + yf_service = get_yfinance_service() + quote = yf_service.get_ticker(symbol.upper()) + + if quote is None: + raise HTTPException(status_code=404, detail=f"无法获取 {symbol} 的行情") + + return { + "success": True, + "symbol": symbol.upper(), + "quote": quote + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取 {symbol} 行情失败: {e}") + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/app/config.py b/backend/app/config.py index c9b97e4..855120a 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -137,6 +137,12 @@ class Settings(BaseSettings): # Agent 模型配置 (可选值: zhipu, deepseek) smart_agent_model: str = "deepseek" # SmartAgent 使用的模型 crypto_agent_model: str = "deepseek" # CryptoAgent 使用的模型 + stock_agent_model: str = "deepseek" # StockAgent 使用的模型 + + # 美股智能体配置 + stock_symbols: str = "AAPL,TSLA,NVDA,MSFT,GOOGL" # 监控的股票代码,逗号分隔 + stock_analysis_interval: int = 300 # 分析间隔(秒,默认5分钟) + stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值 class Config: env_file = find_env_file() diff --git a/backend/app/main.py b/backend/app/main.py index a3713ec..4b89574 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -9,13 +9,14 @@ from fastapi.responses import FileResponse from contextlib import asynccontextmanager from app.config import get_settings from app.utils.logger import logger -from app.api import chat, stock, skills, llm, auth, admin, paper_trading +from app.api import chat, stock, skills, llm, auth, admin, paper_trading, stocks import os # 后台任务 _price_monitor_task = None _report_task = None +_stock_agent_task = None async def price_monitor_loop(): @@ -147,7 +148,7 @@ async def periodic_report_loop(): @asynccontextmanager async def lifespan(app: FastAPI): """应用生命周期管理""" - global _price_monitor_task, _report_task + global _price_monitor_task, _report_task, _stock_agent_task # 启动时执行 logger.info("应用启动") @@ -161,6 +162,19 @@ async def lifespan(app: FastAPI): _report_task = asyncio.create_task(periodic_report_loop()) logger.info("定时报告任务已创建") + # 启动美股智能体 + if getattr(settings, 'stock_symbols', '') and settings.stock_symbols.strip(): + try: + from app.stock_agent.stock_agent import get_stock_agent + stock_agent = get_stock_agent() + _stock_agent_task = asyncio.create_task(stock_agent.start()) + # 设置智能体实例到 API 模块 + stocks.set_stock_agent(stock_agent) + logger.info(f"美股智能体已启动,监控: {settings.stock_symbols}") + except Exception as e: + logger.error(f"美股智能体启动失败: {e}") + logger.error(f"提示: 请确保已安装 yfinance (pip install yfinance)") + yield # 关闭时执行 @@ -180,6 +194,15 @@ async def lifespan(app: FastAPI): pass logger.info("定时报告任务已停止") + # 停止美股智能体 + if _stock_agent_task: + _stock_agent_task.cancel() + try: + await _stock_agent_task + except asyncio.CancelledError: + pass + logger.info("美股智能体已停止") + logger.info("应用关闭") @@ -209,6 +232,7 @@ app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"]) app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"]) app.include_router(llm.router, tags=["LLM模型"]) app.include_router(paper_trading.router, tags=["模拟交易"]) +app.include_router(stocks.router, prefix="/api/stocks", tags=["美股分析"]) # 挂载静态文件 frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend") diff --git a/backend/app/services/yfinance_service.py b/backend/app/services/yfinance_service.py new file mode 100644 index 0000000..e851a64 --- /dev/null +++ b/backend/app/services/yfinance_service.py @@ -0,0 +1,246 @@ +""" +YFinance 服务 - 美股数据获取 +支持获取美股的实时行情和历史 K 线数据 +""" +import pandas as pd +from typing import Dict, List, Optional +from datetime import datetime, timedelta +from app.utils.logger import logger +import time + + +class YFinanceService: + """YFinance 服务类""" + + def __init__(self): + """初始化服务""" + try: + import yfinance as yf + self.yf = yf + self._cache = {} # 数据缓存 + self._cache_time = {} # 缓存时间 + self._cache_ttl = 300 # 缓存有效期(秒) + logger.info("YFinance 服务初始化成功") + except ImportError: + logger.error("yfinance 未安装,请运行: pip install yfinance") + raise + + def get_ticker(self, symbol: str) -> Optional[Dict]: + """ + 获取股票实时行情 + + Args: + symbol: 股票代码,如 'AAPL' + + Returns: + 行情数据字典 + """ + try: + ticker = self.yf.Ticker(symbol) + + # 使用 history 方法获取数据(更可靠,避免 429 错误) + hist = ticker.history(period="2d", interval="1h") + if hist.empty: + logger.warning(f"无法获取 {symbol} 的历史数据") + return None + + latest = hist.iloc[-1] + + return { + 'symbol': symbol, + 'lastPrice': float(latest['Close']), + 'priceChange': float(latest['Close'] - latest['Open']), + 'priceChangePercent': float((latest['Close'] - latest['Open']) / latest['Open'] * 100) if latest['Open'] > 0 else 0, + 'volume': int(latest['Volume']), + 'high': float(latest['High']), + 'low': float(latest['Low']), + 'open': float(latest['Open']), + 'prevClose': float(latest['Close']), + 'timestamp': datetime.now().isoformat() + } + except Exception as e: + error_msg = str(e) + # 过滤掉常见的 429 错误信息 + if "429" in error_msg or "Too Many Requests" in error_msg: + logger.warning(f"YFinance API 限流,请稍后再试 ({symbol})") + else: + logger.error(f"获取 {symbol} 行情失败: {error_msg}") + return None + + def get_multi_timeframe_data( + self, + symbol: str, + timeframes: Optional[Dict[str, tuple]] = None + ) -> Dict[str, pd.DataFrame]: + """ + 获取多时间周期的 K 线数据 + + Args: + symbol: 股票代码 + timeframes: 时间周期配置 {'1d': ('1d', '3mo'), ...} + + Returns: + 多时间周期数据字典 {'1d': df, '1h': df, ...} + """ + if timeframes is None: + # 默认时间周期配置 + timeframes = { + '1d': ('1d', '3mo'), # 日级别,3个月 + '1h': ('1h', '1mo'), # 小时级别,1个月 + } + + result = {} + + for tf_name, (interval, period) in timeframes.items(): + try: + df = self._get_cached_data(symbol, interval, period) + if df is not None and not df.empty: + result[tf_name] = df + logger.debug(f"获取 {symbol} {tf_name} 数据成功: {len(df)} 条") + else: + logger.warning(f"获取 {symbol} {tf_name} 数据失败或为空") + except Exception as e: + logger.error(f"获取 {symbol} {tf_name} 数据出错: {e}") + + return result + + def _get_cached_data( + self, + symbol: str, + interval: str, + period: str + ) -> Optional[pd.DataFrame]: + """获取带缓存的数据""" + cache_key = f"{symbol}_{interval}_{period}" + now = datetime.now() + + # 检查缓存 + if cache_key in self._cache: + cache_time = self._cache_time.get(cache_key) + if cache_time and (now - cache_time).total_seconds() < self._cache_ttl: + logger.debug(f"使用缓存数据: {cache_key}") + return self._cache[cache_key] + + # 获取新数据 + try: + ticker = self.yf.Ticker(symbol) + df = ticker.history(period=period, interval=interval) + + if df.empty: + return None + + # 转换数据格式(兼容现有代码) + df = self._format_dataframe(df) + + # 更新缓存 + self._cache[cache_key] = df + self._cache_time[cache_key] = now + + return df + + except Exception as e: + logger.error(f"获取数据失败 {cache_key}: {e}") + return None + + def _format_dataframe(self, df: pd.DataFrame) -> pd.DataFrame: + """ + 格式化 DataFrame 以兼容现有代码 + + yfinance 原始格式: + - 列名大写: Open, High, Low, Close, Volume + - 索引是 Datetime + + 转换后格式: + - 列名小写: open, high, low, close, volume + - 重置索引,time 作为一列 + - 添加技术指标 + """ + df = df.copy() + + # 列名转为小写 + df.columns = [col.lower() for col in df.columns] + + # 重置索引 + df = df.reset_index() + + # 重命名日期列 + if 'date' in df.columns: + df = df.rename(columns={'date': 'time'}) + elif 'datetime' in df.columns: + df = df.rename(columns={'datetime': 'time'}) + + # 删除不需要的列 + cols_to_keep = ['time', 'open', 'high', 'low', 'close', 'volume'] + df = df[[col for col in cols_to_keep if col in df.columns]] + + # 添加技术指标(与 binance_service 一致) + df = self._add_indicators(df) + + return df + + def _add_indicators(self, df: pd.DataFrame) -> pd.DataFrame: + """ + 添加技术指标到 DataFrame + + Args: + df: 原始数据 + + Returns: + 添加了技术指标的 DataFrame + """ + df = df.copy() + + # 移动平均线 + df['ma5'] = df['close'].rolling(window=5).mean() + df['ma10'] = df['close'].rolling(window=10).mean() + df['ma20'] = df['close'].rolling(window=20).mean() + df['ma50'] = df['close'].rolling(window=50).mean() + + # RSI + delta = df['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 + df['rsi'] = 100 - (100 / (1 + rs)) + + # MACD (使用与 binance_service 相同的计算方法) + ema_fast = df['close'].ewm(span=12, adjust=False).mean() + ema_slow = df['close'].ewm(span=26, adjust=False).mean() + df['macd'] = ema_fast - ema_slow + df['macd_signal'] = df['macd'].ewm(span=9, adjust=False).mean() + df['macd_hist'] = df['macd'] - df['macd_signal'] + + # ATR + high_low = df['high'] - df['low'] + high_close = abs(df['high'] - df['close'].shift()) + low_close = abs(df['low'] - df['close'].shift()) + true_range = pd.concat([high_low, high_close, low_close], axis=1).max(axis=1) + df['atr'] = true_range.rolling(window=14).mean() + + # KDJ 指标 + low_min = df['low'].rolling(window=9).min() + high_max = df['high'].rolling(window=9).max() + rsv = (df['close'] - low_min) / (high_max - low_min) * 100 + df['k'] = rsv.ewm(com=2, adjust=False).mean() + df['d'] = df['k'].ewm(com=2, adjust=False).mean() + df['j'] = 3 * df['k'] - 2 * df['d'] + + return df + + def clear_cache(self): + """清空缓存""" + self._cache.clear() + self._cache_time.clear() + logger.info("YFinance 缓存已清空") + + +# 全局单例 +_yfinance_service: Optional[YFinanceService] = None + + +def get_yfinance_service() -> YFinanceService: + """获取 YFinance 服务单例""" + global _yfinance_service + if _yfinance_service is None: + _yfinance_service = YFinanceService() + return _yfinance_service diff --git a/backend/app/stock_agent/__init__.py b/backend/app/stock_agent/__init__.py new file mode 100644 index 0000000..3183911 --- /dev/null +++ b/backend/app/stock_agent/__init__.py @@ -0,0 +1,6 @@ +""" +美股交易智能体包 +""" +from app.stock_agent.stock_agent import StockAgent, get_stock_agent + +__all__ = ['StockAgent', 'get_stock_agent'] diff --git a/backend/app/stock_agent/stock_agent.py b/backend/app/stock_agent/stock_agent.py new file mode 100644 index 0000000..a6e119b --- /dev/null +++ b/backend/app/stock_agent/stock_agent.py @@ -0,0 +1,356 @@ +""" +美股交易智能体 - 主控制器(LLM 驱动版) +只进行市场分析和通知,不执行模拟交易 +""" +import asyncio +from typing import Dict, Any, List, Optional +from datetime import datetime, timedelta +import pandas as pd + +from app.utils.logger import logger +from app.config import get_settings +from app.services.yfinance_service import get_yfinance_service +from app.services.feishu_service import get_feishu_service +from app.services.telegram_service import get_telegram_service +from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer + + +class StockAgent: + """美股交易信号智能体(LLM 驱动,仅分析通知)""" + + def __init__(self): + """初始化智能体""" + self.settings = get_settings() + self.yfinance = get_yfinance_service() + self.feishu = get_feishu_service() + self.telegram = get_telegram_service() + self.llm_analyzer = LLMSignalAnalyzer() + + # 状态管理 + self.last_signals: Dict[str, Dict[str, Any]] = {} + self.signal_cooldown: Dict[str, datetime] = {} + + # 配置 + self.symbols = self.settings.stock_symbols.split(',') + + # 运行状态 + self.running = False + self._event_loop = None + self._task = None + + logger.info(f"美股智能体初始化完成,监控股票: {self.symbols}") + + async def start(self): + """启动智能体""" + if self.running: + logger.warning("美股智能体已在运行中") + return + + self.running = True + self._event_loop = asyncio.get_event_loop() + + logger.info("美股智能体已启动") + + # 启动分析任务 + self._task = asyncio.create_task(self._analysis_loop()) + + async def stop(self): + """停止智能体""" + self.running = False + if self._task: + self._task.cancel() + try: + await self._task + except asyncio.CancelledError: + pass + + logger.info("美股智能体已停止") + + async def _analysis_loop(self): + """分析循环 - 只在美股交易时间内运行""" + while self.running: + try: + # 检查是否在美股交易时间 + if not self._is_market_hours(): + # 不在交易时间,等待 10 分钟后再次检查 + logger.debug("非美股交易时间,等待中...") + await asyncio.sleep(600) + continue + + # 在交易时间内,分析所有股票 + for symbol in self.symbols: + if not self.running: + break + await self.analyze_symbol(symbol) + + # 等待 1 小时后进行下次分析 + logger.info("本次分析完成,等待 1 小时后进行下次分析...") + await asyncio.sleep(3600) + + except Exception as e: + logger.error(f"分析循环出错: {e}") + await asyncio.sleep(60) # 出错后等待 1 分钟再重试 + + def _is_market_hours(self) -> bool: + """ + 判断当前是否在美股交易时间 + + 美股交易时间: 周一至周五 9:30-16:00 (EST) + 北京时间: + - 冬令时 (11月-3月): 22:30-05:00 (次日) + - 夏令时 (3月-11月): 21:30-04:00 (次日) + + Returns: + 是否在交易时间 + """ + from datetime import datetime + + # 获取当前时间 + now = datetime.now() + + # 检查是否为周末 + if now.weekday() >= 5: # 5=周六, 6=周日 + return False + + # 获取当前小时和分钟 + hour = now.hour + minute = now.minute + current_time = hour * 100 + minute # 转换为数字,如 2130 表示 21:30 + + # 判断夏令时/冬令时(简单判断:3-11月为夏令时) + is_summer = 3 <= now.month <= 11 + + if is_summer: + # 夏令时: 21:30-04:00 (次日) + # 即 2130-2359 或 0000-0400 + if current_time >= 2130 or current_time < 400: + return True + else: + # 冬令时: 22:30-05:00 (次日) + # 即 2230-2359 或 0000-0500 + if current_time >= 2230 or current_time < 500: + return True + + return False + + async def analyze_symbol(self, symbol: str): + """ + 分析单个股票 + + Args: + symbol: 股票代码 + """ + try: + # 1. 获取多时间周期数据 + data = self.yfinance.get_multi_timeframe_data(symbol) + + # 2. 验证数据完整性 + if not self._validate_data(data): + logger.warning(f"{symbol} 数据不完整,跳过本次分析") + return + + # 3. 获取当前价格 + ticker = self.yfinance.get_ticker(symbol) + if not ticker: + logger.warning(f"无法获取 {symbol} 当前价格") + return + current_price = ticker['lastPrice'] + + logger.info(f"\n{'='*60}") + logger.info(f"📊 分析 {symbol} @ ${current_price:,.2f}") + logger.info(f"{'='*60}") + + # 4. LLM 分析 + logger.info(f"\n🤖 【LLM 分析中...】") + result = await self.llm_analyzer.analyze( + symbol, data, + symbols=self.symbols, + position_info=None # 美股不跟踪持仓 + ) + + # 输出分析摘要 + summary = result.get('analysis_summary', '无') + logger.info(f" 市场状态: {summary}") + + # 输出新闻情绪 + news_sentiment = result.get('news_sentiment', '') + news_impact = result.get('news_impact', '') + if news_sentiment: + sentiment_icon = {'positive': '📈', 'negative': '📉', 'neutral': '➖'}.get(news_sentiment, '') + logger.info(f" 新闻情绪: {sentiment_icon} {news_sentiment}") + if news_impact: + logger.info(f" 消息影响: {news_impact}") + + # 输出关键价位 + levels = result.get('key_levels', {}) + if levels.get('support') or levels.get('resistance'): + support_str = ', '.join([f"${s:,.2f}" for s in levels.get('support', [])[:2]]) + resistance_str = ', '.join([f"${r:,.2f}" for r in levels.get('resistance', [])[:2]]) + logger.info(f" 支撑位: {support_str or '-'}") + logger.info(f" 阻力位: {resistance_str or '-'}") + + # 5. 处理信号 + signals = result.get('signals', []) + + if not signals: + logger.info(f"\n⏸️ 结论: 无交易信号,继续观望") + return + + # 输出所有信号 + logger.info(f"\n🎯 【发现 {len(signals)} 个信号】") + + for sig in signals: + signal_type = sig.get('type', 'unknown') + type_map = {'short_term': '短线', 'medium_term': '中线', 'long_term': '长线'} + type_text = type_map.get(signal_type, signal_type) + + action = sig.get('action', 'wait') + action_map = {'buy': '🟢 做多', 'sell': '🔴 做空'} + action_text = action_map.get(action, action) + + grade = sig.get('grade', 'D') + confidence = sig.get('confidence', 0) + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') + + logger.info(f"\n {type_text} {action_text} [{grade}级{grade_icon}] {confidence}%") + + # 6. 过滤并通知最佳信号 + best_signal = self._get_best_signal(signals) + + if not best_signal: + logger.info(f"\n⏸️ 信号质量不高,不发送通知") + return + + # 检查置信度阈值 + threshold = self.settings.stock_llm_threshold * 100 + if best_signal.get('confidence', 0) < threshold: + logger.info(f"\n⏸️ 置信度不足 ({best_signal.get('confidence', 0)}% < {threshold}%)") + return + + # 检查冷却时间 + if not self._should_send_signal(symbol, best_signal): + logger.info(f"\n⏸️ 信号冷却中,不发送通知") + return + + # 发送通知 + await self._send_signal_notification(symbol, best_signal, current_price) + + # 更新状态 + self.last_signals[symbol] = best_signal + self.signal_cooldown[symbol] = datetime.now() + + except Exception as e: + logger.error(f"❌ 分析 {symbol} 出错: {e}") + import traceback + logger.error(traceback.format_exc()) + + def _get_best_signal(self, signals: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: + """获取最佳信号""" + # 过滤掉 D 级信号 + valid_signals = [s for s in signals if s.get('grade', 'D') != 'D'] + + if not valid_signals: + return None + + # 按等级和置信度排序 + grade_order = {'A': 0, 'B': 1, 'C': 2} + valid_signals.sort(key=lambda x: ( + grade_order.get(x.get('grade', 'C'), 3), + -x.get('confidence', 0) + )) + + return valid_signals[0] + + def _should_send_signal(self, symbol: str, signal: Dict[str, Any]) -> bool: + """判断是否应该发送信号""" + action = signal.get('action', 'wait') + if action == 'wait': + return False + + # 检查冷却时间(60分钟内不重复发送相同方向的信号) + if symbol in self.signal_cooldown: + cooldown_end = self.signal_cooldown[symbol] + timedelta(minutes=60) + if datetime.now() < cooldown_end: + if symbol in self.last_signals: + if self.last_signals[symbol].get('action') == action: + logger.debug(f"{symbol} 信号冷却中,跳过") + return False + + return True + + async def _send_signal_notification( + self, + symbol: str, + signal: Dict[str, Any], + current_price: float + ): + """发送信号通知""" + try: + # 使用正确的方法格式化信号 + card = self.llm_analyzer.format_feishu_card(signal, symbol) + title = card['title'] + content = card['content'] + + # 发送到飞书 + await self.feishu.send_markdown(title, content) + + # 发送到 Telegram + await self.telegram.send_message(self.llm_analyzer.format_signal_message(signal, symbol)) + + logger.info(f"✅ 信号通知已发送: {title}") + + except Exception as e: + logger.error(f"发送通知失败: {e}") + + def _validate_data(self, data: Dict[str, pd.DataFrame]) -> bool: + """验证数据完整性""" + required_intervals = ['1d', '1h'] + for interval in required_intervals: + if interval not in data or data[interval].empty: + return False + if len(data[interval]) < 20: + return False + return True + + async def analyze_once(self, symbol: str) -> Dict[str, Any]: + """单次分析(用于测试或手动触发)""" + data = self.yfinance.get_multi_timeframe_data(symbol) + + if not self._validate_data(data): + return {'error': '数据不完整'} + + result = await self.llm_analyzer.analyze( + symbol, data, + symbols=self.symbols, + position_info=None + ) + return result + + def get_status(self) -> Dict[str, Any]: + """获取智能体状态""" + return { + 'running': self.running, + 'symbols': self.symbols, + 'mode': 'LLM 驱动(仅分析通知)', + 'last_signals': { + symbol: { + 'type': sig.get('type'), + 'action': sig.get('action'), + 'confidence': sig.get('confidence'), + 'grade': sig.get('grade') + } + for symbol, sig in self.last_signals.items() + } + } + + +# 全局单例 +_stock_agent: Optional[StockAgent] = None + + +def get_stock_agent() -> StockAgent: + """获取美股智能体单例""" + global _stock_agent + if _stock_agent is None: + _stock_agent = StockAgent() + return _stock_agent diff --git a/scripts/README.md b/scripts/README.md new file mode 100644 index 0000000..161f5df --- /dev/null +++ b/scripts/README.md @@ -0,0 +1,134 @@ +# 美股手动分析脚本使用说明 + +## 📜 脚本列表 + +| 脚本 | 说明 | 推荐度 | +|------|------|--------| +| `scripts/stock.sh` | 快捷分析脚本(推荐) | ⭐⭐⭐ | +| `scripts/analyze_stock_simple.py` | Python 简化版 | ⭐⭐ | +| `scripts/analyze_stock.py` | Python 完整版 | ⭐ | + +## 🚀 快速开始 + +### 方式 1: 使用快捷脚本(推荐) + +```bash +# 分析单只股票 +./scripts/stock.sh AAPL + +# 分析多只股票 +./scripts/stock.sh AAPL TSLA NVDA + +# 分析配置的所有股票 +./scripts/stock.sh +``` + +### 方式 2: 使用 Python 脚本 + +```bash +# 简化版 +cd backend +python3 ../scripts/analyze_stock_simple.py AAPL +python3 ../scripts/analyze_stock_simple.py AAPL TSLA NVDA + +# 完整版 +python3 ../scripts/analyze_stock.py AAPL +python3 ../scripts/analyze_stock.py AAPL TSLA NVDA +``` + +## 📋 输出示例 + +``` +============================================================ +📊 分析 AAPL +============================================================ +价格: $178.50 (+1.25%) +K线: ['1d', '1h'] + +🤖 LLM分析中... + +状态: 震荡上涨,MACD 金叉形成 + +🎯 发现 1 个信号: + + 🟢 做多 [A⭐⭐⭐] 85% + 入场: $178.50 + 止损: $172.80 + 止盈: $205.28 + 理由: 突破 MA20 阻力,RSI=58 进入强势区... + +✅ 完成 +``` + +## ⚙️ 配置说明 + +在 `.env` 文件中配置默认股票列表: + +```bash +# 美股智能体配置 +STOCK_SYMBOLS=AAPL,TSLA,NVDA,MSFT,GOOGL +``` + +## 📝 完整版功能 + +完整版脚本 (`analyze_stock.py`) 提供更多详细信息: + +- ✅ 当前行情详情 +- ✅ K 线数据统计 +- ✅ 新闻情绪分析 +- ✅ 关键支撑/阻力位 +- ✅ 详细信号列表 +- ✅ 风险提示 +- ✅ 通知预览 + +## 🔧 依赖要求 + +确保已安装 yfinance: + +```bash +cd backend +pip install yfinance +``` + +## 💡 提示 + +1. **首次使用**:建议先运行一次 `./scripts/stock.sh` 测试 +2. **分析频率**:不要频繁分析,避免 API 限流 +3. **最佳时间**:在美股交易时间内分析效果最佳 +4. **股票代码**:使用美股代码,如 AAPL, TSLA, NVDA 等 + +## 📊 支持的股票 + +任何美股代码都可以分析,常见示例: + +- 科技股: AAPL, MSFT, GOOGL, META, AMZN, NVDA, TSLA +- 金融股: JPM, BAC, GS, MS +- 医疗股: JNJ, PFE, UNH, ABT +- 消费股: NKE, KO, MCD, SBUX + +## ⚠️ 注意事项 + +1. 数据来源:YFinance(Yahoo Finance) +2. 数据延迟:约 15 分钟 +3. API 限制:过于频繁的请求可能被限流 +4. 市场时间:美股交易时间分析效果最佳 + +## 🆘 故障排除 + +### 错误:ModuleNotFoundError: No module named 'yfinance' + +```bash +cd backend +pip install yfinance +``` + +### 错误:无法获取行情 + +- 检查股票代码是否正确 +- 检查网络连接 +- 稍后重试 + +### 错误:LLM 分析失败 + +- 检查 API 密钥配置 +- 检查 DeepSeek/Zhipu API 是否可用 diff --git a/scripts/STOCK_USAGE.md b/scripts/STOCK_USAGE.md new file mode 100644 index 0000000..20c1f63 --- /dev/null +++ b/scripts/STOCK_USAGE.md @@ -0,0 +1,198 @@ +# 美股分析脚本使用说明 + +## 📊 当前股票池配置 + +### 平衡型配置(20 只股票) + +**配置策略:科技龙头 30% + AI/半导体 30% + 生物医疗 20% + 新能源 10% + 金融 10%** + +``` +# 科技龙头(核心 6 只) +AAPL - 苹果(消费电子生态) +MSFT - 微软(云计算 + AI) +GOOGL - 谷歌(搜索 + 云计算) +META - 元平台(社交 + 元宇宙) +AMZN - 亚马逊(电商 + 云服务) +NVDA - 英伟达(AI 芯片龙头) + +# AI/半导体(成长 6 只) +AMD - 高性能计算芯片 +AVGO - 博通(半导体 + 软件) +ARM - ARM 架构(AI 芯片) +PLTR - Palantir(AI 数据分析) +SNOW - Snowflake(云端数据仓库) + +# 生物医药(创新 3 只) +LLY - 礼来(GLP-1 减肥药) +NVO - 诺和诺德(糖尿病药物) +VRTX - 福泰制药(罕见病) + +# 新能源(趋势 2 只) +TSLA - 特斯拉(电动车) +ENPH - Enphase(太阳能微逆) + +# 金融(防御 2 只) +V - Visa(支付网络) +MA - 万事达卡 + +# 消费品牌(稳定 2 只) +HD - 家得宝 +COST - 好市多 +``` + +### 配置特点 +- ✅ 覆盖 6 大行业板块 +- ✅ 包含 9 只优质大盘股 +- ✅ 包含 6 只高成长股 +- ✅ 包含 5 只创新潜力股 +- ✅ 兼顾收益与风险 +- ✅ 分析间隔:1 小时(美股交易时间内) + +## ⚠️ 当前问题 + +### 1. YFinance API 限流 + +``` +429 Client Error: Too Many Requests +``` + +**原因**: YFinance API 有请求频率限制 + +**解决方案**: +- 等待几分钟后重试 +- 减少同时分析的股票数量 +- 使用缓存的数据 + +### 2. 脚本参数解析问题 + +**症状**: 脚本把 `CONFIG`、`找到.ENV文件` 等当成股票代码 + +**原因**: Shell 脚本参数传递错误 + +**解决方案**: 使用新的测试脚本 + +## ✅ 正确的使用方式 + +### 方式 1: 直接使用 Python 脚本(推荐) + +```bash +# 在项目根目录运行 +python3 scripts/test_stock.py AAPL +python3 scripts/test_stock.py AAPL TSLA +python3 scripts/test_stock.py AAPL TSLA NVDA +``` + +### 方式 2: 通过 API + +```bash +# 启动服务后 +curl http://localhost:8000/api/stocks/quote/AAPL + +# 手动触发分析 +curl -X POST http://localhost:8000/api/stocks/analyze/AAPL +``` + +### 方式 3: 等待自动分析 + +启动服务后,StockAgent 会在美股交易时间内自动分析: +- 夏令时: 21:30 - 04:00 (北京时间) +- 冬令时: 22:30 - 05:00 (北京时间) + +## 🔧 故障排除 + +### 问题 1: ModuleNotFoundError: No module named 'yfinance' + +```bash +cd backend +pip install yfinance +``` + +### 问题 2: 429 Too Many Requests + +**原因**: API 限流 + +**解决**: +```bash +# 等待 5-10 分钟后重试 +# 或者使用 API 端点(有缓存) +curl http://localhost:8000/api/stocks/quote/AAPL +``` + +### 问题 3: Expecting value: line 1 column 1 (char 0) + +**原因**: YFinance 返回了错误页面而非数据 + +**解决**: 通常与 429 错误相关,等待后重试 + +## 📋 脚本对比 + +| 脚本 | 状态 | 说明 | +|------|------|------| +| `scripts/stock.sh` | ⚠️ | Shell 脚本,参数解析有问题 | +| `scripts/analyze_stock_simple.py` | ⚠️ | 路径问题 | +| `scripts/analyze_stock.py` | ⚠️ | 路径问题 | +| `scripts/test_stock.py` | ✅ | **推荐使用** | + +## 🚀 推荐命令 + +```bash +# 分析单只股票(最简单) +python3 scripts/test_stock.py AAPL + +# 分析多只股票 +python3 scripts/test_stock.py AAPL TSLA NVDA + +# 或者使用 API(需要服务运行中) +curl -X POST http://localhost:8000/api/stocks/analyze/AAPL +``` + +## 💡 提示 + +1. **避开高峰期**: 美股开盘前后 (21:30-22:30) API 请求较多 +2. **减少并发**: 不要同时分析太多股票 +3. **使用缓存**: 通过 API 调用可以使用缓存数据 +4. **等待重试**: 遇到 429 错误等待几分钟后重试 + +## 🔄 工作流程 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 美股分析流程 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 1. 获取行情数据 (YFinance) │ +│ ↓ │ +│ 2. 获取 K 线数据 (多时间周期) │ +│ ↓ │ +│ 3. LLM 技术分析 │ +│ ↓ │ +│ 4. 生成交易信号 │ +│ ↓ │ +│ 5. 发送通知 (飞书 + Telegram) │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +## 📊 输出格式 + +``` +============================================================ +📊 分析 AAPL +============================================================ +价格: $178.50 (+1.25%) +成交量: 52,345,600 + +🤖 LLM分析中... + +市场状态: 震荡上涨,MACD 金叉形成 + +🎯 发现 1 个信号: + +🟢 做多 [A⭐⭐⭐] 85% + 入场: $178.50 + 止损: $172.80 + 止盈: $205.28 + 理由: 突破 MA20 阻力,RSI=58 进入强势区... + +✅ 分析完成 +``` diff --git a/scripts/analyze_stock.py b/scripts/analyze_stock.py new file mode 100755 index 0000000..0fa7eae --- /dev/null +++ b/scripts/analyze_stock.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +""" +美股手动分析脚本 + +用法: + python3 scripts/analyze_stock.py AAPL # 分析单只股票 + python3 scripts/analyze_stock.py AAPL TSLA # 分析多只股票 + python3 scripts/analyze_stock.py # 分析配置的所有股票 + +环境: + 需要在 backend 目录下运行,或设置 PYTHONPATH +""" +import sys +import os +import asyncio +from datetime import datetime + +# 添加项目路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend')) + +from app.services.yfinance_service import get_yfinance_service +from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer +from app.config import get_settings +from app.utils.logger import logger + + +async def analyze_stock(symbol: str): + """ + 分析单只股票 + + Args: + symbol: 股票代码 + """ + print("\n" + "=" * 80) + print(f"📊 分析 {symbol}") + print("=" * 80) + + try: + # 1. 获取服务 + yf_service = get_yfinance_service() + llm_analyzer = LLMSignalAnalyzer() + settings = get_settings() + + # 2. 获取当前行情 + print(f"\n📈 获取当前行情...") + ticker = yf_service.get_ticker(symbol) + if not ticker: + print(f"❌ 无法获取 {symbol} 的行情数据") + return + + current_price = ticker['lastPrice'] + price_change = ticker['priceChange'] + price_change_percent = ticker['priceChangePercent'] + + print(f" 最新价格: ${current_price:,.2f}") + print(f" 涨跌额: ${price_change:+,.2f}") + print(f" 涨跌幅: {price_change_percent:+.2f}%") + print(f" 成交量: {ticker['volume']:,}") + + # 3. 获取 K 线数据 + print(f"\n📊 获取 K 线数据...") + data = yf_service.get_multi_timeframe_data(symbol) + + if not data: + print(f"❌ 无法获取 {symbol} 的 K 线数据") + return + + for tf, df in data.items(): + print(f" {tf}: {len(df)} 条数据") + + # 4. LLM 分析 + print(f"\n🤖 LLM 分析中...") + print("-" * 80) + + result = await llm_analyzer.analyze( + symbol, + data, + symbols=[symbol], + position_info=None + ) + + # 5. 输出分析结果 + print("\n📋 分析结果:") + print("-" * 80) + + # 市场状态 + summary = result.get('analysis_summary', '无') + print(f"市场状态: {summary}") + + # 新闻情绪 + news_sentiment = result.get('news_sentiment', '') + if news_sentiment: + sentiment_map = {'positive': '📈 积极', 'negative': '📉 消极', 'neutral': '➖ 中性'} + print(f"新闻情绪: {sentiment_map.get(news_sentiment, news_sentiment)}") + + news_impact = result.get('news_impact', '') + if news_impact: + print(f"消息影响: {news_impact}") + + # 关键价位 + levels = result.get('key_levels', {}) + if levels.get('support') or levels.get('resistance'): + print(f"\n关键价位:") + if levels.get('support'): + support_str = ', '.join([f"${s:,.2f}" for s in levels.get('support', [])[:3]]) + print(f" 支撑位: {support_str}") + if levels.get('resistance'): + resistance_str = ', '.join([f"${r:,.2f}" for r in levels.get('resistance', [])[:3]]) + print(f" 阻力位: {resistance_str}") + + # 信号 + signals = result.get('signals', []) + if not signals: + print(f"\n⏸️ 结论: 无交易信号,继续观望") + else: + print(f"\n🎯 发现 {len(signals)} 个信号:") + print("-" * 80) + + for sig in signals: + signal_type = sig.get('type', 'unknown') + type_map = {'short_term': '短线', 'medium_term': '中线', 'long_term': '长线'} + type_text = type_map.get(signal_type, signal_type) + + action = sig.get('action', 'wait') + action_map = {'buy': '🟢 做多', 'sell': '🔴 做空', 'wait': '⏸️ 观望'} + action_text = action_map.get(action, action) + + grade = sig.get('grade', 'D') + confidence = sig.get('confidence', 0) + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐', 'D': ''}.get(grade, '') + + print(f"\n{type_text} {action_text} [{grade}级{grade_icon}] {confidence}%") + + entry = sig.get('entry_price', 0) + sl = sig.get('stop_loss', 0) + tp = sig.get('take_profit', 0) + + if entry and sl and tp: + print(f" 入场: ${entry:,.2f}") + print(f" 止损: ${sl:,.2f} ({((sl - entry) / entry * 100):+.1f}%)") + print(f" 止盈: ${tp:,.2f} ({((tp - entry) / entry * 100):+.1f}%)") + + reason = sig.get('reason', '') + if reason: + print(f" 理由: {reason}") + + risk_warning = sig.get('risk_warning', '') + if risk_warning: + print(f" 风险提示: {risk_warning}") + + # 6. 格式化通知 + if signals: + print("\n" + "=" * 80) + print("📱 通知预览:") + print("-" * 80) + + for sig in signals: + if sig.get('grade', 'D') != 'D': + formatted = llm_analyzer.format_signal_for_notification(symbol, sig) + print(f"\n{formatted['title']}") + print(formatted['content']) + + except Exception as e: + print(f"\n❌ 分析 {symbol} 失败: {e}") + import traceback + traceback.print_exc() + + +async def main(): + """主函数""" + import argparse + + parser = argparse.ArgumentParser(description='美股手动分析脚本') + parser.add_argument('symbols', nargs='*', help='股票代码(留空则分析配置的所有股票)') + + args = parser.parse_args() + + # 确定要分析的股票 + if args.symbols: + symbols = [s.upper() for s in args.symbols] + else: + settings = get_settings() + symbols = settings.stock_symbols.split(',') if settings.stock_symbols else [] + if not symbols or not symbols[0]: + print("❌ 未指定股票代码,且配置文件中未配置 STOCK_SYMBOLS") + print("\n用法:") + print(" python3 scripts/analyze_stock.py AAPL") + print(" python3 scripts/analyze_stock.py AAPL TSLA NVDA") + print(" python3 scripts/analyze_stock.py # 分析配置的所有股票") + sys.exit(1) + + print("\n" + "=" * 80) + print("🤖 美股手动分析脚本") + print("=" * 80) + print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + print(f"股票: {', '.join(symbols)}") + print("=" * 80) + + # 逐个分析 + for symbol in symbols: + await analyze_stock(symbol) + + print("\n" + "=" * 80) + print("✅ 分析完成!") + print("=" * 80) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n\n⚠️ 用户中断") + sys.exit(0) diff --git a/scripts/analyze_stock_simple.py b/scripts/analyze_stock_simple.py new file mode 100644 index 0000000..f402756 --- /dev/null +++ b/scripts/analyze_stock_simple.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +""" +美股快速分析脚本(简化版) + +用法: + cd backend && python3 ../scripts/analyze_stock_simple.py AAPL + cd backend && python3 ../scripts/analyze_stock_simple.py AAPL TSLA NVDA +""" +import sys +import os +import asyncio + +# 添加项目路径 +sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', 'backend')) + +from app.services.yfinance_service import get_yfinance_service +from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer +from app.utils.logger import logger + + +async def analyze(symbol: str): + """分析单只股票""" + print(f"\n{'='*60}") + print(f"📊 分析 {symbol}") + print(f"{'='*60}") + + try: + # 获取服务和数据 + yf_service = get_yfinance_service() + llm = LLMSignalAnalyzer() + + # 获取行情 + ticker = yf_service.get_ticker(symbol) + if not ticker: + print(f"❌ 无法获取行情") + return + + price = ticker['lastPrice'] + change = ticker['priceChangePercent'] + print(f"价格: ${price:,.2f} ({change:+.2f}%)") + + # 获取K线数据 + data = yf_service.get_multi_timeframe_data(symbol) + print(f"K线: {list(data.keys())}") + + # LLM分析 + print(f"\n🤖 LLM分析中...") + result = await llm.analyze(symbol, data, symbols=[symbol], position_info=None) + + # 输出结果 + summary = result.get('analysis_summary', '') + signals = result.get('signals', []) + + print(f"\n状态: {summary}") + + if signals: + print(f"\n🎯 发现 {len(signals)} 个信号:") + for sig in signals: + action = sig.get('action', 'wait') + grade = sig.get('grade', 'D') + conf = sig.get('confidence', 0) + + action_text = {'buy': '🟢 做多', 'sell': '🔴 做空'}.get(action, action) + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐'}.get(grade, '') + + print(f"\n {action_text} [{grade}{grade_icon}] {conf}%") + + entry = sig.get('entry_price') + sl = sig.get('stop_loss') + tp = sig.get('take_profit') + + if entry and sl and tp: + print(f" 入场: ${entry:,.2f}") + print(f" 止损: ${sl:,.2f}") + print(f" 止盈: ${tp:,.2f}") + + reason = sig.get('reason', '') + if reason: + print(f" 理由: {reason[:100]}...") + else: + print(f"\n⏸️ 无交易信号") + + except Exception as e: + print(f"❌ 错误: {e}") + + +async def main(): + symbols = sys.argv[1:] if len(sys.argv) > 1 else ['AAPL'] + + print(f"🤖 美股快速分析") + print(f"股票: {', '.join(symbols)}") + + for symbol in symbols: + await analyze(symbol.upper()) + + print(f"\n✅ 完成") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/scripts/stock.sh b/scripts/stock.sh new file mode 100755 index 0000000..014582a --- /dev/null +++ b/scripts/stock.sh @@ -0,0 +1,27 @@ +#!/bin/bash +# 美股分析快捷脚本 +# +# 用法: +# ./scripts/stock.sh AAPL +# ./scripts/stock.sh AAPL TSLA +# ./scripts/stock.sh # 分析配置的所有股票(会发送通知) + +cd "$(dirname "$0")/.." || exit 1 + +if [ $# -eq 0 ]; then + # 无参数,分析配置的所有股票 + echo "📊 分析配置的所有股票(将发送通知)..." + python3 -c " +import sys +sys.path.insert(0, 'backend') +from app.config import get_settings +settings = get_settings() +symbols = settings.stock_symbols.split(',') +print(' '.join(symbols)) +" 2>/dev/null | xargs python3 scripts/test_stock.py +else + # 分析指定的股票 + SYMBOLS="$@" + echo "📊 分析股票: $SYMBOLS(将发送通知)" + python3 scripts/test_stock.py $SYMBOLS +fi diff --git a/scripts/test_stock.py b/scripts/test_stock.py new file mode 100755 index 0000000..178825b --- /dev/null +++ b/scripts/test_stock.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +""" +美股分析脚本(修复版) + +用法: + python3 scripts/test_stock.py AAPL + python3 scripts/test_stock.py AAPL TSLA NVDA +""" +import sys +import os + +# 确保路径正确 +script_dir = os.path.dirname(os.path.abspath(__file__)) +project_root = os.path.dirname(script_dir) +backend_dir = os.path.join(project_root, 'backend') +sys.path.insert(0, backend_dir) + +import asyncio +from app.services.yfinance_service import get_yfinance_service +from app.services.feishu_service import get_feishu_service +from app.services.telegram_service import get_telegram_service +from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer +from app.utils.logger import logger + + +async def analyze(symbol: str, send_notification: bool = True): + """分析单只股票 + + Args: + symbol: 股票代码 + send_notification: 是否发送通知(默认True) + """ + print(f"\n{'='*60}") + print(f"📊 分析 {symbol}") + print(f"{'='*60}") + + try: + # 获取服务 + yf_service = get_yfinance_service() + llm = LLMSignalAnalyzer() + feishu = get_feishu_service() + telegram = get_telegram_service() + + # 获取行情 + print(f"获取行情...") + ticker = yf_service.get_ticker(symbol) + if not ticker: + print(f"❌ 无法获取 {symbol} 行情") + return + + price = ticker['lastPrice'] + change = ticker['priceChangePercent'] + print(f"价格: ${price:,.2f} ({change:+.2f}%)") + print(f"成交量: {ticker['volume']:,}") + + # 获取K线 + print(f"获取K线数据...") + data = yf_service.get_multi_timeframe_data(symbol) + + if not data: + print(f"❌ 无法获取K线数据") + return + + print(f"时间周期: {', '.join(data.keys())}") + + # LLM分析 + print(f"\n🤖 LLM分析中...\n") + result = await llm.analyze(symbol, data, symbols=[symbol], position_info=None) + + # 输出结果 + summary = result.get('analysis_summary', '') + signals = result.get('signals', []) + + print(f"市场状态: {summary}") + + if signals: + print(f"\n🎯 发现 {len(signals)} 个信号:") + for sig in signals: + action = sig.get('action', 'wait') + grade = sig.get('grade', 'D') + conf = sig.get('confidence', 0) + + action_text = {'buy': '🟢 做多', 'sell': '🔴 做空'}.get(action, action) + grade_icon = {'A': '⭐⭐⭐', 'B': '⭐⭐', 'C': '⭐'}.get(grade, '') + + print(f"\n{action_text} [{grade}{grade_icon}] {conf}%") + + entry = sig.get('entry_price') + sl = sig.get('stop_loss') + tp = sig.get('take_profit') + + if entry and sl and tp: + print(f" 入场: ${entry:,.2f}") + print(f" 止损: ${sl:,.2f}") + print(f" 止盈: ${tp:,.2f}") + + reason = sig.get('reason', '') + if reason: + # 限制理由长度 + short_reason = reason[:80] + "..." if len(reason) > 80 else reason + print(f" 理由: {short_reason}") + + # 发送通知(仅发送置信度 >= 60% 的信号) + if send_notification: + best_signal = None + for sig in signals: + if sig.get('confidence', 0) >= 60 and sig.get('grade', 'D') != 'D': + best_signal = sig + break + + if best_signal: + # 使用正确的方法格式化通知 + card = llm.format_feishu_card(best_signal, symbol) + title = card['title'] + content = card['content'] + + # 发送通知 + await feishu.send_markdown(title, content) + await telegram.send_message(llm.format_signal_message(best_signal, symbol)) + print(f"\n📬 通知已发送:{title}") + else: + print(f"\n⏸️ 置信度不足,不发送通知") + else: + print(f"\n⏸️ 无交易信号") + + except Exception as e: + print(f"❌ 错误: {e}") + import traceback + traceback.print_exc() + + +async def main(): + # 从命令行参数获取股票代码 + if len(sys.argv) < 2: + print("用法: python3 scripts/test_stock.py AAPL [TSLA] [NVDA] ...") + print("\n示例:") + print(" python3 scripts/test_stock.py AAPL") + print(" python3 scripts/test_stock.py AAPL TSLA NVDA") + sys.exit(1) + + symbols = sys.argv[1:] + + print("="*60) + print("🤖 美股分析脚本") + print("="*60) + print(f"股票: {', '.join(symbols)}") + print(f"时间: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + + for symbol in symbols: + await analyze(symbol.upper()) + + print("\n" + "="*60) + print("✅ 分析完成") + print("="*60) + + +if __name__ == "__main__": + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n\n⚠️ 用户中断")