stock-ai-agent/backend/app/astock_agent/sector_analyzer.py
2026-02-27 09:54:17 +08:00

201 lines
6.9 KiB
Python
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.

"""
板块异动分析
检测板块涨跌幅、量能、资金流向异动
"""
import pandas as pd
from typing import Dict, List, Optional, Tuple
from datetime import datetime
from app.utils.logger import logger
from app.utils.error_handler import notify_error
from .akshare_client import get_akshare_client
class SectorChangeAnalyzer:
"""板块异动分析器"""
def __init__(self, change_threshold: float = 2.0):
"""
初始化异动分析器
Args:
change_threshold: 涨跌幅阈值(%
"""
self.change_threshold = change_threshold
self.akshare = get_akshare_client()
def detect_sector_changes(self) -> List[Dict]:
"""
检测异动板块(使用概念板块)
Returns:
异动板块列表
"""
try:
# 获取概念板块行情
df = self.akshare.get_concept_spot()
if df.empty:
logger.warning("概念板块行情数据为空")
return []
# 转换数据类型(概念板块返回的列名)
df['涨跌幅'] = pd.to_numeric(df['涨跌幅'], errors='coerce')
df['涨跌额'] = pd.to_numeric(df['涨跌额'], errors='coerce')
df['最新价'] = pd.to_numeric(df['最新价'], errors='coerce')
df['成交额'] = pd.to_numeric(df['成交额'], errors='coerce')
# 筛选异动板块
hot_sectors = df[df['涨跌幅'] >= self.change_threshold].copy()
if hot_sectors.empty:
return []
# 排序:涨幅优先,然后成交额
hot_sectors = hot_sectors.sort_values(
by=['涨跌幅', '成交额'],
ascending=[False, False]
)
# 转换为结果列表
results = []
for _, row in hot_sectors.iterrows():
results.append({
'name': row['板块名称'],
'change_pct': float(row['涨跌幅']),
'change_amount': float(row.get('涨跌额', 0)),
'volume': float(row.get('成交量', 0)) if '成交量' in row else 0.0,
'amount': float(row.get('成交额', 0)),
'leading_stock': row.get('领涨股', ''),
'ups': int(row.get('上涨家数', 0)),
'downs': int(row.get('下跌家数', 0)),
'timestamp': datetime.now()
})
logger.info(f"检测到 {len(results)} 个异动概念板块")
return results
except Exception as e:
error_msg = f"检测板块异动失败: {e}"
logger.error(error_msg)
# 如果是连接错误,发送通知
if 'Connection' in str(e) or 'RemoteDisconnected' in str(e):
notify_error(
title="A股板块监控 - 数据源连接失败",
message=f"akshare 概念板块 API 连接失败\n\n错误: {e}\n\n可能原因:\n- eastmoney API 服务不稳定\n- 网络连接问题\n- 建议:稍后自动重试或考虑使用 tushare",
level="warning"
)
return []
def analyze_sector_momentum(self, sector_name: str) -> Dict:
"""
分析板块动能
Args:
sector_name: 板块名称
Returns:
板块动能分析结果
"""
try:
# 获取板块成分股
stocks_df = self.akshare.get_concept_stocks(sector_name)
if stocks_df.empty:
return {}
# 获取实时行情
spot_df = self.akshare.get_stock_spot()
if spot_df.empty:
return {}
# 合并数据
merged = pd.merge(
stocks_df,
spot_df,
on='代码',
how='inner'
)
if merged.empty:
return {}
# 计算统计
total_stocks = len(merged)
up_stocks = len(merged[merged['涨跌幅'] > 0])
down_stocks = len(merged[merged['涨跌幅'] < 0])
avg_change = merged['涨跌幅'].mean()
max_change = merged['涨跌幅'].max()
# 计算总成交额
total_amount = merged['成交额'].sum() if '成交额' in merged.columns else 0
# 找出涨幅最大的股票
if not merged.empty:
top_stock = merged.loc[merged['涨跌幅'].idxmax()]
else:
top_stock = None
return {
'sector_name': sector_name,
'total_stocks': total_stocks,
'up_stocks': up_stocks,
'down_stocks': down_stocks,
'up_down_ratio': f"{up_stocks}:{down_stocks}",
'avg_change': float(avg_change) if pd.notna(avg_change) else 0,
'max_change': float(max_change) if pd.notna(max_change) else 0,
'total_amount': float(total_amount),
'top_stock': {
'code': top_stock['代码'] if top_stock is not None else '',
'name': top_stock['名称'] if top_stock is not None else '',
'change': float(top_stock['涨跌幅']) if top_stock is not None else 0,
} if top_stock is not None else None
}
except Exception as e:
logger.error(f"分析板块动能失败 {sector_name}: {e}")
return {}
def get_hot_reason(self, sector_name: str, top_stocks: List[Dict]) -> str:
"""
推测异动原因(基于龙头股分析)
Args:
sector_name: 板块名称
top_stocks: 龙头股列表
Returns:
异动原因描述
"""
try:
if not top_stocks:
return "板块整体异动"
# 简单的原因分析
reasons = []
# 检查是否有涨停股
limit_up_count = sum(1 for s in top_stocks if s.get('change_pct', 0) >= 9.9)
if limit_up_count > 0:
reasons.append(f"{limit_up_count}只个股涨停")
# 检查平均涨幅
avg_change = sum(s.get('change_pct', 0) for s in top_stocks) / len(top_stocks)
if avg_change >= 7:
reasons.append("板块全线爆发")
# 检查是否集中在某个龙头
if len(top_stocks) >= 2:
top1_change = top_stocks[0].get('change_pct', 0)
top2_change = top_stocks[1].get('change_pct', 0)
if top1_change - top2_change > 3:
reasons.append(f"{top_stocks[0].get('name', '')}龙头领涨")
if reasons:
return "".join(reasons)
else:
return "资金集中流入"
except Exception as e:
logger.error(f"推测异动原因失败: {e}")
return "板块异动"