#!/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.services.fundamental_service import get_fundamental_service from app.services.news_service import get_news_service from app.stock_agent.market_signal_analyzer import StockMarketSignalAnalyzer from app.config import get_settings from app.utils.logger import logger async def analyze(symbol: str, send_notification: bool = True): """分析单只股票 Args: symbol: 股票代码 send_notification: 是否发送通知(默认True) Returns: 分析结果字典 """ # 获取服务 yf_service = get_yfinance_service() market_analyzer = StockMarketSignalAnalyzer() # 使用新的市场信号分析器 fundamental = get_fundamental_service() # 基本面服务 news = get_news_service() # 新闻服务 feishu = get_feishu_service() telegram = get_telegram_service() result = { 'symbol': symbol, 'stock_name': '', # 从基本面数据获取 'price': 0, 'signals': [], 'notified': False } # 获取配置 settings = get_settings() threshold = settings.stock_llm_threshold * 100 # 转换为百分比 print(f"\n{'='*60}") print(f"📊 分析 {symbol}") print(f"{'='*60}") try: # 获取行情 print(f"获取行情...") ticker = yf_service.get_ticker(symbol) if not ticker: print(f"❌ 无法获取 {symbol} 行情") return result price = ticker['lastPrice'] change = ticker['priceChangePercent'] result['price'] = price 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 result print(f"时间周期: {', '.join(data.keys())}") # 获取基本面数据(包含公司名称) print(f"\n📈 基本面分析中...") fundamental_data = None fundamental_summary = "" stock_name = "" try: fundamental_data = fundamental.get_fundamental_data(symbol) if fundamental_data: # 传递已获取的数据,避免重复调用 fundamental_summary = fundamental.get_fundamental_summary(symbol, fundamental_data) # 获取公司名称 stock_name = fundamental_data.get('company_name', '') result['stock_name'] = stock_name # 输出基本面详细信息 score = fundamental_data.get('score', {}) print(f" ✓ 基本面数据获取成功") # 公司信息 company = fundamental_data.get('company_name', 'N/A') sector = fundamental_data.get('sector', 'N/A') print(f" 【公司】{company} | {sector}") # 更新显示 symbol_display = f"{stock_name}({symbol})" if stock_name else symbol print(f"\n{'='*60}") print(f"📊 分析 {symbol_display} @ ${price:,.2f}") print(f"{'='*60}") # 评分 print(f" 【评分】总分: {score.get('total', 0):.0f}/100 ({score.get('rating', 'N/A')}级) | " f"估值:{score.get('valuation', 0)} 盈利:{score.get('profitability', 0)} " f"成长:{score.get('growth', 0)} 财务:{score.get('financial_health', 0)}") # 估值指标 val = fundamental_data.get('valuation', {}) if val.get('pe_ratio'): pe = val['pe_ratio'] pb = val.get('pb_ratio') ps = val.get('ps_ratio') peg = val.get('peg_ratio') pb_str = f"{pb:.2f}" if pb is not None else "N/A" ps_str = f"{ps:.2f}" if ps is not None else "N/A" peg_str = f"{peg:.2f}" if peg is not None else "N/A" print(f" 【估值】PE:{pe:.2f} | PB:{pb_str} | PS:{ps_str} | PEG:{peg_str}") # 盈利能力 prof = fundamental_data.get('profitability', {}) if prof.get('return_on_equity'): roe = prof['return_on_equity'] pm = prof.get('profit_margin') gm = prof.get('gross_margin') pm_str = f"{pm:.1f}" if pm is not None else "N/A" gm_str = f"{gm:.1f}" if gm is not None else "N/A" print(f" 【盈利】ROE:{roe:.2f}% | 净利率:{pm_str}% | 毛利率:{gm_str}%") # 成长性 growth = fundamental_data.get('growth', {}) rg = growth.get('revenue_growth') eg = growth.get('earnings_growth') if rg is not None or eg is not None: rg_str = f"{rg:.1f}" if rg is not None else "N/A" eg_str = f"{eg:.1f}" if eg is not None else "N/A" print(f" 【成长】营收增长:{rg_str}% | 盈利增长:{eg_str}%") # 财务健康 fin = fundamental_data.get('financial_health', {}) if fin.get('debt_to_equity'): de = fin['debt_to_equity'] cr = fin.get('current_ratio') cr_str = f"{cr:.2f}" if cr is not None else "N/A" print(f" 【财务】债务股本比:{de:.2f} | 流动比率:{cr_str}") # 分析师建议 analyst = fundamental_data.get('analyst', {}) tp = analyst.get('target_price') if tp: rec = analyst.get('recommendation', 'N/A') print(f" 【分析师】目标价:${tp:.2f} | 评级:{rec}") else: print(f" ⚠️ 无法获取基本面数据") except Exception as e: print(f" ⚠️ 获取基本面数据失败: {e}") # 获取新闻数据 print(f"\n📰 新闻分析...") news_data = None try: news_data = await news.search_stock_news(symbol, stock_name, max_results=5) if news_data: print(f" 获取到 {len(news_data)} 条相关新闻") else: print(f" 暂无相关新闻") except Exception as e: print(f" ⚠️ 获取新闻数据失败: {e}") # 市场信号分析(使用新架构 - 技术面 + 基本面 + 新闻) print(f"\n🤖 市场信号分析中...\n") market_signal = await market_analyzer.analyze( symbol, data, symbols=[symbol], fundamental_data=fundamental_data, news_data=news_data ) # 输出结果 summary = market_signal.get('analysis_summary', '') signals = market_signal.get('signals', []) result['signals'] = 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}") # 发送通知(仅发送置信度 >= 阈值的信号) if send_notification: best_signal = None for sig in signals: if sig.get('confidence', 0) >= threshold and sig.get('grade', 'D') != 'D': best_signal = sig break if best_signal: # 使用格式化工具格式化通知 from app.utils.signal_formatter import get_signal_formatter formatter = get_signal_formatter() card = formatter.format_feishu_card(best_signal, symbol, agent_type='stock') title = card['title'] content = card['content'] # 发送通知 - 使用 send_card 方法 # 根据信号方向选择颜色 color = "green" if best_signal.get('action') == 'buy' else "red" await feishu.send_card(title, content, color) await telegram.send_message(formatter.format_signal_message(best_signal, symbol, agent_type='stock')) print(f"\n📬 通知已发送:{title}") result['notified'] = True else: print(f"\n⏸️ 置信度不足,不发送通知(阈值: {threshold}%)") else: print(f"\n⏸️ 无交易信号") return result except Exception as e: print(f"❌ 错误: {e}") import traceback traceback.print_exc() return result def print_summary_report(results: list, send_notification: bool = True): """打印汇总报告并发送通知 Args: results: 分析结果列表 send_notification: 是否发送通知(默认True) """ from app.config import get_settings settings = get_settings() threshold = settings.stock_llm_threshold * 100 # 获取阈值 total = len(results) with_signals = [r for r in results if r.get('signals')] notified = [r for r in results if r.get('notified')] # 区分美股和港股 us_results = [r for r in results if not r['symbol'].endswith('.HK')] hk_results = [r for r in results if r['symbol'].endswith('.HK')] # 统计信号 buy_count = 0 sell_count = 0 high_quality_signals = [] # A/B级信号且达到阈值 all_signals = [] # 所有信号 for r in with_signals: for sig in r.get('signals', []): sig['symbol'] = r['symbol'] sig['current_price'] = r.get('price', 0) sig['is_hk'] = r['symbol'].endswith('.HK') sig['stock_name'] = r.get('stock_name', '') all_signals.append(sig) if sig.get('action') == 'buy': buy_count += 1 elif sig.get('action') == 'sell': sell_count += 1 # 只统计达到阈值的A/B级信号 if sig.get('grade') in ['A', 'B'] and sig.get('confidence', 0) >= threshold: high_quality_signals.append(sig) # 按置信度排序 high_quality_signals.sort(key=lambda x: x.get('confidence', 0), reverse=True) all_signals.sort(key=lambda x: x.get('confidence', 0), reverse=True) # 打印汇总 print("\n" + "="*80) print("📊 股票分析汇总报告") print("="*80) print(f"分析总数: {total} 只 (美股: {len(us_results)}, 港股: {len(hk_results)})") print(f"有信号: {len(with_signals)} 只") print(f"已通知: {len(notified)} 只") print(f"通知阈值: {threshold}%") print("") # 显示高等级信号(达到阈值的) if high_quality_signals: print(f"⭐ 高等级信号达到阈值 (A/B级 >= {threshold}%): {len(high_quality_signals)} 个") for sig in high_quality_signals[:10]: symbol = sig['symbol'] stock_name = sig.get('stock_name', '') market_tag = '[港股]' if sig.get('is_hk') else '[美股]' action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空' grade = sig.get('grade', 'D') confidence = sig.get('confidence', 0) price = sig.get('current_price', 0) entry = sig.get('entry_price', 0) # 构建带名称的股票显示 symbol_display = f"{stock_name}({symbol})" if stock_name else symbol print(f" {market_tag} {symbol_display} {action} [{grade}级] {confidence}% @ ${price:,.2f}") if entry > 0: print(f" 入场: ${entry:,.2f}") print("") # 显示未达到阈值但质量不错的信号 below_threshold = [s for s in all_signals if s.get('grade') in ['A', 'B'] and s.get('confidence', 0) < threshold] if below_threshold: print(f"⚠️ 以下信号未达到通知阈值 ({threshold}%):") for sig in below_threshold[:10]: symbol = sig['symbol'] stock_name = sig.get('stock_name', '') market_tag = '[港股]' if sig.get('is_hk') else '[美股]' action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空' grade = sig.get('grade', 'D') confidence = sig.get('confidence', 0) symbol_display = f"{stock_name}({symbol})" if stock_name else symbol print(f" {market_tag} {symbol_display} {action} {grade}级 {confidence}%") print("") # 统计汇总 print(f"📈 做多信号: {buy_count} 个") print(f"📉 做空信号: {sell_count} 个") print("="*80) # 发送汇总通知 if send_notification: asyncio.run(send_summary_notification( results, total, with_signals, notified, buy_count, sell_count, high_quality_signals, all_signals, threshold, len(us_results), len(hk_results) )) async def send_summary_notification( results: list, total: int, with_signals: list, notified: list, buy_count: int, sell_count: int, high_quality_signals: list, all_signals: list, threshold: float, us_count: int = 0, hk_count: int = 0 ): """发送汇总报告到飞书和Telegram Args: results: 分析结果列表 total: 总数 with_signals: 有信号的股票列表 notified: 已通知的股票列表 buy_count: 做多信号数量 sell_count: 做空信号数量 high_quality_signals: 达到阈值的高等级信号列表 all_signals: 所有信号列表 threshold: 通知阈值 us_count: 美股数量 hk_count: 港股数量 """ try: from datetime import datetime feishu = get_feishu_service() telegram = get_telegram_service() now = datetime.now() # 构建飞书汇总内容 content_parts = [ f"**📊 股票分析汇总报告**", f"", f"⏰ 时间: {now.strftime('%Y-%m-%d %H:%M')}", f"", f"📊 **分析概况**", f"• 美股: {us_count} 只 | 港股: {hk_count} 只", f"• 发现信号: {len(with_signals)} 只", f"• 已发通知: {len(notified)} 只", f"• 通知阈值: {threshold:.0f}%", f"", ] # 高等级信号(达到阈值的) if high_quality_signals: content_parts.append(f"⭐ **高等级信号 (A/B级 ≥ {threshold:.0f}%)**") for sig in high_quality_signals[:5]: symbol = sig['symbol'] stock_name = sig.get('stock_name', '') market_tag = '[港股]' if sig.get('is_hk') else '[美股]' action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空' grade = sig.get('grade', 'D') confidence = sig.get('confidence', 0) # 构建带名称的股票显示 symbol_display = f"{stock_name}({symbol})" if stock_name else symbol content_parts.append(f"• {market_tag} {symbol_display} {action} {grade}级 {confidence}%") content_parts.append(f"") # 信号统计 content_parts.extend([ f"📈 做多信号: {buy_count} 个", f"📉 做空信号: {sell_count} 个", f"", f"*⚠️ 仅供参考,不构成投资建议*" ]) content = "\n".join(content_parts) # 发送飞书 title = f"📊 股票分析汇总 ({now.strftime('%H:%M')})" color = "blue" await feishu.send_card(title, content, color) # 发送 Telegram telegram_msg = f"📊 *股票分析汇总*\n\n" telegram_msg += f"时间: {now.strftime('%H:%M')}\n" telegram_msg += f"美股: {us_count}只 | 港股: {hk_count}只\n" telegram_msg += f"信号: {len(with_signals)}只 | 通知: {len(notified)}只\n" telegram_msg += f"阈值: {threshold:.0f}%\n\n" if high_quality_signals: telegram_msg += f"⭐ *高等级信号 (≥{threshold:.0f}%)*\n" for sig in high_quality_signals[:5]: symbol = sig['symbol'] stock_name = sig.get('stock_name', '') market_tag = '[港股]' if sig.get('is_hk') else '[美股]' action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空' grade = sig.get('grade', 'D') confidence = sig.get('confidence', 0) symbol_display = f"{stock_name}({symbol})" if stock_name else symbol telegram_msg += f"{market_tag} {symbol_display} {action} {grade}级 {confidence}%\n" telegram_msg += "\n" telegram_msg += f"做多: {buy_count} | 做空: {sell_count}" await telegram.send_message(telegram_msg) print(f"\n📬 汇总报告已发送到飞书和Telegram") 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) # 过滤掉无效的参数(如环境变量路径、配置项等) # 只接受有效的股票代码(字母开头,1-5个字符,或包含数字的港股代码如0700.HK) raw_symbols = sys.argv[1:] symbols = [] for arg in raw_symbols: # 跳过包含路径分隔符、等号、或明显不是股票代码的参数 if '/' in arg or '=' in arg or ':' in arg or len(arg) > 10: logger.debug(f"跳过无效参数: {arg}") continue # 接受纯字母的参数(美股股票代码) if arg.isalpha() and 1 <= len(arg) <= 5: symbols.append(arg.upper()) # 接受包含数字和点号的参数(港股代码,如0700.HK) elif '.HK' in arg.upper() and len(arg) <= 10: symbols.append(arg.upper()) else: logger.debug(f"跳过非股票代码参数: {arg}") if not symbols: print("❌ 未找到有效的股票代码") print(f"接收到的参数: {raw_symbols}") print("\n用法: python3 scripts/test_stock.py AAPL [TSLA] [NVDA] ...") sys.exit(1) print("="*60) print("🤖 美股分析脚本") print("="*60) print(f"股票: {', '.join(symbols)}") print(f"时间: {__import__('datetime').datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") # 收集所有分析结果 results = [] for symbol in symbols: result = await analyze(symbol.upper(), send_notification=True) results.append(result) # 生成汇总报告并发送通知 await send_summary_notification_async(results) print("\n" + "="*60) print("✅ 分析完成") print("="*60) async def send_summary_notification_async(results: list): """异步发送汇总通知""" from app.config import get_settings settings = get_settings() threshold = settings.stock_llm_threshold * 100 # 获取阈值 total = len(results) with_signals = [r for r in results if r.get('signals')] notified = [r for r in results if r.get('notified')] # 区分美股和港股 us_results = [r for r in results if not r['symbol'].endswith('.HK')] hk_results = [r for r in results if r['symbol'].endswith('.HK')] # 统计信号 buy_count = 0 sell_count = 0 high_quality_signals = [] # A/B级信号且达到阈值 all_signals = [] # 所有信号 for r in with_signals: for sig in r.get('signals', []): sig['symbol'] = r['symbol'] sig['current_price'] = r.get('price', 0) sig['is_hk'] = r['symbol'].endswith('.HK') sig['stock_name'] = r.get('stock_name', '') all_signals.append(sig) if sig.get('action') == 'buy': buy_count += 1 elif sig.get('action') == 'sell': sell_count += 1 # 只统计达到阈值的A/B级信号 if sig.get('grade') in ['A', 'B'] and sig.get('confidence', 0) >= threshold: high_quality_signals.append(sig) # 按置信度排序 high_quality_signals.sort(key=lambda x: x.get('confidence', 0), reverse=True) all_signals.sort(key=lambda x: x.get('confidence', 0), reverse=True) # 打印汇总 print("\n" + "="*80) print("📊 股票分析汇总报告") print("="*80) print(f"分析总数: {total} 只 (美股: {len(us_results)}, 港股: {len(hk_results)})") print(f"有信号: {len(with_signals)} 只") print(f"已通知: {len(notified)} 只") print(f"通知阈值: {threshold}%") print("") # 显示高等级信号(达到阈值的) if high_quality_signals: print(f"⭐ 高等级信号达到阈值 (A/B级 >= {threshold}%): {len(high_quality_signals)} 个") for sig in high_quality_signals[:10]: symbol = sig['symbol'] stock_name = sig.get('stock_name', '') market_tag = '[港股]' if sig.get('is_hk') else '[美股]' action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空' grade = sig.get('grade', 'D') confidence = sig.get('confidence', 0) price = sig.get('current_price', 0) entry = sig.get('entry_price', 0) # 构建带名称的股票显示 symbol_display = f"{stock_name}({symbol})" if stock_name else symbol print(f" {market_tag} {symbol_display} {action} [{grade}级] {confidence}% @ ${price:,.2f}") if entry > 0: print(f" 入场: ${entry:,.2f}") print("") # 显示未达到阈值但质量不错的信号 below_threshold = [s for s in all_signals if s.get('grade') in ['A', 'B'] and s.get('confidence', 0) < threshold] if below_threshold: print(f"⚠️ 以下信号未达到通知阈值 ({threshold}%):") for sig in below_threshold[:10]: symbol = sig['symbol'] stock_name = sig.get('stock_name', '') market_tag = '[港股]' if sig.get('is_hk') else '[美股]' action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空' grade = sig.get('grade', 'D') confidence = sig.get('confidence', 0) # 构建带名称的股票显示 symbol_display = f"{stock_name}({symbol})" if stock_name else symbol print(f" {market_tag} {symbol_display} {action} {grade}级 {confidence}%") print("") # 统计汇总 print(f"📈 做多信号: {buy_count} 个") print(f"📉 做空信号: {sell_count} 个") print("="*80) # 发送汇总通知 await send_summary_notification( results, total, with_signals, notified, buy_count, sell_count, high_quality_signals, all_signals, threshold, len(us_results), len(hk_results) ) if __name__ == "__main__": try: asyncio.run(main()) except KeyboardInterrupt: print("\n\n⚠️ 用户中断")