feat: 改为盘后报告模式 - 正文分析总结+附件详细数据

This commit is contained in:
2026-04-10 17:19:51 +08:00
parent 947c75cd78
commit bb19b67460
3 changed files with 644 additions and 285 deletions

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
"""
A股板块监控系统
获取东方财富板块数据,监控异动,发送通知
A股板块盘后分析系统
获取东方财富板块数据,生成分析报告,发送邮件通知
"""
import urllib.request
@@ -33,23 +33,15 @@ BOARD_TYPES = {
# 数据字段
FIELDS = "f12,f14,f2,f3,f62,f66,f84,f104,f125,f126,f127,f128"
# f12: 板块代码
# f14: 板块名称
# f2: 最新价
# f3: 涨跌幅
# f62: 主力净流入
# f66: 主力净流入-陆股通
# f84: 领涨股代码
# f104: 领涨股名称
def get_board_data(board_type: str, sort_by: str = "f3", limit: int = 50) -> Optional[List[Dict]]:
def get_board_data(board_type: str, sort_by: str = "f3", limit: int = 100) -> Optional[List[Dict]]:
"""
获取板块数据
参数:
board_type: 板块类型 (industry/concept)
sort_by: 排序字段 (f3=涨跌幅, f66=主力资金)
sort_by: 排序字段 (f3=涨跌幅, f62=主力资金)
limit: 返回数量
返回:
@@ -82,7 +74,6 @@ def get_board_data(board_type: str, sort_by: str = "f3", limit: int = 50) -> Opt
'price': item.get('f2', 0) / 100 if item.get('f2') else 0,
'pct_change': item.get('f3', 0) / 100 if item.get('f3') else 0,
'main_flow': item.get('f62', 0) / 1e8 if item.get('f62') else 0, # 亿元
'main_flow_lgt': item.get('f66', 0) / 1e8 if item.get('f66') else 0, # 陆股通流入
'leader_code': item.get('f84', ''),
'leader_name': item.get('f104', ''),
}
@@ -96,157 +87,163 @@ def get_board_data(board_type: str, sort_by: str = "f3", limit: int = 50) -> Opt
except urllib.error.URLError as e:
print(f"❌ 网络请求失败: {e}")
return None
except json.JSONDecodeError as e:
print(f"❌ JSON解析失败: {e}")
return None
except Exception as e:
print(f"❌ 获取数据异常: {e}")
return None
def check_anomaly(boards: List[Dict], pct_threshold: float = 3.0, flow_threshold: float = 10.0) -> Dict:
def generate_daily_report(boards_data: Dict, to_email: str = "wlq@tphai.com") -> bool:
"""
检查板块异动
生成盘后分析报告并发送邮件
参数:
boards: 板块数据列表
pct_threshold: 涨跌幅阈值 (%)
flow_threshold: 资金流入阈值 (亿元)
boards_data: 板块数据字典 {'industry': [], 'concept': []}
to_email: 收件人邮箱
返回:
Dict: 异动信息,包含涨跌异动和资金异动
bool: 是否发送成功
"""
anomaly = {
'pct_up': [], # 涨幅异动
'pct_down': [], # 跌幅异动
'flow_in': [], # 资金流入异动
'flow_out': [], # 资金流出异动
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
all_industry = boards_data.get('industry', [])
all_concept = boards_data.get('concept', [])
for board in boards:
# 涨跌幅异动
if board['pct_change'] >= pct_threshold:
anomaly['pct_up'].append(board)
elif board['pct_change'] <= -pct_threshold:
anomaly['pct_down'].append(board)
# 资金流向异动
if board['main_flow'] >= flow_threshold:
anomaly['flow_in'].append(board)
elif board['main_flow'] <= -flow_threshold:
anomaly['flow_out'].append(board)
if not all_industry and not all_concept:
print("❌ 无数据,无法生成报告")
return False
return anomaly
def format_board_line(board: Dict) -> str:
"""格式化单行板块信息"""
pct = board['pct_change']
flow = board['main_flow']
leader = board['leader_name'] or board['leader_code']
# 分析总结
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
pct_str = f"+{pct:.2f}%" if pct > 0 else f"{pct:.2f}%"
flow_str = f"+{flow:.2f}亿" if flow > 0 else f"{flow:.2f}亿"
leader_str = f"领涨: {leader}" if leader else ""
# 计算市场趋势
avg_pct = 0
if all_industry:
avg_pct = sum(b['pct_change'] for b in all_industry) / len(all_industry)
return f"{board['name']}: {pct_str}, 主力{flow_str} {leader_str}"
def print_board_summary(boards: List[Dict], title: str, limit: int = 10):
"""打印板块摘要"""
if not boards:
return
market_trend = '平稳'
if avg_pct > 0.5:
market_trend = '上涨'
elif avg_pct < -0.5:
market_trend = '下跌'
print(f"\n{title}")
print("=" * 50)
for board in boards[:limit]:
print(format_board_line(board))
def print_anomaly_report(anomaly: Dict):
"""打印异动报告"""
print(f"\n📊 板块异动报告 [{anomaly['timestamp']}]")
print("=" * 60)
# 排序数据
industry_by_pct = sorted(all_industry, key=lambda x: x['pct_change'], reverse=True)
industry_by_flow = sorted(all_industry, key=lambda x: x['main_flow'], reverse=True)
concept_by_pct = sorted(all_concept, key=lambda x: x['pct_change'], reverse=True)
concept_by_flow = sorted(all_concept, key=lambda x: x['main_flow'], reverse=True)
has_anomaly = False
if anomaly['pct_up']:
has_anomaly = True
print(f"\n🔴 涨幅异动 (涨幅 ≥ 3%)")
for board in anomaly['pct_up']:
print(f" {format_board_line(board)}")
if anomaly['pct_down']:
has_anomaly = True
print(f"\n🟢 跌幅异动 (跌幅 ≥ 3%)")
for board in anomaly['pct_down']:
print(f" {format_board_line(board)}")
if anomaly['flow_in']:
has_anomaly = True
print(f"\n💰 资金大幅流入 (≥ 10亿)")
for board in anomaly['flow_in']:
print(f" {format_board_line(board)}")
if anomaly['flow_out']:
has_anomaly = True
print(f"\n💸 资金大幅流出 (≥ 10亿)")
for board in anomaly['flow_out']:
print(f" {format_board_line(board)}")
if not has_anomaly:
print("\n✅ 今日无明显异动")
return has_anomaly
def generate_html_report(anomaly: Dict, board_type: str) -> str:
"""生成HTML格式报告"""
lines = [
"<h2>📊 A股板块异动报告</h2>",
f"<p>检测时间: {anomaly['timestamp']}</p>",
f"<p>板块类型: {board_type}</p>",
# 生成HTML正文分析总结
html_lines = [
"<h2>📊 A股板块盘后分析报告</h2>",
f"<p>报告时间: {timestamp}</p>",
f"<p>市场整体: <strong>{market_trend}</strong> (行业平均涨跌 {avg_pct:+.2f}%)</p>",
"",
"<hr>",
"",
"<h3>🔥 今日热门概念板块 TOP5</h3>",
"<table border='1' cellpadding='6' cellspacing='0' style='border-collapse: collapse;'>",
"<tr style='background:#f0f0f0'><th>板块</th><th>涨跌幅</th><th>主力资金</th></tr>",
]
if anomaly['pct_up']:
lines.append("<h3>🔴 涨幅异动 (涨幅 ≥ 3%)</h3>")
lines.append("<ul>")
for board in anomaly['pct_up']:
lines.append(f"<li>{format_board_line(board)}</li>")
lines.append("</ul>")
for board in concept_by_pct[:5]:
pct_str = f"+{board['pct_change']:.2f}%" if board['pct_change'] > 0 else f"{board['pct_change']:.2f}%"
flow_str = f"+{board['main_flow']:.2f}亿" if board['main_flow'] > 0 else f"{board['main_flow']:.2f}亿"
html_lines.append(f"<tr><td>{board['name']}</td><td>{pct_str}</td><td>{flow_str}</td></tr>")
if anomaly['pct_down']:
lines.append("<h3>🟢 跌幅异动 (跌幅 ≥ 3%)</h3>")
lines.append("<ul>")
for board in anomaly['pct_down']:
lines.append(f"<li>{format_board_line(board)}</li>")
lines.append("</ul>")
html_lines.append("</table>")
html_lines.append("")
if anomaly['flow_in']:
lines.append("<h3>💰 资金大幅流入 (≥ 10亿)</h3>")
lines.append("<ul>")
for board in anomaly['flow_in']:
lines.append(f"<li>{format_board_line(board)}</li>")
lines.append("</ul>")
html_lines.append("<h3>📈 行业板块涨幅 TOP5</h3>")
html_lines.append("<table border='1' cellpadding='6' cellspacing='0' style='border-collapse: collapse;'>")
html_lines.append("<tr style='background:#f0f0f0'><th>板块</th><th>涨跌幅</th><th>主力资金</th><th>领涨股</th></tr>")
if anomaly['flow_out']:
lines.append("<h3>💸 资金大幅流出 (≥ 10亿)</h3>")
lines.append("<ul>")
for board in anomaly['flow_out']:
lines.append(f"<li>{format_board_line(board)}</li>")
lines.append("</ul>")
for board in industry_by_pct[:5]:
pct_str = f"+{board['pct_change']:.2f}%" if board['pct_change'] > 0 else f"{board['pct_change']:.2f}%"
flow_str = f"+{board['main_flow']:.2f}亿" if board['main_flow'] > 0 else f"{board['main_flow']:.2f}亿"
html_lines.append(f"<tr><td>{board['name']}</td><td>{pct_str}</td><td>{flow_str}</td><td>{board['leader_name'] or '-'}</td></tr>")
if not (anomaly['pct_up'] or anomaly['pct_down'] or anomaly['flow_in'] or anomaly['flow_out']):
lines.append("<p>✅ 今日无明显异动</p>")
html_lines.append("</table>")
html_lines.append("")
return "\n".join(lines)
def send_notification(subject: str, html_body: str, to_email: str = "zuitoushang@tphai.com"):
"""发送邮件通知"""
# 使用邮件发送技能
email_script = Path(__file__).parent.parent.parent / "skills/email/scripts/send_email.py"
html_lines.append("<h3>📉 行业板块跌幅 TOP5</h3>")
html_lines.append("<table border='1' cellpadding='6' cellspacing='0' style='border-collapse: collapse;'>")
html_lines.append("<tr style='background:#f0f0f0'><th>板块</th><th>涨跌幅</th><th>主力资金</th></tr>")
for board in industry_by_pct[-5:]:
pct_str = f"+{board['pct_change']:.2f}%" if board['pct_change'] > 0 else f"{board['pct_change']:.2f}%"
flow_str = f"+{board['main_flow']:.2f}亿" if board['main_flow'] > 0 else f"{board['main_flow']:.2f}亿"
html_lines.append(f"<tr><td>{board['name']}</td><td>{pct_str}</td><td>{flow_str}</td></tr>")
html_lines.append("</table>")
html_lines.append("")
html_lines.append("<h3>💰 主力资金大幅流入 TOP10</h3>")
html_lines.append("<table border='1' cellpadding='6' cellspacing='0' style='border-collapse: collapse;'>")
html_lines.append("<tr style='background:#f0f0f0'><th>板块</th><th>资金流入(亿)</th><th>涨跌幅</th></tr>")
inflow_boards = [b for b in industry_by_flow if b['main_flow'] > 10][:10]
for board in inflow_boards:
pct_str = f"+{board['pct_change']:.2f}%" if board['pct_change'] > 0 else f"{board['pct_change']:.2f}%"
html_lines.append(f"<tr><td>{board['name']}</td><td>+{board['main_flow']:.2f}</td><td>{pct_str}</td></tr>")
html_lines.append("</table>")
html_lines.append("")
html_lines.append("<h3>💸 主力资金大幅流出 TOP10</h3>")
html_lines.append("<table border='1' cellpadding='6' cellspacing='0' style='border-collapse: collapse;'>")
html_lines.append("<tr style='background:#f0f0f0'><th>板块</th><th>资金流出(亿)</th><th>涨跌幅</th></tr>")
outflow_boards = [b for b in industry_by_flow if b['main_flow'] < -10][:10]
for board in outflow_boards:
pct_str = f"+{board['pct_change']:.2f}%" if board['pct_change'] > 0 else f"{board['pct_change']:.2f}%"
html_lines.append(f"<tr><td>{board['name']}</td><td>{board['main_flow']:.2f}</td><td>{pct_str}</td></tr>")
html_lines.append("</table>")
html_lines.append("")
html_lines.append("<hr>")
html_lines.append("<p><em>📊 详细数据请查看附件 CSV 文件</em></p>")
html_body = "\n".join(html_lines)
# 生成附件文件CSV格式
attachment_file = DATA_DIR / f"board_detail_{datetime.now().strftime('%Y%m%d')}.csv"
csv_lines = [
"# A股板块详细数据",
f"# 生成时间: {timestamp}",
"",
"=== 行业板块涨跌幅排行 ===",
"板块名称,涨跌幅(%),主力资金(亿),领涨股",
]
for board in industry_by_pct:
csv_lines.append(f"{board['name']},{board['pct_change']:.2f},{board['main_flow']:.2f},{board['leader_name'] or ''}")
csv_lines.append("")
csv_lines.append("=== 行业板块资金流向排行 ===")
csv_lines.append("板块名称,主力资金(亿),涨跌幅(%),领涨股")
for board in industry_by_flow:
csv_lines.append(f"{board['name']},{board['main_flow']:.2f},{board['pct_change']:.2f},{board['leader_name'] or ''}")
csv_lines.append("")
csv_lines.append("=== 概念板块涨跌幅排行 ===")
csv_lines.append("板块名称,涨跌幅(%),主力资金(亿),领涨股")
for board in concept_by_pct:
csv_lines.append(f"{board['name']},{board['pct_change']:.2f},{board['main_flow']:.2f},{board['leader_name'] or ''}")
csv_lines.append("")
csv_lines.append("=== 概念板块资金流向排行 ===")
csv_lines.append("板块名称,主力资金(亿),涨跌幅(%),领涨股")
for board in concept_by_flow:
csv_lines.append(f"{board['name']},{board['main_flow']:.2f},{board['pct_change']:.2f},{board['leader_name'] or ''}")
attachment_file.write_text("\n".join(csv_lines), encoding='utf-8')
# 发送邮件
email_script = SCRIPT_DIR.parent.parent / "skills/email/scripts/send_email.py"
subject = f"【A股板块盘后分析】{datetime.now().strftime('%Y-%m-%d')}"
cmd = [
"python3", str(email_script),
@@ -254,151 +251,89 @@ def send_notification(subject: str, html_body: str, to_email: str = "zuitoushang
"--to", to_email,
"--subject", subject,
"--body", html_body,
"--html"
"--html",
"--attach", str(attachment_file)
]
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
print(f"邮件发送成功: {to_email}")
print(f"报告发送成功: {to_email}")
print(f" 附件: {attachment_file}")
return True
else:
print(f"邮件发送失败: {result.stderr}")
print(f"❌ 发送失败: {result.stderr}")
return False
except Exception as e:
print(f"邮件发送异常: {e}")
print(f"❌ 发送异常: {e}")
return False
def monitor(board_types: List[str] = ["industry", "concept"],
notify: bool = True,
verbose: bool = False) -> Dict:
def run_daily_report(to_email: str = "wlq@tphai.com", verbose: bool = False) -> bool:
"""
执行板块监控
执行盘后报告生成和发送
参数:
board_types: 要监控的板块类型列表
notify: 是否发送通知(仅在发现异动时)
verbose: 显示详细日志
to_email: 收件人邮箱
verbose: 是否显示详细日志
返回:
Dict: 监控结果汇总
bool: 是否成功
"""
import subprocess
if verbose:
print(f"\n📊 A股板块盘后分析")
print("=" * 50)
print(f"收件人: {to_email}")
print(f"时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
results = {
'boards': {},
'anomalies': {},
'has_anomaly': False
}
# 获取所有板块数据
boards_data = {}
for board_type in board_types:
for board_type in ["industry", "concept"]:
if verbose:
print(f"\n📡 获取 {board_type} 板块数据...")
# 获取板块数据(按涨跌幅排序)
boards = get_board_data(board_type, sort_by="f3", limit=100)
boards = get_board_data(board_type, limit=100)
if boards:
results['boards'][board_type] = boards
# 检查异动
anomaly = check_anomaly(boards)
results['anomalies'][board_type] = anomaly
if anomaly['pct_up'] or anomaly['pct_down'] or anomaly['flow_in'] or anomaly['flow_out']:
results['has_anomaly'] = True
boards_data[board_type] = boards
if verbose:
# 打印TOP10涨跌
sorted_by_pct = sorted(boards, key=lambda x: x['pct_change'], reverse=True)
print_board_summary(sorted_by_pct[:10], f"涨幅TOP10 ({board_type})")
print_board_summary(sorted_by_pct[-10:], f"跌幅TOP10 ({board_type})")
# 打印异动报告
print_anomaly_report(anomaly)
print(f"✅ 成功获取 {len(boards)} 条数据")
else:
print(f"❌ 获取 {board_type} 数据失败")
boards_data[board_type] = []
# 发送通知(仅在发现异动时)
if notify and results['has_anomaly']:
subject = "【板块异动警报】检测到板块异动"
# 合并所有异动
combined_anomaly = {
'pct_up': [],
'pct_down': [],
'flow_in': [],
'flow_out': [],
'timestamp': datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
for anomaly in results['anomalies'].values():
combined_anomaly['pct_up'].extend(anomaly['pct_up'])
combined_anomaly['pct_down'].extend(anomaly['pct_down'])
combined_anomaly['flow_in'].extend(anomaly['flow_in'])
combined_anomaly['flow_out'].extend(anomaly['flow_out'])
# 去重(按板块名称)
for key in ['pct_up', 'pct_down', 'flow_in', 'flow_out']:
seen = set()
unique = []
for board in combined_anomaly[key]:
if board['name'] not in seen:
seen.add(board['name'])
unique.append(board)
combined_anomaly[key] = unique
html_body = generate_html_report(combined_anomaly, "行业+概念板块")
send_notification(subject, html_body)
return results
# 生成并发送报告
if boards_data.get('industry') or boards_data.get('concept'):
return generate_daily_report(boards_data, to_email)
else:
print("❌ 所有数据获取失败,无法生成报告")
return False
def main():
"""命令行入口"""
import argparse
parser = argparse.ArgumentParser(description="A股板块监控系统")
parser = argparse.ArgumentParser(description="A股板块盘后分析系统")
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 获取数据命令
get_parser = subparsers.add_parser("get", help="获取板块数据")
get_parser.add_argument("type", choices=["industry", "concept"], help="板块类型")
get_parser.add_argument("--sort", choices=["pct", "flow"], default="pct", help="排序方式")
get_parser.add_argument("--limit", type=int, default=20, help="返回数量")
# 监控命令
monitor_parser = subparsers.add_parser("monitor", help="执行监控检查")
monitor_parser.add_argument("--types", nargs="+", default=["industry", "concept"], help="板块类型")
monitor_parser.add_argument("--no-notify", action="store_true", help="不发送通知")
monitor_parser.add_argument("-v", "--verbose", action="store_true", help="显示详细日志")
# 测试命令
subparsers.add_parser("test", help="测试API连接")
# 获取数据命令
get_parser = subparsers.add_parser("get", help="获取板块数据")
get_parser.add_argument("type", choices=["industry", "concept"], help="板块类型")
get_parser.add_argument("--limit", type=int, default=20, help="返回数量")
# 发送报告命令
report_parser = subparsers.add_parser("report", help="生成并发送盘后报告")
report_parser.add_argument("--to", default="wlq@tphai.com", help="收件人邮箱")
report_parser.add_argument("-v", "--verbose", action="store_true", help="显示详细日志")
args = parser.parse_args()
if args.command == "get":
sort_by = "f3" if args.sort == "pct" else "f66"
boards = get_board_data(args.type, sort_by=sort_by, limit=args.limit)
if boards:
print(f"\n📊 {args.type} 板块数据 ({len(boards)} 条)")
print("=" * 50)
for board in boards:
print(format_board_line(board))
else:
print("❌ 获取数据失败")
elif args.command == "monitor":
monitor(
board_types=args.types,
notify=not args.no_notify,
verbose=args.verbose
)
elif args.command == "test":
if args.command == "test":
print("\n🧪 测试东方财富API连接...")
for board_type in ["industry", "concept"]:
print(f"\n测试 {board_type} 板块...")
@@ -406,10 +341,26 @@ def main():
if boards:
print(f"✅ 成功获取 {len(boards)} 条数据")
for board in boards[:3]:
print(f" - {board['name']}: {board['pct_change']:+.2f}%")
pct_str = f"+{board['pct_change']:.2f}%" if board['pct_change'] > 0 else f"{board['pct_change']:.2f}%"
print(f" - {board['name']}: {pct_str}")
else:
print(f"{board_type} 测试失败")
elif args.command == "get":
boards = get_board_data(args.type, limit=args.limit)
if boards:
print(f"\n📊 {args.type} 板块数据 ({len(boards)} 条)")
print("=" * 50)
for board in boards:
pct_str = f"+{board['pct_change']:.2f}%" if board['pct_change'] > 0 else f"{board['pct_change']:.2f}%"
flow_str = f"+{board['main_flow']:.2f}亿" if board['main_flow'] > 0 else f"{board['main_flow']:.2f}亿"
print(f"{board['name']}: {pct_str}, 主力{flow_str}")
else:
print("❌ 获取数据失败")
elif args.command == "report":
run_daily_report(to_email=args.to, verbose=args.verbose)
else:
parser.print_help()