{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2cf0ca5c",
   "metadata": {},
   "outputs": [],
   "source": [
    "\"\"\"\n",
    "A 股行业 ETF 轮动策略 Demo\n",
    "A-Share Sector ETF Rotation Strategy Demo\n",
    "==========================================\n",
    "\n",
    "策略逻辑 (Strategy Logic):\n",
    "  1. 相对动量轮动 (Relative Momentum Rotation)\n",
    "     - 每月对所有行业 ETF 计算过去 N 个月的动量得分\n",
    "     - 买入得分最高的前 K 个 ETF，等权持有\n",
    "  2. 双动量策略 (Dual Momentum, Gary Antonacci)\n",
    "     - 绝对动量 (Absolute Momentum): 若某 ETF 的动量 < 0，切换至国债 ETF 避险\n",
    "     - 相对动量 (Relative Momentum): 在通过绝对动量过滤的 ETF 中，买入相对最强的\n",
    "  3. 等权基准 (Equal Weight Benchmark)\n",
    "     - 始终等权持有全部行业 ETF，不做轮动\n",
    "\n",
    "成本模拟 (Cost Simulation):\n",
    "  - 印花税 (Stamp Duty): 卖出 0.1%\n",
    "  - 佣金 (Commission): 买卖各 0.03%（万三）\n",
    "  - 滑点 (Slippage): 单边 0.05%\n",
    "  - 每月换仓合计约 0.22% 双边成本\n",
    "\n",
    "作者: GitHub Copilot (教学用途)\n",
    "数据: 合成模拟数据 (Synthetic data for demonstration)\n",
    "\"\"\"\n",
    "\n",
    "# ── 标准库 ──────────────────────────────────────────────────────────────────\n",
    "import warnings\n",
    "warnings.filterwarnings(\"ignore\")\n",
    "\n",
    "# matplotlib 必须在 pyplot 之前设置后端 (must set backend before importing pyplot)\n",
    "import matplotlib\n",
    "matplotlib.use(\"Agg\")   # 无界面后端 (headless backend)\n",
    "\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "import matplotlib.pyplot as plt\n",
    "import matplotlib.gridspec as gridspec\n",
    "from matplotlib.patches import FancyArrowPatch\n",
    "import matplotlib.ticker as mtick\n",
    "\n",
    "# 设置随机种子，保证结果可复现 (fix random seed for reproducibility)\n",
    "np.random.seed(42)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6555659",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §0  ETF 池定义 & 合成价格数据生成\n",
    "#     ETF Universe Definition & Synthetic Price Data Generation\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "\n",
    "# ── A 股行业 ETF 池 (A-Share Sector ETF Universe) ───────────────────────────\n",
    "# 这里用合成数据模拟，真实代码只需替换为 AKShare/Tushare 数据读取\n",
    "# (Synthetic data here; replace with AKShare/Tushare data in production)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "269510d5",
   "metadata": {},
   "outputs": [],
   "source": [
    "ETF_UNIVERSE = {\n",
    "    # 代码 (Code) : (中文名称, 英文名称, 年化收益均值, 年化波动率, 与市场相关性)\n",
    "    \"159995\": (\"芯片ETF\",   \"Chip ETF\",       0.18, 0.45, 0.75),\n",
    "    \"159819\": (\"人工智能ETF\",\"AI ETF\",         0.20, 0.48, 0.72),\n",
    "    \"516160\": (\"新能源ETF\", \"New Energy ETF\", 0.15, 0.42, 0.70),\n",
    "    \"159928\": (\"消费ETF\",   \"Consumer ETF\",   0.10, 0.28, 0.65),\n",
    "    \"512170\": (\"医疗ETF\",   \"Healthcare ETF\", 0.08, 0.32, 0.60),\n",
    "    \"512880\": (\"证券ETF\",   \"Securities ETF\", 0.12, 0.38, 0.80),\n",
    "    # 国债 ETF 作为避险资产 (Bond ETF as safe-haven asset)\n",
    "    \"511010\": (\"国债ETF\",   \"Bond ETF\",       0.03, 0.03, -0.10),\n",
    "}\n",
    "\n",
    "# 分离行业 ETF 和国债 ETF (separate sector ETFs from bond ETF)\n",
    "SECTOR_ETFS = [code for code in ETF_UNIVERSE if code != \"511010\"]\n",
    "BOND_ETF    = \"511010\"\n",
    "ALL_ETFS    = list(ETF_UNIVERSE.keys())\n",
    "\n",
    "# ── 生成合成价格序列 (Generate Synthetic Price Series) ───────────────────────\n",
    "# 用因子模型生成相关价格序列，模拟真实行业之间的联动关系\n",
    "# (Factor model to generate correlated price series, mimicking real sector co-movement)\n",
    "\n",
    "TRADING_DAYS = 252          # 年交易日数 (trading days per year)\n",
    "SIM_YEARS    = 6            # 模拟年数 (simulation years): 2019-2024\n",
    "N_DAYS       = TRADING_DAYS * SIM_YEARS\n",
    "\n",
    "# 市场公共因子 (market common factor) —— 模拟沪深300走势\n",
    "# 牛市/熊市分段：模拟 2019-2021 牛市，2021-2022 熊市，2023-2024 震荡\n",
    "market_factor = np.zeros(N_DAYS)\n",
    "# 第一阶段：2019-2021 牛市 (Bull market phase)\n",
    "bull1 = int(N_DAYS * 0.40)\n",
    "market_factor[:bull1] = np.random.normal(0.0008, 0.012, bull1)\n",
    "# 第二阶段：2021-2022 熊市 (Bear market phase)\n",
    "bear1 = int(N_DAYS * 0.20)\n",
    "market_factor[bull1:bull1+bear1] = np.random.normal(-0.0006, 0.018, bear1)\n",
    "# 第三阶段：2023-2024 震荡 (Sideways phase)\n",
    "market_factor[bull1+bear1:] = np.random.normal(0.0002, 0.014, N_DAYS - bull1 - bear1)\n",
    "\n",
    "# 生成每只 ETF 的日收益率 (generate daily returns for each ETF)\n",
    "daily_returns = {}\n",
    "for code, (cn_name, en_name, ann_ret, ann_vol, mkt_corr) in ETF_UNIVERSE.items():\n",
    "    daily_mu  = ann_ret / TRADING_DAYS          # 日均收益 (daily mean return)\n",
    "    daily_sig = ann_vol / np.sqrt(TRADING_DAYS) # 日波动率 (daily volatility)\n",
    "\n",
    "    # 特异性收益 (idiosyncratic return) = 总收益 - 市场部分\n",
    "    idio_sig = daily_sig * np.sqrt(1 - mkt_corr**2)\n",
    "    idio     = np.random.normal(0, idio_sig, N_DAYS)\n",
    "\n",
    "    # 总日收益 = 市场因子 × 相关系数 + 特异性收益 + 均值漂移\n",
    "    ret = mkt_corr * market_factor + idio + (daily_mu - mkt_corr * 0.0002)\n",
    "    daily_returns[code] = ret\n",
    "\n",
    "# 转为 DataFrame，并生成价格序列 (convert to DataFrame, then price series)\n",
    "start_date = pd.Timestamp(\"2019-01-02\")\n",
    "dates = pd.bdate_range(start=start_date, periods=N_DAYS)  # 仅工作日 (business days only)\n",
    "\n",
    "returns_df = pd.DataFrame(daily_returns, index=dates)\n",
    "# 价格从 1.0 开始（净值形式，Net Asset Value style）\n",
    "price_df = (1 + returns_df).cumprod()\n",
    "\n",
    "print(\"═\" * 60)\n",
    "print(\"§0  ETF 价格数据生成完成 (Price Data Generated)\")\n",
    "print(\"═\" * 60)\n",
    "print(f\"  模拟区间 (Simulation Period): {dates[0].date()} → {dates[-1].date()}\")\n",
    "print(f\"  交易日数 (Trading Days):      {N_DAYS}\")\n",
    "print(f\"  ETF 数量 (Number of ETFs):    {len(ALL_ETFS)}\")\n",
    "print()\n",
    "print(\"  ETF 池 (Universe):\")\n",
    "print(f\"  {'代码':10s} {'中文名':12s} {'英文名':18s} {'年化收益':8s} {'年化波动':8s}\")\n",
    "print(\"  \" + \"-\" * 60)\n",
    "for code, (cn, en, ret, vol, _) in ETF_UNIVERSE.items():\n",
    "    label = \" [避险]\" if code == BOND_ETF else \"\"\n",
    "    print(f\"  {code:10s} {cn:12s} {en:18s} {ret*100:6.1f}%   {vol*100:6.1f}%{label}\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e083547",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §1  动量因子计算\n",
    "#     Momentum Factor Calculation\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e01ab734",
   "metadata": {},
   "outputs": [],
   "source": [
    "def calc_momentum(price_df: pd.DataFrame,\n",
    "                  lookback_months: int = 12,\n",
    "                  skip_months: int = 1) -> pd.DataFrame:\n",
    "    \"\"\"\n",
    "    计算每只 ETF 的动量得分 (Calculate momentum score for each ETF)\n",
    "\n",
    "    经典动量定义 (Classic momentum definition):\n",
    "      过去 L 个月的总收益，跳过最近 S 个月（避免短期反转）\n",
    "      Total return over past L months, skipping most recent S months\n",
    "      (to avoid short-term reversal effect 短期反转效应)\n",
    "\n",
    "    参数 (Parameters):\n",
    "      price_df        : 日频价格 DataFrame (daily price DataFrame)\n",
    "      lookback_months : 回望期（月数）(lookback period in months)\n",
    "      skip_months     : 跳过最近月数（通常为 1）(skip recent months, usually 1)\n",
    "\n",
    "    返回 (Returns):\n",
    "      月频动量得分 DataFrame (monthly momentum score DataFrame)\n",
    "    \"\"\"\n",
    "    # 转为月末价格 (resample to month-end prices)\n",
    "    # 注意：旧版 pandas 用 \"M\"，新版用 \"ME\"\n",
    "    try:\n",
    "        monthly_price = price_df.resample(\"ME\").last()\n",
    "    except ValueError:\n",
    "        monthly_price = price_df.resample(\"M\").last()\n",
    "\n",
    "    # 动量 = 前 (lookback+skip) 月价格 / 前 skip 月价格 - 1\n",
    "    # Momentum = Price[t - skip] / Price[t - lookback - skip] - 1\n",
    "    start_lag = skip_months\n",
    "    end_lag   = lookback_months + skip_months\n",
    "\n",
    "    momentum = monthly_price.shift(start_lag) / monthly_price.shift(end_lag) - 1\n",
    "    return momentum\n",
    "\n",
    "\n",
    "# 计算三种不同回望期的动量（稳健性检验用）\n",
    "# (Calculate momentum with three different lookback windows for robustness)\n",
    "mom_12_1 = calc_momentum(price_df, lookback_months=12, skip_months=1)  # 经典 12-1 月动量\n",
    "mom_6_1  = calc_momentum(price_df, lookback_months=6,  skip_months=1)  # 中期 6-1 月动量\n",
    "mom_3_1  = calc_momentum(price_df, lookback_months=3,  skip_months=1)  # 短期 3-1 月动量\n",
    "\n",
    "# 合成动量得分：三个窗口等权平均（减少单一窗口过拟合风险）\n",
    "# (Composite momentum score: equal-weight average of three windows)\n",
    "mom_composite = (mom_12_1 + mom_6_1 + mom_3_1) / 3\n",
    "\n",
    "print(\"§1  动量因子计算完成 (Momentum Factors Calculated)\")\n",
    "print(f\"    月度数据行数 (Monthly rows): {len(mom_12_1)}\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a098f350",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §2  A 股交易成本模型\n",
    "#     A-Share Transaction Cost Model\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c18a15bd",
   "metadata": {},
   "outputs": [],
   "source": [
    "class AShareCostModel:\n",
    "    \"\"\"\n",
    "    A 股 ETF 交易成本模型 (A-Share ETF Transaction Cost Model)\n",
    "\n",
    "    A 股 ETF 特殊规则 (A-Share ETF Special Rules):\n",
    "      - ETF 可 T+0 交易（当天买当天可卖）Unlike 个股的 T+1 限制\n",
    "        (ETF allows T+0 trading, unlike individual stocks which are T+1)\n",
    "      - 印花税：仅卖出方向收取 0.1%\n",
    "        (Stamp duty: 0.1% on SELL side only)\n",
    "      - 佣金：买卖双向，通常 0.02% - 0.03%\n",
    "        (Commission: both sides, typically 0.02%-0.03%)\n",
    "      - 滑点：ETF 流动性好，通常 0.02%-0.05%\n",
    "        (Slippage: ETF has good liquidity, typically 0.02%-0.05%)\n",
    "    \"\"\"\n",
    "    def __init__(self,\n",
    "                 commission_rate: float = 0.0003,   # 佣金率 (commission rate)\n",
    "                 stamp_duty_rate: float = 0.001,    # 印花税率 (stamp duty rate)\n",
    "                 slippage_rate:   float = 0.0005):  # 滑点率 (slippage rate)\n",
    "        self.commission = commission_rate\n",
    "        self.stamp_duty = stamp_duty_rate\n",
    "        self.slippage   = slippage_rate\n",
    "\n",
    "    def buy_cost(self, trade_value: float) -> float:\n",
    "        \"\"\"买入成本 (buy-side cost): 佣金 + 滑点\"\"\"\n",
    "        return trade_value * (self.commission + self.slippage)\n",
    "\n",
    "    def sell_cost(self, trade_value: float) -> float:\n",
    "        \"\"\"卖出成本 (sell-side cost): 佣金 + 印花税 + 滑点\"\"\"\n",
    "        return trade_value * (self.commission + self.stamp_duty + self.slippage)\n",
    "\n",
    "    def roundtrip_cost(self, trade_value: float) -> float:\n",
    "        \"\"\"\n",
    "        双边成本 (round-trip cost): 买入 + 卖出\n",
    "        月度换仓一次的总成本约 0.22%\n",
    "        \"\"\"\n",
    "        return self.buy_cost(trade_value) + self.sell_cost(trade_value)\n",
    "\n",
    "    @property\n",
    "    def roundtrip_rate(self) -> float:\n",
    "        \"\"\"双边成本率 (round-trip cost rate)\"\"\"\n",
    "        return 2 * self.commission + self.stamp_duty + 2 * self.slippage\n",
    "\n",
    "\n",
    "cost_model = AShareCostModel()\n",
    "print(\"§2  交易成本模型 (Transaction Cost Model):\")\n",
    "print(f\"    佣金率 (Commission):   {cost_model.commission*100:.3f}% × 双边\")\n",
    "print(f\"    印花税 (Stamp Duty):   {cost_model.stamp_duty*100:.3f}% × 卖出方向\")\n",
    "print(f\"    滑点率 (Slippage):     {cost_model.slippage*100:.3f}% × 双边\")\n",
    "print(f\"    双边总成本 (Roundtrip): {cost_model.roundtrip_rate*100:.3f}%\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac6f0f41",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §3  轮动回测引擎\n",
    "#     Rotation Backtest Engine\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c6148ff2",
   "metadata": {},
   "outputs": [],
   "source": [
    "def run_rotation_backtest(\n",
    "    price_df:       pd.DataFrame,\n",
    "    momentum_df:    pd.DataFrame,\n",
    "    strategy:       str = \"relative_momentum\",\n",
    "    top_k:          int = 3,\n",
    "    cost_model:     AShareCostModel = None,\n",
    "    bond_etf:       str = BOND_ETF,\n",
    "    sector_etfs:    list = None,\n",
    ") -> dict:\n",
    "    \"\"\"\n",
    "    行业 ETF 轮动回测引擎 (Sector ETF Rotation Backtest Engine)\n",
    "\n",
    "    参数 (Parameters):\n",
    "      price_df    : 日频价格 (daily prices)\n",
    "      momentum_df : 月频动量得分 (monthly momentum scores)\n",
    "      strategy    : 策略名称 (strategy name)\n",
    "                    \"relative_momentum\" — 相对动量轮动\n",
    "                    \"dual_momentum\"     — 双动量（含绝对动量过滤）\n",
    "                    \"equal_weight\"      — 等权基准\n",
    "      top_k       : 持有前 K 只 ETF (hold top-K ETFs)\n",
    "      cost_model  : 交易成本模型 (transaction cost model)\n",
    "      bond_etf    : 避险 ETF 代码 (safe-haven ETF code)\n",
    "      sector_etfs : 行业 ETF 代码列表 (sector ETF codes)\n",
    "\n",
    "    返回 (Returns):\n",
    "      dict 包含:\n",
    "        \"nav\"         : 每日净值序列 (daily NAV series)\n",
    "        \"weights\"     : 月度持仓权重 (monthly weights)\n",
    "        \"turnover\"    : 月度换手率 (monthly turnover)\n",
    "        \"cost_total\"  : 总交易成本 (total transaction cost)\n",
    "        \"holdings_log\": 持仓记录 (holdings log)\n",
    "    \"\"\"\n",
    "    if sector_etfs is None:\n",
    "        sector_etfs = SECTOR_ETFS\n",
    "    if cost_model is None:\n",
    "        cost_model = AShareCostModel()\n",
    "\n",
    "    # 获取月末再平衡日期 (get month-end rebalancing dates)\n",
    "    try:\n",
    "        monthly_idx = price_df.resample(\"ME\").last().index\n",
    "    except ValueError:\n",
    "        monthly_idx = price_df.resample(\"M\").last().index\n",
    "\n",
    "    # 预热期：动量需要至少 13 个月数据（12 个月回望 + 1 个月跳过）\n",
    "    # (Warm-up period: momentum needs at least 13 months of data)\n",
    "    warmup = 13\n",
    "\n",
    "    # 初始化 (initialization)\n",
    "    nav           = pd.Series(index=price_df.index, dtype=float)\n",
    "    weights_log   = {}   # 每月权重记录 (monthly weights log)\n",
    "    turnover_log  = {}   # 每月换手率记录 (monthly turnover log)\n",
    "    holdings_log  = {}   # 每月持仓记录 (monthly holdings log)\n",
    "    total_cost    = 0.0  # 累计交易成本 (cumulative transaction cost)\n",
    "\n",
    "    current_weights = {}  # 当前持仓权重 {code: weight}\n",
    "    portfolio_value = 1.0 # 组合净值，从 1.0 开始 (portfolio starts at 1.0)\n",
    "\n",
    "    rebal_dates = monthly_idx[warmup:]  # 有效再平衡日期 (valid rebalance dates)\n",
    "\n",
    "    for i, rebal_date in enumerate(rebal_dates):\n",
    "        # ── 1. 确定目标权重 (Determine target weights) ──────────────────────\n",
    "\n",
    "        if strategy == \"equal_weight\":\n",
    "            # 等权：始终持有所有行业 ETF，各占 1/N\n",
    "            # (Equal weight: always hold all sector ETFs equally)\n",
    "            n = len(sector_etfs)\n",
    "            target_weights = {code: 1.0 / n for code in sector_etfs}\n",
    "\n",
    "        elif strategy == \"relative_momentum\":\n",
    "            # 相对动量：选动量得分最高的前 K 只行业 ETF\n",
    "            # (Relative momentum: select top-K sector ETFs by momentum score)\n",
    "            mom_today = momentum_df.loc[:rebal_date, sector_etfs].iloc[-1]\n",
    "            mom_today = mom_today.dropna()\n",
    "            if len(mom_today) == 0:\n",
    "                target_weights = current_weights or {sector_etfs[0]: 1.0}\n",
    "            else:\n",
    "                top_etfs = mom_today.nlargest(top_k).index.tolist()\n",
    "                w = 1.0 / len(top_etfs)\n",
    "                target_weights = {code: w for code in top_etfs}\n",
    "\n",
    "        elif strategy == \"dual_momentum\":\n",
    "            # 双动量：先用绝对动量过滤，再做相对动量排序\n",
    "            # (Dual momentum: filter by absolute momentum, then rank by relative)\n",
    "            mom_today = momentum_df.loc[:rebal_date, sector_etfs].iloc[-1]\n",
    "            mom_today = mom_today.dropna()\n",
    "\n",
    "            # 绝对动量过滤 (absolute momentum filter):\n",
    "            # 如果行业 ETF 动量 < 0（跑输无风险），则替换为国债 ETF\n",
    "            # (If sector ETF momentum < 0, replace with bond ETF)\n",
    "            positive_etfs = mom_today[mom_today > 0].index.tolist()\n",
    "\n",
    "            if len(positive_etfs) == 0:\n",
    "                # 全部动量为负：100% 持有国债 ETF 避险\n",
    "                # (All negative momentum: 100% in bond ETF)\n",
    "                target_weights = {bond_etf: 1.0}\n",
    "            else:\n",
    "                # 从通过绝对动量过滤的 ETF 中，选相对最强的前 K 只\n",
    "                # (From ETFs passing absolute momentum filter, pick top-K by relative)\n",
    "                k_actual = min(top_k, len(positive_etfs))\n",
    "                top_etfs = mom_today[positive_etfs].nlargest(k_actual).index.tolist()\n",
    "                w = 1.0 / k_actual\n",
    "                target_weights = {code: w for code in top_etfs}\n",
    "        else:\n",
    "            raise ValueError(f\"未知策略 (Unknown strategy): {strategy}\")\n",
    "\n",
    "        # ── 2. 计算换手率并扣除交易成本 (Calculate turnover and deduct costs) ──\n",
    "\n",
    "        # 所有涉及的 ETF 代码（新旧持仓并集）\n",
    "        all_codes = set(current_weights) | set(target_weights)\n",
    "        turnover = 0.0\n",
    "\n",
    "        for code in all_codes:\n",
    "            old_w = current_weights.get(code, 0.0)\n",
    "            new_w = target_weights.get(code, 0.0)\n",
    "            delta = abs(new_w - old_w)   # 权重变化 (weight change)\n",
    "            if delta > 1e-6:\n",
    "                turnover += delta\n",
    "                # 对权重变化部分计算成本 (cost on the traded portion)\n",
    "                trade_value = delta * portfolio_value\n",
    "                if new_w > old_w:\n",
    "                    # 增仓 → 买入成本 (increase position → buy cost)\n",
    "                    cost = cost_model.buy_cost(trade_value)\n",
    "                else:\n",
    "                    # 减仓 → 卖出成本（含印花税）\n",
    "                    # (decrease position → sell cost including stamp duty)\n",
    "                    cost = cost_model.sell_cost(trade_value)\n",
    "                total_cost += cost\n",
    "                portfolio_value -= cost   # 成本从组合价值中扣除 (deduct cost from portfolio)\n",
    "\n",
    "        # 换手率（单边）= 买入总金额 / 组合价值 (one-sided turnover)\n",
    "        turnover_log[rebal_date] = turnover / 2  # 双边换手/2 = 单边换手\n",
    "\n",
    "        # ── 3. 更新持仓，计算到下一个再平衡日的收益\n",
    "        #       (Update holdings, compute returns until next rebalance date)\n",
    "        current_weights = target_weights\n",
    "        weights_log[rebal_date] = target_weights.copy()\n",
    "        holdings_log[rebal_date] = list(target_weights.keys())\n",
    "\n",
    "        # 确定持仓持续的日期区间 (determine holding period date range)\n",
    "        if i + 1 < len(rebal_dates):\n",
    "            next_rebal = rebal_dates[i + 1]\n",
    "        else:\n",
    "            next_rebal = price_df.index[-1]\n",
    "\n",
    "        hold_dates = price_df.loc[rebal_date:next_rebal].index\n",
    "\n",
    "        # 计算每日组合收益率 (calculate daily portfolio return)\n",
    "        for j in range(1, len(hold_dates)):\n",
    "            d_prev = hold_dates[j - 1]\n",
    "            d_curr = hold_dates[j]\n",
    "            daily_port_ret = 0.0\n",
    "            for code, w in current_weights.items():\n",
    "                if code in price_df.columns:\n",
    "                    p_prev = price_df.loc[d_prev, code]\n",
    "                    p_curr = price_df.loc[d_curr, code]\n",
    "                    if p_prev > 0:\n",
    "                        daily_port_ret += w * (p_curr / p_prev - 1)\n",
    "            portfolio_value *= (1 + daily_port_ret)\n",
    "            nav[d_curr] = portfolio_value\n",
    "\n",
    "    # 补全初始 NAV（热身期）\n",
    "    nav = nav.ffill()\n",
    "    first_valid = nav.first_valid_index()\n",
    "    if first_valid is not None:\n",
    "        nav[:first_valid] = 1.0\n",
    "    nav = nav.fillna(1.0)\n",
    "\n",
    "    return {\n",
    "        \"nav\":          nav,\n",
    "        \"weights\":      weights_log,\n",
    "        \"turnover\":     pd.Series(turnover_log),\n",
    "        \"cost_total\":   total_cost,\n",
    "        \"holdings_log\": holdings_log,\n",
    "    }"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "12233425",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §4  运行三种策略并计算绩效指标\n",
    "#     Run Three Strategies and Calculate Performance Metrics\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5def8b15",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"§4  运行回测策略 (Running Backtest Strategies)...\")\n",
    "print()\n",
    "\n",
    "# 运行三种策略 (run three strategies)\n",
    "results = {}\n",
    "\n",
    "results[\"equal_weight\"] = run_rotation_backtest(\n",
    "    price_df, mom_composite,\n",
    "    strategy=\"equal_weight\",\n",
    "    top_k=3, cost_model=cost_model,\n",
    ")\n",
    "\n",
    "results[\"relative_momentum\"] = run_rotation_backtest(\n",
    "    price_df, mom_composite,\n",
    "    strategy=\"relative_momentum\",\n",
    "    top_k=3, cost_model=cost_model,\n",
    ")\n",
    "\n",
    "results[\"dual_momentum\"] = run_rotation_backtest(\n",
    "    price_df, mom_composite,\n",
    "    strategy=\"dual_momentum\",\n",
    "    top_k=3, cost_model=cost_model,\n",
    ")\n",
    "\n",
    "# 同时计算国债 ETF 纯避险基准 (bond ETF as pure safe-haven benchmark)\n",
    "bond_nav = price_df[BOND_ETF] / price_df[BOND_ETF].iloc[0]\n",
    "results[\"bond_only\"] = {\"nav\": bond_nav}\n",
    "\n",
    "# ── 绩效计算函数 (Performance Calculation Function) ─────────────────────────\n",
    "\n",
    "def calc_performance(nav: pd.Series,\n",
    "                     risk_free_annual: float = 0.02) -> dict:\n",
    "    \"\"\"\n",
    "    计算常用量化绩效指标 (Calculate common quantitative performance metrics)\n",
    "\n",
    "    指标 (Metrics):\n",
    "      - 年化收益率 (Annualized Return, CAGR)\n",
    "      - 年化波动率 (Annualized Volatility)\n",
    "      - 夏普比率 (Sharpe Ratio): 超额收益 / 波动率\n",
    "      - 最大回撤 (Maximum Drawdown, MaxDD): 从历史最高点的最大跌幅\n",
    "      - 卡玛比率 (Calmar Ratio): 年化收益 / 最大回撤\n",
    "      - 索提诺比率 (Sortino Ratio): 用下行波动率替代总波动率\n",
    "    \"\"\"\n",
    "    nav = nav.dropna()\n",
    "    if len(nav) < 2:\n",
    "        return {}\n",
    "\n",
    "    # 日收益率 (daily returns)\n",
    "    daily_ret = nav.pct_change().dropna()\n",
    "\n",
    "    # 年化收益率 (CAGR - Compound Annual Growth Rate 复合年增长率)\n",
    "    n_years = len(daily_ret) / TRADING_DAYS\n",
    "    total_ret = nav.iloc[-1] / nav.iloc[0] - 1\n",
    "    cagr = (1 + total_ret) ** (1 / n_years) - 1\n",
    "\n",
    "    # 年化波动率 (annualized volatility)\n",
    "    ann_vol = daily_ret.std() * np.sqrt(TRADING_DAYS)\n",
    "\n",
    "    # 夏普比率 (Sharpe Ratio)\n",
    "    # 公式: (年化收益 - 无风险利率) / 年化波动率\n",
    "    rf_daily = risk_free_annual / TRADING_DAYS\n",
    "    sharpe = (daily_ret.mean() - rf_daily) / daily_ret.std() * np.sqrt(TRADING_DAYS)\n",
    "\n",
    "    # 最大回撤 (Maximum Drawdown)\n",
    "    # 定义: max(累计最高净值 - 当日净值) / 累计最高净值\n",
    "    rolling_max = nav.cummax()           # 历史最高净值 (rolling maximum)\n",
    "    drawdown    = nav / rolling_max - 1  # 每日回撤序列 (daily drawdown series)\n",
    "    max_dd      = drawdown.min()         # 最大回撤（负数）(max drawdown, negative)\n",
    "\n",
    "    # 卡玛比率 (Calmar Ratio = CAGR / |MaxDD|)\n",
    "    calmar = cagr / abs(max_dd) if max_dd != 0 else np.nan\n",
    "\n",
    "    # 索提诺比率 (Sortino Ratio): 只惩罚下行波动 (penalizes only downside volatility)\n",
    "    downside_ret = daily_ret[daily_ret < rf_daily]\n",
    "    downside_vol = downside_ret.std() * np.sqrt(TRADING_DAYS) if len(downside_ret) > 0 else ann_vol\n",
    "    sortino = (cagr - risk_free_annual) / downside_vol if downside_vol > 0 else np.nan\n",
    "\n",
    "    # 胜率 (Win Rate): 月度正收益比例 (fraction of months with positive return)\n",
    "    try:\n",
    "        monthly_ret = nav.resample(\"ME\").last().pct_change().dropna()\n",
    "    except ValueError:\n",
    "        monthly_ret = nav.resample(\"M\").last().pct_change().dropna()\n",
    "    win_rate = (monthly_ret > 0).mean()\n",
    "\n",
    "    return {\n",
    "        \"年化收益率 (CAGR)\":           cagr,\n",
    "        \"年化波动率 (Volatility)\":      ann_vol,\n",
    "        \"夏普比率 (Sharpe)\":           sharpe,\n",
    "        \"最大回撤 (MaxDrawdown)\":       max_dd,\n",
    "        \"卡玛比率 (Calmar)\":           calmar,\n",
    "        \"索提诺比率 (Sortino)\":         sortino,\n",
    "        \"月度胜率 (Win Rate)\":          win_rate,\n",
    "        \"累计收益率 (Total Return)\":    total_ret,\n",
    "    }\n",
    "\n",
    "\n",
    "# ── 打印绩效对比表 (Print Performance Comparison Table) ─────────────────────\n",
    "\n",
    "strategy_labels = {\n",
    "    \"equal_weight\":        \"等权基准 (EW Benchmark)\",\n",
    "    \"relative_momentum\":   \"相对动量 (Rel Momentum)\",\n",
    "    \"dual_momentum\":       \"双动量 (Dual Momentum)\",\n",
    "    \"bond_only\":           \"国债避险 (Bond Only)\",\n",
    "}\n",
    "\n",
    "print(\"=\" * 72)\n",
    "print(\"§4  策略绩效对比 (Strategy Performance Comparison)\")\n",
    "print(\"=\" * 72)\n",
    "\n",
    "perf_all = {}\n",
    "metrics_display = [\n",
    "    (\"年化收益率 (CAGR)\",        \"{:>8.2%}\"),\n",
    "    (\"年化波动率 (Volatility)\",   \"{:>8.2%}\"),\n",
    "    (\"夏普比率 (Sharpe)\",        \"{:>8.3f}\"),\n",
    "    (\"最大回撤 (MaxDrawdown)\",    \"{:>8.2%}\"),\n",
    "    (\"卡玛比率 (Calmar)\",        \"{:>8.3f}\"),\n",
    "    (\"月度胜率 (Win Rate)\",       \"{:>8.2%}\"),\n",
    "    (\"累计收益率 (Total Return)\", \"{:>8.2%}\"),\n",
    "]\n",
    "\n",
    "for key, label in strategy_labels.items():\n",
    "    nav = results[key][\"nav\"]\n",
    "    perf = calc_performance(nav)\n",
    "    perf_all[key] = perf\n",
    "\n",
    "header = f\"  {'指标':26s}\"\n",
    "for label in strategy_labels.values():\n",
    "    header += f\"  {label[:20]:>20s}\"\n",
    "print(header)\n",
    "print(\"  \" + \"-\" * 90)\n",
    "\n",
    "for metric, fmt in metrics_display:\n",
    "    row = f\"  {metric:26s}\"\n",
    "    for key in strategy_labels:\n",
    "        val = perf_all[key].get(metric, np.nan)\n",
    "        row += \"  \" + fmt.format(val).rjust(20)\n",
    "    print(row)\n",
    "\n",
    "print()\n",
    "# 打印成本统计\n",
    "for key in [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]:\n",
    "    cost = results[key].get(\"cost_total\", 0)\n",
    "    to   = results[key].get(\"turnover\", pd.Series()).mean()\n",
    "    print(f\"  [{strategy_labels[key][:16]}]  \"\n",
    "          f\"总成本={cost*100:.3f}%净值  平均月换手={to*100:.1f}%\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45bf4a54",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §5  持仓分析：统计各行业 ETF 的被选中频次\n",
    "#     Holdings Analysis: Count How Often Each Sector ETF Was Selected\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "19761832",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"§5  持仓分析 (Holdings Analysis):\")\n",
    "print()\n",
    "\n",
    "for strategy_key in [\"relative_momentum\", \"dual_momentum\"]:\n",
    "    holdings_log = results[strategy_key][\"holdings_log\"]\n",
    "    count = {code: 0 for code in ALL_ETFS}\n",
    "\n",
    "    for date, held in holdings_log.items():\n",
    "        for code in held:\n",
    "            count[code] = count.get(code, 0) + 1\n",
    "\n",
    "    total_months = len(holdings_log)\n",
    "    label = strategy_labels[strategy_key]\n",
    "    print(f\"  [{label}]  共 {total_months} 次再平衡:\")\n",
    "\n",
    "    # 按持有频次降序排列 (sort by holding frequency descending)\n",
    "    sorted_count = sorted(count.items(), key=lambda x: x[1], reverse=True)\n",
    "    for code, cnt in sorted_count:\n",
    "        if cnt > 0:\n",
    "            cn_name = ETF_UNIVERSE[code][0]\n",
    "            pct = cnt / total_months\n",
    "            bar = \"█\" * int(pct * 20)\n",
    "            print(f\"    {code}  {cn_name:10s}  {cnt:3d}次  {pct:5.1%}  {bar}\")\n",
    "    print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "659ce25c",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §6  可视化：9 宫格图表\n",
    "#     Visualization: 9-Panel Chart\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "da21c149",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"§6  生成可视化图表 (Generating Charts)...\")\n",
    "\n",
    "# 配置中文字体 (configure Chinese font)\n",
    "plt.rcParams[\"font.sans-serif\"] = [\"WenQuanYi Zen Hei\", \"Arial Unicode MS\",\n",
    "                                   \"SimHei\", \"DejaVu Sans\"]\n",
    "plt.rcParams[\"axes.unicode_minus\"] = False\n",
    "\n",
    "# 颜色方案 (color scheme)\n",
    "COLORS = {\n",
    "    \"equal_weight\":      \"#95a5a6\",   # 灰色（基准）\n",
    "    \"relative_momentum\": \"#2ecc71\",   # 绿色\n",
    "    \"dual_momentum\":     \"#e74c3c\",   # 红色（主策略）\n",
    "    \"bond_only\":         \"#3498db\",   # 蓝色（国债）\n",
    "}\n",
    "LABELS = {\n",
    "    \"equal_weight\":      \"等权基准\",\n",
    "    \"relative_momentum\": \"相对动量\",\n",
    "    \"dual_momentum\":     \"双动量\",\n",
    "    \"bond_only\":         \"国债\",\n",
    "}\n",
    "\n",
    "fig = plt.figure(figsize=(20, 16), facecolor=\"#1a1a2e\")\n",
    "fig.suptitle(\n",
    "    \"A 股行业 ETF 轮动策略回测  (A-Share Sector ETF Rotation Backtest)\\n\"\n",
    "    \"合成模拟数据 2019-2024  (Synthetic Data 2019-2024)\",\n",
    "    fontsize=16, color=\"white\", fontweight=\"bold\", y=0.98\n",
    ")\n",
    "\n",
    "gs = gridspec.GridSpec(3, 3, figure=fig,\n",
    "                       hspace=0.42, wspace=0.35,\n",
    "                       left=0.07, right=0.97, top=0.93, bottom=0.06)\n",
    "\n",
    "ax_style = dict(facecolor=\"#16213e\", labelcolor=\"white\",\n",
    "                tick_params=dict(colors=\"white\"))\n",
    "\n",
    "def style_ax(ax, title, xlabel=\"\", ylabel=\"\"):\n",
    "    ax.set_facecolor(\"#16213e\")\n",
    "    ax.set_title(title, color=\"white\", fontsize=10, fontweight=\"bold\", pad=6)\n",
    "    ax.set_xlabel(xlabel, color=\"#aaaaaa\", fontsize=8)\n",
    "    ax.set_ylabel(ylabel, color=\"#aaaaaa\", fontsize=8)\n",
    "    ax.tick_params(colors=\"white\", labelsize=7)\n",
    "    ax.spines[:].set_color(\"#444466\")\n",
    "    ax.grid(True, alpha=0.25, color=\"#666699\", linewidth=0.5)\n",
    "\n",
    "# ── 图1: 累计净值曲线 (Cumulative NAV) ───────────────────────────────────────\n",
    "ax1 = fig.add_subplot(gs[0, :2])   # 跨两列（宽幅）\n",
    "style_ax(ax1, \"① 累计净值对比  Cumulative NAV Comparison\", ylabel=\"净值 NAV\")\n",
    "for key, color in COLORS.items():\n",
    "    nav = results[key][\"nav\"]\n",
    "    ax1.plot(nav.index, nav.values, color=color, linewidth=1.8,\n",
    "             label=LABELS[key], alpha=0.9)\n",
    "ax1.axhline(1.0, color=\"#888888\", linewidth=0.8, linestyle=\"--\", alpha=0.6)\n",
    "ax1.legend(loc=\"upper left\", fontsize=8, facecolor=\"#1a1a2e\",\n",
    "           labelcolor=\"white\", framealpha=0.7)\n",
    "ax1.yaxis.set_major_formatter(mtick.FuncFormatter(lambda x, _: f\"{x:.1f}x\"))\n",
    "\n",
    "# ── 图2: 绩效雷达图 (Performance Radar) ─────────────────────────────────────\n",
    "ax2 = fig.add_subplot(gs[0, 2], projection=\"polar\")\n",
    "ax2.set_facecolor(\"#16213e\")\n",
    "ax2.set_title(\"② 绩效雷达图  Performance Radar\",\n",
    "              color=\"white\", fontsize=10, fontweight=\"bold\", pad=15)\n",
    "ax2.tick_params(colors=\"white\", labelsize=7)\n",
    "ax2.spines[\"polar\"].set_color(\"#444466\")\n",
    "\n",
    "radar_metrics = [\"年化收益\", \"夏普比率\", \"月胜率\", \"抗回撤\"]\n",
    "radar_keys    = [\"年化收益率 (CAGR)\", \"夏普比率 (Sharpe)\",\n",
    "                 \"月度胜率 (Win Rate)\", \"卡玛比率 (Calmar)\"]\n",
    "n_radar       = len(radar_metrics)\n",
    "angles        = np.linspace(0, 2 * np.pi, n_radar, endpoint=False).tolist()\n",
    "angles       += angles[:1]\n",
    "\n",
    "# 归一化各指标到 0-1 (normalize metrics to 0-1)\n",
    "def normalize_radar(values, min_val=0.0, max_val=1.0):\n",
    "    rng = max_val - min_val\n",
    "    return [(v - min_val) / rng if rng > 0 else 0.5 for v in values]\n",
    "\n",
    "radar_strats = [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]\n",
    "for rkey in radar_strats:\n",
    "    perf = perf_all[rkey]\n",
    "    raw  = [perf.get(m, 0) for m in radar_keys]\n",
    "    # 简单归一化（基于各策略范围）\n",
    "    vals = [\n",
    "        min(max(raw[0] / 0.30, 0), 1),   # CAGR: 0~30%\n",
    "        min(max(raw[1] / 3.0,  0), 1),   # Sharpe: 0~3\n",
    "        min(max(raw[2],        0), 1),   # Win rate: 0~1\n",
    "        min(max(raw[3] / 3.0,  0), 1),   # Calmar: 0~3\n",
    "    ]\n",
    "    vals += vals[:1]\n",
    "    ax2.plot(angles, vals, color=COLORS[rkey], linewidth=2, label=LABELS[rkey])\n",
    "    ax2.fill(angles, vals, color=COLORS[rkey], alpha=0.15)\n",
    "\n",
    "ax2.set_xticks(angles[:-1])\n",
    "ax2.set_xticklabels(radar_metrics, color=\"white\", fontsize=8)\n",
    "ax2.set_yticklabels([])\n",
    "ax2.legend(loc=\"upper right\", bbox_to_anchor=(1.35, 1.15),\n",
    "           fontsize=7, facecolor=\"#1a1a2e\", labelcolor=\"white\")\n",
    "\n",
    "# ── 图3: 最大回撤曲线 (Drawdown Curve) ──────────────────────────────────────\n",
    "ax3 = fig.add_subplot(gs[1, :2])\n",
    "style_ax(ax3, \"③ 水下曲线（回撤）  Underwater Chart (Drawdown)\", ylabel=\"回撤 Drawdown\")\n",
    "for key, color in COLORS.items():\n",
    "    nav = results[key][\"nav\"]\n",
    "    dd  = nav / nav.cummax() - 1\n",
    "    ax3.fill_between(dd.index, dd.values, 0, color=color, alpha=0.4, label=LABELS[key])\n",
    "    ax3.plot(dd.index, dd.values, color=color, linewidth=0.8, alpha=0.8)\n",
    "ax3.set_ylim(-0.65, 0.05)\n",
    "ax3.yaxis.set_major_formatter(mtick.PercentFormatter(1.0))\n",
    "ax3.legend(loc=\"lower left\", fontsize=8, facecolor=\"#1a1a2e\",\n",
    "           labelcolor=\"white\", framealpha=0.7)\n",
    "\n",
    "# ── 图4: 绩效指标条形图 (Performance Bar Chart) ──────────────────────────────\n",
    "ax4 = fig.add_subplot(gs[1, 2])\n",
    "style_ax(ax4, \"④ 夏普 & 卡玛  Sharpe & Calmar\", ylabel=\"比率 Ratio\")\n",
    "strats_bar  = [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]\n",
    "labels_bar  = [LABELS[k] for k in strats_bar]\n",
    "sharpes     = [perf_all[k][\"夏普比率 (Sharpe)\"] for k in strats_bar]\n",
    "calmars     = [perf_all[k][\"卡玛比率 (Calmar)\"] for k in strats_bar]\n",
    "x_bar       = np.arange(len(strats_bar))\n",
    "w_bar       = 0.35\n",
    "bars1 = ax4.bar(x_bar - w_bar/2, sharpes, w_bar, color=\"#2ecc71\", alpha=0.8,\n",
    "                label=\"夏普 Sharpe\")\n",
    "bars2 = ax4.bar(x_bar + w_bar/2, calmars, w_bar, color=\"#e74c3c\", alpha=0.8,\n",
    "                label=\"卡玛 Calmar\")\n",
    "ax4.set_xticks(x_bar)\n",
    "ax4.set_xticklabels(labels_bar, fontsize=7, color=\"white\")\n",
    "ax4.legend(fontsize=8, facecolor=\"#1a1a2e\", labelcolor=\"white\")\n",
    "for bar in bars1:\n",
    "    ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,\n",
    "             f\"{bar.get_height():.2f}\", ha=\"center\", va=\"bottom\",\n",
    "             color=\"white\", fontsize=7)\n",
    "for bar in bars2:\n",
    "    ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,\n",
    "             f\"{bar.get_height():.2f}\", ha=\"center\", va=\"bottom\",\n",
    "             color=\"white\", fontsize=7)\n",
    "\n",
    "# ── 图5: 双动量月度持仓热力图 (Dual Momentum Monthly Holdings Heatmap) ────────\n",
    "ax5 = fig.add_subplot(gs[2, :2])\n",
    "style_ax(ax5, \"⑤ 双动量月度持仓热力图  Dual Momentum Monthly Holdings\")\n",
    "\n",
    "holdings_log = results[\"dual_momentum\"][\"holdings_log\"]\n",
    "all_codes_sorted = SECTOR_ETFS + [BOND_ETF]\n",
    "etf_names = [ETF_UNIVERSE[c][0] for c in all_codes_sorted]\n",
    "\n",
    "heatmap_dates = sorted(holdings_log.keys())\n",
    "heatmap_data  = np.zeros((len(all_codes_sorted), len(heatmap_dates)))\n",
    "for j, d in enumerate(heatmap_dates):\n",
    "    held = holdings_log[d]\n",
    "    for i, code in enumerate(all_codes_sorted):\n",
    "        if code in held:\n",
    "            heatmap_data[i, j] = 1.0 / len(held)  # 持仓权重 (holding weight)\n",
    "\n",
    "im = ax5.imshow(heatmap_data, aspect=\"auto\", cmap=\"YlOrRd\",\n",
    "                interpolation=\"nearest\", vmin=0, vmax=0.5)\n",
    "ax5.set_yticks(range(len(all_codes_sorted)))\n",
    "ax5.set_yticklabels(etf_names, fontsize=7, color=\"white\")\n",
    "\n",
    "# X 轴：每年一个刻度 (X-axis: one tick per year)\n",
    "year_ticks  = []\n",
    "year_labels = []\n",
    "for j, d in enumerate(heatmap_dates):\n",
    "    if d.month == 1:\n",
    "        year_ticks.append(j)\n",
    "        year_labels.append(str(d.year))\n",
    "ax5.set_xticks(year_ticks)\n",
    "ax5.set_xticklabels(year_labels, color=\"white\", fontsize=8)\n",
    "plt.colorbar(im, ax=ax5, fraction=0.02, label=\"权重 Weight\").ax.yaxis.set_tick_params(color=\"white\", labelcolor=\"white\")\n",
    "\n",
    "# ── 图6: 月度换手率 (Monthly Turnover) ──────────────────────────────────────\n",
    "ax6 = fig.add_subplot(gs[2, 2])\n",
    "style_ax(ax6, \"⑥ 月度换手率  Monthly Turnover\", ylabel=\"换手率 Turnover\")\n",
    "for key in [\"equal_weight\", \"relative_momentum\", \"dual_momentum\"]:\n",
    "    to_series = results[key].get(\"turnover\", pd.Series())\n",
    "    if len(to_series) > 0:\n",
    "        ax6.plot(to_series.index, to_series.values * 100,\n",
    "                 color=COLORS[key], linewidth=1.2, label=LABELS[key], alpha=0.8)\n",
    "        # 平均换手率水平线 (average turnover horizontal line)\n",
    "        avg_to = to_series.mean() * 100\n",
    "        ax6.axhline(avg_to, color=COLORS[key], linewidth=0.8,\n",
    "                    linestyle=\":\", alpha=0.6)\n",
    "ax6.yaxis.set_major_formatter(mtick.PercentFormatter())\n",
    "ax6.legend(fontsize=7, facecolor=\"#1a1a2e\", labelcolor=\"white\")\n",
    "\n",
    "# 保存图表 (save chart)\n",
    "out_path = \"/Users/tigeren/Dev/xorbitlab/trading/etf_rotation_demo.png\"\n",
    "plt.savefig(out_path, dpi=150, bbox_inches=\"tight\",\n",
    "            facecolor=fig.get_facecolor())\n",
    "plt.close()\n",
    "print(f\"    图表已保存 (Chart saved): {out_path}\")\n",
    "print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b24aac17",
   "metadata": {},
   "source": [
    "# ══════════════════════════════════════════════════════════════════════════════\n",
    "# §7  关键学习要点总结\n",
    "#     Key Learning Takeaways\n",
    "\n",
    "# ══════════════════════════════════════════════════════════════════════════════"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "838d08d5",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"═\" * 60)\n",
    "print(\"§7  关键学习要点 (Key Learning Takeaways)\")\n",
    "print(\"═\" * 60)\n",
    "print(\"\"\"\n",
    "  1. ETF 轮动 vs 个股选股 (ETF Rotation vs. Stock Picking)\n",
    "     ───────────────────────────────────────────────────\n",
    "     ETF 轮动：买\"赛道\"，避免个股黑天鹅事件\n",
    "     (ETF rotation: bet on sectors, avoid individual stock blow-ups)\n",
    "     \n",
    "     个股选股：需要更强的信息优势，难度更高\n",
    "     (Stock picking: requires information edge, much harder)\n",
    "\n",
    "  2. 绝对动量的价值 (Value of Absolute Momentum)\n",
    "     ───────────────────────────────────────────\n",
    "     双动量策略在熊市中会自动切换至国债 ETF 避险\n",
    "     最大回撤通常比纯相对动量低 10-15 个百分点\n",
    "     (Dual momentum auto-switches to bonds in bear markets,\n",
    "      reducing max drawdown by ~10-15 percentage points)\n",
    "\n",
    "  3. 动量的局限性 (Momentum Limitations)\n",
    "     ────────────────────────────────────\n",
    "     • 趋势逆转（动量崩溃）风险：牛熊切换时可能大幅亏损\n",
    "       (Momentum crash risk: large loss when trend reverses)\n",
    "     • 主题 ETF 历史短：A 股主题 ETF 多数 2019 年后才成立\n",
    "       (Short history: most A-share thematic ETFs launched post-2019)\n",
    "     • 需要结合估值/基本面作为反向过滤器\n",
    "       (Should combine with valuation/fundamentals as counter-filter)\n",
    "\n",
    "  4. 成本控制 (Cost Management)\n",
    "     ───────────────────────────\n",
    "     每月换仓一次约消耗 0.22% 双边成本\n",
    "     一年 12 次再平衡 ≈ 2.6% 年化成本拖拽\n",
    "     (Monthly rebalancing ≈ 0.22% per round-trip, ~2.6% annual drag)\n",
    "     → 适当降低再平衡频率（如季度）可以减少成本\n",
    "       (Reduce rebalance frequency, e.g., quarterly, to cut costs)\n",
    "\n",
    "  5. 进阶方向 (Next Steps)\n",
    "     ───────────────────────\n",
    "     • 接入真实数据（AKShare）替换合成数据\n",
    "       (Replace synthetic data with real data via AKShare)\n",
    "     • 加入估值信号（PE 分位数）作为过滤器\n",
    "       (Add valuation signal: sector PE percentile as filter)\n",
    "     • 加入波动率调仓（高波动期降低持仓比例）\n",
    "       (Add volatility scaling: reduce position in high-vol regimes)\n",
    "     • 结合宏观信号（利率、信用利差）增强国债 ETF 切换时机\n",
    "       (Combine macro signals for better timing of bond ETF switch)\n",
    "\"\"\")\n",
    "\n",
    "print(\"✅  Demo 完成！(Demo Complete!)\")\n",
    "print(f\"    输出文件: etf_rotation_demo.png\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (trading)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11.0"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
