增加 HK 的支持
This commit is contained in:
parent
df19ce15dd
commit
df250a920b
@ -139,8 +139,9 @@ class Settings(BaseSettings):
|
|||||||
crypto_agent_model: str = "deepseek" # CryptoAgent 使用的模型
|
crypto_agent_model: str = "deepseek" # CryptoAgent 使用的模型
|
||||||
stock_agent_model: str = "deepseek" # StockAgent 使用的模型
|
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_analysis_interval: int = 300 # 分析间隔(秒,默认5分钟)
|
||||||
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
|
stock_llm_threshold: float = 0.70 # 触发 LLM 分析的置信度阈值
|
||||||
|
|
||||||
|
|||||||
@ -222,13 +222,20 @@ class LLMSignalAnalyzer:
|
|||||||
|
|
||||||
agent_name_map = {
|
agent_name_map = {
|
||||||
'crypto': '加密货币',
|
'crypto': '加密货币',
|
||||||
'stock': '美股',
|
'stock': '股票', # 改为通用的"股票",具体市场类型会在分析时根据符号判断
|
||||||
'smart': '智能助手'
|
'smart': '智能助手'
|
||||||
}
|
}
|
||||||
agent_name = agent_name_map.get(agent_type, '未知')
|
agent_name = agent_name_map.get(agent_type, '未知')
|
||||||
|
|
||||||
logger.info(f"LLM 信号分析器初始化完成({agent_name},模型: {self.model_override or '默认'})")
|
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],
|
async def analyze(self, symbol: str, data: Dict[str, pd.DataFrame],
|
||||||
symbols: List[str] = None,
|
symbols: List[str] = None,
|
||||||
position_info: Dict[str, Any] = None) -> Dict[str, Any]:
|
position_info: Dict[str, Any] = None) -> Dict[str, Any]:
|
||||||
@ -249,6 +256,9 @@ class LLMSignalAnalyzer:
|
|||||||
分析结果
|
分析结果
|
||||||
"""
|
"""
|
||||||
try:
|
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])
|
news_text = await self._get_news_context(symbol, symbols or [symbol])
|
||||||
|
|
||||||
@ -274,11 +284,11 @@ class LLMSignalAnalyzer:
|
|||||||
signals = result.get('signals', [])
|
signals = result.get('signals', [])
|
||||||
if signals:
|
if signals:
|
||||||
for sig in 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['confidence']}% 等级:{sig['grade']} "
|
||||||
f"原因:{sig['reason'][:50]}...")
|
f"原因:{sig['reason'][:50]}...")
|
||||||
else:
|
else:
|
||||||
logger.info(f"{symbol} 无交易信号 - {result.get('analysis_summary', '观望')}")
|
logger.info(f"{symbol} [{market_type}] 无交易信号 - {result.get('analysis_summary', '观望')}")
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
@ -1066,6 +1076,10 @@ class LLMSignalAnalyzer:
|
|||||||
Returns:
|
Returns:
|
||||||
格式化的消息文本
|
格式化的消息文本
|
||||||
"""
|
"""
|
||||||
|
# 获取股票名称
|
||||||
|
from app.stock_agent.stock_agent import STOCK_NAMES
|
||||||
|
stock_name = STOCK_NAMES.get(symbol, '')
|
||||||
|
|
||||||
type_map = {
|
type_map = {
|
||||||
'short_term': '短线',
|
'short_term': '短线',
|
||||||
'medium_term': '中线',
|
'medium_term': '中线',
|
||||||
@ -1105,7 +1119,10 @@ class LLMSignalAnalyzer:
|
|||||||
sl_percent = ((sl - entry) / entry * 100) if entry else 0
|
sl_percent = ((sl - entry) / entry * 100) if entry else 0
|
||||||
tp_percent = ((tp - 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}
|
{action_icon} **方向**: {action}
|
||||||
{entry_type_icon} **入场**: {entry_type_text}
|
{entry_type_icon} **入场**: {entry_type_text}
|
||||||
@ -1136,6 +1153,10 @@ class LLMSignalAnalyzer:
|
|||||||
Returns:
|
Returns:
|
||||||
包含 title, content, color 的字典
|
包含 title, content, color 的字典
|
||||||
"""
|
"""
|
||||||
|
# 获取股票名称
|
||||||
|
from app.stock_agent.stock_agent import STOCK_NAMES
|
||||||
|
stock_name = STOCK_NAMES.get(symbol, '')
|
||||||
|
|
||||||
type_map = {
|
type_map = {
|
||||||
'short_term': '短线',
|
'short_term': '短线',
|
||||||
'medium_term': '中线',
|
'medium_term': '中线',
|
||||||
@ -1165,16 +1186,24 @@ class LLMSignalAnalyzer:
|
|||||||
position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱')
|
position_icon = {'heavy': '🔥', 'medium': '📊', 'light': '🌱'}.get(position_size, '🌱')
|
||||||
position_text = position_map.get(position_size, '轻仓')
|
position_text = position_map.get(position_size, '轻仓')
|
||||||
|
|
||||||
# 标题和颜色 - 添加美股标记
|
# 标题和颜色 - 区分美股/港股
|
||||||
is_market_order = entry_type == 'market'
|
is_market_order = entry_type == 'market'
|
||||||
market_badge = '【现价】' if is_market_order else ''
|
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':
|
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"
|
color = "green"
|
||||||
else:
|
else:
|
||||||
title = f"🔴 {stock_tag}{symbol} {signal_type}做空信号 {market_badge}"
|
title = f"🔴 {market_tag}{symbol_display} {signal_type}做空信号 {market_badge}"
|
||||||
color = "red"
|
color = "red"
|
||||||
|
|
||||||
# 计算风险收益比
|
# 计算风险收益比
|
||||||
|
|||||||
@ -16,6 +16,66 @@ from app.services.signal_database_service import get_signal_db_service
|
|||||||
from app.crypto_agent.llm_signal_analyzer import LLMSignalAnalyzer
|
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:
|
class StockAgent:
|
||||||
"""美股交易信号智能体(LLM 驱动,仅分析通知)"""
|
"""美股交易信号智能体(LLM 驱动,仅分析通知)"""
|
||||||
|
|
||||||
@ -32,15 +92,22 @@ class StockAgent:
|
|||||||
self.last_signals: Dict[str, Dict[str, Any]] = {}
|
self.last_signals: Dict[str, Dict[str, Any]] = {}
|
||||||
self.signal_cooldown: Dict[str, datetime] = {}
|
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.running = False
|
||||||
self._event_loop = None
|
self._event_loop = None
|
||||||
self._task = 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):
|
async def start(self):
|
||||||
"""启动智能体"""
|
"""启动智能体"""
|
||||||
@ -69,7 +136,7 @@ class StockAgent:
|
|||||||
logger.info("美股智能体已停止")
|
logger.info("美股智能体已停止")
|
||||||
|
|
||||||
async def _analysis_loop(self):
|
async def _analysis_loop(self):
|
||||||
"""分析循环 - 只在美股交易时间内运行,整点执行"""
|
"""分析循环 - 根据交易时间分析对应市场的股票"""
|
||||||
while self.running:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
# 计算距离下一个整点的时间
|
# 计算距离下一个整点的时间
|
||||||
@ -82,17 +149,36 @@ class StockAgent:
|
|||||||
# 等待到整点
|
# 等待到整点
|
||||||
await asyncio.sleep(wait_seconds)
|
await asyncio.sleep(wait_seconds)
|
||||||
|
|
||||||
# 检查是否在美股交易时间
|
# 分类股票:美股和港股
|
||||||
if not self._is_market_hours():
|
us_stocks = [s for s in self.symbols if not s.endswith('.HK')]
|
||||||
logger.debug("非美股交易时间,跳过本次分析")
|
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
|
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 = []
|
analysis_results = []
|
||||||
|
|
||||||
for symbol in self.symbols:
|
for symbol in stocks_to_analyze:
|
||||||
if not self.running:
|
if not self.running:
|
||||||
break
|
break
|
||||||
result = await self.analyze_symbol(symbol)
|
result = await self.analyze_symbol(symbol)
|
||||||
@ -108,15 +194,23 @@ class StockAgent:
|
|||||||
logger.error(f"分析循环出错: {e}")
|
logger.error(f"分析循环出错: {e}")
|
||||||
await asyncio.sleep(60) # 出错后等待 1 分钟再重试
|
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)
|
美股交易时间: 周一至周五 9:30-16:00 (EST)
|
||||||
北京时间:
|
北京时间:
|
||||||
- 冬令时 (11月-3月): 22:30-05:00 (次日)
|
- 冬令时 (11月-3月): 22:30-05:00 (次日)
|
||||||
- 夏令时 (3月-11月): 21:30-04:00 (次日)
|
- 夏令时 (3月-11月): 21:30-04:00 (次日)
|
||||||
|
|
||||||
|
港股交易时间: 周一至周五
|
||||||
|
北京时间:
|
||||||
|
- 上午: 09:30-12:00
|
||||||
|
- 下午: 13:00-16:00
|
||||||
|
|
||||||
|
Args:
|
||||||
|
symbol: 股票代码(用于判断是美股还是港股)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
是否在交易时间
|
是否在交易时间
|
||||||
"""
|
"""
|
||||||
@ -129,27 +223,39 @@ class StockAgent:
|
|||||||
if now.weekday() >= 5: # 5=周六, 6=周日
|
if now.weekday() >= 5: # 5=周六, 6=周日
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# 判断是港股还是美股
|
||||||
|
is_hk_stock = symbol and symbol.endswith('.HK') if symbol else False
|
||||||
|
|
||||||
# 获取当前小时和分钟
|
# 获取当前小时和分钟
|
||||||
hour = now.hour
|
hour = now.hour
|
||||||
minute = now.minute
|
minute = now.minute
|
||||||
current_time = hour * 100 + minute # 转换为数字,如 2130 表示 21:30
|
current_time = hour * 100 + minute # 转换为数字,如 2130 表示 21:30
|
||||||
|
|
||||||
# 判断夏令时/冬令时(简单判断:3-11月为夏令时)
|
if is_hk_stock:
|
||||||
is_summer = 3 <= now.month <= 11
|
# 港股交易时间: 09:30-12:00 或 13:00-16:00
|
||||||
|
return (930 <= current_time < 1200) or (1300 <= current_time < 1600)
|
||||||
if is_summer:
|
|
||||||
# 夏令时: 21:30-04:00 (次日)
|
|
||||||
# 即 2130-2359 或 0000-0400
|
|
||||||
if current_time >= 2130 or current_time < 400:
|
|
||||||
return True
|
|
||||||
else:
|
else:
|
||||||
# 冬令时: 22:30-05:00 (次日)
|
# 美股交易时间
|
||||||
# 即 2230-2359 或 0000-0500
|
# 判断夏令时/冬令时(简单判断:3-11月为夏令时)
|
||||||
if current_time >= 2230 or current_time < 500:
|
is_summer = 3 <= now.month <= 11
|
||||||
return True
|
|
||||||
|
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
|
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]]:
|
async def analyze_symbol(self, symbol: str) -> Optional[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
分析单个股票
|
分析单个股票
|
||||||
@ -185,8 +291,12 @@ class StockAgent:
|
|||||||
current_price = ticker['lastPrice']
|
current_price = ticker['lastPrice']
|
||||||
result['current_price'] = current_price
|
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"\n{'='*60}")
|
||||||
logger.info(f"📊 分析 {symbol} @ ${current_price:,.2f}")
|
logger.info(f"📊 分析 {symbol_display} @ ${current_price:,.2f}")
|
||||||
logger.info(f"{'='*60}")
|
logger.info(f"{'='*60}")
|
||||||
|
|
||||||
# 4. LLM 分析
|
# 4. LLM 分析
|
||||||
@ -402,6 +512,12 @@ class StockAgent:
|
|||||||
with_signals = [r for r in results if r.get('signals')]
|
with_signals = [r for r in results if r.get('signals')]
|
||||||
notified = [r for r in results if r.get('notified')]
|
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 = []
|
buy_signals = []
|
||||||
sell_signals = []
|
sell_signals = []
|
||||||
@ -411,6 +527,8 @@ class StockAgent:
|
|||||||
for sig in r.get('signals', []):
|
for sig in r.get('signals', []):
|
||||||
sig['symbol'] = r['symbol']
|
sig['symbol'] = r['symbol']
|
||||||
sig['current_price'] = r.get('current_price', 0)
|
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':
|
if sig.get('action') == 'buy':
|
||||||
buy_signals.append(sig)
|
buy_signals.append(sig)
|
||||||
@ -425,11 +543,11 @@ class StockAgent:
|
|||||||
|
|
||||||
# 构建汇总报告
|
# 构建汇总报告
|
||||||
logger.info(f"\n{'='*80}")
|
logger.info(f"\n{'='*80}")
|
||||||
logger.info(f"📊 美股分析汇总报告")
|
logger.info(f"📊 股票分析汇总报告")
|
||||||
logger.info(f"{'='*80}")
|
logger.info(f"{'='*80}")
|
||||||
logger.info(f"时间: {now.strftime('%Y-%m-%d %H:%M:%S')}")
|
logger.info(f"时间: {now.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||||
logger.info(f"分析数量: {total} 只股票")
|
logger.info(f"分析总数: {total} 只 (美股: {len(us_results)}, 港股: {len(hk_results)})")
|
||||||
logger.info(f"有信号: {len(with_signals)} 只")
|
logger.info(f"有信号: {len(with_signals)} 只 (美股: {len(us_with_signals)}, 港股: {len(hk_with_signals)})")
|
||||||
logger.info(f"已通知: {len(notified)} 只")
|
logger.info(f"已通知: {len(notified)} 只")
|
||||||
logger.info(f"")
|
logger.info(f"")
|
||||||
|
|
||||||
@ -438,13 +556,18 @@ class StockAgent:
|
|||||||
logger.info(f"⭐ 高等级信号 (A/B级): {len(high_quality_signals)} 个")
|
logger.info(f"⭐ 高等级信号 (A/B级): {len(high_quality_signals)} 个")
|
||||||
for sig in high_quality_signals[:10]: # 最多显示10个
|
for sig in high_quality_signals[:10]: # 最多显示10个
|
||||||
symbol = sig['symbol']
|
symbol = sig['symbol']
|
||||||
|
stock_name = sig.get('stock_name', '')
|
||||||
|
market_tag = '[港股]' if sig.get('is_hk') else '[美股]'
|
||||||
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
||||||
grade = sig.get('grade', 'D')
|
grade = sig.get('grade', 'D')
|
||||||
confidence = sig.get('confidence', 0)
|
confidence = sig.get('confidence', 0)
|
||||||
price = sig.get('current_price', 0)
|
price = sig.get('current_price', 0)
|
||||||
entry = sig.get('entry_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:
|
if entry > 0:
|
||||||
logger.info(f" 入场: ${entry:,.2f}")
|
logger.info(f" 入场: ${entry:,.2f}")
|
||||||
logger.info(f"")
|
logger.info(f"")
|
||||||
@ -457,7 +580,8 @@ class StockAgent:
|
|||||||
# 发送飞书汇总
|
# 发送飞书汇总
|
||||||
await self._send_feishu_summary(
|
await self._send_feishu_summary(
|
||||||
now, total, with_signals, notified,
|
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:
|
except Exception as e:
|
||||||
@ -473,18 +597,20 @@ class StockAgent:
|
|||||||
notified: List,
|
notified: List,
|
||||||
buy_signals: List,
|
buy_signals: List,
|
||||||
sell_signals: List,
|
sell_signals: List,
|
||||||
high_quality_signals: List
|
high_quality_signals: List,
|
||||||
|
us_count: int = 0,
|
||||||
|
hk_count: int = 0
|
||||||
):
|
):
|
||||||
"""发送飞书汇总报告"""
|
"""发送飞书汇总报告"""
|
||||||
try:
|
try:
|
||||||
# 构建内容
|
# 构建内容
|
||||||
content_parts = [
|
content_parts = [
|
||||||
f"**美股分析汇总报告**",
|
f"**📊 股票分析汇总报告**",
|
||||||
f"",
|
f"",
|
||||||
f"⏰ 时间: {now.strftime('%Y-%m-%d %H:%M')}",
|
f"⏰ 时间: {now.strftime('%Y-%m-%d %H:%M')}",
|
||||||
f"",
|
f"",
|
||||||
f"📊 **分析概况**",
|
f"📊 **分析概况**",
|
||||||
f"• 分析总数: {total} 只",
|
f"• 美股: {us_count} 只 | 港股: {hk_count} 只",
|
||||||
f"• 发现信号: {len(with_signals)} 只",
|
f"• 发现信号: {len(with_signals)} 只",
|
||||||
f"• 已发通知: {len(notified)} 只",
|
f"• 已发通知: {len(notified)} 只",
|
||||||
f"",
|
f"",
|
||||||
@ -495,10 +621,16 @@ class StockAgent:
|
|||||||
content_parts.append(f"⭐ **高等级信号 (A/B级)**")
|
content_parts.append(f"⭐ **高等级信号 (A/B级)**")
|
||||||
for sig in high_quality_signals[:5]:
|
for sig in high_quality_signals[:5]:
|
||||||
symbol = sig['symbol']
|
symbol = sig['symbol']
|
||||||
|
stock_name = sig.get('stock_name', '')
|
||||||
|
market_tag = '[港股]' if sig.get('is_hk') else '[美股]'
|
||||||
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
||||||
grade = sig.get('grade', 'D')
|
grade = sig.get('grade', 'D')
|
||||||
confidence = sig.get('confidence', 0)
|
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"")
|
content_parts.append(f"")
|
||||||
|
|
||||||
# 信号统计
|
# 信号统计
|
||||||
@ -512,7 +644,7 @@ class StockAgent:
|
|||||||
content = "\n".join(content_parts)
|
content = "\n".join(content_parts)
|
||||||
|
|
||||||
# 发送飞书
|
# 发送飞书
|
||||||
title = f"📊 美股分析汇总 ({now.strftime('%H:%M')})"
|
title = f"📊 股票分析汇总 ({now.strftime('%H:%M')})"
|
||||||
color = "blue"
|
color = "blue"
|
||||||
|
|
||||||
await self.feishu.send_card(title, content, color)
|
await self.feishu.send_card(title, content, color)
|
||||||
|
|||||||
@ -1,34 +1,77 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 美股分析快捷脚本
|
# 股票分析快捷脚本
|
||||||
#
|
#
|
||||||
# 用法:
|
# 用法:
|
||||||
# ./scripts/stock.sh AAPL
|
# ./scripts/stock.sh # 分析配置的所有股票(美股+港股)
|
||||||
# ./scripts/stock.sh AAPL TSLA
|
# ./scripts/stock.sh us # 只分析美股
|
||||||
# ./scripts/stock.sh # 分析配置的所有股票(会发送通知)
|
# ./scripts/stock.sh hk # 只分析港股
|
||||||
|
# ./scripts/stock.sh AAPL # 分析指定股票
|
||||||
|
# ./scripts/stock.sh AAPL TSLA # 分析多个指定股票
|
||||||
|
|
||||||
cd "$(dirname "$0")/.." || exit 1
|
cd "$(dirname "$0")/.." || exit 1
|
||||||
|
|
||||||
if [ $# -eq 0 ]; then
|
if [ $# -eq 0 ]; then
|
||||||
# 无参数,分析配置的所有股票
|
# 无参数,分析配置的所有股票(美股+港股)
|
||||||
echo "📊 分析配置的所有股票(将发送通知)..."
|
echo "📊 分析配置的所有股票(美股+港股,将发送通知)..."
|
||||||
|
|
||||||
# 直接从 .env 文件读取股票代码
|
# 直接从 .env 文件读取股票代码
|
||||||
if [ -f .env ]; then
|
if [ -f .env ]; then
|
||||||
# 使用 grep 提取 STOCK_SYMBOLS 行,然后提取值
|
# 使用 grep 提取美股和港股代码
|
||||||
STOCKS=$(grep "^STOCK_SYMBOLS=" .env | cut -d'=' -f2)
|
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 文件读取股票列表"
|
echo "❌ 无法从 .env 文件读取股票列表"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
python3 scripts/test_stock.py $STOCKS_SPACE
|
||||||
else
|
else
|
||||||
echo "❌ .env 文件不存在"
|
echo "❌ .env 文件不存在"
|
||||||
|
|||||||
@ -34,6 +34,9 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
Returns:
|
Returns:
|
||||||
分析结果字典
|
分析结果字典
|
||||||
"""
|
"""
|
||||||
|
# 导入股票名称映射
|
||||||
|
from app.stock_agent.stock_agent import STOCK_NAMES
|
||||||
|
|
||||||
result = {
|
result = {
|
||||||
'symbol': symbol,
|
'symbol': symbol,
|
||||||
'price': 0,
|
'price': 0,
|
||||||
@ -45,8 +48,12 @@ async def analyze(symbol: str, send_notification: bool = True):
|
|||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
threshold = settings.stock_llm_threshold * 100 # 转换为百分比
|
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"\n{'='*60}")
|
||||||
print(f"📊 分析 {symbol}")
|
print(f"📊 分析 {symbol_display}")
|
||||||
print(f"{'='*60}")
|
print(f"{'='*60}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -160,6 +167,7 @@ def print_summary_report(results: list, send_notification: bool = True):
|
|||||||
send_notification: 是否发送通知(默认True)
|
send_notification: 是否发送通知(默认True)
|
||||||
"""
|
"""
|
||||||
from app.config import get_settings
|
from app.config import get_settings
|
||||||
|
from app.stock_agent.stock_agent import STOCK_NAMES
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
threshold = settings.stock_llm_threshold * 100 # 获取阈值
|
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')]
|
with_signals = [r for r in results if r.get('signals')]
|
||||||
notified = [r for r in results if r.get('notified')]
|
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
|
buy_count = 0
|
||||||
sell_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', []):
|
for sig in r.get('signals', []):
|
||||||
sig['symbol'] = r['symbol']
|
sig['symbol'] = r['symbol']
|
||||||
sig['current_price'] = r.get('price', 0)
|
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)
|
all_signals.append(sig)
|
||||||
|
|
||||||
if sig.get('action') == 'buy':
|
if sig.get('action') == 'buy':
|
||||||
@ -194,9 +208,9 @@ def print_summary_report(results: list, send_notification: bool = True):
|
|||||||
|
|
||||||
# 打印汇总
|
# 打印汇总
|
||||||
print("\n" + "="*80)
|
print("\n" + "="*80)
|
||||||
print("📊 美股分析汇总报告")
|
print("📊 股票分析汇总报告")
|
||||||
print("="*80)
|
print("="*80)
|
||||||
print(f"分析数量: {total} 只股票")
|
print(f"分析总数: {total} 只 (美股: {len(us_results)}, 港股: {len(hk_results)})")
|
||||||
print(f"有信号: {len(with_signals)} 只")
|
print(f"有信号: {len(with_signals)} 只")
|
||||||
print(f"已通知: {len(notified)} 只")
|
print(f"已通知: {len(notified)} 只")
|
||||||
print(f"通知阈值: {threshold}%")
|
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)} 个")
|
print(f"⭐ 高等级信号达到阈值 (A/B级 >= {threshold}%): {len(high_quality_signals)} 个")
|
||||||
for sig in high_quality_signals[:10]:
|
for sig in high_quality_signals[:10]:
|
||||||
symbol = sig['symbol']
|
symbol = sig['symbol']
|
||||||
|
stock_name = sig.get('stock_name', '')
|
||||||
|
market_tag = '[港股]' if sig.get('is_hk') else '[美股]'
|
||||||
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
||||||
grade = sig.get('grade', 'D')
|
grade = sig.get('grade', 'D')
|
||||||
confidence = sig.get('confidence', 0)
|
confidence = sig.get('confidence', 0)
|
||||||
price = sig.get('current_price', 0)
|
price = sig.get('current_price', 0)
|
||||||
entry = sig.get('entry_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:
|
if entry > 0:
|
||||||
print(f" 入场: ${entry:,.2f}")
|
print(f" 入场: ${entry:,.2f}")
|
||||||
print("")
|
print("")
|
||||||
@ -225,10 +244,14 @@ def print_summary_report(results: list, send_notification: bool = True):
|
|||||||
print(f"⚠️ 以下信号未达到通知阈值 ({threshold}%):")
|
print(f"⚠️ 以下信号未达到通知阈值 ({threshold}%):")
|
||||||
for sig in below_threshold[:10]:
|
for sig in below_threshold[:10]:
|
||||||
symbol = sig['symbol']
|
symbol = sig['symbol']
|
||||||
|
stock_name = sig.get('stock_name', '')
|
||||||
|
market_tag = '[港股]' if sig.get('is_hk') else '[美股]'
|
||||||
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
||||||
grade = sig.get('grade', 'D')
|
grade = sig.get('grade', 'D')
|
||||||
confidence = sig.get('confidence', 0)
|
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("")
|
print("")
|
||||||
|
|
||||||
# 统计汇总
|
# 统计汇总
|
||||||
@ -240,7 +263,8 @@ def print_summary_report(results: list, send_notification: bool = True):
|
|||||||
if send_notification:
|
if send_notification:
|
||||||
asyncio.run(send_summary_notification(
|
asyncio.run(send_summary_notification(
|
||||||
results, total, with_signals, notified,
|
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,
|
sell_count: int,
|
||||||
high_quality_signals: list,
|
high_quality_signals: list,
|
||||||
all_signals: list,
|
all_signals: list,
|
||||||
threshold: float
|
threshold: float,
|
||||||
|
us_count: int = 0,
|
||||||
|
hk_count: int = 0
|
||||||
):
|
):
|
||||||
"""发送汇总报告到飞书和Telegram
|
"""发送汇总报告到飞书和Telegram
|
||||||
|
|
||||||
@ -267,6 +293,8 @@ async def send_summary_notification(
|
|||||||
high_quality_signals: 达到阈值的高等级信号列表
|
high_quality_signals: 达到阈值的高等级信号列表
|
||||||
all_signals: 所有信号列表
|
all_signals: 所有信号列表
|
||||||
threshold: 通知阈值
|
threshold: 通知阈值
|
||||||
|
us_count: 美股数量
|
||||||
|
hk_count: 港股数量
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
@ -277,12 +305,12 @@ async def send_summary_notification(
|
|||||||
|
|
||||||
# 构建飞书汇总内容
|
# 构建飞书汇总内容
|
||||||
content_parts = [
|
content_parts = [
|
||||||
f"**📊 美股分析汇总报告**",
|
f"**📊 股票分析汇总报告**",
|
||||||
f"",
|
f"",
|
||||||
f"⏰ 时间: {now.strftime('%Y-%m-%d %H:%M')}",
|
f"⏰ 时间: {now.strftime('%Y-%m-%d %H:%M')}",
|
||||||
f"",
|
f"",
|
||||||
f"📊 **分析概况**",
|
f"📊 **分析概况**",
|
||||||
f"• 分析总数: {total} 只",
|
f"• 美股: {us_count} 只 | 港股: {hk_count} 只",
|
||||||
f"• 发现信号: {len(with_signals)} 只",
|
f"• 发现信号: {len(with_signals)} 只",
|
||||||
f"• 已发通知: {len(notified)} 只",
|
f"• 已发通知: {len(notified)} 只",
|
||||||
f"• 通知阈值: {threshold:.0f}%",
|
f"• 通知阈值: {threshold:.0f}%",
|
||||||
@ -294,10 +322,16 @@ async def send_summary_notification(
|
|||||||
content_parts.append(f"⭐ **高等级信号 (A/B级 ≥ {threshold:.0f}%)**")
|
content_parts.append(f"⭐ **高等级信号 (A/B级 ≥ {threshold:.0f}%)**")
|
||||||
for sig in high_quality_signals[:5]:
|
for sig in high_quality_signals[:5]:
|
||||||
symbol = sig['symbol']
|
symbol = sig['symbol']
|
||||||
|
stock_name = sig.get('stock_name', '')
|
||||||
|
market_tag = '[港股]' if sig.get('is_hk') else '[美股]'
|
||||||
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
||||||
grade = sig.get('grade', 'D')
|
grade = sig.get('grade', 'D')
|
||||||
confidence = sig.get('confidence', 0)
|
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"")
|
content_parts.append(f"")
|
||||||
|
|
||||||
# 信号统计
|
# 信号统计
|
||||||
@ -311,25 +345,30 @@ async def send_summary_notification(
|
|||||||
content = "\n".join(content_parts)
|
content = "\n".join(content_parts)
|
||||||
|
|
||||||
# 发送飞书
|
# 发送飞书
|
||||||
title = f"📊 美股分析汇总 ({now.strftime('%H:%M')})"
|
title = f"📊 股票分析汇总 ({now.strftime('%H:%M')})"
|
||||||
color = "blue"
|
color = "blue"
|
||||||
|
|
||||||
await feishu.send_card(title, content, color)
|
await feishu.send_card(title, content, color)
|
||||||
|
|
||||||
# 发送 Telegram
|
# 发送 Telegram
|
||||||
telegram_msg = f"📊 *美股分析汇总*\n\n"
|
telegram_msg = f"📊 *股票分析汇总*\n\n"
|
||||||
telegram_msg += f"时间: {now.strftime('%H:%M')}\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"
|
telegram_msg += f"阈值: {threshold:.0f}%\n\n"
|
||||||
|
|
||||||
if high_quality_signals:
|
if high_quality_signals:
|
||||||
telegram_msg += f"⭐ *高等级信号 (≥{threshold:.0f}%)*\n"
|
telegram_msg += f"⭐ *高等级信号 (≥{threshold:.0f}%)*\n"
|
||||||
for sig in high_quality_signals[:5]:
|
for sig in high_quality_signals[:5]:
|
||||||
symbol = sig['symbol']
|
symbol = sig['symbol']
|
||||||
|
stock_name = sig.get('stock_name', '')
|
||||||
|
market_tag = '[港股]' if sig.get('is_hk') else '[美股]'
|
||||||
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
action = '🟢 做多' if sig.get('action') == 'buy' else '🔴 做空'
|
||||||
grade = sig.get('grade', 'D')
|
grade = sig.get('grade', 'D')
|
||||||
confidence = sig.get('confidence', 0)
|
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 += "\n"
|
||||||
|
|
||||||
telegram_msg += f"做多: {buy_count} | 做空: {sell_count}"
|
telegram_msg += f"做多: {buy_count} | 做空: {sell_count}"
|
||||||
@ -354,7 +393,7 @@ async def main():
|
|||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
# 过滤掉无效的参数(如环境变量路径、配置项等)
|
# 过滤掉无效的参数(如环境变量路径、配置项等)
|
||||||
# 只接受有效的股票代码(字母开头,1-5个字符)
|
# 只接受有效的股票代码(字母开头,1-5个字符,或包含数字的港股代码如0700.HK)
|
||||||
raw_symbols = sys.argv[1:]
|
raw_symbols = sys.argv[1:]
|
||||||
symbols = []
|
symbols = []
|
||||||
for arg in raw_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:
|
if '/' in arg or '=' in arg or ':' in arg or len(arg) > 10:
|
||||||
logger.debug(f"跳过无效参数: {arg}")
|
logger.debug(f"跳过无效参数: {arg}")
|
||||||
continue
|
continue
|
||||||
# 只保留纯字母的参数(股票代码)
|
# 接受纯字母的参数(美股股票代码)
|
||||||
if arg.isalpha() and 1 <= len(arg) <= 5:
|
if arg.isalpha() and 1 <= len(arg) <= 5:
|
||||||
symbols.append(arg.upper())
|
symbols.append(arg.upper())
|
||||||
|
# 接受包含数字和点号的参数(港股代码,如0700.HK)
|
||||||
|
elif '.HK' in arg.upper() and len(arg) <= 10:
|
||||||
|
symbols.append(arg.upper())
|
||||||
else:
|
else:
|
||||||
logger.debug(f"跳过非股票代码参数: {arg}")
|
logger.debug(f"跳过非股票代码参数: {arg}")
|
||||||
|
|
||||||
@ -387,13 +429,118 @@ async def main():
|
|||||||
results.append(result)
|
results.append(result)
|
||||||
|
|
||||||
# 生成汇总报告并发送通知
|
# 生成汇总报告并发送通知
|
||||||
print_summary_report(results, send_notification=True)
|
await send_summary_notification_async(results)
|
||||||
|
|
||||||
print("\n" + "="*60)
|
print("\n" + "="*60)
|
||||||
print("✅ 分析完成")
|
print("✅ 分析完成")
|
||||||
print("="*60)
|
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__":
|
if __name__ == "__main__":
|
||||||
try:
|
try:
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user