stock-ai-agent/scripts/test_stock.py
2026-02-26 13:00:37 +08:00

649 lines
25 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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⚠️ 用户中断")