From df250a920ba6380a840398eddce00957b51f49ac Mon Sep 17 00:00:00 2001 From: aaron <> Date: Fri, 20 Feb 2026 12:47:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=20HK=20=E7=9A=84=E6=94=AF?= =?UTF-8?q?=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/config.py | 5 +- .../app/crypto_agent/llm_signal_analyzer.py | 45 +++- backend/app/stock_agent/stock_agent.py | 204 ++++++++++++++---- scripts/stock.sh | 71 ++++-- scripts/test_stock.py | 181 ++++++++++++++-- 5 files changed, 429 insertions(+), 77 deletions(-) diff --git a/backend/app/config.py b/backend/app/config.py index 855120a..fee1386 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -139,8 +139,9 @@ class Settings(BaseSettings): crypto_agent_model: str = "deepseek" # CryptoAgent 使用的模型 stock_agent_model: str = "deepseek" # StockAgent 使用的模型 - # 美股智能体配置 - stock_symbols: str = "AAPL,TSLA,NVDA,MSFT,GOOGL" # 监控的股票代码,逗号分隔 + # 股票智能体配置 + stock_symbols_us: str = "" # 美股代码,逗号分隔 + stock_symbols_hk: str = "" # 港股代码,逗号分隔(以.HK结尾) stock_analysis_interval: int = 300 # 分析间隔(秒,默认5分钟) stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值 diff --git a/backend/app/crypto_agent/llm_signal_analyzer.py b/backend/app/crypto_agent/llm_signal_analyzer.py index c262af9..ec04475 100644 --- a/backend/app/crypto_agent/llm_signal_analyzer.py +++ b/backend/app/crypto_agent/llm_signal_analyzer.py @@ -222,13 +222,20 @@ class LLMSignalAnalyzer: agent_name_map = { 'crypto': '加密货币', - 'stock': '美股', + 'stock': '股票', # 改为通用的"股票",具体市场类型会在分析时根据符号判断 'smart': '智能助手' } agent_name = agent_name_map.get(agent_type, '未知') logger.info(f"LLM 信号分析器初始化完成({agent_name},模型: {self.model_override or '默认'})") + def _get_market_type(self, symbol: str) -> str: + """根据股票代码判断市场类型""" + if symbol.endswith('.HK'): + return '港股' + else: + return '美股' + async def analyze(self, symbol: str, data: Dict[str, pd.DataFrame], symbols: List[str] = None, position_info: Dict[str, Any] = None) -> Dict[str, Any]: @@ -249,6 +256,9 @@ class LLMSignalAnalyzer: 分析结果 """ try: + # 获取市场类型 + market_type = self._get_market_type(symbol) if self.agent_type == 'stock' else '' + # 获取新闻数据 news_text = await self._get_news_context(symbol, symbols or [symbol]) @@ -274,11 +284,11 @@ class LLMSignalAnalyzer: signals = result.get('signals', []) if signals: for sig in signals: - logger.info(f"{symbol} [{sig['type']}] {sig['action']} " + logger.info(f"{symbol} [{market_type}][{sig['type']}] {sig['action']} " f"置信度:{sig['confidence']}% 等级:{sig['grade']} " f"原因:{sig['reason'][:50]}...") else: - logger.info(f"{symbol} 无交易信号 - {result.get('analysis_summary', '观望')}") + logger.info(f"{symbol} [{market_type}] 无交易信号 - {result.get('analysis_summary', '观望')}") return result @@ -1066,6 +1076,10 @@ class LLMSignalAnalyzer: Returns: 格式化的消息文本 """ + # 获取股票名称 + from app.stock_agent.stock_agent import STOCK_NAMES + stock_name = STOCK_NAMES.get(symbol, '') + type_map = { 'short_term': '短线', 'medium_term': '中线', @@ -1105,7 +1119,10 @@ class LLMSignalAnalyzer: sl_percent = ((sl - entry) / entry * 100) if entry else 0 tp_percent = ((tp - entry) / entry * 100) if entry else 0 - message = f"""📊 {symbol} {signal_type}信号 + # 构建标题(带股票名称) + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol + + message = f"""📊 {symbol_display} {signal_type}信号 {action_icon} **方向**: {action} {entry_type_icon} **入场**: {entry_type_text} @@ -1136,6 +1153,10 @@ class LLMSignalAnalyzer: Returns: 包含 title, content, color 的字典 """ + # 获取股票名称 + from app.stock_agent.stock_agent import STOCK_NAMES + stock_name = STOCK_NAMES.get(symbol, '') + type_map = { 'short_term': '短线', 'medium_term': '中线', @@ -1165,16 +1186,24 @@ class LLMSignalAnalyzer: position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱') position_text = position_map.get(position_size, '轻仓') - # 标题和颜色 - 添加美股标记 + # 标题和颜色 - 区分美股/港股 is_market_order = entry_type == 'market' market_badge = '【现价】' if is_market_order else '' - stock_tag = '[美股] ' + + # 识别市场类型(港股以 .HK 结尾) + if symbol.endswith('.HK'): + market_tag = '[港股] ' + else: + market_tag = '[美股] ' + + # 构建带名称的股票显示 + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol if signal['action'] == 'buy': - title = f"🟢 {stock_tag}{symbol} {signal_type}做多信号 {market_badge}" + title = f"🟢 {market_tag}{symbol_display} {signal_type}做多信号 {market_badge}" color = "green" else: - title = f"🔴 {stock_tag}{symbol} {signal_type}做空信号 {market_badge}" + title = f"🔴 {market_tag}{symbol_display} {signal_type}做空信号 {market_badge}" color = "red" # 计算风险收益比 diff --git a/backend/app/stock_agent/stock_agent.py b/backend/app/stock_agent/stock_agent.py index c13afc8..3aa220f 100644 --- a/backend/app/stock_agent/stock_agent.py +++ b/backend/app/stock_agent/stock_agent.py @@ -16,6 +16,66 @@ from app.services.signal_database_service import get_signal_db_service from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer +# 股票名称映射表 +STOCK_NAMES = { + # 美股 - 科技龙头 + 'AAPL': '苹果', + 'MSFT': '微软', + 'GOOGL': '谷歌', + 'META': 'Meta', + 'AMZN': '亚马逊', + 'NVDA': '英伟达', + 'AMD': 'AMD', + 'AVGO': '博通', + 'ARM': 'ARM', + 'PLTR': 'Palantir', + 'SNOW': 'Snowflake', + + # 美股 - 生物医疗 + 'LLY': '礼来', + 'NVO': '诺和诺德', + 'VRTX': 'Vertex', + + # 美股 - 新能源/汽车 + 'TSLA': '特斯拉', + 'ENPH': 'Enphase', + + # 美股 - 金融 + 'V': 'Visa', + 'MA': 'Mastercard', + + # 美股 - 消费 + 'HD': 'Home Depot', + 'COST': 'Costco', + + # 美股 - 其他 + 'RKLB': 'Relativity Space', + 'HOOD': 'Robinhood', + 'DXYZ': 'DEX', + 'GLW': '康宁', + 'UNTY': 'Unity', + 'CRM': 'Salesforce', + 'ADBE': 'Adobe', + 'INTC': '英特尔', + 'FSLR': 'First Solar', + 'CRWD': 'CrowdStrike', + 'SHOP': 'Shopify', + 'NET': 'Cloudflare', + 'COIN': 'Coinbase', + 'MSTR': 'MicroStrategy', + + # 港股 + '0700.HK': '腾讯', + '9988.HK': '阿里巴巴', + '1810.HK': '小米', + '2015.HK': '理想汽车', + '9866.HK': '蔚来', + '9992.HK': '泡泡玛特', + '9626.HK': '哔哩哔哩', + '9880.HK': '优必选', +} + + class StockAgent: """美股交易信号智能体(LLM 驱动,仅分析通知)""" @@ -32,15 +92,22 @@ class StockAgent: self.last_signals: Dict[str, Dict[str, Any]] = {} self.signal_cooldown: Dict[str, datetime] = {} - # 配置 - self.symbols = self.settings.stock_symbols.split(',') + # 配置 - 分别读取美股和港股 + us_symbols = self.settings.stock_symbols_us.split(',') if self.settings.stock_symbols_us else [] + hk_symbols = self.settings.stock_symbols_hk.split(',') if self.settings.stock_symbols_hk else [] + self.symbols = us_symbols + hk_symbols # 运行状态 self.running = False self._event_loop = None self._task = None - logger.info(f"美股智能体初始化完成,监控股票: {self.symbols}") + logger.info(f"股票智能体初始化完成 - 美股: {len(us_symbols)}只, 港股: {len(hk_symbols)}只, 总计: {len(self.symbols)}只") + + @staticmethod + def get_stock_name(symbol: str) -> str: + """获取股票中文名称""" + return STOCK_NAMES.get(symbol, symbol) async def start(self): """启动智能体""" @@ -69,7 +136,7 @@ class StockAgent: logger.info("美股智能体已停止") async def _analysis_loop(self): - """分析循环 - 只在美股交易时间内运行,整点执行""" + """分析循环 - 根据交易时间分析对应市场的股票""" while self.running: try: # 计算距离下一个整点的时间 @@ -82,17 +149,36 @@ class StockAgent: # 等待到整点 await asyncio.sleep(wait_seconds) - # 检查是否在美股交易时间 - if not self._is_market_hours(): - logger.debug("非美股交易时间,跳过本次分析") - # 继续等待下一个整点 + # 分类股票:美股和港股 + us_stocks = [s for s in self.symbols if not s.endswith('.HK')] + hk_stocks = [s for s in self.symbols if s.endswith('.HK')] + + # 检查各市场交易时间 + us_market_open = self._is_market_hours('US') + hk_market_open = self._is_market_hours('0700.HK') + + if not us_market_open and not hk_market_open: + logger.debug("非交易时间(美股和港股均闭市),跳过本次分析") continue - # 在交易时间内,分析所有股票并收集结果 - logger.info(f"开始分析 {len(self.symbols)} 只股票") + # 确定要分析的股票列表 + stocks_to_analyze = [] + if us_market_open: + stocks_to_analyze.extend(us_stocks) + logger.info(f"美股交易时间,分析 {len(us_stocks)} 只美股") + if hk_market_open: + stocks_to_analyze.extend(hk_stocks) + logger.info(f"港股交易时间,分析 {len(hk_stocks)} 只港股") + + if not stocks_to_analyze: + logger.debug("没有需要分析的股票") + continue + + # 分析股票并收集结果 + logger.info(f"开始分析 {len(stocks_to_analyze)} 只股票") analysis_results = [] - for symbol in self.symbols: + for symbol in stocks_to_analyze: if not self.running: break result = await self.analyze_symbol(symbol) @@ -108,15 +194,23 @@ class StockAgent: logger.error(f"分析循环出错: {e}") await asyncio.sleep(60) # 出错后等待 1 分钟再重试 - def _is_market_hours(self) -> bool: + def _is_market_hours(self, symbol: str = None) -> bool: """ - 判断当前是否在美股交易时间 + 判断当前是否在交易时间 美股交易时间: 周一至周五 9:30-16:00 (EST) 北京时间: - 冬令时 (11月-3月): 22:30-05:00 (次日) - 夏令时 (3月-11月): 21:30-04:00 (次日) + 港股交易时间: 周一至周五 + 北京时间: + - 上午: 09:30-12:00 + - 下午: 13:00-16:00 + + Args: + symbol: 股票代码(用于判断是美股还是港股) + Returns: 是否在交易时间 """ @@ -129,27 +223,39 @@ class StockAgent: if now.weekday() >= 5: # 5=周六, 6=周日 return False + # 判断是港股还是美股 + is_hk_stock = symbol and symbol.endswith('.HK') if symbol else 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 + if is_hk_stock: + # 港股交易时间: 09:30-12:00 或 13:00-16:00 + return (930 <= current_time < 1200) or (1300 <= current_time < 1600) else: - # 冬令时: 22:30-05:00 (次日) - # 即 2230-2359 或 0000-0500 - if current_time >= 2230 or current_time < 500: - return True + # 美股交易时间 + # 判断夏令时/冬令时(简单判断: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 + def _is_any_market_hours(self) -> bool: + """判断当前是否在任一市场的交易时间(美股或港股)""" + return self._is_market_hours('US') or self._is_market_hours('0700.HK') + async def analyze_symbol(self, symbol: str) -> Optional[Dict[str, Any]]: """ 分析单个股票 @@ -185,8 +291,12 @@ class StockAgent: current_price = ticker['lastPrice'] result['current_price'] = current_price + # 获取股票中文名称 + stock_name = STOCK_NAMES.get(symbol, '') + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol + logger.info(f"\n{'='*60}") - logger.info(f"📊 分析 {symbol} @ ${current_price:,.2f}") + logger.info(f"📊 分析 {symbol_display} @ ${current_price:,.2f}") logger.info(f"{'='*60}") # 4. LLM 分析 @@ -402,6 +512,12 @@ class StockAgent: 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')] + us_with_signals = [r for r in us_results if r.get('signals')] + hk_with_signals = [r for r in hk_results if r.get('signals')] + # 统计信号 buy_signals = [] sell_signals = [] @@ -411,6 +527,8 @@ class StockAgent: for sig in r.get('signals', []): sig['symbol'] = r['symbol'] sig['current_price'] = r.get('current_price', 0) + sig['is_hk'] = r['symbol'].endswith('.HK') + sig['stock_name'] = STOCK_NAMES.get(r['symbol'], '') if sig.get('action') == 'buy': buy_signals.append(sig) @@ -425,11 +543,11 @@ class StockAgent: # 构建汇总报告 logger.info(f"\n{'='*80}") - logger.info(f"📊 美股分析汇总报告") + logger.info(f"📊 股票分析汇总报告") logger.info(f"{'='*80}") logger.info(f"时间: {now.strftime('%Y-%m-%d %H:%M:%S')}") - logger.info(f"分析数量: {total} 只股票") - logger.info(f"有信号: {len(with_signals)} 只") + logger.info(f"分析总数: {total} 只 (美股: {len(us_results)}, 港股: {len(hk_results)})") + logger.info(f"有信号: {len(with_signals)} 只 (美股: {len(us_with_signals)}, 港股: {len(hk_with_signals)})") logger.info(f"已通知: {len(notified)} 只") logger.info(f"") @@ -438,13 +556,18 @@ class StockAgent: logger.info(f"⭐ 高等级信号 (A/B级): {len(high_quality_signals)} 个") for sig in high_quality_signals[:10]: # 最多显示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) - logger.info(f" {symbol} {action} [{grade}级] {confidence}% @ ${price:,.2f}") + # 构建带名称的股票显示 + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol + + logger.info(f" {market_tag} {symbol_display} {action} [{grade}级] {confidence}% @ ${price:,.2f}") if entry > 0: logger.info(f" 入场: ${entry:,.2f}") logger.info(f"") @@ -457,7 +580,8 @@ class StockAgent: # 发送飞书汇总 await self._send_feishu_summary( now, total, with_signals, notified, - buy_signals, sell_signals, high_quality_signals + buy_signals, sell_signals, high_quality_signals, + len(us_results), len(hk_results) ) except Exception as e: @@ -473,18 +597,20 @@ class StockAgent: notified: List, buy_signals: List, sell_signals: List, - high_quality_signals: List + high_quality_signals: List, + us_count: int = 0, + hk_count: int = 0 ): """发送飞书汇总报告""" try: # 构建内容 content_parts = [ - f"**美股分析汇总报告**", + f"**📊 股票分析汇总报告**", f"", f"⏰ 时间: {now.strftime('%Y-%m-%d %H:%M')}", f"", f"📊 **分析概况**", - f"• 分析总数: {total} 只", + f"• 美股: {us_count} 只 | 港股: {hk_count} 只", f"• 发现信号: {len(with_signals)} 只", f"• 已发通知: {len(notified)} 只", f"", @@ -495,10 +621,16 @@ class StockAgent: content_parts.append(f"⭐ **高等级信号 (A/B级)**") 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) - content_parts.append(f"• {symbol} {action} {grade}级 {confidence}%") + + # 构建带名称的股票显示 + 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"") # 信号统计 @@ -512,7 +644,7 @@ class StockAgent: content = "\n".join(content_parts) # 发送飞书 - title = f"📊 美股分析汇总 ({now.strftime('%H:%M')})" + title = f"📊 股票分析汇总 ({now.strftime('%H:%M')})" color = "blue" await self.feishu.send_card(title, content, color) diff --git a/scripts/stock.sh b/scripts/stock.sh index 04b0776..e482108 100755 --- a/scripts/stock.sh +++ b/scripts/stock.sh @@ -1,34 +1,77 @@ #!/bin/bash -# 美股分析快捷脚本 +# 股票分析快捷脚本 # # 用法: -# ./scripts/stock.sh AAPL -# ./scripts/stock.sh AAPL TSLA -# ./scripts/stock.sh # 分析配置的所有股票(会发送通知) +# ./scripts/stock.sh # 分析配置的所有股票(美股+港股) +# ./scripts/stock.sh us # 只分析美股 +# ./scripts/stock.sh hk # 只分析港股 +# ./scripts/stock.sh AAPL # 分析指定股票 +# ./scripts/stock.sh AAPL TSLA # 分析多个指定股票 cd "$(dirname "$0")/.." || exit 1 if [ $# -eq 0 ]; then - # 无参数,分析配置的所有股票 - echo "📊 分析配置的所有股票(将发送通知)..." + # 无参数,分析配置的所有股票(美股+港股) + echo "📊 分析配置的所有股票(美股+港股,将发送通知)..." # 直接从 .env 文件读取股票代码 if [ -f .env ]; then - # 使用 grep 提取 STOCK_SYMBOLS 行,然后提取值 - STOCKS=$(grep "^STOCK_SYMBOLS=" .env | cut -d'=' -f2) + # 使用 grep 提取美股和港股代码 + US_STOCKS=$(grep "^STOCK_SYMBOLS_US=" .env | cut -d'=' -f2) + HK_STOCKS=$(grep "^STOCK_SYMBOLS_HK=" .env | cut -d'=' -f2) - if [ -z "$STOCKS" ]; then + # 合并股票列表 + if [ -z "$US_STOCKS" ] && [ -z "$HK_STOCKS" ]; then echo "❌ 无法从 .env 文件读取股票列表" exit 1 fi - echo "📋 股票列表: $STOCKS" - - # 使用 read array 来正确处理空格分隔的股票代码 # 将逗号分隔转换为空格分隔 - STOCKS_SPACE=$(echo "$STOCKS" | tr ',' ' ') + STOCKS_SPACE=$(echo "$US_STOCKS,$HK_STOCKS" | tr ',' ' ') + + echo "📋 股票列表: $STOCKS_SPACE" + + # 直接传递给 test_stock.py + python3 scripts/test_stock.py $STOCKS_SPACE + else + echo "❌ .env 文件不存在" + exit 1 + fi +elif [ "$1" = "us" ]; then + # 只分析美股 + echo "📊 分析美股(将发送通知)..." + + if [ -f .env ]; then + US_STOCKS=$(grep "^STOCK_SYMBOLS_US=" .env | cut -d'=' -f2) + + if [ -z "$US_STOCKS" ]; then + echo "❌ 无法从 .env 文件读取美股列表" + exit 1 + fi + + STOCKS_SPACE=$(echo "$US_STOCKS" | tr ',' ' ') + echo "📋 美股列表: $STOCKS_SPACE" + + python3 scripts/test_stock.py $STOCKS_SPACE + else + echo "❌ .env 文件不存在" + exit 1 + fi +elif [ "$1" = "hk" ]; then + # 只分析港股 + echo "📊 分析港股(将发送通知)..." + + if [ -f .env ]; then + HK_STOCKS=$(grep "^STOCK_SYMBOLS_HK=" .env | cut -d'=' -f2) + + if [ -z "$HK_STOCKS" ]; then + echo "❌ 无法从 .env 文件读取港股列表" + exit 1 + fi + + STOCKS_SPACE=$(echo "$HK_STOCKS" | tr ',' ' ') + echo "📋 港股列表: $STOCKS_SPACE" - # 直接传递给 test_stock.py(不要用 while read 循环) python3 scripts/test_stock.py $STOCKS_SPACE else echo "❌ .env 文件不存在" diff --git a/scripts/test_stock.py b/scripts/test_stock.py index 8de92ca..b270e46 100755 --- a/scripts/test_stock.py +++ b/scripts/test_stock.py @@ -34,6 +34,9 @@ async def analyze(symbol: str, send_notification: bool = True): Returns: 分析结果字典 """ + # 导入股票名称映射 + from app.stock_agent.stock_agent import STOCK_NAMES + result = { 'symbol': symbol, 'price': 0, @@ -45,8 +48,12 @@ async def analyze(symbol: str, send_notification: bool = True): settings = get_settings() threshold = settings.stock_llm_threshold * 100 # 转换为百分比 + # 获取股票中文名称 + stock_name = STOCK_NAMES.get(symbol, '') + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol + print(f"\n{'='*60}") - print(f"📊 分析 {symbol}") + print(f"📊 分析 {symbol_display}") print(f"{'='*60}") try: @@ -160,6 +167,7 @@ def print_summary_report(results: list, send_notification: bool = True): send_notification: 是否发送通知(默认True) """ from app.config import get_settings + from app.stock_agent.stock_agent import STOCK_NAMES settings = get_settings() threshold = settings.stock_llm_threshold * 100 # 获取阈值 @@ -167,6 +175,10 @@ def print_summary_report(results: list, send_notification: bool = True): 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 @@ -177,6 +189,8 @@ def print_summary_report(results: list, send_notification: bool = True): 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'] = STOCK_NAMES.get(r['symbol'], '') all_signals.append(sig) if sig.get('action') == 'buy': @@ -194,9 +208,9 @@ def print_summary_report(results: list, send_notification: bool = True): # 打印汇总 print("\n" + "="*80) - print("📊 美股分析汇总报告") + print("📊 股票分析汇总报告") print("="*80) - print(f"分析数量: {total} 只股票") + print(f"分析总数: {total} 只 (美股: {len(us_results)}, 港股: {len(hk_results)})") print(f"有信号: {len(with_signals)} 只") print(f"已通知: {len(notified)} 只") print(f"通知阈值: {threshold}%") @@ -207,13 +221,18 @@ def print_summary_report(results: list, send_notification: bool = True): 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) - print(f" {symbol} {action} [{grade}级] {confidence}% @ ${price:,.2f}") + # 构建带名称的股票显示 + 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("") @@ -225,10 +244,14 @@ def print_summary_report(results: list, send_notification: bool = True): 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) - print(f" {symbol} {action} {grade}级 {confidence}%") + + symbol_display = f"{stock_name}({symbol})" if stock_name else symbol + print(f" {market_tag} {symbol_display} {action} {grade}级 {confidence}%") print("") # 统计汇总 @@ -240,7 +263,8 @@ def print_summary_report(results: list, send_notification: bool = True): if send_notification: asyncio.run(send_summary_notification( results, total, with_signals, notified, - buy_count, sell_count, high_quality_signals, all_signals, threshold + buy_count, sell_count, high_quality_signals, all_signals, threshold, + len(us_results), len(hk_results) )) @@ -253,7 +277,9 @@ async def send_summary_notification( sell_count: int, high_quality_signals: list, all_signals: list, - threshold: float + threshold: float, + us_count: int = 0, + hk_count: int = 0 ): """发送汇总报告到飞书和Telegram @@ -267,6 +293,8 @@ async def send_summary_notification( high_quality_signals: 达到阈值的高等级信号列表 all_signals: 所有信号列表 threshold: 通知阈值 + us_count: 美股数量 + hk_count: 港股数量 """ try: from datetime import datetime @@ -277,12 +305,12 @@ async def send_summary_notification( # 构建飞书汇总内容 content_parts = [ - f"**📊 美股分析汇总报告**", + f"**📊 股票分析汇总报告**", f"", f"⏰ 时间: {now.strftime('%Y-%m-%d %H:%M')}", f"", f"📊 **分析概况**", - f"• 分析总数: {total} 只", + f"• 美股: {us_count} 只 | 港股: {hk_count} 只", f"• 发现信号: {len(with_signals)} 只", f"• 已发通知: {len(notified)} 只", f"• 通知阈值: {threshold:.0f}%", @@ -294,10 +322,16 @@ async def send_summary_notification( 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) - content_parts.append(f"• {symbol} {action} {grade}级 {confidence}%") + + # 构建带名称的股票显示 + 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"") # 信号统计 @@ -311,25 +345,30 @@ async def send_summary_notification( content = "\n".join(content_parts) # 发送飞书 - title = f"📊 美股分析汇总 ({now.strftime('%H:%M')})" + title = f"📊 股票分析汇总 ({now.strftime('%H:%M')})" color = "blue" await feishu.send_card(title, content, color) # 发送 Telegram - telegram_msg = f"📊 *美股分析汇总*\n\n" + telegram_msg = f"📊 *股票分析汇总*\n\n" telegram_msg += f"时间: {now.strftime('%H:%M')}\n" - telegram_msg += f"分析: {total}只 | 信号: {len(with_signals)}只 | 通知: {len(notified)}只\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) - telegram_msg += f"{symbol} {action} {grade}级 {confidence}%\n" + + 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}" @@ -354,7 +393,7 @@ async def main(): sys.exit(1) # 过滤掉无效的参数(如环境变量路径、配置项等) - # 只接受有效的股票代码(字母开头,1-5个字符) + # 只接受有效的股票代码(字母开头,1-5个字符,或包含数字的港股代码如0700.HK) raw_symbols = sys.argv[1:] symbols = [] for arg in raw_symbols: @@ -362,9 +401,12 @@ async def main(): 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}") @@ -387,13 +429,118 @@ async def main(): results.append(result) # 生成汇总报告并发送通知 - print_summary_report(results, send_notification=True) + 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 + from app.stock_agent.stock_agent import STOCK_NAMES + 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'] = STOCK_NAMES.get(r['symbol'], '') + 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())