Skill: laigen/financial-event-impact-analyzer (version 1.0.5)

--- name: financial-event-impact-analyzer description: "Analyze historical impact of financial events on related assets. Kensho-style event-driven analysis. Use when: asking about asset reactions to events (oil surge, gold rise, rate hikes), historical precedent analysis, causal indicator relationships. Outputs Chinese reports and charts." dependencies: - python3 - pandas - numpy - matplotlib - yfinance - tushare (TUSHARE_TOKEN env variable) - chinese-fonts: SimHei / WenQuanYi Micro Hei / Noto Sans CJK SC env: TUSHARE_TOKEN: required: true description: "Tushare Pro API Token for Chinese A-share market data. Register at: https://tushare.pro/register" sensitive: true FRED_API_KEY: required: false description: "FRED API Key for US macroeconomic data (optional). Register at: https://fred.stlouisfed.org/docs/api/api_key.html" sensitive: true --- # Financial Event Impact Analyzer (金融事件影响分析器) ## Overview 分析特定金融事件发生时相关资产的历史表现。类似 Kensho 的事件驱动分析：识别事件 → 查找历史先例 → 推论相关资产 → 分析历史表现。 **输出**: 中文分析报告 + 中文标注图表 --- ## 环境依赖 ### ⚠️ 前置检查（重要） **每次执行前必须检查**: | 检查项 | 命令 | 失败修复 | |--------|------|---------| | **中文字体** | `fc-list :lang=zh family` | `sudo apt install fonts-wqy-microhei fonts-noto-cjk` | | **TUSHARE_TOKEN** | `echo $TUSHARE_TOKEN` | 在 ~/.bashrc 中设置 | | **输出目录** | `mkdir -p memory/reports/` | 确保有写权限 | **自动检查**: `generate_charts.py` 会自动执行前置检查，输出结果。 ### Python 包依赖 ```bash pip install yfinance pandas numpy matplotlib tushare ``` ### 🔤 中文字体依赖（重要） 图表使用中文标注，需要系统安装中文字体： | 系统 | 安装命令 | |------|---------| | **Ubuntu/Debian** | `sudo apt install fonts-wqy-microhei fonts-noto-cjk` | | **CentOS/RHEL** | `sudo yum install wqy-microhei-fonts` | | **macOS** | 系统自带 PingFang SC / Heiti SC | | **Windows** | 系统自带 SimHei（黑体） | **验证字体**: ```bash # 查看可用中文字体 fc-list :lang=zh family # matplotlib 查找字体 python3 -c "import matplotlib.font_manager; print([f.name for f in matplotlib.font_manager.fontManager.ttflist if 'Hei' in f.name or 'Noto' in f.name])" ``` **备选字体优先级**: 1. `SimHei` (黑体，Windows/macOS) 2. `WenQuanYi Micro Hei` (Linux常用) 3. `Noto Sans CJK SC` (现代Linux) 4. `PingFang SC` (macOS) ### 数据源环境变量 ```bash export TUSHARE_TOKEN="your_token_here" # 注册: https://tushare.pro export FRED_API_KEY="your_key_here" # 可选，用于 FRED 数据 ``` --- ## Workflow（10步工作流程） ### Step 0: 创建分析输出目录 ⭐ 重要 **目标**: 创建本次分析的专用输出文件夹，放置在 **workspace** 目录下，避免污染 skill 目录。 **命名规则**: `<indicator>_<YYYYMMDD>_<direction>` - 例如: `brent_crude_20260328_increase` **输出目录**: `~/.openclaw/workspace/memory/reports/<analysis_folder>/` **目录结构**: ``` ~/.openclaw/workspace/memory/reports/brent_crude_20260328_increase/ ├── data/ # 中间数据文件 │ ├── brent_crude.json │ ├── xle.json │ └── jets.json │ └── ... ├── charts/ # 图表文件 │ ├── brent_crude_vs_xle.png │ ├── brent_crude_vs_jets.png │ ├── performance_summary.png │ ├── event_matrix_heatmap.png │ └── charts_manifest.json ├── events.json # 相似事件列表 ├── related.json # 相关资产推论 ├── performance.json # 表现分析结果 └── report.md # 最终分析报告 ``` **执行**: ```bash # 创建输出目录（使用 workspace 绝对路径） OUTPUT_DIR="$HOME/.openclaw/workspace/memory/reports/brent_crude_20260328_increase" mkdir -p "${OUTPUT_DIR}/data" "${OUTPUT_DIR}/charts" ``` --- ### Step 1: 识别表征指标 **目标**: 将用户的事件描述映射到具体指标。 **事件 → 指标映射表**: | 事件类型 | 表征指标 | |---------|---------| | 油价暴涨/暴跌 | `brent_crude` 或 `wti_crude` | | 黄金价格变动 | `gold` | | 利率变化 | `us_10y_treasury` 或 `fed_funds_rate` | | VIX/波动率飙升 | `vix` | | 美元走强/走弱 | `usd_index` | | 通胀变化 | `cpi_us` | | 衰退信号 | `unemployment_us` | | 美联储缩表 | `fed_funds_rate` + `us_10y_treasury` | **执行**: ```bash python scripts/fetch_indicator_data.py --list ``` ### Step 2: 获取历史数据 **目标**: 获取表征指标 30+ 年时序数据。 **执行**: ```bash python scripts/fetch_indicator_data.py <indicator_id> --years 30 --output ${OUTPUT_DIR}/data/<indicator_id>.json --json ``` ### Step 3: 提取当前事件特征 **目标**: 量化当前事件（变动幅度、方向、波动率）。 **执行**: ```bash python scripts/find_similar_events.py --data ${OUTPUT_DIR}/data/<indicator_id>.json --output ${OUTPUT_DIR}/events.json ``` ### Step 4: 查找历史相似事件 已在 Step 3 完成，`events.json` 包含相似事件列表。 ### Step 5: 推论相关资产（多经济体分析） ⭐ 核心改动 **目标**: 基于因果关系确定受影响资产，覆盖多个经济体，**不仅限于可交易的金融资产标的**。 **因果分析框架（扩展）**: | 维度 | 说明 | 示例 | |------|------|------| | **上下游** | 供应链传导效应 | 油价→能源股(受益)→航空股(受损) | | **替代效应** | 替代产品/服务 | 油价涨→电动车需求↑ | | **互补效应** | 联合需求模式 | 油价涨→天然气联动 | | **金融关联** | 资本流动、风险情绪 | 美股波动→A股联动 | | **多经济体传导** ⭐ 核心 | 跨市场传导 | 美元走强→新兴市场资本流出 | | **宏观经济指标** ⭐ 核心 | 非交易性指标传导 | 油价暴涨→各国CPI/PPI/M2变化 | **多经济体分析范围** ⭐ 核心改动（必须覆盖）: | 经济体 | 分析范围 | 具体指标 | 获取方式 | |-------|---------|---------|---------| | **中国** | 股指 + 宏观 + 利率 + 货币 + 汇率 | 沪深300/创业板、CPI/PPI/GDP/PMI、LPR、M2/M1、USD/CNY | Tushare | | **日本** | 股指 + 利率 + 汇率 + 宏观 | 日经225/TOPIX、10Y国债、USD/JPY、GDP/CPI/M2 | Yahoo Finance / FRED | | **韩国** | 股指 + 汇率 + 宏观 | KOSPI/KOSDAQ、USD/KRW、GDP/CPI | Yahoo Finance / FRED | | **欧洲** | 股指 + 利率 + 汇率 + 宏观 | 斯托克50/DAX/CAC40/富时100、德国10Y、EUR/USD、GDP/CPI/M2 | Yahoo Finance / FRED | | **亚太新兴** | 股指 + 汇率 | 恒生指数、印度Nifty、东南亚指数、汇率 | Yahoo Finance | **⚠️ 强制要求**: 每次分析必须覆盖以上所有经济体的**股市、宏观经济指标、利率、货币发行量、汇率**等维度，不仅限于可交易的金融资产标的（ETF、股票）。 **多经济体因果映射** (详见 `references/indicator_mapping.md`): | 事件类型 | 中国影响 | 日本影响 | 韩国影响 | 欧洲影响 | |---------|---------|---------|---------|---------| | **油价暴涨** | 能源股受益，航空化工受损 | 能源股受益，制造业受损 | 能源股受益，化工受损 | 能源股受益，航空汽车受损 | | **美元走强** | 出口股受益，资本流出压力 | 出口股受益，日元贬值 | 出口股受益，韩元贬值 | 出口股受益，欧元贬值 | | **利率上升** | 银行保险受益，成长股受损 | 银行股受益 | 银行股受益 | 银行股受益，地产股受损 | | **通胀上升** | 抗通胀资产受益 | 通胀摆脱通缩 | 抗通胀资产受益 | 抗通胀资产受益 | | **VIX飙升** | 防守板块受益，成长股受损 | 防守板块受益 | 防守板块受益 | 防守板块受益 | **执行**: ```bash python scripts/infer_related_assets.py --indicator <indicator_id> --direction <increase|decrease> --multi-economy --output ${OUTPUT_DIR}/related.json ``` **输出格式扩展**: ```json { "benefited": [ {"indicator": "xle", "reason": "能源ETF直接受益", "economy": "US"}, {"indicator": "csi300_energy", "reason": "中国能源板块受益", "economy": "China"}, {"indicator": "nikkei225_energy", "reason": "日本能源股受益", "economy": "Japan"} ], "harmed": [ {"indicator": "jets", "reason": "航空ETF燃油成本上升", "economy": "US"}, {"indicator": "csi300_airline", "reason": "中国航空股成本上升", "economy": "China"}, {"indicator": "dax_auto", "reason": "德国汽车股成本上升", "economy": "Europe"} ], "macro_indicators": [ {"indicator": "cpi_china", "reason": "油价上涨推升中国CPI", "economy": "China"}, {"indicator": "cpi_eurozone", "reason": "油价上涨推升欧元区CPI", "economy": "Europe"} ] } ``` ### Step 6: 分析历史表现 **目标**: 计算每类资产在各历史事件期间的表现。 **执行**: ```bash # 先获取所有相关指标数据 for indicator in $(cat ${OUTPUT_DIR}/related.json | jq -r '.benefited[].indicator, .harmed[].indicator'); do python scripts/fetch_indicator_data.py $indicator --years 30 --output ${OUTPUT_DIR}/data/$indicator.json --json done # 分析各事件期间表现 python scripts/analyze_related_performance.py \ --events ${OUTPUT_DIR}/events.json \ --related ${OUTPUT_DIR}/related.json \ --data-dir ${OUTPUT_DIR}/data \ --output ${OUTPUT_DIR}/performance.json ``` ### Step 7: 生成图表（前置检查） ⭐ 核心改动 **目标**: 可视化时序对比和表现统计，确保中文不乱码，X轴标签不重叠。 **前置检查项**: | 检查项 | 要求 | 失败处理 | |--------|------|---------| | **中文字体** | WenQuanYi/SimHei/Noto CJK 可用 | 安装 fonts-wqy-microhei | | **输出目录** | 写权限正常 | 检查目录权限 | | **matplotlib** | 已安装 | pip install matplotlib | **图表规范**（核心改动）: | 图表类型 | 内容 | 格式 | 要求 | |---------|------|------|------| | **时序对比图** | 每类资产单独一张，双Y轴叠加 | 中文标题/标签 | ⭐ **强制每一项生成单独图表** | | **表现汇总图** | 各资产平均收益对比 | 中文标注 | - | | **事件矩阵图** | 资产×事件收益矩阵 | 中文热力图 | - | **时序对比图增强功能** ⭐ 核心改动: - ✅ **红色虚线方框**：标记过往同类事件发生时间区间 - ✅ **虚线框高度覆盖整个Y轴区域**：从Y轴底部到顶部，**不能仅覆盖表征指标的数值区域** - ✅ **事件标签**：显示事件编号和涨跌幅 - ✅ **X轴自适应**：根据数据跨度自动调整刻度间隔，避免标签重叠 - ✅ **图表宽度自适应**：长时间跨度使用更宽图表 **⚠️ 强制要求**: - **每一项相关指标都必须生成单独的时序对比图**（不能合并或省略） - **红色虚线框必须覆盖整个Y轴区域**（从底部到顶部），而非仅覆盖数值范围 **技术实现说明**（关键代码）: ```python # ⭐ 核心技术：使用 ax.get_xaxis_transform() 让矩形覆盖整个Y轴区域 rect = plt.Rectangle( (mdates.date2num(start_date), 0), # Y=0（图表底部） mdates.date2num(end_date) - mdates.date2num(start_date), 1, # Y高度=1（从底部到顶部，覆盖整个图表区域） transform=ax1.get_xaxis_transform(), # ⭐ 关键：X轴数据坐标，Y轴axes坐标(0-1) clip_on=False # 允许超出轴边界 ) ``` **原理**: `ax.get_xaxis_transform()` 使用混合坐标系： - X轴：数据坐标（实际日期） - Y轴：axes坐标（0-1，覆盖整个图表高度） 这样矩形会覆盖整个Y轴区域，无论表征指标的实际数值范围如何。 **X轴间隔自适应规则**: | 数据跨度 | 刻度间隔 | |:--------:|:--------:| | ≤12个月 | 每月 | | ≤24个月 | 每季度 | | ≤60个月 | 半年 | | ≤120个月 | 每年 | | >120个月 | 每两年 | **执行**: ```bash python scripts/generate_charts.py \ --primary-indicator <indicator_id> \ --primary-data ${OUTPUT_DIR}/data/<indicator_id>.json \ --events ${OUTPUT_DIR}/events.json \ --related ${OUTPUT_DIR}/related.json \ --performance ${OUTPUT_DIR}/performance.json \ --output-dir ${OUTPUT_DIR}/charts ``` --- ### Step 8: 生成报告（嵌入图片） ⭐ 新增 **目标**: 生成完整中文分析报告，将所有时序对比图嵌入 Markdown。 **报告结构**: 1. **🎯 核心结论**（优先展示） 2. 当前事件特征 3. 历史相似事件列表 4. 变动幅度表格（从最新到最早） 5. 汇总统计 6. **📈 时序对比图（嵌入Markdown）** ⭐ 新增 7. 📊 表现汇总图 8. 🔥 事件矩阵热力图 9. 分析局限性 **图片嵌入格式**: ```markdown ![图表名称](charts/图表文件.png) ``` **执行**: ```bash python scripts/generate_report.py \ --primary-indicator <indicator_id> \ --events ${OUTPUT_DIR}/events.json \ --related ${OUTPUT_DIR}/related.json \ --performance ${OUTPUT_DIR}/performance.json \ --charts-manifest ${OUTPUT_DIR}/charts/charts_manifest.json \ --output-dir ${OUTPUT_DIR} \ --output report.md ``` --- ### Step 9: 展示报告和图表 **目标**: 输出完整中文报告，向 chat 窗口展示报告内容和图表。 **报告结构** (详见 `references/report_template.md`): 1. **🎯 核心结论**（优先展示） 2. 事件概述 3. 当前事件特征 4. 历史相似事件 5. 相关资产推论 6. **历史变动幅度表格**（新增，从最新到最早） 7. 历史表现汇总 8. 图表说明 9. 分析局限性 **展示要求**: 报告生成后，将完整内容输出到 chat 窗口，并附带已生成的图表图片。 --- ## 图表规范详解 ### 1. 时序对比图（每类资产一张） **文件名**: `<primary_indicator>_vs_<related_indicator>.png` **结构**: ``` ┌─────────────────────────────────────────────┐ │ 油价 vs 能源ETF 历史走势对比 │ ← 中文标题 ├─────────────────────────────────────────────┤ │ │ │ ┌───────┐ 红色虚线框 │ │ │事件区间│ 标记历史事件 │ │ └───────┘ │ │ │ │ 左Y轴: 油价(美元/桶) 右Y轴: 能源ETF价格 │ ← 双Y轴中文标注 │ ── 油价曲线 ── ETF曲线 │ │ │ ├─────────────────────────────────────────────┤ │ 时间轴: 2010.01.01 - 2024.12.31 │ ← YYYY.MM.DD 格式 └─────────────────────────────────────────────┘ ``` **颜色规范**: - 🔴 **红色** = 上涨 (+) - 🟢 **绿色** = 下跌 (-) - ⬛ **红色虚线框** = 历史事件区间 ### 2. 变动幅度表格（新增） **格式**: Markdown 表格，按时间从最新到最早排列 | 历史事件 | 时间区间 | 表征指标变动 | 相关资产A | 相关资产B | ... | |---------|---------|-------------|----------|----------|-----| | **事件1**（最新） | 2024.03.15-2024.06.20 | 🔴 +42% | 🔴 +15% | 🟢 -8% | ... | | **事件2** | 2022.02.10-2022.04.05 | 🔴 +38% | 🔴 +12% | 🟢 -5% | ... | | **事件3** | 2020.03.09-2020.04.21 | 🟢 -55% | 🟢 -28% | 🔴 +10% | ... | | ... | ... | ... | ... | ... | ... | **高亮规则**: - 相关性高（同向变动）的单元格加粗显示 - 反向变动的单元格使用不同颜色标记 --- ## 指标映射参考 详见 `references/indicator_mapping.md`： | 类别 | 可用指标 | |------|---------| | **大宗商品** | brent_crude, wti_crude, gold, silver, copper, aluminum, natural_gas | | **美股指数** | sp500, nasdaq, dow_jones, russell2000, vix | | **美股行业ETF** | xlk, xle, xlf, xlv, xli, jets, iyr, gdx, tlt | | **外汇** | usd_cny, usd_index, eur_usd | | **美债** | us_10y_treasury, us_2y_treasury | | **FRED宏观** | fed_funds_rate, cpi_us, gdp_us, unemployment_us | | **A股指数** | csi300, sse_composite, chinext, sse50 | --- ## 事件 → 相关资产因果逻辑 ### 油价暴涨 (`oil_price_increase`) | 类别 | 资产 | 效果 | 原因 | |------|------|------|------| | **受益** | 能源ETF (xle) | ↑ | 收入增加 | | **受益** | 黄金 (gold) | ↑ | 通胀对冲 | | **受损** | 航空ETF (jets) | ↓ | 燃油成本增加 | | **受损** | 工业ETF (xli) | ↓ | 能源成本上升 | ### 黄金暴涨 (`gold_price_increase`) | 类别 | 资产 | 效果 | 原因 | |------|------|------|------| | **受益** | 黄金矿业ETF (gdx) | ↑ | 直接受益 | | **受益** | 白银 (silver) | ↑ | 贵金属联动 | | **受损** | 股市 (sp500) | ↓ | 避险信号 | | **受损** | 金融ETF (xlf) | ↓ | 风险规避 | ### 加息 (`interest_rate_increase`) | 类别 | 资产 | 效果 | 原因 | |------|------|------|------| | **受益** | 金融ETF (xlf) | ↑ | 利差扩大 | | **受益** | 美元指数 (usd_index) | ↑ | 资本流入 | | **受损** | 债券ETF (tlt) | ↓ | 利率上升 | | **受损** | REITs (iyr) | ↓ | 融资成本 | | **受损** | 成长股 (nasdaq) | ↓ | 估值压制 | ### VIX飙升 (`vix_increase`) | 类别 | 资产 | 效果 | 原因 | |------|------|------|------| | **受益** | 防守板块 (xlp, xlv) | ↑ | 避险 | | **受益** | 黄金 (gold) | ↑ | 风险规避 | | **受损** | 股市 (sp500) | ↓ | 风险规避 | | **受损** | 金融ETF (xlf) | ↓ | 风险规避 | ### 美联储缩表 (`fed_balance_sheet_reduction`) | 类别 | 资产 | 效果 | 原因 | |------|------|------|------| | **受益** | 美元指数 (usd_index) | ↑ | 流动性收紧 | | **受益** | 金融ETF (xlf) | ↑ | 利差扩大 | | **受损** | 债券ETF (tlt) | ↓ | 收益率上行 | | **受损** | 成长股 (nasdaq) | ↓ | 流动性压制 | --- ## 输出要求 ### 图表 - **语言**: 中文标题、标签、图例 - **时间格式**: YYYY.MM.DD - **颜色**: 🔴 红色 = 上涨 (+)，🟢 绿色 = 下跌 (-) - **事件标记**: 红色虚线框（历史），红色实线框（当前） - **双Y轴**: 左轴表征指标，右轴关联资产 ### 报告 - **语言**: 中文 - **格式**: Markdown 表格 - **结构**: 结论优先 → 详细统计 - **署名**: 🐂 --- ## 示例：油价暴涨分析 **用户输入**: "分析历史上油价快速上涨后各类资产的表现情况" **工作流程**: 1. 创建输出目录 → `~/.openclaw/workspace/memory/reports/brent_crude_20260328_increase/` 2. 识别 → `brent_crude` 3. 获取 → 30年布伦特原油数据 → `${OUTPUT_DIR}/data/brent_crude.json` 4. 提取 → 当前: 90天内+40%（上涨） 5. 查找 → 5次历史油价暴涨事件 → `${OUTPUT_DIR}/events.json` 6. 推论 → 能源ETF(受益)、航空ETF(受损)、黄金(对冲) → `${OUTPUT_DIR}/related.json` 7. 分析 → 各资产在各历史事件期间的收益 → `${OUTPUT_DIR}/performance.json` 8. 图表 → 时序对比图(每类资产一张)、表现汇总图 → `${OUTPUT_DIR}/charts/` 9. 报告 → 中文分析报告 + 变动幅度表格 → `${OUTPUT_DIR}/report.md` --- ## 参考文件 - `references/indicator_mapping.md`: 完整指标列表和数据源 - `references/report_template.md`: 中文报告模板结构

path: skill.json
content: {
  "id": "financial-event-impact-analyzer",
  "name": "financial-event-impact-analyzer",
  "version": "1.1.0",
  "description": "Analyze historical impact of financial events on related assets. Event-driven analysis similar to Kensho: identify event -> find historical precedents -> infer related assets -> analyze historical performance. Outputs Chinese reports and Chinese-labeled charts.",
  "author": "laigen",
  "created": "2026-03-27",
  "updated": "2026-03-29",
  "tags": ["financial-events", "event-analysis", "kensho", "historical-impact", "asset-performance", "causal-analysis"],
  "entry": "scripts/fetch_indicator_data.py",
  "triggers": [
    "financial event impact analysis",
    "event-driven analysis",
    "historical impact analysis",
    "oil price surge impact",
    "gold price increase impact",
    "interest rate hike impact",
    "VIX spike impact",
    "Fed balance sheet reduction impact",
    "USD strength impact",
    "Kensho analysis"
  ],
  "registry": {
    "summary": {
      "requiredEnvVars": ["TUSHARE_TOKEN"],
      "optionalEnvVars": ["FRED_API_KEY"],
      "primaryCredential": "TUSHARE_TOKEN",
      "networkAccess": true,
      "externalServices": ["Yahoo Finance", "Tushare API", "FRED API"],
      "outputsFiles": true,
      "writesFiles": true
    }
  },
  "env": {
    "TUSHARE_TOKEN": {
      "required": true,
      "description": "Tushare Pro API Token for Chinese A-share market data. Register at: https://tushare.pro/register",
      "sensitive": true
    },
    "FRED_API_KEY": {
      "required": false,
      "description": "FRED API Key for US macroeconomic data (optional). Register at: https://fred.stlouisfed.org/docs/api/api_key.html",
      "sensitive": true
    }
  },
  "dependencies": {
    "pip": [
      "pandas>=1.5.0",
      "numpy>=1.20.0",
      "matplotlib>=3.5.0",
      "yfinance>=0.2.0",
      "tushare>=1.2.0"
    ],
    "fonts": [
      "fonts-wqy-microhei",
      "fonts-noto-cjk"
    ]
  },
  "sideEffects": {
    "readsFiles": true,
    "writesFiles": true,
    "networkAccess": true,
    "externalServices": ["Yahoo Finance", "Tushare API", "FRED API"]
  },
  "security": {
    "requiresApproval": false,
    "sensitiveData": true,
    "riskLevel": "low",
    "credentialsRequired": ["TUSHARE_TOKEN"],
    "credentialsOptional": ["FRED_API_KEY"],
    "credentialSources": {
      "TUSHARE_TOKEN": "https://tushare.pro/register",
      "FRED_API_KEY": "https://fred.stlouisfed.org/docs/api/api_key.html"
    }
  },
  "output": {
    "formats": ["markdown", "png", "json"],
    "location": "~/.openclaw/workspace/memory/reports/<analysis_folder>/",
    "description": "Generate Chinese analysis reports, dual-axis time series comparison charts (one per asset), performance summary charts, event matrix heatmaps. Charts use Chinese labels. All output files are placed under workspace/memory/reports/<analysis_folder>/, not polluting the skill directory.",
    "folderNaming": "<indicator>_<YYYYMMDD>_<direction> (e.g., brent_crude_20260328_increase)"
  },
  "preflight": {
    "checks": [
      "Chinese font availability check",
      "TUSHARE_TOKEN environment variable check",
      "Output directory permission check"
    ],
    "fontPriority": ["WenQuanYi Micro Hei", "SimHei", "Noto Sans CJK SC", "PingFang SC"],
    "fallbackFont": "DejaVu Sans"
  },
  "workflow": [
    {
      "step": 0,
      "description": "Create analysis output directory",
      "command": "OUTPUT_DIR=\"$HOME/.openclaw/workspace/memory/reports/<indicator>_<timestamp>_<direction>\" && mkdir -p \"${OUTPUT_DIR}/data\" \"${OUTPUT_DIR}/charts\""
    },
    {
      "step": 1,
      "description": "Identify primary indicator",
      "script": "scripts/fetch_indicator_data.py --list"
    },
    {
      "step": 2,
      "description": "Fetch historical data",
      "script": "scripts/fetch_indicator_data.py <indicator> --years 30 --output ${OUTPUT_DIR}/data/<indicator>.json --json"
    },
    {
      "step": 3,
      "description": "Extract event characteristics and find similar events",
      "script": "scripts/find_similar_events.py --data ${OUTPUT_DIR}/data/<indicator>.json --output ${OUTPUT_DIR}/events.json"
    },
    {
      "step": 4,
      "description": "Infer related assets",
      "script": "scripts/infer_related_assets.py --indicator <indicator> --direction <increase|decrease> --output ${OUTPUT_DIR}/related.json"
    },
    {
      "step": 5,
      "description": "Fetch related indicator data",
      "script": "for ind in $(cat ${OUTPUT_DIR}/related.json | jq -r '.benefited[].indicator, .harmed[].indicator'); do scripts/fetch_indicator_data.py $ind --years 30 --output ${OUTPUT_DIR}/data/$ind.json --json; done"
    },
    {
      "step": 6,
      "description": "Analyze historical performance",
      "script": "scripts/analyze_related_performance.py --events ${OUTPUT_DIR}/events.json --related ${OUTPUT_DIR}/related.json --primary-indicator <indicator> --data-dir ${OUTPUT_DIR}/data --output ${OUTPUT_DIR}/performance.json"
    },
    {
      "step": 7,
      "description": "Generate charts (preflight check + red dashed boxes for historical event intervals + X-axis auto-adjust to avoid overlap)",
      "script": "scripts/generate_charts.py --primary-indicator <indicator> --primary-data ${OUTPUT_DIR}/data/<indicator>.json --events ${OUTPUT_DIR}/events.json --related ${OUTPUT_DIR}/related.json --performance ${OUTPUT_DIR}/performance.json --output-dir ${OUTPUT_DIR}/charts"
    },
    {
      "step": 8,
      "description": "Generate report (embed all time series charts in Markdown)",
      "script": "scripts/generate_report.py --primary-indicator <indicator> --events ${OUTPUT_DIR}/events.json --related ${OUTPUT_DIR}/related.json --performance ${OUTPUT_DIR}/performance.json --charts-manifest ${OUTPUT_DIR}/charts/charts_manifest.json --output-dir ${OUTPUT_DIR} --output report.md"
    },
    {
      "step": 9,
      "description": "Display report and charts",
      "script": "Display ${OUTPUT_DIR}/report.md content with chart images"
    }
  ],
  "usage": {
    "example": "Analyze historical impact of oil price surge on related assets",
    "workflow_example": [
      "# Step 0: Create analysis output directory (workspace absolute path)",
      "OUTPUT_DIR=\"$HOME/.openclaw/workspace/memory/reports/brent_crude_20260328_increase\"",
      "mkdir -p \"${OUTPUT_DIR}/data\" \"${OUTPUT_DIR}/charts\"",
      "",
      "# Step 1-2: Fetch primary indicator data",
      "python3 scripts/fetch_indicator_data.py brent_crude --years 30 --output \"${OUTPUT_DIR}/data/brent_crude.json\" --json",
      "",
      "# Step 3-4: Find similar events",
      "python3 scripts/find_similar_events.py --data \"${OUTPUT_DIR}/data/brent_crude.json\" --output \"${OUTPUT_DIR}/events.json\"",
      "",
      "# Step 5: Infer related assets",
      "python3 scripts/infer_related_assets.py --indicator brent_crude --direction increase --output \"${OUTPUT_DIR}/related.json\"",
      "",
      "# Step 6: Fetch related indicator data and analyze performance",
      "for indicator in $(cat \"${OUTPUT_DIR}/related.json\" | jq -r '.benefited[].indicator, .harmed[].indicator'); do",
      "    python3 scripts/fetch_indicator_data.py $indicator --years 30 --output \"${OUTPUT_DIR}/data/$indicator.json\" --json",
      "done",
      "python3 scripts/analyze_related_performance.py --events \"${OUTPUT_DIR}/events.json\" --related \"${OUTPUT_DIR}/related.json\" --primary-indicator brent_crude --data-dir \"${OUTPUT_DIR}/data\" --output \"${OUTPUT_DIR}/performance.json\"",
      "",
      "# Step 7: Generate charts (preflight check + red dashed boxes for historical event intervals + X-axis auto-adjust)",
      "python3 scripts/generate_charts.py --primary-indicator brent_crude --primary-data \"${OUTPUT_DIR}/data/brent_crude.json\" --events \"${OUTPUT_DIR}/events.json\" --related \"${OUTPUT_DIR}/related.json\" --performance \"${OUTPUT_DIR}/performance.json\" --output-dir \"${OUTPUT_DIR}/charts\"",
      "",
      "# Step 8: Generate report (embed all time series charts in Markdown)",
      "python3 scripts/generate_report.py --primary-indicator brent_crude --events \"${OUTPUT_DIR}/events.json\" --related \"${OUTPUT_DIR}/related.json\" --performance \"${OUTPUT_DIR}/performance.json\" --charts-manifest \"${OUTPUT_DIR}/charts/charts_manifest.json\" --output-dir \"${OUTPUT_DIR}\" --output report.md",
      "",
      "# Step 9: Display report and charts",
      "# Report saved to: ${OUTPUT_DIR}/report.md, charts embedded in Markdown"
    ]
  },
  "chart_specs": {
    "dual_axis_timeseries": {
      "description": "One dual-axis time series comparison chart per related asset",
      "axes": {
        "left": "Primary indicator (red line)",
        "right": "Related asset indicator (blue line)"
      },
      "markers": {
        "historical_events": "Red dashed boxes (with 20% opacity fill), marking past similar event time intervals",
        "current_event": "Red solid boxes (with 30% opacity fill), marking current event",
        "event_labels": "Event number + change percentage, displayed above the rectangle"
      },
      "x_axis": {
        "auto_interval": "Auto-adjust tick interval based on data span",
        "intervals": {
          "≤12 months": "Monthly",
          "≤24 months": "Quarterly",
          "≤60 months": "Half-yearly",
          "≤120 months": "Yearly",
          ">120 months": "Biennially"
        },
        "format": "YYYY.MM",
        "rotation": "30 degrees slanted, avoid label overlap"
      },
      "figure_size": {
        "auto_width": "Auto-adjust chart width based on data span",
        "width_range": "14-26 inches",
        "height": "10 inches"
      },
      "language": "Chinese"
    },
    "performance_summary": {
      "description": "Average return and win rate comparison chart for each asset",
      "colors": {
        "positive": "Red (#E74C3C)",
        "negative": "Green (#27AE60)"
      },
      "language": "Chinese"
    },
    "event_matrix_heatmap": {
      "description": "Event × Asset return matrix heatmap",
      "colormap": "RdYlGn_r (red = positive return, green = negative return)",
      "language": "Chinese"
    }
  },
  "report_specs": {
    "embedded_charts": {
      "description": "All time series comparison charts embedded in Markdown report",
      "format": "![Chart Name](charts/chart_file.png)",
      "sections": [
        "🎯 Core Conclusions",
        "📋 Current Event Characteristics",
        "📜 Historical Similar Events List",
        "📊 Change Percentage Table",
        "🔑 Summary Statistics",
        "📈 Time Series Comparison Charts (Embedded)",
        "📊 Performance Summary Chart",
        "🔥 Event Matrix Heatmap",
        "⚠️ Analysis Limitations"
      ]
    },
    "change_table": {
      "description": "Change percentage table (sorted from latest to earliest)",
      "highlight_rule": "Same-direction change (high correlation) cells bolded"
    },
    "color_convention": {
      "positive": "🔴 Red = Up (+)",
      "negative": "🟢 Green = Down (-)"
    }
  }
}

path: scripts/analyze_qt_performance.py
content: #!/usr/bin/env python3
"""分析美联储缩表周期内各类资产的历史表现"""

import json
import pandas as pd
import numpy as np
from pathlib import Path
import sys

def load_indicator_data(filepath):
    """加载指标数据"""
    with open(filepath, 'r') as f:
        data = json.load(f)
    
    df = pd.DataFrame(data['data'])
    df['date'] = pd.to_datetime(df['date'])
    df.set_index('date', inplace=True)
    df.sort_index(inplace=True)
    
    return df

def calculate_performance(df, start_date, end_date):
    """计算指定期间的涨跌幅"""
    start = pd.to_datetime(start_date)
    end = pd.to_datetime(end_date)
    
    # 获取期间数据
    period_data = df.loc[start:end]
    
    if len(period_data) < 2:
        return None
    
    start_price = period_data.iloc[0]['close']
    end_price = period_data.iloc[-1]['close']
    
    pct_change = (end_price - start_price) / start_price * 100
    
    # 计算期间最大涨跌幅
    max_price = period_data['close'].max()
    min_price = period_data['close'].min()
    
    max_drawdown = (min_price - max_price) / max_price * 100
    max_gain = (max_price - start_price) / start_price * 100
    
    return {
        'pct_change': pct_change,
        'max_gain': max_gain,
        'max_drawdown': max_drawdown,
        'start_price': start_price,
        'end_price': end_price,
        'data_points': len(period_data)
    }

def main():
    output_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home() / '.openclaw/workspace/memory/reports/fed_qt_20260328'
    data_dir = output_dir / 'data'
    
    # 加载事件定义
    with open(output_dir / 'events.json', 'r') as f:
        events = json.load(f)
    
    # 指标列表
    indicators = ['sp500', 'nasdaq', 'usd_index', 'xlf', 'tlt', 'xlk', 'xle', 'us_10y_treasury']
    
    results = {
        'events': [],
        'performance': {}
    }
    
    # 分析每个缩表周期
    for event in events['historical_events']:
        event_result = {
            'event_id': event['event_id'],
            'name': event['name'],
            'start_date': event['start_date'],
            'end_date': event['end_date'],
            'description': event['description'],
            'assets': {}
        }
        
        for indicator in indicators:
            filepath = data_dir / f'{indicator}.json'
            if filepath.exists():
                df = load_indicator_data(filepath)
                perf = calculate_performance(df, event['start_date'], event['end_date'])
                if perf:
                    event_result['assets'][indicator] = perf
        
        results['events'].append(event_result)
    
    # 计算汇总统计
    summary = {}
    for indicator in indicators:
        changes = []
        for event in results['events']:
            if indicator in event['assets']:
                changes.append(event['assets'][indicator]['pct_change'])
        
        if changes:
            summary[indicator] = {
                'avg_change': np.mean(changes),
                'min_change': np.min(changes),
                'max_change': np.max(changes),
                'win_rate': sum(1 for c in changes if c > 0) / len(changes) * 100,
                'event_count': len(changes)
            }
    
    results['summary'] = summary
    
    # 保存结果
    with open(output_dir / 'performance.json', 'w') as f:
        json.dump(results, f, indent=2, ensure_ascii=False)
    
    print(f"分析完成，结果保存到: {output_dir / 'performance.json'}")
    
    # 打印汇总
    print("\n📊 美联储缩表周期资产表现汇总:")
    print("=" * 60)
    
    indicator_names = {
        'sp500': '标普500',
        'nasdaq': '纳斯达克',
        'usd_index': '美元指数',
        'xlf': '金融ETF',
        'tlt': '长债ETF',
        'xlk': '科技ETF',
        'xle': '能源ETF',
        'us_10y_treasury': '10年美债收益率'
    }
    
    for event in results['events']:
        print(f"\n【{event['name']}】 ({event['start_date']} ~ {event['end_date']})")
        for indicator, perf in event['assets'].items():
            name = indicator_names.get(indicator, indicator)
            change = perf['pct_change']
            color = '🔴' if change > 0 else '🟢'
            print(f"  {name}: {color} {change:+.2f}%")
    
    print("\n📈 平均表现统计:")
    for indicator, stats in summary.items():
        name = indicator_names.get(indicator, indicator)
        avg = stats['avg_change']
        win_rate = stats['win_rate']
        color = '🔴' if avg > 0 else '🟢'
        print(f"  {name}: 平均{color} {avg:+.2f}%, 胜率 {win_rate:.0f}%")
    
    return results

if __name__ == '__main__':
    main()

path: scripts/analyze_related_performance.py
content: #!/usr/bin/env python3
"""
Analyze performance of related assets during historical events.
Calculates returns, volatility, and comparative metrics for each event period.
新增：生成变动幅度表格（从最新到最早排序），支持相关性高亮。
"""

import argparse
import json
import pandas as pd
import numpy as np
from datetime import datetime
from typing import Dict, List, Tuple
import os


def load_indicator_data(indicator_id: str, data_dir: str = None) -> pd.DataFrame:
    """Load historical data for an indicator."""
    if data_dir:
        # 尝试多种文件格式
        for ext in ['.json', '.csv']:
            filepath = f"{data_dir}/{indicator_id}{ext}"
            if os.path.exists(filepath):
                if ext == '.json':
                    with open(filepath, 'r') as f:
                        data = json.load(f)
                    return pd.DataFrame(data['data'])
                else:
                    return pd.read_csv(filepath)
    return None


def calculate_period_performance(df: pd.DataFrame, start_date: str, end_date: str) -> Dict:
    """Calculate performance metrics for a specific period."""
    # Filter data for the period
    df['date'] = pd.to_datetime(df['date'])
    period_df = df[(df['date'] >= start_date) & (df['date'] <= end_date)]
    
    if len(period_df) < 2:
        return None
    
    start_price = period_df['close'].iloc[0]
    end_price = period_df['close'].iloc[-1]
    
    # Calculate metrics
    total_return = ((end_price - start_price) / start_price) * 100
    
    # Daily returns
    daily_returns = period_df['close'].pct_change().dropna()
    volatility = daily_returns.std() * 100 if len(daily_returns) > 0 else 0
    max_drawdown = calculate_max_drawdown(period_df['close'])
    sharpe_ratio = calculate_sharpe_ratio(daily_returns)
    
    # Price statistics
    max_price = period_df['close'].max()
    min_price = period_df['close'].min()
    
    return {
        'start_date': start_date,
        'end_date': end_date,
        'start_price': round(float(start_price), 4),
        'end_price': round(float(end_price), 4),
        'total_return_pct': round(total_return, 2),
        'volatility_pct': round(volatility, 2),
        'max_drawdown_pct': round(max_drawdown, 2),
        'sharpe_ratio': round(sharpe_ratio, 2),
        'max_price': round(float(max_price), 4),
        'min_price': round(float(min_price), 4),
        'period_days': len(period_df)
    }


def calculate_max_drawdown(prices: pd.Series) -> float:
    """Calculate maximum drawdown percentage."""
    cumulative = prices / prices.iloc[0]
    running_max = cumulative.cummax()
    drawdown = (cumulative - running_max) / running_max
    return float(drawdown.min()) * 100


def calculate_sharpe_ratio(daily_returns: pd.Series, risk_free_rate: float = 0.02) -> float:
    """Calculate Sharpe ratio (annualized)."""
    if len(daily_returns) == 0 or daily_returns.std() == 0:
        return 0.0
    excess_return = daily_returns.mean() * 252 - risk_free_rate
    return float(excess_return / (daily_returns.std() * np.sqrt(252)))


def analyze_single_event(event: Dict, related_indicators: List[str], 
                          data_cache: Dict, primary_indicator: str) -> Dict:
    """Analyze performance of all related indicators during a single event."""
    results = {
        'event': event,
        'related_performance': {},
        'primary_performance': {}
    }
    
    # 计算主指标表现
    if primary_indicator in data_cache:
        primary_perf = calculate_period_performance(
            data_cache[primary_indicator], 
            event['start_date'], 
            event['end_date']
        )
        if primary_perf:
            results['primary_performance'] = primary_perf
    
    # 计算各相关指标表现
    for indicator_id in related_indicators:
        if indicator_id not in data_cache:
            continue
        
        df = data_cache[indicator_id]
        performance = calculate_period_performance(df, event['start_date'], event['end_date'])
        
        if performance:
            results['related_performance'][indicator_id] = performance
    
    return results


def analyze_all_events(events: List[Dict], related_indicators: List[str], 
                       data_cache: Dict, primary_indicator: str) -> List[Dict]:
    """Analyze performance across all historical events."""
    results = []
    
    # 按时间从最新到最早排序事件
    sorted_events = sorted(events, key=lambda x: x['start_date'], reverse=True)
    
    for event in sorted_events:
        result = analyze_single_event(event, related_indicators, data_cache, primary_indicator)
        results.append(result)
    
    return results


def create_change_table(event_results: List[Dict], primary_indicator: str) -> Dict:
    """
    创建变动幅度表格数据（从最新到最早排序）
    
    Returns:
        包含表格数据和相关性分析的字典
    """
    if not event_results:
        return {'table_data': [], 'high_correlation': []}
    
    table_rows = []
    high_correlation_pairs = []
    
    for i, result in enumerate(event_results):
        event = result['event']
        primary_perf = result.get('primary_performance', {})
        related_perf = result.get('related_performance', {})
        
        row = {
            'event_num': i + 1,
            'time_range': f"{event['start_date']} ~ {event['end_date']}",
            'start_date': event['start_date'],
            'end_date': event['end_date'],
            'primary_change_pct': primary_perf.get('total_return_pct', 'N/A'),
            'related_changes': {}
        }
        
        # 收集各相关指标变动
        for indicator, perf in related_perf.items():
            change_pct = perf.get('total_return_pct', 'N/A')
            row['related_changes'][indicator] = change_pct
            
            # 判断相关性（同向变动为高相关）
            if primary_perf.get('total_return_pct') is not None and change_pct != 'N/A':
                primary_change = primary_perf['total_return_pct']
                related_change = change_pct
                
                # 同向变动判断（都上涨或都下跌）
                is_same_direction = (primary_change > 0 and related_change > 0) or \
                                   (primary_change < 0 and related_change < 0)
                
                if is_same_direction:
                    high_correlation_pairs.append({
                        'event_num': i + 1,
                        'indicator': indicator,
                        'primary_change': primary_change,
                        'related_change': related_change,
                        'same_direction': True
                    })
        
        table_rows.append(row)
    
    return {
        'table_data': table_rows,
        'high_correlation': high_correlation_pairs,
        'total_events': len(event_results)
    }


def calculate_aggregate_stats(event_results: List[Dict]) -> Dict:
    """Calculate aggregate statistics by indicator."""
    if not event_results:
        return {}
    
    # 收集所有指标
    all_indicators = set()
    for result in event_results:
        all_indicators.update(result['related_performance'].keys())
    
    agg_stats = {}
    
    for indicator in all_indicators:
        returns = []
        volatilities = []
        sharpes = []
        positive_count = 0
        negative_count = 0
        
        for result in event_results:
            perf = result['related_performance'].get(indicator)
            if perf:
                returns.append(perf['total_return_pct'])
                volatilities.append(perf['volatility_pct'])
                sharpes.append(perf['sharpe_ratio'])
                if perf['total_return_pct'] > 0:
                    positive_count += 1
                elif perf['total_return_pct'] < 0:
                    negative_count += 1
        
        if returns:
            agg_stats[indicator] = {
                'avg_return_pct': round(np.mean(returns), 2),
                'std_return_pct': round(np.std(returns), 2),
                'min_return_pct': round(np.min(returns), 2),
                'max_return_pct': round(np.max(returns), 2),
                'avg_volatility_pct': round(np.mean(volatilities), 2),
                'avg_sharpe': round(np.mean(sharpes), 2),
                'positive_events': positive_count,
                'negative_events': negative_count,
                'total_events': len(returns),
                'win_rate_pct': round(positive_count / len(returns) * 100, 1)
            }
    
    return agg_stats


def create_summary_table(event_results: List[Dict]) -> pd.DataFrame:
    """Create summary table of all event performances."""
    rows = []
    
    for result in event_results:
        event = result['event']
        event_num = event_results.index(result) + 1
        
        for indicator_id, perf in result['related_performance'].items():
            rows.append({
                'Event': f"Event {event_num}",
                'Start': event['start_date'],
                'End': event['end_date'],
                'Indicator': indicator_id,
                'Return (%)': perf['total_return_pct'],
                'Volatility (%)': perf['volatility_pct'],
                'Max Drawdown (%)': perf['max_drawdown_pct'],
                'Sharpe': perf['sharpe_ratio']
            })
    
    return pd.DataFrame(rows)


def main():
    parser = argparse.ArgumentParser(
        description='分析历史事件期间相关资产表现'
    )
    parser.add_argument('--events', required=True, 
                        help='事件JSON文件 (来自 find_similar_events)')
    parser.add_argument('--related', required=True, 
                        help='相关资产JSON文件 (来自 infer_related_assets)')
    parser.add_argument('--primary-indicator', required=True,
                        help='表征指标ID')
    parser.add_argument('--data-dir', required=True, 
                        help='指标数据文件目录')
    parser.add_argument('--output', '-o', help='输出文件 (JSON)')
    parser.add_argument('--json', action='store_true', help='输出为JSON')
    
    args = parser.parse_args()
    
    # 加载事件数据
    with open(args.events, 'r') as f:
        events_data = json.load(f)
    events = events_data.get('similar_events', [])
    
    # 加载相关资产
    with open(args.related, 'r') as f:
        related_data = json.load(f)
    
    # 收集所有相关指标ID
    all_indicators = set()
    for category in ['benefited', 'harmed', 'neutral_uncertain']:
        for asset in related_data.get(category, []):
            if 'indicator' in asset:
                all_indicators.add(asset['indicator'])
    
    primary_indicator = args.primary_indicator
    all_indicators.add(primary_indicator)
    all_indicators = sorted(list(all_indicators))
    
    print(f"\n分析 {len(events)} 个历史事件，涉及 {len(all_indicators)} 个指标...")
    
    # 加载所有指标数据
    data_cache = {}
    for indicator_id in all_indicators:
        try:
            df = load_indicator_data(indicator_id, args.data_dir)
            if df is not None:
                data_cache[indicator_id] = df
                print(f"  已加载 {indicator_id}: {len(df)} 条记录")
            else:
                print(f"  未找到 {indicator_id} 数据文件")
        except Exception as e:
            print(f"  加载 {indicator_id} 失败: {e}")
    
    # 获取相关指标列表（不含主指标）
    related_indicators = [ind for ind in all_indicators if ind != primary_indicator]
    
    # 分析所有事件
    event_results = analyze_all_events(
        events, related_indicators, data_cache, primary_indicator
    )
    
    # 创建变动幅度表格
    change_table = create_change_table(event_results, primary_indicator)
    
    # 计算汇总统计
    aggregate_stats = calculate_aggregate_stats(event_results)
    
    # 创建详细表格
    summary_df = create_summary_table(event_results)
    
    # 输出
    result = {
        'primary_indicator': primary_indicator,
        'events_count': len(events),
        'related_indicators': related_indicators,
        'event_results': event_results,
        'change_table': change_table,  # 新增：变动幅度表格数据
        'aggregate_stats': aggregate_stats,
        'summary_table': summary_df.to_dict('records') if not summary_df.empty else []
    }
    
    # 打印变动幅度表格
    print(f"\n=== 变动幅度表格（从最新到最早） ===")
    print(f"\n| 事件 | 时间区间 | {primary_indicator} |", end='')
    for ind in related_indicators[:5]:  # 只显示前5个相关指标
        print(f" {ind} |", end='')
    print()
    print('|------|---------|------|', end='')
    for _ in range(min(5, len(related_indicators))):
        print('------|', end='')
    print()
    
    for row in change_table['table_data']:
        primary_change = row['primary_change_pct']
        if primary_change != 'N/A':
            primary_str = f"{'🔴' if primary_change > 0 else '🟢'} {primary_change:.1f}%"
        else:
            primary_str = 'N/A'
        
        print(f"| 事件{row['event_num']} | {row['time_range']} | {primary_str} |", end='')
        
        for ind in related_indicators[:5]:
            change = row['related_changes'].get(ind, 'N/A')
            if change != 'N/A':
                # 判断是否高相关（同向变动）
                is_high_corr = any(
                    p['event_num'] == row['event_num'] and p['indicator'] == ind
                    for p in change_table['high_correlation']
                )
                emoji = '🔴' if change > 0 else '🟢'
                if is_high_corr:
                    print(f" **{emoji}{change:.1f}%** |", end='')
                else:
                    print(f" {emoji}{change:.1f}% |", end='')
            else:
                print(' N/A |', end='')
        print()
    
    # 打印汇总统计
    print(f"\n=== 汇总统计 ===")
    for indicator, stats in aggregate_stats.items():
        win_rate = stats['win_rate_pct']
        avg_return = stats['avg_return_pct']
        print(f"\n{indicator}:")
        print(f"  平均收益: {'🔴' if avg_return > 0 else '🟢'} {avg_return:.2f}%")
        print(f"  收益波动: ±{stats['std_return_pct']:.2f}%")
        print(f"  胜率: {win_rate:.1f}% ({stats['positive_events']}/{stats['total_events']})")
    
    if args.output:
        with open(args.output, 'w') as f:
            json.dump(result, f, indent=2, ensure_ascii=False)
        print(f"\n已保存至: {args.output}")
    
    return result


if __name__ == '__main__':
    main()

path: scripts/fetch_indicator_data.py
content: #!/usr/bin/env python3
"""
Fetch historical time series data for financial indicators.
Supports: FRED (US macro), Yahoo Finance (commodities/indices), Tushare (A-shares)
"""

import argparse
import json
import os
import sys
from datetime import datetime, timedelta
import pandas as pd

# Try imports with fallbacks
try:
    import yfinance as yf
    YFINANCE_AVAILABLE = True
except ImportError:
    YFINANCE_AVAILABLE = False

try:
    import tushare as ts
    TUSHARE_AVAILABLE = True
    # Set token from environment
    ts.set_token(os.environ.get('TUSHARE_TOKEN', ''))
except ImportError:
    TUSHARE_AVAILABLE = False

# Indicator mapping: indicator_id -> data source config
INDICATOR_MAPPING = {
    # Commodities (Yahoo Finance)
    'brent_crude': {'source': 'yahoo', 'symbol': 'BZ=F', 'name': 'Brent Crude Oil'},
    'wti_crude': {'source': 'yahoo', 'symbol': 'CL=F', 'name': 'WTI Crude Oil'},
    'gold': {'source': 'yahoo', 'symbol': 'GC=F', 'name': 'Gold Futures'},
    'silver': {'source': 'yahoo', 'symbol': 'SI=F', 'name': 'Silver Futures'},
    'copper': {'source': 'yahoo', 'symbol': 'HG=F', 'name': 'Copper Futures'},
    'aluminum': {'source': 'yahoo', 'symbol': 'ALI=F', 'name': 'Aluminum Futures'},
    'natural_gas': {'source': 'yahoo', 'symbol': 'NG=F', 'name': 'Natural Gas Futures'},
    
    # US Indices (Yahoo Finance)
    'sp500': {'source': 'yahoo', 'symbol': '^GSPC', 'name': 'S&P 500'},
    'nasdaq': {'source': 'yahoo', 'symbol': '^IXIC', 'name': 'NASDAQ Composite'},
    'dow_jones': {'source': 'yahoo', 'symbol': '^DJI', 'name': 'Dow Jones Industrial'},
    'russell2000': {'source': 'yahoo', 'symbol': '^RUT', 'name': 'Russell 2000'},
    'vix': {'source': 'yahoo', 'symbol': '^VIX', 'name': 'VIX Index'},
    
    # US ETFs (sector representatives)
    'xlk': {'source': 'yahoo', 'symbol': 'XLK', 'name': 'Technology ETF'},
    'xle': {'source': 'yahoo', 'symbol': 'XLE', 'name': 'Energy ETF'},
    'xlf': {'source': 'yahoo', 'symbol': 'XLF', 'name': 'Financial ETF'},
    'xlv': {'source': 'yahoo', 'symbol': 'XLV', 'name': 'Healthcare ETF'},
    'xli': {'source': 'yahoo', 'symbol': 'XLI', 'name': 'Industrial ETF'},
    'xly': {'source': 'yahoo', 'symbol': 'XLY', 'name': 'Consumer Discretionary ETF'},
    'xlp': {'source': 'yahoo', 'symbol': 'XLP', 'name': 'Consumer Staples ETF'},
    'xlu': {'source': 'yahoo', 'symbol': 'XLU', 'name': 'Utilities ETF'},
    'jets': {'source': 'yahoo', 'symbol': 'JETS', 'name': 'US Global Jets ETF'},
    'iyr': {'source': 'yahoo', 'symbol': 'IYR', 'name': 'Real Estate ETF'},
    'gdx': {'source': 'yahoo', 'symbol': 'GDX', 'name': 'Gold Miners ETF'},
    'tlt': {'source': 'yahoo', 'symbol': 'TLT', 'name': '20+ Year Treasury Bond ETF'},
    
    # Foreign Exchange (Yahoo Finance)
    'usd_cny': {'source': 'yahoo', 'symbol': 'CNY=X', 'name': 'USD/CNY'},
    'usd_index': {'source': 'yahoo', 'symbol': 'DX-Y.NYB', 'name': 'US Dollar Index'},
    'eur_usd': {'source': 'yahoo', 'symbol': 'EURUSD=X', 'name': 'EUR/USD'},
    
    # US Treasury (Yahoo Finance)
    'us_10y_treasury': {'source': 'yahoo', 'symbol': '^TNX', 'name': '10-Year Treasury Yield'},
    'us_2y_treasury': {'source': 'yahoo', 'symbol': '^FVX', 'name': '2-Year Treasury Yield'},
    
    # FRED (Federal Reserve Economic Data)
    'fed_funds_rate': {'source': 'fred', 'symbol': 'FEDFUNDS', 'name': 'Federal Funds Rate'},
    'cpi_us': {'source': 'fred', 'symbol': 'CPIAUCSL', 'name': 'US CPI'},
    'gdp_us': {'source': 'fred', 'symbol': 'GDP', 'name': 'US GDP'},
    'unemployment_us': {'source': 'fred', 'symbol': 'UNRATE', 'name': 'US Unemployment Rate'},
    'oil_production_us': {'source': 'fred', 'symbol': 'IPG2111111SQ', 'name': 'US Oil Production'},
    
    # A-share Indices (Tushare)
    'csi300': {'source': 'tushare', 'symbol': '000300.SH', 'name': 'CSI 300 Index'},
    'sse_composite': {'source': 'tushare', 'symbol': '000001.SH', 'name': 'Shanghai Composite'},
    'chinext': {'source': 'tushare', 'symbol': '399006.SZ', 'name': 'ChiNext Index'},
    'sse50': {'source': 'tushare', 'symbol': '000016.SH', 'name': 'SSE 50 Index'},
}


def fetch_yahoo(symbol: str, years: int = 30) -> pd.DataFrame:
    """Fetch data from Yahoo Finance."""
    if not YFINANCE_AVAILABLE:
        raise ImportError("yfinance not installed. Run: pip install yfinance")
    
    end_date = datetime.now()
    start_date = end_date - timedelta(days=years * 365)
    
    ticker = yf.Ticker(symbol)
    df = ticker.history(start=start_date, end=end_date, auto_adjust=True)
    
    if df.empty:
        raise ValueError(f"No data returned for symbol: {symbol}")
    
    # Handle MultiIndex or flat columns from yfinance
    df = df.reset_index()
    
    # Flatten MultiIndex columns if present
    if isinstance(df.columns, pd.MultiIndex):
        df.columns = df.columns.get_level_values(0)
    
    # Rename columns to lowercase for consistency
    col_map = {col: col.lower() for col in df.columns}
    df = df.rename(columns=col_map)
    
    # Handle date column (could be 'date' or index name)
    date_col = None
    for col in df.columns:
        if 'date' in col.lower() or col.lower() in ['datetime', 'index']:
            date_col = col
            break
    if date_col:
        df = df.rename(columns={date_col: 'date'})
    elif df.columns[0].lower() not in ['open', 'high', 'low', 'close']:
        df.columns = ['date'] + list(df.columns[1:])
    
    df['date'] = pd.to_datetime(df['date']).dt.strftime('%Y-%m-%d')
    
    return df[['date', 'close']]


def fetch_fred(symbol: str, years: int = 30) -> pd.DataFrame:
    """Fetch data from FRED API."""
    try:
        import fredapi
        fred = fredapi.Fred([REDACTED_SECRET]'FRED_API_KEY', ''))
    except ImportError:
        # Fallback: use pandas-datareader or manual download
        raise ImportError("fredapi not installed. Run: pip install fredapi")
    
    end_date = datetime.now()
    start_date = end_date - timedelta(days=years * 365)
    
    data = fred.get_series(symbol, observation_start=start_date, observation_end=end_date)
    
    df = pd.DataFrame({'date': data.index, 'close': data.values})
    df['date'] = pd.to_datetime(df['date']).dt.strftime('%Y-%m-%d')
    
    return df


def fetch_tushare(symbol: str, years: int = 30) -> pd.DataFrame:
    """Fetch A-share index data from Tushare."""
    if not TUSHARE_AVAILABLE:
        raise ImportError("tushare not installed. Run: pip install tushare")
    
    pro = ts.pro_api()
    end_date = datetime.now().strftime('%Y%m%d')
    start_date = (datetime.now() - timedelta(days=years * 365)).strftime('%Y%m%d')
    
    df = pro.index_daily(ts_code=symbol, start_date=start_date, end_date=end_date)
    
    if df.empty:
        raise ValueError(f"No data returned for symbol: {symbol}")
    
    df = df.sort_values('trade_date')
    df['date'] = pd.to_datetime(df['trade_date']).dt.strftime('%Y-%m-%d')
    
    return df[['date', 'close']]


def fetch_indicator(indicator_id: str, years: int = 30) -> pd.DataFrame:
    """Fetch data for a given indicator."""
    if indicator_id not in INDICATOR_MAPPING:
        raise ValueError(f"Unknown indicator: {indicator_id}. Use --list to see available indicators.")
    
    config = INDICATOR_MAPPING[indicator_id]
    source = config['source']
    symbol = config['symbol']
    
    print(f"Fetching {config['name']} ({symbol}) from {source}...")
    
    if source == 'yahoo':
        return fetch_yahoo(symbol, years)
    elif source == 'fred':
        return fetch_fred(symbol, years)
    elif source == 'tushare':
        return fetch_tushare(symbol, years)
    else:
        raise ValueError(f"Unknown data source: {source}")


def list_indicators():
    """List all available indicators."""
    print("\nAvailable Indicators:")
    print("=" * 60)
    
    categories = {
        'Commodities': ['brent_crude', 'wti_crude', 'gold', 'silver', 'copper', 'aluminum', 'natural_gas'],
        'US Indices': ['sp500', 'nasdaq', 'dow_jones', 'russell2000', 'vix'],
        'US Sector ETFs': ['xlk', 'xle', 'xlf', 'xlv', 'xli', 'jets', 'iyr'],
        'Foreign Exchange': ['usd_cny', 'usd_index', 'eur_usd'],
        'US Treasury': ['us_10y_treasury', 'us_2y_treasury'],
        'FRED Macro': ['fed_funds_rate', 'cpi_us', 'gdp_us', 'unemployment_us', 'oil_production_us'],
        'A-share Indices': ['csi300', 'sse_composite', 'chinext', 'sse50'],
    }
    
    for category, indicators in categories.items():
        print(f"\n{category}:")
        for ind in indicators:
            config = INDICATOR_MAPPING.get(ind, {})
            print(f"  {ind}: {config.get('name', 'N/A')}")
    
    print("\n" + "=" * 60)


def main():
    parser = argparse.ArgumentParser(description='Fetch financial indicator historical data')
    parser.add_argument('indicator', nargs='?', help='Indicator ID to fetch')
    parser.add_argument('--years', type=int, default=30, help='Years of historical data (default: 30)')
    parser.add_argument('--output', '-o', help='Output file path (CSV or JSON)')
    parser.add_argument('--list', action='store_true', help='List available indicators')
    parser.add_argument('--json', action='store_true', help='Output as JSON')
    
    args = parser.parse_args()
    
    if args.list:
        list_indicators()
        return
    
    if not args.indicator:
        parser.error("Please specify an indicator or use --list")
    
    df = fetch_indicator(args.indicator, args.years)
    
    # Print summary
    print(f"\nData Summary:")
    print(f"  Total records: {len(df)}")
    print(f"  Date range: {df['date'].min()} to {df['date'].max()}")
    print(f"  Price range: {df['close'].min():.2f} to {df['close'].max():.2f}")
    
    # Output
    if args.output:
        if args.json:
            data = {
                'indicator': args.indicator,
                'name': INDICATOR_MAPPING[args.indicator]['name'],
                'data': df.to_dict('records')
            }
            with open(args.output, 'w') as f:
                json.dump(data, f, indent=2)
        else:
            df.to_csv(args.output, index=False)
        print(f"\nSaved to: {args.output}")
    else:
        if args.json:
            print(json.dumps(df.to_dict('records'), indent=2))
        else:
            print(df.to_string())


if __name__ == '__main__':
    main()

path: scripts/find_similar_events.py
content: #!/usr/bin/env python3
"""
Find historical events similar to current market conditions.
Detects rapid price changes, volatility spikes, trend reversals.
"""

import argparse
import json
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
from typing import List, Dict, Tuple


def calculate_change(df: pd.DataFrame, window_days: int = 90) -> float:
    """Calculate percentage change over a window."""
    if len(df) < window_days:
        return 0.0
    start_price = df['close'].iloc[-window_days]
    end_price = df['close'].iloc[-1]
    return ((end_price - start_price) / start_price) * 100


def calculate_volatility(df: pd.DataFrame, window_days: int = 30) -> float:
    """Calculate rolling volatility (standard deviation of daily returns)."""
    if len(df) < window_days:
        return 0.0
    returns = df['close'].pct_change().dropna()
    return returns.iloc[-window_days:].std() * 100


def detect_rapid_change(df: pd.DataFrame, threshold_pct: float = 20.0, min_window: int = 30, max_window: int = 90) -> List[Dict]:
    """Detect periods of rapid price change in historical data."""
    events = []
    
    for window in range(min_window, min(max_window + 1, len(df))):
        # Rolling window calculation
        for i in range(window, len(df)):
            start_price = df['close'].iloc[i - window]
            end_price = df['close'].iloc[i]
            change_pct = ((end_price - start_price) / start_price) * 100
            
            # Check if change exceeds threshold
            if abs(change_pct) >= threshold_pct:
                start_date = df['date'].iloc[i - window]
                end_date = df['date'].iloc[i]
                
                # Check if this event overlaps with existing ones (merge overlapping)
                is_overlapping = False
                for existing in events:
                    if start_date <= existing['end_date'] and end_date >= existing['start_date']:
                        is_overlapping = True
                        break
                
                if not is_overlapping:
                    events.append({
                        'start_date': start_date,
                        'end_date': end_date,
                        'change_pct': round(change_pct, 2),
                        'direction': 'increase' if change_pct > 0 else 'decrease',
                        'window_days': window,
                        'start_price': round(start_price, 4),
                        'end_price': round(end_price, 4)
                    })
    
    # Sort by absolute change magnitude and return top events
    events.sort(key=lambda x: abs(x['change_pct']), reverse=True)
    return events


def detect_volatility_spike(df: pd.DataFrame, threshold_vol: float = 3.0, window_days: int = 30) -> List[Dict]:
    """Detect periods of elevated volatility."""
    events = []
    returns = df['close'].pct_change().dropna()
    
    for i in range(window_days, len(returns)):
        vol = returns.iloc[i - window_days:i].std() * 100
        
        if vol >= threshold_vol:
            start_date = df['date'].iloc[i - window_days]
            end_date = df['date'].iloc[i]
            
            events.append({
                'start_date': start_date,
                'end_date': end_date,
                'volatility': round(vol, 2),
                'threshold': threshold_vol
            })
    
    # Deduplicate overlapping events
    deduped = []
    for event in events:
        is_overlapping = False
        for existing in deduped:
            if event['start_date'] <= existing['end_date'] and event['end_date'] >= existing['start_date']:
                is_overlapping = True
                break
        if not is_overlapping:
            deduped.append(event)
    
    return deduped


def get_current_event_characteristics(df: pd.DataFrame, lookback_days: int = 90) -> Dict:
    """Extract current event characteristics from recent data."""
    change_90d = calculate_change(df, 90)
    change_60d = calculate_change(df, 60)
    change_30d = calculate_change(df, 30)
    volatility_30d = calculate_volatility(df, 30)
    
    direction = 'increase' if change_90d > 0 else 'decrease'
    
    # Determine event type based on characteristics
    event_type = 'unknown'
    if abs(change_90d) >= 40:
        event_type = 'major_price_move'
    elif abs(change_90d) >= 20:
        event_type = 'rapid_price_change'
    elif volatility_30d >= 3.0:
        event_type = 'high_volatility'
    
    return {
        'change_90d': round(change_90d, 2),
        'change_60d': round(change_60d, 2),
        'change_30d': round(change_30d, 2),
        'volatility_30d': round(volatility_30d, 2),
        'direction': direction,
        'event_type': event_type,
        'current_price': round(df['close'].iloc[-1], 4),
        'current_date': df['date'].iloc[-1]
    }


def find_similar_historical_events(df: pd.DataFrame, current_event: Dict, similarity_threshold: float = 0.5) -> List[Dict]:
    """Find historical events similar to current event characteristics."""
    target_change = abs(current_event['change_90d'])
    target_direction = current_event['direction']
    
    # Find all rapid changes in history
    threshold_pct = target_change * similarity_threshold
    all_events = detect_rapid_change(df, threshold_pct=max(threshold_pct, 10))
    
    # Filter by direction
    similar_events = [e for e in all_events if e['direction'] == target_direction]
    
    # Add similarity score
    for event in similar_events:
        similarity = min(abs(event['change_pct']) / target_change, 1.0)
        event['similarity_score'] = round(similarity, 2)
    
    # Sort by similarity and return top events
    similar_events.sort(key=lambda x: x['similarity_score'], reverse=True)
    
    return similar_events[:10]  # Return top 10 similar events


def main():
    parser = argparse.ArgumentParser(description='Find historical events similar to current conditions')
    parser.add_argument('--data', required=True, help='Input data file (CSV or JSON from fetch_indicator)')
    parser.add_argument('--threshold', type=float, default=20.0, help='Minimum change percentage threshold')
    parser.add_argument('--window-min', type=int, default=30, help='Minimum window days')
    parser.add_argument('--window-max', type=int, default=90, help='Maximum window days')
    parser.add_argument('--output', '-o', help='Output file (JSON)')
    parser.add_argument('--json', action='store_true', help='Output as JSON')
    
    args = parser.parse_args()
    
    # Load data
    if args.data.endswith('.json'):
        with open(args.data, 'r') as f:
            data = json.load(f)
        df = pd.DataFrame(data['data'])
    else:
        df = pd.read_csv(args.data)
    
    # Get current event characteristics
    current_event = get_current_event_characteristics(df)
    print(f"\nCurrent Event Characteristics:")
    print(f"  90-day change: {current_event['change_90d']}% ({current_event['direction']})")
    print(f"  60-day change: {current_event['change_60d']}%")
    print(f"  30-day change: {current_event['change_30d']}%")
    print(f"  30-day volatility: {current_event['volatility_30d']}%")
    print(f"  Event type: {current_event['event_type']}")
    
    # Find similar historical events
    similar_events = find_similar_historical_events(df, current_event)
    
    print(f"\nSimilar Historical Events Found: {len(similar_events)}")
    for i, event in enumerate(similar_events[:5], 1):
        print(f"\n  Event {i}:")
        print(f"    Period: {event['start_date']} to {event['end_date']} ({event['window_days']} days)")
        print(f"    Change: {event['change_pct']}% ({event['direction']})")
        print(f"    Similarity: {event['similarity_score']}")
    
    # Output
    result = {
        'current_event': current_event,
        'similar_events': similar_events
    }
    
    if args.output:
        with open(args.output, 'w') as f:
            json.dump(result, f, indent=2)
        print(f"\nSaved to: {args.output}")
    else:
        if args.json:
            print(json.dumps(result, indent=2))


if __name__ == '__main__':
    main()

path: scripts/generate_charts.py
content: #!/usr/bin/env python3
"""
Generate charts for financial event impact analysis.
Creates dual-axis time series comparison charts with Chinese labels.
每类受影响资产单独生成一张时序对比图。
"""

import argparse
import json
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from datetime import datetime
from typing import Dict, List, Tuple
import os
import warnings

warnings.filterwarnings('ignore')

# === 中文字体配置 ===
# 字体优先级列表（skill.json 中约定的顺序）
FONT_PRIORITY = [
    'WenQuanYi Micro Hei',  # Linux 常用（优先，已安装）
    'WenQuanYi Zen Hei',    # Linux 文泉驿正黑
    'SimHei',               # Windows 黑体
    'Noto Sans CJK SC',     # 现代 Linux
    'PingFang SC',          # macOS
    'Heiti SC',             # macOS
    'Microsoft YaHei',      # Windows 微软雅黑
    'Source Han Sans CN',   # 思源黑体
]
FALLBACK_FONT = 'DejaVu Sans'


def check_chinese_font_available() -> Tuple[bool, str, str]:
    """
    前置检查：验证中文字体是否可用
    
    Returns:
        (is_available, selected_font, message)
    """
    import matplotlib.font_manager as fm
    
    # 强制重建字体缓存（确保识别系统字体）
    fm._load_fontmanager(try_read_cache=False)
    
    # 查找可用字体
    available_fonts = [f.name for f in fm.fontManager.ttflist]
    
    # 按优先级选择
    for font in FONT_PRIORITY:
        if font in available_fonts:
            return True, font, f"✓ 中文字体可用: {font}"
    
    # 检查是否有任何包含 CJK 的字体
    cjk_fonts = [f for f in available_fonts if any(kw in f for kw in ['CJK', 'Hei', 'Noto', 'WenQuanYi', 'Chinese', 'PingFang', 'Hiragino'])]
    
    if cjk_fonts:
        return True, cjk_fonts[0], f"✓ 找到备选CJK字体: {cjk_fonts[0]}"
    
    # 无中文字体
    return False, FALLBACK_FONT, f"❌ 未找到中文字体，图表将显示方框。请安装: sudo apt install fonts-wqy-microhei fonts-noto-cjk"


def setup_chinese_font() -> str:
    """
    配置中文字体，必须在生成图表前调用
    
    Returns:
        使用的字体名称
    """
    is_available, selected_font, message = check_chinese_font_available()
    print(message)
    
    if is_available:
        plt.rcParams['font.family'] = selected_font
        plt.rcParams['axes.unicode_minus'] = False  # 解决负号显示问题
    else:
        # 使用回退字体，但图表中文会显示异常
        plt.rcParams['font.family'] = FALLBACK_FONT
        plt.rcParams['axes.unicode_minus'] = False
        print("⚠ 严重警告: 图表中文标注将显示为方框！")
    
    return selected_font


# 初始化字体
setup_chinese_font()

# 图表样式设置
plt.style.use('seaborn-v0_8-whitegrid')
plt.rcParams['figure.figsize'] = (16, 9)
plt.rcParams['figure.dpi'] = 150
plt.rcParams['axes.titlesize'] = 16
plt.rcParams['axes.labelsize'] = 12


def load_indicator_data(filepath: str) -> pd.DataFrame:
    """Load indicator data from CSV or JSON."""
    if filepath.endswith('.json'):
        with open(filepath, 'r') as f:
            data = json.load(f)
        df = pd.DataFrame(data['data'])
    else:
        df = pd.read_csv(filepath)
    
    df['date'] = pd.to_datetime(df['date'])
    df = df.sort_values('date')
    return df


def calculate_date_range_months(df: pd.DataFrame) -> int:
    """计算数据跨度（月数）"""
    date_range = df['date'].max() - df['date'].min()
    return int(date_range.days / 30)


def auto_adjust_x_axis_interval(ax, df: pd.DataFrame) -> int:
    """
    根据数据跨度自动调整X轴时间刻度间隔，避免标签重叠
    
    Returns:
        间隔月数
    """
    months = calculate_date_range_months(df)
    
    # 根据数据跨度自动调整间隔
    if months <= 12:
        interval = 1  # 每月
    elif months <= 24:
        interval = 3  # 每季度
    elif months <= 60:
        interval = 6  # 半年
    elif months <= 120:
        interval = 12  # 每年
    else:
        interval = 24  # 每两年
    
    ax.xaxis.set_major_locator(mdates.MonthLocator(interval=interval))
    return interval


def plot_dual_axis_comparison(
    primary_df: pd.DataFrame, 
    primary_name: str,
    related_df: pd.DataFrame, 
    related_name: str,
    events: List[Dict],
    output_path: str,
    current_event: Dict = None,
    event_results: List[Dict] = None
) -> str:
    """
    绘制双Y轴时序对比图：表征指标（左轴）vs 关联资产指标（右轴）
    
    ⭐ 核心改动：
    - 过往同类事件时间区间红色虚线方框，**覆盖整个Y轴区域**（从底部到顶部）
    - 每一项相关指标都必须生成单独的图表
    - X轴自动调整间隔避免重叠
    - 每个事件区间添加事件编号和涨跌幅标注
    
    Args:
        primary_df: 表征指标数据
        primary_name: 表征指标中文名称
        related_df: 关联资产数据
        related_name: 关联资产中文名称
        events: 历史事件列表
        output_path: 输出文件路径
        current_event: 当前事件
        event_results: 事件结果数据
    
    Returns:
        保存的文件路径
    """
    # 根据数据跨度调整图表宽度，避免标签重叠
    months = calculate_date_range_months(primary_df)
    if months <= 24:
        fig_width = 14
    elif months <= 60:
        fig_width = 18
    elif months <= 120:
        fig_width = 22
    else:
        fig_width = 26  # 长时间跨度使用更宽的图表
    
    fig, ax1 = plt.subplots(figsize=(fig_width, 10))
    
    # 左Y轴：表征指标
    color_primary = '#E74C3C'  # 红色
    ax1.plot(primary_df['date'], primary_df['close'], 
             linewidth=2, color=color_primary, label=primary_name)
    ax1.set_xlabel('日期', fontsize=12)
    ax1.set_ylabel(primary_name, fontsize=12, color=color_primary)
    ax1.tick_params(axis='y', labelcolor=color_primary)
    
    # 右Y轴：关联资产指标
    ax2 = ax1.twinx()
    color_related = '#3498DB'  # 蓝色
    ax2.plot(related_df['date'], related_df['close'], 
             linewidth=2, color=color_related, label=related_name)
    ax2.set_ylabel(related_name, fontsize=12, color=color_related)
    ax2.tick_params(axis='y', labelcolor=color_related)
    
    # ⭐ 核心改动：在绘制所有曲线后，强制确定Y轴范围
    ax1.relim()
    ax1.autoscale_view(scalex=False, scaley=True)
    ax2.relim()
    ax2.autoscale_view(scalex=False, scaley=True)
    
    # === 添加历史事件红色虚线方框（覆盖整个Y轴区域） ===
    # 最多显示5个历史事件，避免图表过于拥挤
    max_events_to_show = min(5, len(events))
    
    # 使用不同颜色深度区分不同事件
    event_colors = ['#C0392B', '#A93226', '#922B21', '#7B241C', '#641E16']
    
    # ⭐ 核心修改：使用 ax.get_xaxis_transform() 让矩形覆盖整个Y轴区域
    # Y坐标从 0（底部）到 1（顶部），覆盖整个图表区域
    
    for i, event in enumerate(events[:max_events_to_show]):
        start_date = pd.to_datetime(event['start_date'])
        end_date = pd.to_datetime(event['end_date'])
        
        # ⭐ 核心修改：使用 axes coordinates，Y从0到1覆盖整个图表区域
        rect = plt.Rectangle(
            (mdates.date2num(start_date), 0),  # Y=0（底部）
            mdates.date2num(end_date) - mdates.date2num(start_date),
            1,  # Y高度=1（从底部到顶部，覆盖整个Y轴）
            fill=True, facecolor=event_colors[i % len(event_colors)] + '15',  # 15%透明度填充
            edgecolor=event_colors[i % len(event_colors)], linestyle='--',
            linewidth=2.5, alpha=0.8,
            transform=ax1.get_xaxis_transform(),  # ⭐ 关键：X轴数据坐标，Y轴axes坐标
            clip_on=False  # 允许超出轴边界
        )
        ax1.add_patch(rect)
        
        # 添加事件标签（显示事件编号 + 主指标涨跌幅）
        primary_change = event.get('change_pct', 0)
        change_str = f"+{primary_change}%" if primary_change > 0 else f"{primary_change}%"
        
        # 标签位置：在矩形框上方中央（使用 axes coordinates）
        mid_date = start_date + (end_date - start_date) / 2
        ax1.annotate(
            f"事件{i+1} [{change_str}]",
            xy=(mdates.date2num(mid_date), 1.05),  # Y=1.05（稍超出顶部）
            fontsize=11, color=event_colors[i % len(event_colors)], 
            weight='bold', alpha=0.9,
            ha='center', va='bottom',
            bbox=dict(boxstyle='round,pad=0.3', facecolor='white', 
                      edgecolor=event_colors[i % len(event_colors)], alpha=0.7),
            clip_on=False
        )
    
    # === 标记当前事件（红色实线框，覆盖整个Y轴区域） ===
    if current_event:
        curr_start = pd.to_datetime(current_event.get('start_date', 
                              primary_df['date'].iloc[-90].strftime('%Y-%m-%d')))
        curr_end = pd.to_datetime(current_event.get('current_date', 
                              primary_df['date'].iloc[-1].strftime('%Y-%m-%d')))
        
        # ⭐ 核心修改：实线框使用 axes coordinates 覆盖整个Y轴高度
        rect = plt.Rectangle(
            (mdates.date2num(curr_start), 0),  # Y=0（底部）
            mdates.date2num(curr_end) - mdates.date2num(curr_start),
            1,  # Y高度=1（覆盖整个Y轴）
            fill=True, facecolor='#E74C3C' + '25',  # 25%透明度填充
            edgecolor='#E74C3C', linestyle='-',
            linewidth=3.5, alpha=1.0,
            transform=ax1.get_xaxis_transform(),  # ⭐ 关键：X轴数据坐标，Y轴axes坐标
            clip_on=False
        )
        ax1.add_patch(rect)
        
        # 当前事件标签（使用 axes coordinates）
        ax1.annotate(
            "当前事件",
            xy=(mdates.date2num(curr_end), 1.05),  # Y=1.05（稍超出顶部）
            fontsize=13, color='#E74C3C', weight='bold',
            ha='right', va='bottom',
            bbox=dict(boxstyle='round,pad=0.4', facecolor='white', 
                      edgecolor='#E74C3C', linewidth=2, alpha=0.9),
            clip_on=False
        )
    
    # === 自动调整X轴间隔，避免标签重叠 ===
    interval = auto_adjust_x_axis_interval(ax1, primary_df)
    ax1.xaxis.set_major_formatter(mdates.DateFormatter('%Y.%m'))
    
    # 旋转标签并调整位置，避免重叠
    plt.xticks(rotation=30, ha='right')
    
    # 设置标题（中文）
    ax1.set_title(f'{primary_name} vs {related_name} 历史走势对比', 
                  fontsize=16, weight='bold', pad=20)
    
    # 合并图例
    lines1, labels1 = ax1.get_legend_handles_labels()
    lines2, labels2 = ax2.get_legend_handles_labels()
    
    # 添加事件区间图例说明
    event_legend_text = "红色虚线框 = 历史事件区间"
    ax1.legend(lines1 + lines2, labels1 + labels2 + [event_legend_text], 
               loc='upper left', fontsize=10, framealpha=0.9)
    
    ax1.grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    
    print(f"已生成: {output_path}")
    return output_path


def plot_all_dual_axis_charts(
    primary_indicator: str,
    primary_data_path: str,
    related_data_path: str,
    performance_data_path: str,
    output_dir: str,
    indicator_names: Dict[str, str] = None
) -> Dict[str, str]:
    """
    为每类相关资产生成单独的双Y轴时序对比图
    
    新增功能：
    - 传入 event_results 用于显示关联资产涨跌幅
    
    Returns:
        图表类型到文件路径的映射
    """
    charts = {}
    
    # 默认指标中文名称映射
    default_names = {
        # 大宗商品
        'brent_crude': '布伦特原油',
        'wti_crude': 'WTI原油',
        'gold': '黄金',
        'silver': '白银',
        'copper': '铜',
        'aluminum': '铝',
        'natural_gas': '天然气',
        # 美股指数
        'sp500': '标普500',
        'nasdaq': '纳斯达克',
        'dow_jones': '道琼斯',
        'russell2000': '罗素2000',
        'vix': 'VIX恐慌指数',
        # 美股行业ETF
        'xlk': '科技ETF',
        'xle': '能源ETF',
        'xlf': '金融ETF',
        'xlv': '医疗ETF',
        'xli': '工业ETF',
        'jets': '航空ETF',
        'iyr': '房地产ETF',
        'gdx': '黄金矿业ETF',
        'tlt': '长期美债ETF',
        'xlp': '必需消费ETF',
        'xlu': '公用事业ETF',
        # 外汇
        'usd_index': '美元指数',
        'usd_cny': '美元/人民币',
        'usd_jpy': '美元/日元',
        'usd_krw': '美元/韩元',
        'eur_usd': '欧元/美元',
        'gbp_usd': '英镑/美元',
        'usd_hkd': '美元/港币',
        # 美债
        'us_10y_treasury': '10年期美债收益率',
        'us_2y_treasury': '2年期美债收益率',
        # ⭐ 中国市场（新增）
        'csi300': '沪深300',
        'sse_composite': '上证指数',
        'chinext': '创业板指',
        'sse50': '上证50',
        'csi500': '中证500',
        'csi1000': '中证1000',
        'shanghai_tech': '科创50',
        'hsi': '恒生指数',
        'hsi_tech': '恒生科技',
        'hang_seng_china': '恒生中国企业',
        # 中国宏观
        'cpi_china': '中国CPI',
        'ppi_china': '中国PPI',
        'gdp_china': '中国GDP',
        'm2_china': '中国M2',
        'm1_china': '中国M1',
        'social_financing': '社会融资规模',
        'pmi_china': '中国PMI',
        'loan_rate_china': '中国LPR',
        'bond_10y_china': '中国10Y国债收益率',
        # ⭐ 日本市场（新增）
        'nikkei225': '日经225',
        'topix': 'TOPIX东证指数',
        'japan_10y_bond': '日本10Y国债',
        'gdp_japan': '日本GDP',
        'cpi_japan': '日本CPI',
        'm2_japan': '日本M2',
        # ⭐ 韩国市场（新增）
        'kospi': '韩国KOSPI',
        'kosdaq': '韩国KOSDAQ',
        'gdp_korea': '韩国GDP',
        'cpi_korea': '韩国CPI',
        # ⭐ 欧洲市场（新增）
        'eu_stoxx50': '欧洲斯托克50',
        'dax': '德国DAX',
        'cac40': '法国CAC40',
        'ftse100': '英国富时100',
        'ftse_mib': '意大利MIB',
        'ibex35': '西班牙IBEX35',
        'euro_10y_bond': '德国10Y国债',
        'gdp_eurozone': '欧元区GDP',
        'cpi_eurozone': '欧元区CPI',
        'm2_eurozone': '欧元区M2',
        # ⭐ 亚太新兴市场（新增）
        'sgx_nifty': '印度Nifty50',
        'sensex': '印度BSE Sensex',
        'jakarta': '雅加达指数',
        'bangkok': '泰国SET指数',
        # 美国宏观
        'fed_funds_rate': '联邦基金利率',
        'cpi_us': '美国CPI',
        'gdp_us': '美国GDP',
        'unemployment_us': '美国失业率',
    }
    
    names = indicator_names or default_names
    
    # 加载主指标数据
    primary_df = load_indicator_data(primary_data_path)
    primary_name = names.get(primary_indicator, primary_indicator)
    
    # 加载相关资产数据
    with open(related_data_path, 'r') as f:
        related_data = json.load(f)
    
    # 加载表现数据（获取事件列表和事件结果）
    with open(performance_data_path, 'r') as f:
        perf_data = json.load(f)
    
    events = perf_data.get('events', [])
    current_event = perf_data.get('current_event', {})
    event_results = perf_data.get('event_results', [])
    
    # 如果 perf_data 中没有 events，从 events.json 加载
    if not events and perf_data.get('primary_indicator'):
        # 尝试从同目录下的 events.json 加载
        events_path = f"{output_dir}/../events.json"
        if os.path.exists(events_path):
            with open(events_path, 'r') as f:
                events_data = json.load(f)
            events = events_data.get('similar_events', [])
            current_event = events_data.get('current_event', {})
    
    # 收集所有相关指标
    all_related = []
    for category in ['benefited', 'harmed', 'neutral_uncertain']:
        for asset in related_data.get(category, []):
            if 'indicator' in asset:
                all_related.append({
                    'indicator': asset['indicator'],
                    'category': category,
                    'reason': asset.get('reason', '')
                })
    
    print(f"\n正在生成 {len(all_related)} 张双Y轴时序对比图...")
    print(f"  主指标: {primary_name}")
    print(f"  历史事件数: {len(events)}")
    print(f"  数据跨度: {calculate_date_range_months(primary_df)} 个月")
    
    # 为每个相关资产生成单独的图表
    for related_info in all_related:
        related_indicator = related_info['indicator']
        related_name = names.get(related_indicator, related_indicator)
        
        # 尝试加载相关资产数据
        related_data_file = None
        for ext in ['.json', '.csv']:
            test_path = f"{output_dir}/../data/{related_indicator}{ext}"
            if os.path.exists(test_path):
                related_data_file = test_path
                break
        
        if not related_data_file:
            print(f"  跳过 {related_indicator}: 数据文件不存在")
            continue
        
        try:
            related_df = load_indicator_data(related_data_file)
            
            # 生成双Y轴对比图
            chart_path = f"{output_dir}/{primary_indicator}_vs_{related_indicator}.png"
            
            plot_dual_axis_comparison(
                primary_df, primary_name,
                related_df, related_name,
                events, chart_path, current_event,
                event_results=event_results  # 传入事件结果
            )
            
            charts[f'{primary_indicator}_vs_{related_indicator}'] = chart_path
            
        except Exception as e:
            print(f"  生成 {related_indicator} 图表失败: {e}")
    
    return charts


def plot_performance_comparison(performance_data: Dict, output_path: str,
                                 indicator_names: Dict[str, str] = None) -> str:
    """
    绘制各资产平均收益对比图（中文标签）
    """
    if not performance_data:
        return None
    
    # 中文名称映射
    default_names = {
        'xlk': '科技ETF', 'xle': '能源ETF', 'xlf': '金融ETF',
        'xlv': '医疗ETF', 'xli': '工业ETF', 'jets': '航空ETF',
        'gdx': '黄金矿业', 'tlt': '长期美债', 'gold': '黄金',
        'silver': '白银', 'sp500': '标普500', 'nasdaq': '纳斯达克',
    }
    names = indicator_names or default_names
    
    fig, axes = plt.subplots(1, 2, figsize=(16, 8))
    
    # 准备数据
    indicators = list(performance_data.keys())
    indicator_labels = [names.get(ind, ind) for ind in indicators]
    avg_returns = [performance_data[ind]['avg_return_pct'] for ind in indicators]
    positive_pct = [performance_data[ind]['positive_events'] / 
                    performance_data[ind]['total_events'] * 100 
                    for ind in indicators]
    
    # 中国习惯：红色=上涨，绿色=下跌
    colors_returns = ['#E74C3C' if r > 0 else '#27AE60' for r in avg_returns]
    
    # 图1：平均收益
    ax1 = axes[0]
    bars1 = ax1.barh(indicator_labels, avg_returns, color=colors_returns)
    ax1.set_xlabel('平均收益率 (%)', fontsize=12)
    ax1.set_title('历史事件期间各资产平均收益', fontsize=14, weight='bold')
    ax1.axvline(x=0, color='black', linewidth=0.8)
    ax1.grid(True, alpha=0.3, axis='x')
    
    # 添加数值标签
    for bar, val in zip(bars1, avg_returns):
        ax1.text(val + 0.5 if val >= 0 else val - 0.5, 
                 bar.get_y() + bar.get_height()/2,
                 f"{val:.1f}%", 
                 ha='left' if val >= 0 else 'right', 
                 va='center', fontsize=10)
    
    # 图2：正向事件占比
    ax2 = axes[1]
    bars2 = ax2.barh(indicator_labels, positive_pct, color='#3498DB')
    ax2.set_xlabel('正向事件占比 (%)', fontsize=12)
    ax2.set_title('历史事件中录得正收益的比例', fontsize=14, weight='bold')
    ax2.set_xlim(0, 100)
    ax2.grid(True, alpha=0.3, axis='x')
    
    # 添加数值标签
    for bar, val in zip(bars2, positive_pct):
        ax2.text(val + 2, bar.get_y() + bar.get_height()/2,
                 f"{val:.0f}%", ha='left', va='center', fontsize=10)
    
    plt.tight_layout()
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    
    print(f"已生成: {output_path}")
    return output_path


def plot_event_matrix_heatmap(event_results: List[Dict], output_path: str,
                               indicator_names: Dict[str, str] = None) -> str:
    """
    绘制事件×资产收益矩阵热力图（中文标签）
    """
    if not event_results:
        return None
    
    # 中文名称映射
    default_names = {
        'xlk': '科技ETF', 'xle': '能源ETF', 'xlf': '金融ETF',
        'xlv': '医疗ETF', 'xli': '工业ETF', 'jets': '航空ETF',
        'gdx': '黄金矿业', 'tlt': '长期美债', 'gold': '黄金',
        'silver': '白银', 'sp500': '标普500', 'nasdaq': '纳斯达克',
    }
    names = indicator_names or default_names
    
    # 收集所有指标
    all_indicators = set()
    for result in event_results:
        all_indicators.update(result['related_performance'].keys())
    all_indicators = sorted(list(all_indicators))
    
    # 构建数据矩阵
    event_labels = [f"事件{i+1}" for i in range(len(event_results))]
    indicator_labels = [names.get(ind, ind) for ind in all_indicators]
    
    data_matrix = []
    for indicator in all_indicators:
        row = []
        for result in event_results:
            perf = result['related_performance'].get(indicator, {})
            row.append(perf.get('total_return_pct', 0))
        data_matrix.append(row)
    
    data_array = np.array(data_matrix)
    
    fig, ax = plt.subplots(figsize=(14, max(8, len(all_indicators) * 0.5)))
    
    # 热力图（中国习惯：红色=正收益，绿色=负收益）
    im = ax.imshow(data_array, cmap='RdYlGn_r', aspect='auto')
    
    # 设置标签
    ax.set_xticks(np.arange(len(event_labels)))
    ax.set_xticklabels(event_labels, fontsize=10, rotation=45, ha='right')
    ax.set_yticks(np.arange(len(indicator_labels)))
    ax.set_yticklabels(indicator_labels, fontsize=10)
    
    # 添加数值标注
    for i in range(len(all_indicators)):
        for j in range(len(event_labels)):
            val = data_array[i, j]
            color = 'white' if abs(val) > 10 else 'black'
            ax.text(j, i, f"{val:.1f}", ha='center', va='center', 
                    color=color, fontsize=9)
    
    ax.set_title('各资产在各历史事件中的收益率 (%)', fontsize=14, weight='bold')
    
    # 添加颜色条
    cbar = plt.colorbar(im, ax=ax, shrink=0.6)
    cbar.set_label('收益率 (%)', fontsize=12)
    
    plt.tight_layout()
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    
    print(f"已生成: {output_path}")
    return output_path


def generate_all_charts(
    primary_indicator: str,
    primary_data_path: str,
    events_data_path: str,
    related_data_path: str,
    performance_data_path: str,
    output_dir: str
) -> Dict[str, str]:
    """
    生成所有图表
    
    Returns:
        图表类型到文件路径的映射
    """
    charts = {}
    
    # 创建输出目录
    os.makedirs(output_dir, exist_ok=True)
    
    # 加载事件数据
    with open(events_data_path, 'r') as f:
        events_data = json.load(f)
    events = events_data.get('similar_events', [])
    current_event = events_data.get('current_event', {})
    
    # 加载表现数据
    with open(performance_data_path, 'r') as f:
        perf_data = json.load(f)
    
    aggregate_stats = perf_data.get('aggregate_stats', {})
    event_results = perf_data.get('event_results', [])
    
    # 1. 生成双Y轴时序对比图（每类资产一张）
    dual_charts = plot_all_dual_axis_charts(
        primary_indicator,
        primary_data_path,
        related_data_path,
        performance_data_path,
        output_dir
    )
    charts.update(dual_charts)
    
    # 2. 生成表现汇总图
    if aggregate_stats:
        comp_path = f"{output_dir}/performance_summary.png"
        charts['performance_summary'] = plot_performance_comparison(
            aggregate_stats, comp_path
        )
    
    # 3. 生成事件矩阵热力图
    if event_results:
        matrix_path = f"{output_dir}/event_matrix_heatmap.png"
        charts['event_matrix'] = plot_event_matrix_heatmap(
            event_results, matrix_path
        )
    
    return charts


def preflight_check(output_dir: str) -> Tuple[bool, List[str]]:
    """
    执行前置条件检查
    
    Returns:
        (all_passed, messages)
    """
    messages = []
    all_passed = True
    
    # 1. 中文字体检查
    is_font_ok, font_name, font_msg = check_chinese_font_available()
    messages.append(font_msg)
    if not is_font_ok:
        all_passed = False
    
    # 2. 输出目录权限检查
    try:
        os.makedirs(output_dir, exist_ok=True)
        test_file = f"{output_dir}/.write_test"
        with open(test_file, 'w') as f:
            f.write('test')
        os.remove(test_file)
        messages.append(f"✓ 输出目录权限正常: {output_dir}")
    except Exception as e:
        messages.append(f"❌ 输出目录权限异常: {e}")
        all_passed = False
    
    # 3. matplotlib 基础检查
    try:
        import matplotlib
        messages.append(f"✓ matplotlib 版本: {matplotlib.__version__}")
    except ImportError:
        messages.append("❌ matplotlib 未安装")
        all_passed = False
    
    return all_passed, messages


def main():
    parser = argparse.ArgumentParser(
        description='生成金融事件影响分析图表（中文标签）'
    )
    parser.add_argument('--primary-indicator', required=True, 
                        help='表征指标ID')
    parser.add_argument('--primary-data', required=True, 
                        help='表征指标数据文件')
    parser.add_argument('--events', required=True, 
                        help='事件JSON文件')
    parser.add_argument('--related', required=True, 
                        help='相关资产JSON文件')
    parser.add_argument('--performance', required=True, 
                        help='表现分析JSON文件')
    parser.add_argument('--output-dir', default='./charts', 
                        help='图表输出目录')
    parser.add_argument('--skip-preflight', action='store_true',
                        help='跳过前置检查')
    
    args = parser.parse_args()
    
    # 前置检查
    if not args.skip_preflight:
        print("\n=== 前置条件检查 ===")
        passed, msgs = preflight_check(args.output_dir)
        for msg in msgs:
            print(f"  {msg}")
        
        if not passed:
            print("\n❌ 前置检查失败，建议修复后再继续")
            print("继续执行可能导致图表中文乱码")
        else:
            print("\n✓ 前置检查全部通过")
    
    # 设置字体（必须在绘图前）
    setup_chinese_font()
    
    charts = generate_all_charts(
        args.primary_indicator,
        args.primary_data,
        args.events,
        args.related,
        args.performance,
        args.output_dir
    )
    
    print(f"\n=== 已生成图表 ===")
    for chart_type, path in charts.items():
        print(f"  {chart_type}: {path}")
    
    # 保存图表路径清单
    manifest_path = f"{args.output_dir}/charts_manifest.json"
    with open(manifest_path, 'w') as f:
        json.dump(charts, f, indent=2)
    print(f"\n图表清单已保存: {manifest_path}")


if __name__ == '__main__':
    main()

path: scripts/generate_qt_charts.py
content: #!/usr/bin/env python3
"""生成美联储缩表周期分析图表"""

import json
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
from pathlib import Path
import sys
from datetime import datetime

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['WenQuanYi Micro Hei', 'SimHei', 'Noto Sans CJK SC', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False

def load_indicator_data(filepath):
    """加载指标数据"""
    with open(filepath, 'r') as f:
        data = json.load(f)
    
    df = pd.DataFrame(data['data'])
    df['date'] = pd.to_datetime(df['date'])
    df.set_index('date', inplace=True)
    df.sort_index(inplace=True)
    
    return df

def generate_comparison_chart(df_primary, df_related, events, primary_name, related_name, output_path):
    """生成时序对比图（含事件区间标记）"""
    fig, ax1 = plt.subplots(figsize=(16, 8))
    
    # 绘制主指标
    ax1.plot(df_primary.index, df_primary['close'], 'b-', linewidth=1.5, label=primary_name, alpha=0.8)
    ax1.set_xlabel('时间', fontsize=12)
    ax1.set_ylabel(primary_name, color='b', fontsize=12)
    ax1.tick_params(axis='y', labelcolor='b')
    
    # 绘制相关指标（双Y轴）
    ax2 = ax1.twinx()
    ax2.plot(df_related.index, df_related['close'], 'r-', linewidth=1.5, label=related_name, alpha=0.8)
    ax2.set_ylabel(related_name, color='r', fontsize=12)
    ax2.tick_params(axis='y', labelcolor='r')
    
    # 标记缩表事件区间（红色虚线框）
    for event in events['historical_events']:
        start = pd.to_datetime(event['start_date'])
        end = pd.to_datetime(event['end_date'])
        
        # 添加红色虚线矩形框
        rect = mpatches.Rectangle(
            (start, ax1.get_ylim()[0]),
            end - start,
            ax1.get_ylim()[1] - ax1.get_ylim()[0],
            linewidth=2,
            edgecolor='red',
            facecolor='lightyellow',
            alpha=0.3,
            linestyle='--',
            zorder=0
        )
        ax1.add_patch(rect)
        
        # 添加事件标签
        mid_date = start + (end - start) / 2
        ax1.annotate(
            f'缩表周期{event["event_id"]}',
            xy=(mid_date, ax1.get_ylim()[1] * 0.95),
            fontsize=10,
            ha='center',
            color='red',
            weight='bold'
        )
    
    # 设置标题
    title = f'{primary_name} vs {related_name} 历史走势对比\n（红色虚线框标记缩表周期）'
    plt.title(title, fontsize=14, weight='bold')
    
    # 合并图例
    lines1, labels1 = ax1.get_legend_handles_labels()
    lines2, labels2 = ax2.get_legend_handles_labels()
    ax1.legend(lines1 + lines2, labels1 + labels2, loc='upper left', fontsize=10)
    
    # 格式化X轴
    plt.gcf().autofmt_xdate()
    
    plt.tight_layout()
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    
    print(f"图表保存: {output_path}")

def generate_summary_chart(performance, output_path):
    """生成表现汇总图"""
    indicator_names = {
        'sp500': '标普500',
        'nasdaq': '纳斯达克',
        'usd_index': '美元指数',
        'xlf': '金融ETF',
        'tlt': '长债ETF',
        'xlk': '科技ETF',
        'xle': '能源ETF',
        'us_10y_treasury': '10年美债收益率'
    }
    
    # 提取汇总数据
    summary = performance['summary']
    indicators = list(summary.keys())
    names = [indicator_names.get(i, i) for i in indicators]
    avg_changes = [summary[i]['avg_change'] for i in indicators]
    win_rates = [summary[i]['win_rate'] for i in indicators]
    
    # 创建图表
    fig, ax = plt.subplots(figsize=(12, 8))
    
    # 颜色：正收益红色，负收益绿色（中国习惯）
    colors = ['#ff4444' if c > 0 else '#44ff44' for c in avg_changes]
    
    # 绘制柱状图
    bars = ax.bar(names, avg_changes, color=colors, alpha=0.8, edgecolor='black')
    
    # 添加数值标签
    for bar, change, win_rate in zip(bars, avg_changes, win_rates):
        height = bar.get_height()
        sign = '+' if change > 0 else ''
        ax.annotate(
            f'{sign}{change:.2f}%\n胜率{win_rate:.0f}%',
            xy=(bar.get_x() + bar.get_width() / 2, height),
            xytext=(0, 5),
            textcoords='offset points',
            ha='center',
            va='bottom',
            fontsize=10,
            weight='bold'
        )
    
    ax.set_ylabel('平均涨跌幅 (%)', fontsize=12)
    ax.set_xlabel('资产类别', fontsize=12)
    ax.set_title('美联储缩表周期内各类资产平均表现\n（🔴红涨 🟢绿跌）', fontsize=14, weight='bold')
    
    # 添加0线
    ax.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
    
    plt.xticks(rotation=30, ha='right')
    plt.tight_layout()
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    
    print(f"图表保存: {output_path}")

def generate_event_comparison_chart(performance, output_path):
    """生成事件对比图"""
    indicator_names = {
        'sp500': '标普500',
        'nasdaq': '纳斯达克',
        'usd_index': '美元指数',
        'xlf': '金融ETF',
        'tlt': '长债ETF',
        'xlk': '科技ETF',
        'xle': '能源ETF',
        'us_10y_treasury': '10年美债收益率'
    }
    
    # 提取事件数据
    events_data = performance['events']
    
    # 创建图表
    fig, axes = plt.subplots(1, 2, figsize=(16, 8))
    
    for ax, event in zip(axes, events_data):
        assets = event['assets']
        indicators = list(assets.keys())
        names = [indicator_names.get(i, i) for i in indicators]
        changes = [assets[i]['pct_change'] for i in indicators]
        
        colors = ['#ff4444' if c > 0 else '#44ff44' for c in changes]
        bars = ax.bar(names, changes, color=colors, alpha=0.8, edgecolor='black')
        
        for bar, change in zip(bars, changes):
            height = bar.get_height()
            sign = '+' if change > 0 else ''
            ax.annotate(
                f'{sign}{change:.2f}%',
                xy=(bar.get_x() + bar.get_width() / 2, height),
                xytext=(0, 5),
                textcoords='offset points',
                ha='center',
                va='bottom',
                fontsize=9,
                weight='bold'
            )
        
        ax.set_ylabel('涨跌幅 (%)', fontsize=10)
        ax.set_title(f'【{event["name"]}】\n{event["start_date"]} ~ {event["end_date"]}', fontsize=12, weight='bold')
        ax.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
        ax.tick_params(axis='x', rotation=30)
    
    plt.suptitle('美联储缩表周期各资产表现对比\n（🔴红涨 🟢绿跌）', fontsize=14, weight='bold')
    plt.tight_layout()
    plt.savefig(output_path, dpi=150, bbox_inches='tight')
    plt.close()
    
    print(f"图表保存: {output_path}")

def main():
    output_dir = Path(sys.argv[1]) if len(sys.argv) > 1 else Path.home() / '.openclaw/workspace/memory/reports/fed_qt_20260328'
    data_dir = output_dir / 'data'
    charts_dir = output_dir / 'charts'
    
    # 加载数据
    with open(output_dir / 'events.json', 'r') as f:
        events = json.load(f)
    
    with open(output_dir / 'performance.json', 'r') as f:
        performance = json.load(f)
    
    # 加载指标数据
    indicators_data = {}
    for indicator in ['sp500', 'nasdaq', 'usd_index', 'xlf', 'tlt', 'xlk', 'xle', 'us_10y_treasury']:
        filepath = data_dir / f'{indicator}.json'
        if filepath.exists():
            indicators_data[indicator] = load_indicator_data(filepath)
    
    indicator_names = {
        'sp500': '标普500指数',
        'nasdaq': '纳斯达克指数',
        'usd_index': '美元指数',
        'xlf': '金融ETF',
        'tlt': '长债ETF',
        'xlk': '科技ETF',
        'xle': '能源ETF',
        'us_10y_treasury': '10年美债收益率'
    }
    
    # 生成时序对比图
    comparison_pairs = [
        ('us_10y_treasury', 'sp500'),
        ('us_10y_treasury', 'nasdaq'),
        ('us_10y_treasury', 'tlt'),
        ('us_10y_treasury', 'xlf'),
        ('us_10y_treasury', 'usd_index'),
    ]
    
    for primary, related in comparison_pairs:
        if primary in indicators_data and related in indicators_data:
            output_path = charts_dir / f'{primary}_vs_{related}.png'
            generate_comparison_chart(
                indicators_data[primary],
                indicators_data[related],
                events,
                indicator_names[primary],
                indicator_names[related],
                output_path
            )
    
    # 生成汇总图
    generate_summary_chart(performance, charts_dir / 'performance_summary.png')
    
    # 生成事件对比图
    generate_event_comparison_chart(performance, charts_dir / 'event_comparison.png')
    
    # 保存图表清单
    manifest = {
        'charts': [
            {'file': 'us_10y_treasury_vs_sp500.png', 'title': '10年美债收益率 vs 标普500'},
            {'file': 'us_10y_treasury_vs_nasdaq.png', 'title': '10年美债收益率 vs 纳斯达克'},
            {'file': 'us_10y_treasury_vs_tlt.png', 'title': '10年美债收益率 vs 长债ETF'},
            {'file': 'us_10y_treasury_vs_xlf.png', 'title': '10年美债收益率 vs 金融ETF'},
            {'file': 'us_10y_treasury_vs_usd_index.png', 'title': '10年美债收益率 vs 美元指数'},
            {'file': 'performance_summary.png', 'title': '资产表现汇总'},
            {'file': 'event_comparison.png', 'title': '缩表周期对比'},
        ],
        'generated_at': datetime.now().isoformat()
    }
    
    with open(charts_dir / 'charts_manifest.json', 'w') as f:
        json.dump(manifest, f, indent=2)
    
    print(f"\n所有图表生成完成！保存目录: {charts_dir}")

if __name__ == '__main__':
    main()

path: scripts/generate_report.py
content: #!/usr/bin/env python3
"""
Generate final analysis report in Markdown format.
Embeds all generated charts into the report for easy viewing.
"""

import argparse
import json
import os
from datetime import datetime
from typing import Dict, List, Tuple


# 中文名称映射
INDICATOR_NAMES = {
    'brent_crude': '布伦特原油',
    'wti_crude': 'WTI原油',
    'gold': '黄金',
    'silver': '白银',
    'copper': '铜',
    'sp500': '标普500',
    'nasdaq': '纳斯达克',
    'dow_jones': '道琼斯',
    'vix': 'VIX恐慌指数',
    'xlk': '科技ETF',
    'xle': '能源ETF',
    'xlf': '金融ETF',
    'xlv': '医疗ETF',
    'xli': '工业ETF',
    'xly': '消费ETF',
    'jets': '航空ETF',
    'iyr': '房地产ETF',
    'gdx': '黄金矿业ETF',
    'tlt': '长期美债ETF',
    'usd_index': '美元指数',
    'usd_cny': '美元/人民币',
    'us_10y_treasury': '10年期美债收益率',
    'csi300': '沪深300',
    'sse_composite': '上证指数',
    'chinext': '创业板指',
}

# 颜色约定（中国习惯）
COLOR_POSITIVE = '🔴'
COLOR_NEGATIVE = '🟢'


def load_json(filepath: str) -> Dict:
    """Load JSON file."""
    with open(filepath, 'r') as f:
        return json.load(f)


def get_indicator_name(indicator_id: str) -> str:
    """Get Chinese name for indicator."""
    return INDICATOR_NAMES.get(indicator_id, indicator_id)


def format_change(change_pct: float) -> str:
    """Format change percentage with color (Chinese convention)."""
    if change_pct > 0:
        return f"{COLOR_POSITIVE} +{change_pct:.1f}%"
    elif change_pct < 0:
        return f"{COLOR_NEGATIVE} {change_pct:.1f}%"
    else:
        return f"0.0%"


def generate_change_table(event_results: List[Dict], primary_indicator: str, 
                          related_indicators: List[str]) -> str:
    """Generate change table in Markdown format."""
    
    primary_name = get_indicator_name(primary_indicator)
    
    # Table header
    header = "| 事件 | 时间区间 | " + primary_name + " |"
    for ind in related_indicators[:6]:  # 最多显示6个相关指标
        header += f" {get_indicator_name(ind)} |"
    header += "\n"
    
    # Separator
    sep = "|:----:|:--------:|:------:|"
    for _ in range(min(6, len(related_indicators))):
        sep += ":------:|"
    sep += "\n"
    
    # Table rows (从最新到最早)
    rows = ""
    for i, result in enumerate(event_results):
        event = result.get('event', {})
        primary_perf = result.get('primary_performance', {})
        related_perf = result.get('related_performance', {})
        
        # 事件编号和时间区间
        time_range = f"{event['start_date'][:7]} ~ {event['end_date'][:7]}"
        row = f"| **事件{i+1}** | {time_range} | "
        
        # 主指标涨跌幅
        primary_change = primary_perf.get('total_return_pct', 0)
        row += format_change(primary_change) + " |"
        
        # 相关指标涨跌幅
        for ind in related_indicators[:6]:
            perf = related_perf.get(ind, {})
            change = perf.get('total_return_pct', 0)
            row += f" {format_change(change)} |"
        
        row += "\n"
        rows += row
    
    return header + sep + rows


def generate_summary_table(aggregate_stats: Dict) -> str:
    """Generate summary statistics table."""
    
    header = "| 资产 | 平均收益 | 胜率 | 收益波动 | 确定性评级 |\n"
    sep = "|:-----|:-------:|:----:|:--------:|:----------:|\n"
    
    rows = ""
    # 按胜率排序
    sorted_stats = sorted(aggregate_stats.items(), 
                          key=lambda x: x[1]['win_rate_pct'], reverse=True)
    
    for indicator, stats in sorted_stats:
        avg_return = stats['avg_return_pct']
        win_rate = stats['win_rate_pct']
        std_return = stats['std_return_pct']
        
        # 确定性评级（基于胜率）
        if win_rate >= 90:
            rating = "⭐⭐⭐⭐⭐"
        elif win_rate >= 70:
            rating = "⭐⭐⭐⭐"
        elif win_rate >= 50:
            rating = "⭐⭐⭐"
        elif win_rate >= 30:
            rating = "⭐⭐"
        else:
            rating = "⭐"
        
        rows += f"| **{get_indicator_name(indicator)}** | {format_change(avg_return)} | {win_rate:.0f}% | ±{std_return:.1f}% | {rating} |\n"
    
    return header + sep + rows


def generate_chart_section(charts: Dict[str, str], output_dir: str) -> str:
    """Generate chart embedding section with Markdown image references."""
    
    section = "## 📈 时序对比图（各资产与表征指标）\n\n"
    section += "> 🔴 **红色虚线框** = 历史事件发生时间区间\n\n"
    
    # 按相关性分类展示图表
    dual_charts = {k: v for k, v in charts.items() if '_vs_' in k}
    
    for chart_type, chart_path in dual_charts.items():
        # 解析指标名称
        parts = chart_type.split('_vs_')
        if len(parts) == 2:
            primary = get_indicator_name(parts[0])
            related = get_indicator_name(parts[1])
            section += f"### {primary} vs {related}\n\n"
        
        # 使用相对路径嵌入图片
        # Markdown 图片语法：![描述](路径)
        relative_path = os.path.basename(chart_path)
        section += f"![{chart_type}](charts/{relative_path})\n\n"
    
    # 添加表现汇总图
    if 'performance_summary' in charts:
        section += "## 📊 表现汇总图\n\n"
        section += f"![表现汇总](charts/performance_summary.png)\n\n"
    
    # 添加事件矩阵热力图
    if 'event_matrix' in charts:
        section += "## 🔥 事件矩阵热力图\n\n"
        section += f"![事件矩阵](charts/event_matrix_heatmap.png)\n\n"
    
    return section


def generate_full_report(
    primary_indicator: str,
    events_data: Dict,
    related_data: Dict,
    performance_data: Dict,
    charts_manifest: Dict,
    output_dir: str
) -> str:
    """Generate full analysis report in Markdown."""
    
    primary_name = get_indicator_name(primary_indicator)
    current_event = events_data.get('current_event', {})
    similar_events = events_data.get('similar_events', [])
    event_results = performance_data.get('event_results', [])
    aggregate_stats = performance_data.get('aggregate_stats', {})
    
    # 收集相关指标
    related_indicators = []
    for category in ['benefited', 'harmed', 'neutral_uncertain']:
        for asset in related_data.get(category, []):
            if 'indicator' in asset:
                related_indicators.append(asset['indicator'])
    
    # 报告头部
    report = f"""# {primary_name}快速上涨历史影响分析报告

**生成时间**: {datetime.now().strftime('%Y年%m月%d日 %H:%M')}  
**分析工具**: financial-event-impact-analyzer 🐂

---

## 🎯 核心结论

"""
    
    # 核心结论（基于胜率最高的资产）
    if aggregate_stats:
        top_asset = max(aggregate_stats.items(), 
                        key=lambda x: x[1]['win_rate_pct'])
        top_name = get_indicator_name(top_asset[0])
        top_win_rate = top_asset[1]['win_rate_pct']
        top_avg_return = top_asset[1]['avg_return_pct']
        
        report += f"**{top_name}是{primary_name}上涨最确定的受益资产**："
        report += f"在历史上{len(similar_events)}次{primary_name}快速上涨事件中，"
        report += f"{top_name}**{top_win_rate:.0f}%录得正收益**，"
        report += f"平均涨幅{top_avg_return:.1f}%。\n\n"
    
    # 当前事件特征
    report += f"""---

## 📋 当前事件特征

| 特征 | 数值 |
|:-----|:----:|
| **90天涨跌幅** | {format_change(current_event.get('change_90d', 0))} |
| **当前价格** | $ {current_event.get('current_price', 'N/A')} |
| **事件类型** | {current_event.get('event_type', '重大价格上涨')} |
| **波动率** | {current_event.get('volatility_30d', 0):.1f}% |

---

## 📜 历史相似事件 ({len(similar_events)}个)

"""
    
    # 事件列表
    report += "| 事件 | 时间区间 | 涨跌幅 | 背景 |\n"
    report += "|:----:|:--------:|:------:|:-----|\n"
    
    for i, event in enumerate(similar_events[:10]):  # 最多显示10个
        time_range = f"{event['start_date'][:10]} ~ {event['end_date'][:10]}"
        change = event.get('change_pct', 0)
        report += f"| **事件{i+1}** | {time_range} | {format_change(change)} | - |\n"
    
    # 变动幅度表格
    report += f"""

---

## 📊 变动幅度表格（从最新到最早）

{generate_change_table(event_results, primary_indicator, related_indicators)}

---

## 🔑 汇总统计

{generate_summary_table(aggregate_stats)}

"""
    
    # 图表部分（嵌入图片）
    if charts_manifest:
        report += generate_chart_section(charts_manifest, output_dir)
    
    # 分析局限性
    report += """
---

## ⚠️ 分析局限性

1. **历史不代表未来**：历史表现仅供参考，不构成投资建议
2. **样本数量有限**：相似事件数量可能较少，统计显著性有待验证
3. **宏观环境差异**：每次事件的宏观背景不同，需结合当前环境判断
4. **数据源局限**：部分数据可能缺失或存在误差

---

🐂 **报告生成完毕** | 基于 {len(similar_events)} 个历史事件分析
"""
    
    return report


def main():
    parser = argparse.ArgumentParser(
        description='生成金融事件影响分析报告（Markdown格式，嵌入图片）'
    )
    parser.add_argument('--primary-indicator', required=True, 
                        help='表征指标ID')
    parser.add_argument('--events', required=True, 
                        help='事件JSON文件')
    parser.add_argument('--related', required=True, 
                        help='相关资产JSON文件')
    parser.add_argument('--performance', required=True, 
                        help='表现分析JSON文件')
    parser.add_argument('--charts-manifest', required=True,
                        help='图表清单JSON文件')
    parser.add_argument('--output-dir', required=True,
                        help='报告输出目录')
    parser.add_argument('--output', '-o', default='report.md',
                        help='报告文件名')
    
    args = parser.parse_args()
    
    # 加载所有数据
    events_data = load_json(args.events)
    related_data = load_json(args.related)
    performance_data = load_json(args.performance)
    charts_manifest = load_json(args.charts_manifest)
    
    # 生成报告
    report = generate_full_report(
        args.primary_indicator,
        events_data,
        related_data,
        performance_data,
        charts_manifest,
        args.output_dir
    )
    
    # 保存报告
    output_path = f"{args.output_dir}/{args.output}"
    with open(output_path, 'w', encoding='utf-8') as f:
        f.write(report)
    
    print(f"\n✓ 报告已生成: {output_path}")
    print(f"  包含 {len(charts_manifest)} 张嵌入图表")
    
    # 输出报告内容预览
    print(f"\n=== 报告预览 ===")
    preview_lines = report.split('\n')[:30]
    for line in preview_lines:
        print(line)
    print("...")


if __name__ == '__main__':
    main()

path: scripts/infer_related_assets.py
content: #!/usr/bin/env python3
"""
Infer related assets based on causal relationships, supply chain, substitution effects.
Maps events to potentially affected asset classes and provides indicator IDs for analysis.
"""

import argparse
import json
from typing import Dict, List, Tuple

# Event-to-asset causal mapping
# Based on economic theory: supply chain, substitution, complementary, financial linkages
# ⭐ 核心改动：多经济体全维度分析框架 (中国、日本、韩国、欧洲)
# ⭐ 核心改动：不仅限于可交易金融资产，包括宏观经济指标、利率、货币发行量、汇率

EVENT_ASSET_MAPPING = {
    'oil_price_increase': {
        'benefited': [
            # Direct beneficiaries: oil producers (US)
            {'category': 'energy_producers', 'indicator': 'xle', 'relation': 'upstream', 'reason': '油价上涨增加能源公司收入', 'economy': 'US'},
            {'category': 'oil_services', 'indicator': 'oef', 'relation': 'upstream', 'reason': '钻井活动增加', 'economy': 'US'},
            {'category': 'commodities', 'indicator': 'natural_gas', 'relation': 'substitution', 'reason': '天然气作为替代能源', 'economy': 'Global'},
            
            # Inflation hedges
            {'category': 'precious_metals', 'indicator': 'gold', 'relation': 'inflation_hedge', 'reason': '油价推升通胀预期', 'economy': 'Global'},
            {'category': 'precious_metals', 'indicator': 'silver', 'relation': 'inflation_hedge', 'reason': '商品通胀代理', 'economy': 'Global'},
            
            # ⭐ 多经济体股市受益资产
            {'category': 'china_energy', 'indicator': '159930', 'relation': 'direct', 'reason': '中国能源ETF受益', 'economy': 'China'},
            {'category': 'china_oil_stock', 'indicator': '600938', 'relation': 'direct', 'reason': '中国海油受益油价上涨', 'economy': 'China'},
            {'category': 'china_oil_stock', 'indicator': '601857', 'relation': 'direct', 'reason': '中国石油受益油价上涨', 'economy': 'China'},
            {'category': 'china_oil_stock', 'indicator': '601088', 'relation': 'direct', 'reason': '中国神华能源溢价', 'economy': 'China'},
            {'category': 'hk_energy', 'indicator': 'hsi', 'relation': 'sector', 'reason': '港股能源股受益', 'economy': 'HK'},
            
            {'category': 'japan_energy', 'indicator': 'nikkei225', 'relation': 'sector', 'reason': '日本能源股受益', 'economy': 'Japan'},
            {'category': 'japan_export', 'indicator': 'nikkei225', 'relation': 'currency', 'reason': '日元贬值利好出口', 'economy': 'Japan'},
            
            {'category': 'europe_energy', 'indicator': 'dax', 'relation': 'sector', 'reason': '德国能源股受益', 'economy': 'Europe'},
            {'category': 'europe_energy', 'indicator': 'cac40', 'relation': 'sector', 'reason': '法国能源股受益', 'economy': 'Europe'},
            
            {'category': 'korea_export', 'indicator': 'kospi', 'relation': 'currency', 'reason': '韩元贬值利好出口', 'economy': 'Korea'},
        ],
        'harmed': [
            # Direct sufferers: fuel consumers (US)
            {'category': 'airlines', 'indicator': 'jets', 'relation': 'fuel_cost', 'reason': '燃油是主要运营成本', 'economy': 'US'},
            {'category': 'transportation', 'indicator': 'xli', 'relation': 'fuel_cost', 'reason': '物流成本上升', 'economy': 'US'},
            {'category': 'consumer_discretionary', 'indicator': 'xly', 'relation': 'demand_shift', 'reason': '能源成本上升减少可支配消费', 'economy': 'US'},
            {'category': 'chemicals', 'indicator': 'dow', 'relation': 'input_cost', 'reason': '石油基原料成本上升', 'economy': 'US'},
            
            # ⭐ 多经济体股市受损资产
            {'category': 'china_airline', 'indicator': '601111', 'relation': 'fuel_cost', 'reason': '中国国航燃油成本上升', 'economy': 'China'},
            {'category': 'china_chemical', 'indicator': 'csi300', 'relation': 'input_cost', 'reason': '中国化工板块成本压力', 'economy': 'China'},
            {'category': 'china_transport', 'indicator': 'chinext', 'relation': 'cost', 'reason': '中国运输板块成本上升', 'economy': 'China'},
            
            {'category': 'japan_auto', 'indicator': 'nikkei225', 'relation': 'input_cost', 'reason': '日本汽车股成本上升', 'economy': 'Japan'},
            {'category': 'japan_chemical', 'indicator': 'topix', 'relation': 'input_cost', 'reason': '日本化工股成本压力', 'economy': 'Japan'},
            
            {'category': 'korea_chemical', 'indicator': 'kospi', 'relation': 'input_cost', 'reason': '韩国化工股成本压力', 'economy': 'Korea'},
            {'category': 'korea_airline', 'indicator': 'kospi', 'relation': 'fuel_cost', 'reason': '韩国航空股燃油成本上升', 'economy': 'Korea'},
            
            {'category': 'europe_auto', 'indicator': 'dax', 'relation': 'input_cost', 'reason': '德国汽车股成本上升', 'economy': 'Europe'},
            {'category': 'europe_airline', 'indicator': 'cac40', 'relation': 'fuel_cost', 'reason': '法航燃油成本上升', 'economy': 'Europe'},
            {'category': 'europe_chemical', 'indicator': 'dax', 'relation': 'input_cost', 'reason': '德国化工股成本压力', 'economy': 'Europe'},
            {'category': 'uk_auto', 'indicator': 'ftse100', 'relation': 'input_cost', 'reason': '英国汽车股成本上升', 'economy': 'Europe'},
            
            {'category': 'india_airline', 'indicator': 'sgx_nifty', 'relation': 'fuel_cost', 'reason': '印度航空股燃油成本上升', 'economy': 'India'},
        ],
        'macro_indicators': [
            # ⭐ 美国宏观指标（必须覆盖）
            {'category': 'inflation', 'indicator': 'cpi_us', 'relation': 'cost_push', 'reason': '油价上涨推升美国CPI', 'economy': 'US'},
            {'category': 'inflation', 'indicator': 'ppi_us', 'relation': 'cost_push', 'reason': '油价上涨推升美国PPI', 'economy': 'US'},
            {'category': 'rate', 'indicator': 'fed_funds_rate', 'relation': 'policy_response', 'reason': '通胀压力可能触发加息', 'economy': 'US'},
            {'category': 'rate', 'indicator': 'us_10y_treasury', 'relation': 'inflation_expectation', 'reason': '通胀预期推升美债收益率', 'economy': 'US'},
            {'category': 'money', 'indicator': 'm2_us', 'relation': 'policy', 'reason': '美联储可能收紧货币', 'economy': 'US'},
            
            # ⭐ 中国宏观指标（必须覆盖）
            {'category': 'inflation', 'indicator': 'cpi_china', 'relation': 'cost_push', 'reason': '油价上涨推升中国CPI', 'economy': 'China'},
            {'category': 'inflation', 'indicator': 'ppi_china', 'relation': 'cost_push', 'reason': '油价上涨推升中国PPI', 'economy': 'China'},
            {'category': 'growth', 'indicator': 'gdp_china', 'relation': 'input_cost', 'reason': '油价上涨增加进口成本', 'economy': 'China'},
            {'category': 'activity', 'indicator': 'pmi_china', 'relation': 'cost', 'reason': '油价上涨影响制造业PMI', 'economy': 'China'},
            {'category': 'rate', 'indicator': 'loan_rate_china', 'relation': 'policy', 'reason': '通胀压力可能影响LPR', 'economy': 'China'},
            {'category': 'rate', 'indicator': 'bond_10y_china', 'relation': 'inflation', 'reason': '通胀预期推升国债收益率', 'economy': 'China'},
            {'category': 'money', 'indicator': 'm2_china', 'relation': 'policy', 'reason': '央行可能收紧货币', 'economy': 'China'},
            {'category': 'money', 'indicator': 'm1_china', 'relation': 'policy', 'reason': '货币供应量变化', 'economy': 'China'},
            {'category': 'credit', 'indicator': 'social_financing', 'relation': 'credit', 'reason': '社融规模可能调整', 'economy': 'China'},
            
            # ⭐ 日本宏观指标（必须覆盖）
            {'category': 'inflation', 'indicator': 'cpi_japan', 'relation': 'cost_push', 'reason': '油价上涨推升日本CPI', 'economy': 'Japan'},
            {'category': 'growth', 'indicator': 'gdp_japan', 'relation': 'import_cost', 'reason': '油价上涨增加进口成本', 'economy': 'Japan'},
            {'category': 'rate', 'indicator': 'japan_10y_bond', 'relation': 'policy', 'reason': '日本央行政策调整', 'economy': 'Japan'},
            {'category': 'money', 'indicator': 'm2_japan', 'relation': 'policy', 'reason': '日本央行货币政策', 'economy': 'Japan'},
            
            # ⭐ 韩国宏观指标（必须覆盖）
            {'category': 'inflation', 'indicator': 'cpi_korea', 'relation': 'cost_push', 'reason': '油价上涨推升韩国CPI', 'economy': 'Korea'},
            {'category': 'growth', 'indicator': 'gdp_korea', 'relation': 'import_cost', 'reason': '油价上涨增加进口成本', 'economy': 'Korea'},
            
            # ⭐ 欧洲宏观指标（必须覆盖）
            {'category': 'inflation', 'indicator': 'cpi_eurozone', 'relation': 'cost_push', 'reason': '油价上涨推升欧元区CPI', 'economy': 'Europe'},
            {'category': 'growth', 'indicator': 'gdp_eurozone', 'relation': 'import_cost', 'reason': '油价上涨增加进口成本', 'economy': 'Europe'},
            {'category': 'rate', 'indicator': 'euro_10y_bond', 'relation': 'policy', 'reason': '欧央行政策调整', 'economy': 'Europe'},
            {'category': 'money', 'indicator': 'm2_eurozone', 'relation': 'policy', 'reason': '欧央行货币政策', 'economy': 'Europe'},
            
            # ⭐ 汇率指标（必须覆盖）
            {'category': 'currency', 'indicator': 'usd_cny', 'relation': 'import_cost', 'reason': '油价上涨→中国进口成本→人民币压力', 'economy': 'China'},
            {'category': 'currency', 'indicator': 'usd_jpy', 'relation': 'import_cost', 'reason': '油价上涨→日本进口成本→日元贬值', 'economy': 'Japan'},
            {'category': 'currency', 'indicator': 'usd_krw', 'relation': 'import_cost', 'reason': '油价上涨→韩国进口成本→韩元贬值', 'economy': 'Korea'},
            {'category': 'currency', 'indicator': 'eur_usd', 'relation': 'import_cost', 'reason': '油价上涨→欧洲进口成本→欧元压力', 'economy': 'Europe'},
            {'category': 'currency', 'indicator': 'gbp_usd', 'relation': 'import_cost', 'reason': '油价上涨→英国进口成本→英镑压力', 'economy': 'Europe'},
            {'category': 'currency', 'indicator': 'usd_index', 'relation': 'capital_flow', 'reason': '油价上涨→美元需求', 'economy': 'US'},
        ],
        'neutral_uncertain': [
            {'category': 'financials', 'indicator': 'xlf', 'relation': 'mixed', 'reason': '能源贷款受益，但通胀压制估值', 'economy': 'US'},
            {'category': 'technology', 'indicator': 'xlk', 'relation': 'mixed', 'reason': '能源成本压力，但需求可能转移', 'economy': 'US'},
            {'category': 'china_financials', 'indicator': 'sse50', 'relation': 'mixed', 'reason': '银行股不确定性', 'economy': 'China'},
        ]
    },
    
    'oil_price_decrease': {
        'benefited': [
            {'category': 'airlines', 'indicator': 'jets', 'relation': 'fuel_cost', 'reason': 'Lower fuel costs boost margins'},
            {'category': 'transportation', 'indicator': 'xli', 'relation': 'fuel_cost', 'reason': 'Logistics cost reduction'},
            {'category': 'consumer_discretionary', 'indicator': 'xly', 'relation': 'demand_shift', 'reason': 'Lower energy costs boost spending'},
            {'category': 'chemicals', 'indicator': 'dow', 'relation': 'input_cost', 'reason': 'Lower feedstock costs'},
            {'category': 'asia_equity', 'indicator': 'csi300', 'relation': 'trade_flow', 'reason': 'Import costs reduced'},
        ],
        'harmed': [
            {'category': 'energy_producers', 'indicator': 'xle', 'relation': 'upstream', 'reason': 'Lower revenue'},
            {'category': 'oil_services', 'indicator': 'oef', 'relation': 'upstream', 'reason': 'Reduced drilling activity'},
            {'category': 'precious_metals', 'indicator': 'gold', 'relation': 'inflation', 'reason': 'Inflation expectations decline'},
        ],
    },
    
    'gold_price_increase': {
        'benefited': [
            {'category': 'gold_miners', 'indicator': 'gdx', 'relation': 'direct', 'reason': 'Higher gold prices boost miner revenue'},
            {'category': 'precious_metals', 'indicator': 'silver', 'relation': 'complementary', 'reason': 'Silver often follows gold'},
        ],
        'harmed': [
            {'category': 'equities', 'indicator': 'sp500', 'relation': 'risk_off', 'reason': 'Gold rise signals risk aversion'},
            {'category': 'financials', 'indicator': 'xlf', 'relation': 'inflation', 'reason': 'Lower inflation expectations'},
        ],
        'neutral': [
            {'category': 'commodities', 'indicator': 'copper', 'relation': 'mixed', 'reason': 'Industrial vs precious divergence'},
        ]
    },
    
    'interest_rate_increase': {
        'benefited': [
            {'category': 'financials', 'indicator': 'xlf', 'relation': 'spread', 'reason': 'Higher net interest margin'},
            {'category': 'dollar', 'indicator': 'usd_index', 'relation': 'capital_flow', 'reason': 'Higher yields attract foreign capital'},
        ],
        'harmed': [
            {'category': 'bonds', 'indicator': 'tlt', 'relation': 'price_inverse', 'reason': 'Bond prices fall when rates rise'},
            {'category': 'real_estate', 'indicator': 'iyr', 'relation': 'financing', 'reason': 'Higher mortgage rates'},
            {'category': 'growth_equity', 'indicator': 'nasdaq', 'relation': 'valuation', 'reason': 'Higher discount rate hurts growth stocks'},
            {'category': 'utilities', 'indicator': 'xlu', 'relation': 'financing', 'reason': 'Capital-intensive, debt-heavy'},
        ],
    },
    
    'vix_increase': {
        'benefited': [
            {'category': 'volatility_products', 'indicator': 'vix', 'relation': 'direct', 'reason': 'Direct VIX exposure'},
            {'category': 'defensive', 'indicator': 'xlp', 'relation': 'flight_to_safety', 'reason': 'Consumer staples are defensive'},
            {'category': 'precious_metals', 'indicator': 'gold', 'relation': 'safe_haven', 'reason': 'Traditional safe haven'},
        ],
        'harmed': [
            {'category': 'equities', 'indicator': 'sp500', 'relation': 'risk_off', 'reason': 'Higher volatility = lower risk appetite'},
            {'category': 'growth_equity', 'indicator': 'nasdaq', 'relation': 'risk_off', 'reason': 'Growth stocks more volatility-sensitive'},
            {'category': 'financials', 'indicator': 'xlf', 'relation': 'risk', 'reason': 'Market stress hurts financials'},
        ],
    },
    
    'dollar_strength': {
        'benefited': [
            {'category': 'us_equity', 'indicator': 'sp500', 'relation': 'capital_flow', 'reason': 'Foreign capital inflows'},
            {'category': 'us_bonds', 'indicator': 'tlt', 'relation': 'capital_flow', 'reason': 'Foreign demand for US assets'},
        ],
        'harmed': [
            {'category': 'emerging_equity', 'indicator': 'csi300', 'relation': 'currency_pressure', 'reason': 'EM currencies weaken'},
            {'category': 'commodities', 'indicator': 'gold', 'relation': 'inverse', 'reason': 'Gold priced in USD'},
            {'category': 'commodities', 'indicator': 'brent_crude', 'relation': 'inverse', 'reason': 'Oil priced in USD'},
            {'category': 'exporters', 'indicator': 'xli', 'relation': 'competitive', 'reason': 'US exports less competitive'},
        ],
    },
    
    'inflation_increase': {
        'benefited': [
            {'category': 'commodities', 'indicator': 'gold', 'relation': 'inflation_hedge', 'reason': 'Traditional inflation hedge'},
            {'category': 'commodities', 'indicator': 'silver', 'relation': 'inflation_hedge', 'reason': 'Commodity inflation proxy'},
            {'category': 'energy', 'indicator': 'xle', 'relation': 'commodity', 'reason': 'Energy prices drive inflation'},
            {'category': 'real_assets', 'indicator': 'iyr', 'relation': 'real_asset', 'reason': 'Real estate appreciates with inflation'},
        ],
        'harmed': [
            {'category': 'bonds', 'indicator': 'tlt', 'relation': 'inflation', 'reason': 'Fixed income loses purchasing power'},
            {'category': 'consumer_staples', 'indicator': 'xlp', 'relation': 'margin_pressure', 'reason': 'Input cost pressure'},
            {'category': 'growth_equity', 'indicator': 'nasdaq', 'relation': 'valuation', 'reason': 'Higher discount rate'},
        ],
    },
    
    'recession_signal': {
        'benefited': [
            {'category': 'defensive', 'indicator': 'xlp', 'relation': 'essential', 'reason': 'Staples demand stable'},
            {'category': 'healthcare', 'indicator': 'xlv', 'relation': 'essential', 'reason': 'Healthcare demand stable'},
            {'category': 'utilities', 'indicator': 'xlu', 'relation': 'essential', 'reason': 'Utilities demand stable'},
            {'category': 'precious_metals', 'indicator': 'gold', 'relation': 'safe_haven', 'reason': 'Flight to safety'},
            {'category': 'bonds', 'indicator': 'tlt', 'relation': 'flight_to_quality', 'reason': 'Safe asset demand'},
        ],
        'harmed': [
            {'category': 'consumer_discretionary', 'indicator': 'xly', 'relation': 'cyclical', 'reason': ' discretionary spending falls'},
            {'category': 'industrials', 'indicator': 'xli', 'relation': 'cyclical', 'reason': 'Industrial demand weakens'},
            {'category': 'financials', 'indicator': 'xlf', 'relation': 'credit_risk', 'reason': 'Loan defaults increase'},
            {'category': 'technology', 'indicator': 'xlk', 'relation': 'growth', 'reason': 'Growth stocks hurt in recession'},
        ],
    },
}


# Primary indicator to event type mapping
INDICATOR_EVENT_TYPE = {
    'brent_crude': {'increase': 'oil_price_increase', 'decrease': 'oil_price_decrease'},
    'wti_crude': {'increase': 'oil_price_increase', 'decrease': 'oil_price_decrease'},
    'gold': {'increase': 'gold_price_increase', 'decrease': 'gold_price_decrease'},
    'silver': {'increase': 'precious_metals_increase', 'decrease': 'precious_metals_decrease'},
    'vix': {'increase': 'vix_increase', 'decrease': 'vix_decrease'},
    'usd_index': {'increase': 'dollar_strength', 'decrease': 'dollar_weakness'},
    'us_10y_treasury': {'increase': 'interest_rate_increase', 'decrease': 'interest_rate_decrease'},
    'fed_funds_rate': {'increase': 'interest_rate_increase', 'decrease': 'interest_rate_decrease'},
    'cpi_us': {'increase': 'inflation_increase', 'decrease': 'inflation_decrease'},
    'unemployment_us': {'increase': 'recession_signal', 'decrease': 'recovery_signal'},
}


def infer_related_assets(primary_indicator: str, direction: str) -> Dict:
    """
    Infer related assets based on primary indicator and direction.
    
    Args:
        primary_indicator: The main indicator ID (e.g., 'brent_crude')
        direction: 'increase' or 'decrease'
    
    Returns:
        Dictionary with benefited, harmed, and neutral asset lists
    """
    # Determine event type
    if primary_indicator in INDICATOR_EVENT_TYPE:
        event_type = INDICATOR_EVENT_TYPE[primary_indicator].get(direction, 'unknown')
    else:
        # Try to infer from indicator name patterns
        if 'crude' in primary_indicator or 'oil' in primary_indicator:
            event_type = f"oil_price_{direction}"
        elif 'gold' in primary_indicator or 'silver' in primary_indicator:
            event_type = f"precious_metals_{direction}"
        elif 'vix' in primary_indicator:
            event_type = f"vix_{direction}"
        else:
            event_type = f"generic_price_{direction}"
    
    # Get mapping for event type
    if event_type in EVENT_ASSET_MAPPING:
        mapping = EVENT_ASSET_MAPPING[event_type]
    else:
        # Default mapping for unknown events
        mapping = {
            'benefited': [],
            'harmed': [],
            'neutral_uncertain': []
        }
    
    result = {
        'event_type': event_type,
        'primary_indicator': primary_indicator,
        'direction': direction,
        'benefited': mapping.get('benefited', []),
        'harmed': mapping.get('harmed', []),
        'neutral_uncertain': mapping.get('neutral_uncertain', [])
    }
    
    return result


def get_all_related_indicators(result: Dict) -> List[str]:
    """
    Extract all indicator IDs from the result.
    ⭐ 核心改动：确保每一项都会生成图表
    """
    indicators = set()
    
    # 遍历所有类别（包括 macro_indicators）
    for category in ['benefited', 'harmed', 'neutral_uncertain', 'macro_indicators']:
        for asset in result.get(category, []):
            if 'indicator' in asset:
                indicators.add(asset['indicator'])
    
    return sorted(list(indicators))


def get_multi_economy_summary(result: Dict) -> Dict:
    """
    ⭐ 核心新增：生成多经济体分析摘要
    
    Returns:
        各经济体受影响指标数量统计
    """
    economy_stats = {
        'US': {'benefited': 0, 'harmed': 0, 'macro': 0},
        'China': {'benefited': 0, 'harmed': 0, 'macro': 0},
        'Japan': {'benefited': 0, 'harmed': 0, 'macro': 0},
        'Korea': {'benefited': 0, 'harmed': 0, 'macro': 0},
        'Europe': {'benefited': 0, 'harmed': 0, 'macro': 0},
        'Global': {'benefited': 0, 'harmed': 0, 'macro': 0},
        'HK': {'benefited': 0, 'harmed': 0, 'macro': 0},
        'India': {'benefited': 0, 'harmed': 0, 'macro': 0},
    }
    
    for category in ['benefited', 'harmed', 'neutral_uncertain']:
        for asset in result.get(category, []):
            economy = asset.get('economy', 'Global')
            if economy in economy_stats:
                if category == 'benefited':
                    economy_stats[economy]['benefited'] += 1
                elif category == 'harmed':
                    economy_stats[economy]['harmed'] += 1
    
    for asset in result.get('macro_indicators', []):
        economy = asset.get('economy', 'Global')
        if economy in economy_stats:
            economy_stats[economy]['macro'] += 1
    
    return economy_stats


def list_event_types():
    """List all supported event types."""
    print("\nSupported Event Types:")
    print("=" * 60)
    for event_type in sorted(EVENT_ASSET_MAPPING.keys()):
        mapping = EVENT_ASSET_MAPPING[event_type]
        print(f"\n{event_type}:")
        print(f"  Benefited: {len(mapping.get('benefited', []))} assets")
        print(f"  Harmed: {len(mapping.get('harmed', []))} assets")
        print(f"  Neutral: {len(mapping.get('neutral_uncertain', []))} assets")


def main():
    parser = argparse.ArgumentParser(description='Infer related assets for an event (Multi-Economy Analysis)')
    parser.add_argument('--indicator', required=True, help='Primary indicator ID')
    parser.add_argument('--direction', choices=['increase', 'decrease'], required=True, help='Price direction')
    parser.add_argument('--output', '-o', help='Output file (JSON)')
    parser.add_argument('--multi-economy', action='store_true', help='Enable multi-economy analysis (default: True)')
    parser.add_argument('--list-events', action='store_true', help='List all event types')
    parser.add_argument('--json', action='store_true', help='Output as JSON')
    
    args = parser.parse_args()
    
    if args.list_events:
        list_event_types()
        return
    
    result = infer_related_assets(args.indicator, args.direction)
    
    # ⭐ 添加多经济体摘要
    economy_summary = get_multi_economy_summary(result)
    result['economy_summary'] = economy_summary
    
    print(f"\n=== {result['event_type']} 多经济体分析 ===")
    print(f"Primary Indicator: {result['primary_indicator']} ({result['direction']})")
    
    # ⭐ 输出多经济体统计
    print(f"\n📊 多经济体影响统计:")
    for economy, stats in economy_summary.items():
        total = stats['benefited'] + stats['harmed'] + stats['macro']
        if total > 0:
            print(f"  {economy}: 受益{stats['benefited']}项 / 受损{stats['harmed']}项 / 宏观{stats['macro']}项")
    
    print(f"\n📈 受益资产 ({len(result['benefited'])}):")
    for asset in result['benefited']:
        economy_tag = f"[{asset.get('economy', 'Global')}]"
        print(f"  - {economy_tag} {asset['category']}: {asset['indicator']}")
        print(f"    Reason: {asset['reason']}")
    
    print(f"\n📉 受损资产 ({len(result['harmed'])}):")
    for asset in result['harmed']:
        economy_tag = f"[{asset.get('economy', 'Global')}]"
        print(f"  - {economy_tag} {asset['category']}: {asset['indicator']}")
        print(f"    Reason: {asset['reason']}")
    
    # ⭐ 输出宏观指标（新增）
    macro_indicators = result.get('macro_indicators', [])
    if macro_indicators:
        print(f"\n📊 宏观指标 ({len(macro_indicators)}):")
        for asset in macro_indicators:
            economy_tag = f"[{asset.get('economy', 'Global')}]"
            print(f"  - {economy_tag} {asset['category']}: {asset['indicator']}")
            print(f"    Reason: {asset['reason']}")
    
    print(f"\n📋 所有相关指标（将生成单独图表）: {get_all_related_indicators(result)}")
    print(f"   总计: {len(get_all_related_indicators(result))} 项")
    
    if args.output:
        with open(args.output, 'w') as f:
            json.dump(result, f, indent=2, ensure_ascii=False)
        print(f"\n✓ 已保存: {args.output}")
    else:
        if args.json:
            print(json.dumps(result, indent=2, ensure_ascii=False))


if __name__ == '__main__':
    main()

path: references/report_template.md
content: # 中文报告模板

## 报告结构

以下是金融事件影响分析报告的标准模板，输出时使用中文。

---

# 金融事件历史影响分析报告

## 🎯 核心结论（优先展示）

### 最重要发现

**一句话总结**: [最关键的结论，如：能源ETF在历史上100%跟随油价上涨]

### 分项结论

| 结论 | 说明 |
|------|------|
| **确定性最高** | [资产名称]: 胜率XX%，平均收益XX% |
| **风险提示** | [资产名称]: 胜率仅XX%，需谨慎 |
| **颠覆认知** | [传统认知与历史数据的差异] |

---

## 一、事件概述

**分析事件**: [事件名称]

**表征指标**: [指标名称] ([指标ID])

**当前特征**: [量化特征描述，如：近90天上涨40%]

**分析日期**: [YYYY年MM月DD日]

---

## 二、当前事件特征分析

### 近期走势概况

| 时间窗口 | 变动幅度 | 方向 |
|---------|---------|------|
| 90天 | XX% | ↑/↓ |
| 60天 | XX% | ↑/↓ |
| 30天 | XX% | ↑/↓ |

**波动率**: XX% (30日)

**事件类型判定**: [事件类型]

---

## 三、历史相似事件

共发现 N 次历史相似事件（按时间从最新到 earliest 排序）：

| 序号 | 时间区间 | 变动幅度 | 相似度 |
|-----|---------|---------|--------|
| 1（最新） | YYYY.MM.DD ~ YYYY.MM.DD | XX% | XX% |
| 2 | YYYY.MM.DD ~ YYYY.MM.DD | XX% | XX% |
| 3 | YYYY.MM.DD ~ YYYY.MM.DD | XX% | XX% |
| ... | ... | ... | ... |

---

## 四、相关资产推论

基于因果关系、上下游产业链、替代效应分析：

### 受益资产

| 资产类别 | 指标 | 推论依据 |
|---------|------|---------|
| [类别] | [指标] | [原因] |
| ... | ... | ... |

### 受损资产

| 资产类别 | 指标 | 推论依据 |
|---------|------|---------|
| [类别] | [指标] | [原因] |
| ... | ... | ... |

### 不确定/中性资产

| 资产类别 | 指标 | 推论依据 |
|---------|------|---------|
| [类别] | [指标] | [原因] |
| ... | ... | ... |

---

## 五、历史变动幅度表格（新增核心章节）

### 各历史事件期间资产变动幅度

**表格说明**: 按时间从最新到最早排列，**加粗**表示高相关性（同向变动）

| 历史事件 | 时间区间 | 表征指标变动 | [资产A] | [资产B] | [资产C] | ... |
|---------|---------|-------------|---------|---------|---------|-----|
| **事件1**（最新） | YYYY.MM.DD ~ YYYY.MM.DD | 🔴 +XX% | 🔴 **+XX%** | 🟢 -XX% | 🔴 +XX% | ... |
| **事件2** | YYYY.MM.DD ~ YYYY.MM.DD | 🔴 +XX% | 🔴 **+XX%** | 🔴 **+XX%** | 🟢 -XX% | ... |
| **事件3** | YYYY.MM.DD ~ YYYY.MM.DD | 🟢 -XX% | 🟢 **-XX%** | 🔴 +XX% | 🟢 **-XX%** | ... |
| **事件4** | YYYY.MM.DD ~ YYYY.MM.DD | 🔴 +XX% | 🔴 +XX% | 🔴 **+XX%** | 🟢 -XX% | ... |
| ... | ... | ... | ... | ... | ... | ... |

**颜色规范**:
- 🔴 红色 = 上涨 (+)
- 🟢 绿色 = 下跌 (-)
- **加粗** = 与表征指标同向变动（高相关性）

---

### 相关性统计

| 资产 | 同向变动次数 | 同向占比 | 相关性评级 |
|------|:-----------:|:-------:|:---------:|
| [资产A] | X/N | XX% | ⭐⭐⭐⭐⭐ |
| [资产B] | X/N | XX% | ⭐⭐⭐ |
| [资产C] | X/N | XX% | ⭐⭐ |
| ... | ... | ... | ... |

---

## 六、历史表现汇总

### 各资产历史表现统计

| 指标 | 平均收益 | 收益标准差 | 胜率 | 最佳表现 | 最差表现 |
|------|:-------:|:---------:|:---:|:-------:|:-------:|
| [指标A] | 🔴 XX% | XX% | XX% | XX% | XX% |
| [指标B] | 🟢 XX% | XX% | XX% | XX% | XX% |
| ... | ... | ... | ... | ... | ... |

### 各历史事件详细表现

**事件1** (YYYY.MM.DD ~ YYYY.MM.DD):

| 指标 | 收益率 | 波动率 | 最大回撤 |
|------|:------:|:------:|:-------:|
| [指标] | 🔴 XX% | XX% | XX% |
| ... | ... | ... | ... |

**事件2** (YYYY.MM.DD ~ YYYY.MM.DD):
...

---

## 七、图表展示

### 📊 双Y轴时序对比图

每类相关资产单独一张时序对比图：

| 图表 | 文件名 | 说明 |
|------|--------|------|
| [表征指标] vs [资产A] | `primary_vs_assetA.png` | 左轴表征指标，右轴关联资产 |
| [表征指标] vs [资产B] | `primary_vs_assetB.png` | 红色虚线框标记历史事件区间 |
| ... | ... | ... |

**图表规范**:
- **左Y轴**: 表征指标（如油价）
- **右Y轴**: 关联资产指标（如能源ETF）
- **红色虚线框**: 标记历史事件时间区间
- **中文标注**: 标题、轴标签、图例均为中文

---

### 📈 表现汇总图

- **平均收益对比图**: `performance_summary.png`
- **事件矩阵热力图**: `event_matrix_heatmap.png`

---

## 八、分析局限性

1. **数据限制**: [描述]
2. **历史不重复**: [警示]
3. **因果关系**: [说明]
4. **其他因素**: [其他可能影响资产表现的因素]

---

## ⚠️ 免责声明

本报告仅供学习研究使用，不构成投资建议。历史表现不代表未来结果。投资有风险，入市需谨慎。

---

🐂 [署名]

---

## 图表生成规范

### 双Y轴时序对比图

```
┌─────────────────────────────────────────────┐
│  [表征指标] vs [关联资产] 历史走势对比         │  ← 中文标题
├─────────────────────────────────────────────┤
│                                             │
│  ┌───────┐                                  │
│  │事件区间│  红色虚线框标记历史事件             │
│  └───────┘                                  │
│                                             │
│  左Y轴: [表征指标]     右Y轴: [关联资产]      │  ← 双Y轴中文标注
│  ── 表征指标曲线        ── 关联资产曲线        │
│                                             │
├─────────────────────────────────────────────┤
│  时间轴: YYYY.MM.DD                         │  ← 时间格式
└─────────────────────────────────────────────┘
```

### 颜色规范（中国习惯）

- 🔴 **红色** = 上涨 (+)
- 🟢 **绿色** = 下跌 (-)
- ⬛ **红色虚线框** = 历史事件区间

### 高亮规则

变动幅度表格中，**加粗**单元格表示：
- 与表征指标**同向变动**（都上涨或都下跌）
- 表示**高相关性**，值得重点关注

Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)