skills/ppt-station-skill/scripts/gen_moutai_report.py
wangyitong a65adcc2e5 Initial commit: merged, deduplicated, and vetted skill collection
Sources: extracted from two upstream archives (skill-repo, skills-main),
merged with the following policy:

- 15 broken symlinks (pointing to /Users/jameslee/.cc-switch/skills or
  ../../.agents/skills on a foreign machine) discarded
- 3 real name collisions with identical content (ai-pair, ifind-http-api,
  zhipu-websearch) kept as one copy
- Functional overlaps deduped keeping the strongest variant:
  - docx family: kept docx (official, full toolchain) + docx-cn
    (GB/T 9704 Chinese official-document constants),
    dropped docx_writer (no scripts, name collided with docx)
  - humanizer family: kept humanizer-zh (6 zh reference docs),
    dropped humanizer (en, redundant for CN workflow)
- Skills that only ran in a foreign environment removed:
  ablemind-ops, app-publish, hlb-design-system, openclaw-adj-skill,
  claude-driver
- alphapai excluded from this public repo because its SKILL.md hard-coded
  live credentials

Result: 25 skills, 572 files, ~7.5 MB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 14:47:12 +08:00

339 lines
12 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.

#!/usr/bin/env python3
"""贵州茅台年度分析报告生成器
独立示例脚本,演示如何使用 tushare + ppt_station 生成完整投资分析报告。
新报告请优先使用 run_job.py + Job JSON 声明式编排。
使用 tushare 获取数据,生成包含复杂双轴图表的 PPT。
"""
import sys
import json
from pathlib import Path
from datetime import datetime
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import tushare as ts
import pandas as pd
import numpy as np
from ppt_station.config import settings
# ============================================================================
# 配置
# ============================================================================
STOCK_CODE = "600519.SH"
STOCK_NAME = "贵州茅台"
INDEX_CODE = "000300.SH"
INDEX_NAME = "沪深300"
YEAR = 2025
OUTPUT_DIR = Path(__file__).resolve().parent.parent.parent / "output" / "moutai_report"
TEMPLATE_PATH = Path(__file__).resolve().parent.parent.parent / "aim" / "aim03.pptx"
def fetch_data():
"""从 tushare 获取所有需要的数据"""
pro = ts.pro_api(settings.tushare_token)
print("📊 获取贵州茅台日线数据...", file=sys.stderr)
# 当年日线数据
df_stock = ts.pro_bar(
ts_code=STOCK_CODE,
start_date=f"{YEAR}0101",
end_date=f"{YEAR}1231",
adj="qfq",
)
df_stock = df_stock.sort_values("trade_date").reset_index(drop=True)
df_stock["trade_date"] = pd.to_datetime(df_stock["trade_date"], format="%Y%m%d")
print("📊 获取沪深300指数数据...", file=sys.stderr)
# 对应的沪深300指数使用 pro_bar + asset='I' 替代 index_daily
df_index = ts.pro_bar(
ts_code=INDEX_CODE,
start_date=f"{YEAR}0101",
end_date=f"{YEAR}1231",
asset="I",
)
df_index = df_index.sort_values("trade_date").reset_index(drop=True)
df_index["trade_date"] = pd.to_datetime(df_index["trade_date"], format="%Y%m%d")
print("📊 获取近5年日线数据...", file=sys.stderr)
# 近5年数据成立以来走势
df_stock_5y = ts.pro_bar(
ts_code=STOCK_CODE,
start_date=f"{YEAR - 4}0101",
end_date=f"{YEAR}1231",
adj="qfq",
)
df_stock_5y = df_stock_5y.sort_values("trade_date").reset_index(drop=True)
df_stock_5y["trade_date"] = pd.to_datetime(df_stock_5y["trade_date"], format="%Y%m%d")
df_index_5y = ts.pro_bar(
ts_code=INDEX_CODE,
start_date=f"{YEAR - 4}0101",
end_date=f"{YEAR}1231",
asset="I",
)
df_index_5y = df_index_5y.sort_values("trade_date").reset_index(drop=True)
df_index_5y["trade_date"] = pd.to_datetime(df_index_5y["trade_date"], format="%Y%m%d")
return df_stock, df_index, df_stock_5y, df_index_5y
def compute_ytd_chart(df_stock, df_index):
"""计算当年收益率走势图数据:茅台收益率 vs 沪深300收盘价"""
# 合并数据
df = pd.merge(
df_stock[["trade_date", "close", "vol"]],
df_index[["trade_date", "close"]],
on="trade_date",
how="inner",
suffixes=("_stock", "_index"),
)
# 计算累计收益率
base_stock = df["close_stock"].iloc[0]
base_index = df["close_index"].iloc[0]
df["茅台累计收益率"] = (df["close_stock"] / base_stock - 1)
df["沪深300指数"] = df["close_index"]
# 日期列
df = df.rename(columns={"trade_date": "日期"})
return df[["日期", "沪深300指数", "茅台累计收益率"]]
def compute_5y_chart(df_stock_5y, df_index_5y):
"""计算近5年走势数据茅台累计收益率 + 成交额"""
stock = df_stock_5y[["trade_date", "close", "amount"]].copy()
stock.columns = ["trade_date", "close_stock", "amount_stock"]
index = df_index_5y[["trade_date", "close"]].copy()
index.columns = ["trade_date", "close_index"]
df = pd.merge(stock, index, on="trade_date", how="inner")
# 累计收益率
base_stock = df["close_stock"].iloc[0]
df["茅台累计收益率"] = (df["close_stock"] / base_stock - 1)
# 成交额(万元)
df["成交额(万元)"] = df["amount_stock"] / 10 # tushare amount 单位是千元
df = df.rename(columns={"trade_date": "日期"})
return df[["日期", "茅台累计收益率", "成交额(万元)"]]
def compute_stats(df_stock, df_stock_5y):
"""计算关键统计指标"""
# 当年收益率
ytd_return = df_stock["close"].iloc[-1] / df_stock["close"].iloc[0] - 1
# 5年累计收益率
total_return = df_stock_5y["close"].iloc[-1] / df_stock_5y["close"].iloc[0] - 1
# 年化收益率
n_years = len(df_stock_5y) / 245 # 大约交易日数
annualized = (1 + total_return) ** (1 / n_years) - 1 if n_years > 0 else 0
# 最新价
latest_price = df_stock["close"].iloc[-1]
# 最新市值假设总股本约12.56亿股)
total_shares = 12.56 # 亿股
market_cap = latest_price * total_shares # 亿元
return {
"ytd_return": ytd_return,
"total_return": total_return,
"annualized": annualized,
"latest_price": latest_price,
"market_cap": market_cap,
"report_date": df_stock["trade_date"].iloc[-1].strftime("%Y-%m-%d"),
"start_date": df_stock_5y["trade_date"].iloc[0].strftime("%Y-%m-%d"),
}
def main():
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
# 获取数据
df_stock, df_index, df_stock_5y, df_index_5y = fetch_data()
print(f"✅ 获取到 {len(df_stock)} 条当年数据, {len(df_stock_5y)} 条5年数据", file=sys.stderr)
# 计算图表数据
ytd_df = compute_ytd_chart(df_stock, df_index)
fivey_df = compute_5y_chart(df_stock_5y, df_index_5y)
stats = compute_stats(df_stock, df_stock_5y)
print(f"📈 当年收益率: {stats['ytd_return']:.2%}", file=sys.stderr)
print(f"📈 5年累计收益率: {stats['total_return']:.2%}", file=sys.stderr)
print(f"📈 年化收益率: {stats['annualized']:.2%}", file=sys.stderr)
print(f"📈 最新价: {stats['latest_price']:.2f}", file=sys.stderr)
# 保存 CSV
ytd_csv = OUTPUT_DIR / "ytd_chart.csv"
fivey_csv = OUTPUT_DIR / "fivey_chart.csv"
ytd_df.to_csv(ytd_csv, index=False)
fivey_df.to_csv(fivey_csv, index=False)
print(f"💾 已保存 CSV: {ytd_csv}, {fivey_csv}", file=sys.stderr)
# 构建 config.json
config = {
"text_data": {
"标题": f"{STOCK_NAME}{YEAR}年度投资分析报告",
"作者": "PPT-Station 自动生成",
"日期": stats["report_date"],
"标题1": f"{STOCK_NAME}股价与估值分析",
"标题2": f"{STOCK_NAME}财务与经营分析",
"标题3": f"{STOCK_NAME}投资价值总结",
"组合名称": f"{STOCK_NAME}{STOCK_CODE}",
"报告日期": stats["report_date"],
"成立日期": stats["start_date"],
"资产规模": f"{stats['market_cap']:.0f}亿元",
"本年收益": f"{stats['ytd_return']:.2%}",
"本年收益金额": f"{stats['latest_price']:.2f}元/股",
"累计收益率": f"{stats['total_return']:.2%}",
"累计收益金额": f"近5年({YEAR-4}-{YEAR})",
"平均年化": f"{stats['annualized']:.2%}",
},
"chart_configs": {
"当年收益率走势图": {
"csv_path": "ytd_chart.csv",
"categories_col": "日期",
"series_config": [
{
"key": "沪深300指数",
"name": f"{INDEX_NAME}指数(收盘价)",
"type": "bar",
"axis": "secondary",
},
{
"key": "茅台累计收益率",
"name": f"{STOCK_NAME}累计收益率(左轴)",
"type": "line",
"axis": "primary",
},
],
"style": {
"color_scheme": "aim00",
"line_width_pt": 2.0,
"marker_style": "none",
},
"layout": {
"title": f"{STOCK_NAME}{YEAR}年累计收益率走势图",
"legend": {
"position": "top",
"font_size_pt": 9,
"font_name": "黑体",
},
"value_axis": {
"number_format": "0%",
"font_size_pt": 9,
"font_name": "黑体",
"has_major_gridlines": False,
},
"secondary_value_axis": {
"number_format": "#,##0",
"font_size_pt": 9,
"font_name": "黑体",
"has_major_gridlines": False,
},
"date_axis": {
"base_unit": "days",
"number_format": "yyyy/mm",
},
},
},
"组合成立以来收益率走势": {
"csv_path": "fivey_chart.csv",
"categories_col": "日期",
"series_config": [
{
"key": "茅台累计收益率",
"name": f"{STOCK_NAME}累计收益率",
"type": "line",
"axis": "primary",
},
{
"key": "成交额(万元)",
"name": "成交额(万元)",
"type": "area",
"axis": "secondary",
},
],
"style": {
"color_scheme": "aim00",
"line_width_pt": 2.0,
"marker_style": "none",
},
"layout": {
"title": f"{STOCK_NAME}近5年走势{YEAR-4}-{YEAR}",
"legend": {
"position": "bottom",
"font_size_pt": 9,
"font_name": "黑体",
},
"value_axis": {
"number_format": "0%",
"font_size_pt": 9,
"font_name": "黑体",
"has_major_gridlines": False,
},
"secondary_value_axis": {
"number_format": "#,##0",
"font_size_pt": 9,
"font_name": "黑体",
"has_major_gridlines": False,
},
"date_axis": {
"base_unit": "days",
"number_format": "yyyy/mm",
},
},
},
},
}
config_path = OUTPUT_DIR / "config.json"
with open(config_path, "w", encoding="utf-8") as f:
json.dump(config, f, ensure_ascii=False, indent=2)
print(f"💾 已保存配置: {config_path}", file=sys.stderr)
# 调用 generate_ppt
output_pptx = OUTPUT_DIR / f"moutai_{YEAR}_report.pptx"
print(f"\n🔧 生成 PPT...", file=sys.stderr)
from ppt_station.template.replacer import TemplateReplacer
from ppt_station.chart_builder.styles import StyleConfig
from ppt_station.chart_builder.layout import (
ChartLayoutConfig,
LegendConfig,
ValueAxisConfig,
)
from ppt_station.chart_builder.date_axis import DateAxisConfig
from pptx.enum.chart import XL_LEGEND_POSITION
# 导入 generate_ppt 的 build_chart_config
sys.path.insert(0, str(Path(__file__).resolve().parent))
from generate_ppt import build_chart_config
chart_configs = {}
for chart_name, chart_def in config["chart_configs"].items():
print(f" 构建图表: {chart_name}", file=sys.stderr)
chart_configs[chart_name] = build_chart_config(chart_name, chart_def, OUTPUT_DIR)
replacer = TemplateReplacer(TEMPLATE_PATH)
replacer.replace(data=config["text_data"], chart_configs=chart_configs)
replacer.save(output_pptx)
print(f"\n✅ 报告已生成: {output_pptx}", file=sys.stderr)
print(json.dumps({"status": "ok", "output": str(output_pptx)}))
if __name__ == "__main__":
main()