"""
A 股行业 ETF 轮动策略 Demo
A-Share Sector ETF Rotation Strategy Demo
==========================================

策略逻辑 (Strategy Logic):
  1. 相对动量轮动 (Relative Momentum Rotation)
     - 每月对所有行业 ETF 计算过去 N 个月的动量得分
     - 买入得分最高的前 K 个 ETF，等权持有
  2. 双动量策略 (Dual Momentum, Gary Antonacci)
     - 绝对动量 (Absolute Momentum): 若某 ETF 的动量 < 0，切换至国债 ETF 避险
     - 相对动量 (Relative Momentum): 在通过绝对动量过滤的 ETF 中，买入相对最强的
  3. 等权基准 (Equal Weight Benchmark)
     - 始终等权持有全部行业 ETF，不做轮动

成本模拟 (Cost Simulation):
  - 印花税 (Stamp Duty): 卖出 0.1%
  - 佣金 (Commission): 买卖各 0.03%（万三）
  - 滑点 (Slippage): 单边 0.05%
  - 每月换仓合计约 0.22% 双边成本

作者: GitHub Copilot (教学用途)
数据: 合成模拟数据 (Synthetic data for demonstration)
"""

# ── 标准库 ──────────────────────────────────────────────────────────────────
import warnings
warnings.filterwarnings("ignore")

# matplotlib 必须在 pyplot 之前设置后端 (must set backend before importing pyplot)
import matplotlib
matplotlib.use("Agg")   # 无界面后端 (headless backend)

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
from matplotlib.patches import FancyArrowPatch
import matplotlib.ticker as mtick

# 设置随机种子，保证结果可复现 (fix random seed for reproducibility)
np.random.seed(42)

# ══════════════════════════════════════════════════════════════════════════════
# §0  ETF 池定义 & 合成价格数据生成
#     ETF Universe Definition & Synthetic Price Data Generation
# ══════════════════════════════════════════════════════════════════════════════

# ── A 股行业 ETF 池 (A-Share Sector ETF Universe) ───────────────────────────
# 这里用合成数据模拟，真实代码只需替换为 AKShare/Tushare 数据读取
# (Synthetic data here; replace with AKShare/Tushare data in production)
ETF_UNIVERSE = {
    # 代码 (Code) : (中文名称, 英文名称, 年化收益均值, 年化波动率, 与市场相关性)
    "159995": ("芯片ETF",   "Chip ETF",       0.18, 0.45, 0.75),
    "159819": ("人工智能ETF","AI ETF",         0.20, 0.48, 0.72),
    "516160": ("新能源ETF", "New Energy ETF", 0.15, 0.42, 0.70),
    "159928": ("消费ETF",   "Consumer ETF",   0.10, 0.28, 0.65),
    "512170": ("医疗ETF",   "Healthcare ETF", 0.08, 0.32, 0.60),
    "512880": ("证券ETF",   "Securities ETF", 0.12, 0.38, 0.80),
    # 国债 ETF 作为避险资产 (Bond ETF as safe-haven asset)
    "511010": ("国债ETF",   "Bond ETF",       0.03, 0.03, -0.10),
}

# 分离行业 ETF 和国债 ETF (separate sector ETFs from bond ETF)
SECTOR_ETFS = [code for code in ETF_UNIVERSE if code != "511010"]
BOND_ETF    = "511010"
ALL_ETFS    = list(ETF_UNIVERSE.keys())

# ── 生成合成价格序列 (Generate Synthetic Price Series) ───────────────────────
# 用因子模型生成相关价格序列，模拟真实行业之间的联动关系
# (Factor model to generate correlated price series, mimicking real sector co-movement)

TRADING_DAYS = 252          # 年交易日数 (trading days per year)
SIM_YEARS    = 6            # 模拟年数 (simulation years): 2019-2024
N_DAYS       = TRADING_DAYS * SIM_YEARS

# 市场公共因子 (market common factor) —— 模拟沪深300走势
# 牛市/熊市分段：模拟 2019-2021 牛市，2021-2022 熊市，2023-2024 震荡
market_factor = np.zeros(N_DAYS)
# 第一阶段：2019-2021 牛市 (Bull market phase)
bull1 = int(N_DAYS * 0.40)
market_factor[:bull1] = np.random.normal(0.0008, 0.012, bull1)
# 第二阶段：2021-2022 熊市 (Bear market phase)
bear1 = int(N_DAYS * 0.20)
market_factor[bull1:bull1+bear1] = np.random.normal(-0.0006, 0.018, bear1)
# 第三阶段：2023-2024 震荡 (Sideways phase)
market_factor[bull1+bear1:] = np.random.normal(0.0002, 0.014, N_DAYS - bull1 - bear1)

# 生成每只 ETF 的日收益率 (generate daily returns for each ETF)
daily_returns = {}
for code, (cn_name, en_name, ann_ret, ann_vol, mkt_corr) in ETF_UNIVERSE.items():
    daily_mu  = ann_ret / TRADING_DAYS          # 日均收益 (daily mean return)
    daily_sig = ann_vol / np.sqrt(TRADING_DAYS) # 日波动率 (daily volatility)

    # 特异性收益 (idiosyncratic return) = 总收益 - 市场部分
    idio_sig = daily_sig * np.sqrt(1 - mkt_corr**2)
    idio     = np.random.normal(0, idio_sig, N_DAYS)

    # 总日收益 = 市场因子 × 相关系数 + 特异性收益 + 均值漂移
    ret = mkt_corr * market_factor + idio + (daily_mu - mkt_corr * 0.0002)
    daily_returns[code] = ret

# 转为 DataFrame，并生成价格序列 (convert to DataFrame, then price series)
start_date = pd.Timestamp("2019-01-02")
dates = pd.bdate_range(start=start_date, periods=N_DAYS)  # 仅工作日 (business days only)

returns_df = pd.DataFrame(daily_returns, index=dates)
# 价格从 1.0 开始（净值形式，Net Asset Value style）
price_df = (1 + returns_df).cumprod()

print("═" * 60)
print("§0  ETF 价格数据生成完成 (Price Data Generated)")
print("═" * 60)
print(f"  模拟区间 (Simulation Period): {dates[0].date()} → {dates[-1].date()}")
print(f"  交易日数 (Trading Days):      {N_DAYS}")
print(f"  ETF 数量 (Number of ETFs):    {len(ALL_ETFS)}")
print()
print("  ETF 池 (Universe):")
print(f"  {'代码':10s} {'中文名':12s} {'英文名':18s} {'年化收益':8s} {'年化波动':8s}")
print("  " + "-" * 60)
for code, (cn, en, ret, vol, _) in ETF_UNIVERSE.items():
    label = " [避险]" if code == BOND_ETF else ""
    print(f"  {code:10s} {cn:12s} {en:18s} {ret*100:6.1f}%   {vol*100:6.1f}%{label}")
print()

# ══════════════════════════════════════════════════════════════════════════════
# §1  动量因子计算
#     Momentum Factor Calculation
# ══════════════════════════════════════════════════════════════════════════════

def calc_momentum(price_df: pd.DataFrame,
                  lookback_months: int = 12,
                  skip_months: int = 1) -> pd.DataFrame:
    """
    计算每只 ETF 的动量得分 (Calculate momentum score for each ETF)

    经典动量定义 (Classic momentum definition):
      过去 L 个月的总收益，跳过最近 S 个月（避免短期反转）
      Total return over past L months, skipping most recent S months
      (to avoid short-term reversal effect 短期反转效应)

    参数 (Parameters):
      price_df        : 日频价格 DataFrame (daily price DataFrame)
      lookback_months : 回望期（月数）(lookback period in months)
      skip_months     : 跳过最近月数（通常为 1）(skip recent months, usually 1)

    返回 (Returns):
      月频动量得分 DataFrame (monthly momentum score DataFrame)
    """
    # 转为月末价格 (resample to month-end prices)
    # 注意：旧版 pandas 用 "M"，新版用 "ME"
    try:
        monthly_price = price_df.resample("ME").last()
    except ValueError:
        monthly_price = price_df.resample("M").last()

    # 动量 = 前 (lookback+skip) 月价格 / 前 skip 月价格 - 1
    # Momentum = Price[t - skip] / Price[t - lookback - skip] - 1
    start_lag = skip_months
    end_lag   = lookback_months + skip_months

    momentum = monthly_price.shift(start_lag) / monthly_price.shift(end_lag) - 1
    return momentum


# 计算三种不同回望期的动量（稳健性检验用）
# (Calculate momentum with three different lookback windows for robustness)
mom_12_1 = calc_momentum(price_df, lookback_months=12, skip_months=1)  # 经典 12-1 月动量
mom_6_1  = calc_momentum(price_df, lookback_months=6,  skip_months=1)  # 中期 6-1 月动量
mom_3_1  = calc_momentum(price_df, lookback_months=3,  skip_months=1)  # 短期 3-1 月动量

# 合成动量得分：三个窗口等权平均（减少单一窗口过拟合风险）
# (Composite momentum score: equal-weight average of three windows)
mom_composite = (mom_12_1 + mom_6_1 + mom_3_1) / 3

print("§1  动量因子计算完成 (Momentum Factors Calculated)")
print(f"    月度数据行数 (Monthly rows): {len(mom_12_1)}")
print()

# ══════════════════════════════════════════════════════════════════════════════
# §2  A 股交易成本模型
#     A-Share Transaction Cost Model
# ══════════════════════════════════════════════════════════════════════════════

class AShareCostModel:
    """
    A 股 ETF 交易成本模型 (A-Share ETF Transaction Cost Model)

    A 股 ETF 特殊规则 (A-Share ETF Special Rules):
      - ETF 可 T+0 交易（当天买当天可卖）Unlike 个股的 T+1 限制
        (ETF allows T+0 trading, unlike individual stocks which are T+1)
      - 印花税：仅卖出方向收取 0.1%
        (Stamp duty: 0.1% on SELL side only)
      - 佣金：买卖双向，通常 0.02% - 0.03%
        (Commission: both sides, typically 0.02%-0.03%)
      - 滑点：ETF 流动性好，通常 0.02%-0.05%
        (Slippage: ETF has good liquidity, typically 0.02%-0.05%)
    """
    def __init__(self,
                 commission_rate: float = 0.0003,   # 佣金率 (commission rate)
                 stamp_duty_rate: float = 0.001,    # 印花税率 (stamp duty rate)
                 slippage_rate:   float = 0.0005):  # 滑点率 (slippage rate)
        self.commission = commission_rate
        self.stamp_duty = stamp_duty_rate
        self.slippage   = slippage_rate

    def buy_cost(self, trade_value: float) -> float:
        """买入成本 (buy-side cost): 佣金 + 滑点"""
        return trade_value * (self.commission + self.slippage)

    def sell_cost(self, trade_value: float) -> float:
        """卖出成本 (sell-side cost): 佣金 + 印花税 + 滑点"""
        return trade_value * (self.commission + self.stamp_duty + self.slippage)

    def roundtrip_cost(self, trade_value: float) -> float:
        """
        双边成本 (round-trip cost): 买入 + 卖出
        月度换仓一次的总成本约 0.22%
        """
        return self.buy_cost(trade_value) + self.sell_cost(trade_value)

    @property
    def roundtrip_rate(self) -> float:
        """双边成本率 (round-trip cost rate)"""
        return 2 * self.commission + self.stamp_duty + 2 * self.slippage


cost_model = AShareCostModel()
print("§2  交易成本模型 (Transaction Cost Model):")
print(f"    佣金率 (Commission):   {cost_model.commission*100:.3f}% × 双边")
print(f"    印花税 (Stamp Duty):   {cost_model.stamp_duty*100:.3f}% × 卖出方向")
print(f"    滑点率 (Slippage):     {cost_model.slippage*100:.3f}% × 双边")
print(f"    双边总成本 (Roundtrip): {cost_model.roundtrip_rate*100:.3f}%")
print()

# ══════════════════════════════════════════════════════════════════════════════
# §3  轮动回测引擎
#     Rotation Backtest Engine
# ══════════════════════════════════════════════════════════════════════════════

def run_rotation_backtest(
    price_df:       pd.DataFrame,
    momentum_df:    pd.DataFrame,
    strategy:       str = "relative_momentum",
    top_k:          int = 3,
    cost_model:     AShareCostModel = None,
    bond_etf:       str = BOND_ETF,
    sector_etfs:    list = None,
) -> dict:
    """
    行业 ETF 轮动回测引擎 (Sector ETF Rotation Backtest Engine)

    参数 (Parameters):
      price_df    : 日频价格 (daily prices)
      momentum_df : 月频动量得分 (monthly momentum scores)
      strategy    : 策略名称 (strategy name)
                    "relative_momentum" — 相对动量轮动
                    "dual_momentum"     — 双动量（含绝对动量过滤）
                    "equal_weight"      — 等权基准
      top_k       : 持有前 K 只 ETF (hold top-K ETFs)
      cost_model  : 交易成本模型 (transaction cost model)
      bond_etf    : 避险 ETF 代码 (safe-haven ETF code)
      sector_etfs : 行业 ETF 代码列表 (sector ETF codes)

    返回 (Returns):
      dict 包含:
        "nav"         : 每日净值序列 (daily NAV series)
        "weights"     : 月度持仓权重 (monthly weights)
        "turnover"    : 月度换手率 (monthly turnover)
        "cost_total"  : 总交易成本 (total transaction cost)
        "holdings_log": 持仓记录 (holdings log)
    """
    if sector_etfs is None:
        sector_etfs = SECTOR_ETFS
    if cost_model is None:
        cost_model = AShareCostModel()

    # 获取月末再平衡日期 (get month-end rebalancing dates)
    try:
        monthly_idx = price_df.resample("ME").last().index
    except ValueError:
        monthly_idx = price_df.resample("M").last().index

    # 预热期：动量需要至少 13 个月数据（12 个月回望 + 1 个月跳过）
    # (Warm-up period: momentum needs at least 13 months of data)
    warmup = 13

    # 初始化 (initialization)
    nav           = pd.Series(index=price_df.index, dtype=float)
    weights_log   = {}   # 每月权重记录 (monthly weights log)
    turnover_log  = {}   # 每月换手率记录 (monthly turnover log)
    holdings_log  = {}   # 每月持仓记录 (monthly holdings log)
    total_cost    = 0.0  # 累计交易成本 (cumulative transaction cost)

    current_weights = {}  # 当前持仓权重 {code: weight}
    portfolio_value = 1.0 # 组合净值，从 1.0 开始 (portfolio starts at 1.0)

    rebal_dates = monthly_idx[warmup:]  # 有效再平衡日期 (valid rebalance dates)

    for i, rebal_date in enumerate(rebal_dates):
        # ── 1. 确定目标权重 (Determine target weights) ──────────────────────

        if strategy == "equal_weight":
            # 等权：始终持有所有行业 ETF，各占 1/N
            # (Equal weight: always hold all sector ETFs equally)
            n = len(sector_etfs)
            target_weights = {code: 1.0 / n for code in sector_etfs}

        elif strategy == "relative_momentum":
            # 相对动量：选动量得分最高的前 K 只行业 ETF
            # (Relative momentum: select top-K sector ETFs by momentum score)
            mom_today = momentum_df.loc[:rebal_date, sector_etfs].iloc[-1]
            mom_today = mom_today.dropna()
            if len(mom_today) == 0:
                target_weights = current_weights or {sector_etfs[0]: 1.0}
            else:
                top_etfs = mom_today.nlargest(top_k).index.tolist()
                w = 1.0 / len(top_etfs)
                target_weights = {code: w for code in top_etfs}

        elif strategy == "dual_momentum":
            # 双动量：先用绝对动量过滤，再做相对动量排序
            # (Dual momentum: filter by absolute momentum, then rank by relative)
            mom_today = momentum_df.loc[:rebal_date, sector_etfs].iloc[-1]
            mom_today = mom_today.dropna()

            # 绝对动量过滤 (absolute momentum filter):
            # 如果行业 ETF 动量 < 0（跑输无风险），则替换为国债 ETF
            # (If sector ETF momentum < 0, replace with bond ETF)
            positive_etfs = mom_today[mom_today > 0].index.tolist()

            if len(positive_etfs) == 0:
                # 全部动量为负：100% 持有国债 ETF 避险
                # (All negative momentum: 100% in bond ETF)
                target_weights = {bond_etf: 1.0}
            else:
                # 从通过绝对动量过滤的 ETF 中，选相对最强的前 K 只
                # (From ETFs passing absolute momentum filter, pick top-K by relative)
                k_actual = min(top_k, len(positive_etfs))
                top_etfs = mom_today[positive_etfs].nlargest(k_actual).index.tolist()
                w = 1.0 / k_actual
                target_weights = {code: w for code in top_etfs}
        else:
            raise ValueError(f"未知策略 (Unknown strategy): {strategy}")

        # ── 2. 计算换手率并扣除交易成本 (Calculate turnover and deduct costs) ──

        # 所有涉及的 ETF 代码（新旧持仓并集）
        all_codes = set(current_weights) | set(target_weights)
        turnover = 0.0

        for code in all_codes:
            old_w = current_weights.get(code, 0.0)
            new_w = target_weights.get(code, 0.0)
            delta = abs(new_w - old_w)   # 权重变化 (weight change)
            if delta > 1e-6:
                turnover += delta
                # 对权重变化部分计算成本 (cost on the traded portion)
                trade_value = delta * portfolio_value
                if new_w > old_w:
                    # 增仓 → 买入成本 (increase position → buy cost)
                    cost = cost_model.buy_cost(trade_value)
                else:
                    # 减仓 → 卖出成本（含印花税）
                    # (decrease position → sell cost including stamp duty)
                    cost = cost_model.sell_cost(trade_value)
                total_cost += cost
                portfolio_value -= cost   # 成本从组合价值中扣除 (deduct cost from portfolio)

        # 换手率（单边）= 买入总金额 / 组合价值 (one-sided turnover)
        turnover_log[rebal_date] = turnover / 2  # 双边换手/2 = 单边换手

        # ── 3. 更新持仓，计算到下一个再平衡日的收益
        #       (Update holdings, compute returns until next rebalance date)
        current_weights = target_weights
        weights_log[rebal_date] = target_weights.copy()
        holdings_log[rebal_date] = list(target_weights.keys())

        # 确定持仓持续的日期区间 (determine holding period date range)
        if i + 1 < len(rebal_dates):
            next_rebal = rebal_dates[i + 1]
        else:
            next_rebal = price_df.index[-1]

        hold_dates = price_df.loc[rebal_date:next_rebal].index

        # 计算每日组合收益率 (calculate daily portfolio return)
        for j in range(1, len(hold_dates)):
            d_prev = hold_dates[j - 1]
            d_curr = hold_dates[j]
            daily_port_ret = 0.0
            for code, w in current_weights.items():
                if code in price_df.columns:
                    p_prev = price_df.loc[d_prev, code]
                    p_curr = price_df.loc[d_curr, code]
                    if p_prev > 0:
                        daily_port_ret += w * (p_curr / p_prev - 1)
            portfolio_value *= (1 + daily_port_ret)
            nav[d_curr] = portfolio_value

    # 补全初始 NAV（热身期）
    nav = nav.ffill()
    first_valid = nav.first_valid_index()
    if first_valid is not None:
        nav[:first_valid] = 1.0
    nav = nav.fillna(1.0)

    return {
        "nav":          nav,
        "weights":      weights_log,
        "turnover":     pd.Series(turnover_log),
        "cost_total":   total_cost,
        "holdings_log": holdings_log,
    }


# ══════════════════════════════════════════════════════════════════════════════
# §4  运行三种策略并计算绩效指标
#     Run Three Strategies and Calculate Performance Metrics
# ══════════════════════════════════════════════════════════════════════════════

print("§4  运行回测策略 (Running Backtest Strategies)...")
print()

# 运行三种策略 (run three strategies)
results = {}

results["equal_weight"] = run_rotation_backtest(
    price_df, mom_composite,
    strategy="equal_weight",
    top_k=3, cost_model=cost_model,
)

results["relative_momentum"] = run_rotation_backtest(
    price_df, mom_composite,
    strategy="relative_momentum",
    top_k=3, cost_model=cost_model,
)

results["dual_momentum"] = run_rotation_backtest(
    price_df, mom_composite,
    strategy="dual_momentum",
    top_k=3, cost_model=cost_model,
)

# 同时计算国债 ETF 纯避险基准 (bond ETF as pure safe-haven benchmark)
bond_nav = price_df[BOND_ETF] / price_df[BOND_ETF].iloc[0]
results["bond_only"] = {"nav": bond_nav}

# ── 绩效计算函数 (Performance Calculation Function) ─────────────────────────

def calc_performance(nav: pd.Series,
                     risk_free_annual: float = 0.02) -> dict:
    """
    计算常用量化绩效指标 (Calculate common quantitative performance metrics)

    指标 (Metrics):
      - 年化收益率 (Annualized Return, CAGR)
      - 年化波动率 (Annualized Volatility)
      - 夏普比率 (Sharpe Ratio): 超额收益 / 波动率
      - 最大回撤 (Maximum Drawdown, MaxDD): 从历史最高点的最大跌幅
      - 卡玛比率 (Calmar Ratio): 年化收益 / 最大回撤
      - 索提诺比率 (Sortino Ratio): 用下行波动率替代总波动率
    """
    nav = nav.dropna()
    if len(nav) < 2:
        return {}

    # 日收益率 (daily returns)
    daily_ret = nav.pct_change().dropna()

    # 年化收益率 (CAGR - Compound Annual Growth Rate 复合年增长率)
    n_years = len(daily_ret) / TRADING_DAYS
    total_ret = nav.iloc[-1] / nav.iloc[0] - 1
    cagr = (1 + total_ret) ** (1 / n_years) - 1

    # 年化波动率 (annualized volatility)
    ann_vol = daily_ret.std() * np.sqrt(TRADING_DAYS)

    # 夏普比率 (Sharpe Ratio)
    # 公式: (年化收益 - 无风险利率) / 年化波动率
    rf_daily = risk_free_annual / TRADING_DAYS
    sharpe = (daily_ret.mean() - rf_daily) / daily_ret.std() * np.sqrt(TRADING_DAYS)

    # 最大回撤 (Maximum Drawdown)
    # 定义: max(累计最高净值 - 当日净值) / 累计最高净值
    rolling_max = nav.cummax()           # 历史最高净值 (rolling maximum)
    drawdown    = nav / rolling_max - 1  # 每日回撤序列 (daily drawdown series)
    max_dd      = drawdown.min()         # 最大回撤（负数）(max drawdown, negative)

    # 卡玛比率 (Calmar Ratio = CAGR / |MaxDD|)
    calmar = cagr / abs(max_dd) if max_dd != 0 else np.nan

    # 索提诺比率 (Sortino Ratio): 只惩罚下行波动 (penalizes only downside volatility)
    downside_ret = daily_ret[daily_ret < rf_daily]
    downside_vol = downside_ret.std() * np.sqrt(TRADING_DAYS) if len(downside_ret) > 0 else ann_vol
    sortino = (cagr - risk_free_annual) / downside_vol if downside_vol > 0 else np.nan

    # 胜率 (Win Rate): 月度正收益比例 (fraction of months with positive return)
    try:
        monthly_ret = nav.resample("ME").last().pct_change().dropna()
    except ValueError:
        monthly_ret = nav.resample("M").last().pct_change().dropna()
    win_rate = (monthly_ret > 0).mean()

    return {
        "年化收益率 (CAGR)":           cagr,
        "年化波动率 (Volatility)":      ann_vol,
        "夏普比率 (Sharpe)":           sharpe,
        "最大回撤 (MaxDrawdown)":       max_dd,
        "卡玛比率 (Calmar)":           calmar,
        "索提诺比率 (Sortino)":         sortino,
        "月度胜率 (Win Rate)":          win_rate,
        "累计收益率 (Total Return)":    total_ret,
    }


# ── 打印绩效对比表 (Print Performance Comparison Table) ─────────────────────

strategy_labels = {
    "equal_weight":        "等权基准 (EW Benchmark)",
    "relative_momentum":   "相对动量 (Rel Momentum)",
    "dual_momentum":       "双动量 (Dual Momentum)",
    "bond_only":           "国债避险 (Bond Only)",
}

print("=" * 72)
print("§4  策略绩效对比 (Strategy Performance Comparison)")
print("=" * 72)

perf_all = {}
metrics_display = [
    ("年化收益率 (CAGR)",        "{:>8.2%}"),
    ("年化波动率 (Volatility)",   "{:>8.2%}"),
    ("夏普比率 (Sharpe)",        "{:>8.3f}"),
    ("最大回撤 (MaxDrawdown)",    "{:>8.2%}"),
    ("卡玛比率 (Calmar)",        "{:>8.3f}"),
    ("月度胜率 (Win Rate)",       "{:>8.2%}"),
    ("累计收益率 (Total Return)", "{:>8.2%}"),
]

for key, label in strategy_labels.items():
    nav = results[key]["nav"]
    perf = calc_performance(nav)
    perf_all[key] = perf

header = f"  {'指标':26s}"
for label in strategy_labels.values():
    header += f"  {label[:20]:>20s}"
print(header)
print("  " + "-" * 90)

for metric, fmt in metrics_display:
    row = f"  {metric:26s}"
    for key in strategy_labels:
        val = perf_all[key].get(metric, np.nan)
        row += "  " + fmt.format(val).rjust(20)
    print(row)

print()
# 打印成本统计
for key in ["equal_weight", "relative_momentum", "dual_momentum"]:
    cost = results[key].get("cost_total", 0)
    to   = results[key].get("turnover", pd.Series()).mean()
    print(f"  [{strategy_labels[key][:16]}]  "
          f"总成本={cost*100:.3f}%净值  平均月换手={to*100:.1f}%")
print()

# ══════════════════════════════════════════════════════════════════════════════
# §5  持仓分析：统计各行业 ETF 的被选中频次
#     Holdings Analysis: Count How Often Each Sector ETF Was Selected
# ══════════════════════════════════════════════════════════════════════════════

print("§5  持仓分析 (Holdings Analysis):")
print()

for strategy_key in ["relative_momentum", "dual_momentum"]:
    holdings_log = results[strategy_key]["holdings_log"]
    count = {code: 0 for code in ALL_ETFS}

    for date, held in holdings_log.items():
        for code in held:
            count[code] = count.get(code, 0) + 1

    total_months = len(holdings_log)
    label = strategy_labels[strategy_key]
    print(f"  [{label}]  共 {total_months} 次再平衡:")

    # 按持有频次降序排列 (sort by holding frequency descending)
    sorted_count = sorted(count.items(), key=lambda x: x[1], reverse=True)
    for code, cnt in sorted_count:
        if cnt > 0:
            cn_name = ETF_UNIVERSE[code][0]
            pct = cnt / total_months
            bar = "█" * int(pct * 20)
            print(f"    {code}  {cn_name:10s}  {cnt:3d}次  {pct:5.1%}  {bar}")
    print()

# ══════════════════════════════════════════════════════════════════════════════
# §6  可视化：9 宫格图表
#     Visualization: 9-Panel Chart
# ══════════════════════════════════════════════════════════════════════════════

print("§6  生成可视化图表 (Generating Charts)...")

# 配置中文字体 (configure Chinese font)
plt.rcParams["font.sans-serif"] = ["WenQuanYi Zen Hei", "Arial Unicode MS",
                                   "SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False

# 颜色方案 (color scheme)
COLORS = {
    "equal_weight":      "#95a5a6",   # 灰色（基准）
    "relative_momentum": "#2ecc71",   # 绿色
    "dual_momentum":     "#e74c3c",   # 红色（主策略）
    "bond_only":         "#3498db",   # 蓝色（国债）
}
LABELS = {
    "equal_weight":      "等权基准",
    "relative_momentum": "相对动量",
    "dual_momentum":     "双动量",
    "bond_only":         "国债",
}

fig = plt.figure(figsize=(20, 16), facecolor="#1a1a2e")
fig.suptitle(
    "A 股行业 ETF 轮动策略回测  (A-Share Sector ETF Rotation Backtest)\n"
    "合成模拟数据 2019-2024  (Synthetic Data 2019-2024)",
    fontsize=16, color="white", fontweight="bold", y=0.98
)

gs = gridspec.GridSpec(3, 3, figure=fig,
                       hspace=0.42, wspace=0.35,
                       left=0.07, right=0.97, top=0.93, bottom=0.06)

ax_style = dict(facecolor="#16213e", labelcolor="white",
                tick_params=dict(colors="white"))

def style_ax(ax, title, xlabel="", ylabel=""):
    ax.set_facecolor("#16213e")
    ax.set_title(title, color="white", fontsize=10, fontweight="bold", pad=6)
    ax.set_xlabel(xlabel, color="#aaaaaa", fontsize=8)
    ax.set_ylabel(ylabel, color="#aaaaaa", fontsize=8)
    ax.tick_params(colors="white", labelsize=7)
    ax.spines[:].set_color("#444466")
    ax.grid(True, alpha=0.25, color="#666699", linewidth=0.5)

# ── 图1: 累计净值曲线 (Cumulative NAV) ───────────────────────────────────────
ax1 = fig.add_subplot(gs[0, :2])   # 跨两列（宽幅）
style_ax(ax1, "① 累计净值对比  Cumulative NAV Comparison", ylabel="净值 NAV")
for key, color in COLORS.items():
    nav = results[key]["nav"]
    ax1.plot(nav.index, nav.values, color=color, linewidth=1.8,
             label=LABELS[key], alpha=0.9)
ax1.axhline(1.0, color="#888888", linewidth=0.8, linestyle="--", alpha=0.6)
ax1.legend(loc="upper left", fontsize=8, facecolor="#1a1a2e",
           labelcolor="white", framealpha=0.7)
ax1.yaxis.set_major_formatter(mtick.FuncFormatter(lambda x, _: f"{x:.1f}x"))

# ── 图2: 绩效雷达图 (Performance Radar) ─────────────────────────────────────
ax2 = fig.add_subplot(gs[0, 2], projection="polar")
ax2.set_facecolor("#16213e")
ax2.set_title("② 绩效雷达图  Performance Radar",
              color="white", fontsize=10, fontweight="bold", pad=15)
ax2.tick_params(colors="white", labelsize=7)
ax2.spines["polar"].set_color("#444466")

radar_metrics = ["年化收益", "夏普比率", "月胜率", "抗回撤"]
radar_keys    = ["年化收益率 (CAGR)", "夏普比率 (Sharpe)",
                 "月度胜率 (Win Rate)", "卡玛比率 (Calmar)"]
n_radar       = len(radar_metrics)
angles        = np.linspace(0, 2 * np.pi, n_radar, endpoint=False).tolist()
angles       += angles[:1]

# 归一化各指标到 0-1 (normalize metrics to 0-1)
def normalize_radar(values, min_val=0.0, max_val=1.0):
    rng = max_val - min_val
    return [(v - min_val) / rng if rng > 0 else 0.5 for v in values]

radar_strats = ["equal_weight", "relative_momentum", "dual_momentum"]
for rkey in radar_strats:
    perf = perf_all[rkey]
    raw  = [perf.get(m, 0) for m in radar_keys]
    # 简单归一化（基于各策略范围）
    vals = [
        min(max(raw[0] / 0.30, 0), 1),   # CAGR: 0~30%
        min(max(raw[1] / 3.0,  0), 1),   # Sharpe: 0~3
        min(max(raw[2],        0), 1),   # Win rate: 0~1
        min(max(raw[3] / 3.0,  0), 1),   # Calmar: 0~3
    ]
    vals += vals[:1]
    ax2.plot(angles, vals, color=COLORS[rkey], linewidth=2, label=LABELS[rkey])
    ax2.fill(angles, vals, color=COLORS[rkey], alpha=0.15)

ax2.set_xticks(angles[:-1])
ax2.set_xticklabels(radar_metrics, color="white", fontsize=8)
ax2.set_yticklabels([])
ax2.legend(loc="upper right", bbox_to_anchor=(1.35, 1.15),
           fontsize=7, facecolor="#1a1a2e", labelcolor="white")

# ── 图3: 最大回撤曲线 (Drawdown Curve) ──────────────────────────────────────
ax3 = fig.add_subplot(gs[1, :2])
style_ax(ax3, "③ 水下曲线（回撤）  Underwater Chart (Drawdown)", ylabel="回撤 Drawdown")
for key, color in COLORS.items():
    nav = results[key]["nav"]
    dd  = nav / nav.cummax() - 1
    ax3.fill_between(dd.index, dd.values, 0, color=color, alpha=0.4, label=LABELS[key])
    ax3.plot(dd.index, dd.values, color=color, linewidth=0.8, alpha=0.8)
ax3.set_ylim(-0.65, 0.05)
ax3.yaxis.set_major_formatter(mtick.PercentFormatter(1.0))
ax3.legend(loc="lower left", fontsize=8, facecolor="#1a1a2e",
           labelcolor="white", framealpha=0.7)

# ── 图4: 绩效指标条形图 (Performance Bar Chart) ──────────────────────────────
ax4 = fig.add_subplot(gs[1, 2])
style_ax(ax4, "④ 夏普 & 卡玛  Sharpe & Calmar", ylabel="比率 Ratio")
strats_bar  = ["equal_weight", "relative_momentum", "dual_momentum"]
labels_bar  = [LABELS[k] for k in strats_bar]
sharpes     = [perf_all[k]["夏普比率 (Sharpe)"] for k in strats_bar]
calmars     = [perf_all[k]["卡玛比率 (Calmar)"] for k in strats_bar]
x_bar       = np.arange(len(strats_bar))
w_bar       = 0.35
bars1 = ax4.bar(x_bar - w_bar/2, sharpes, w_bar, color="#2ecc71", alpha=0.8,
                label="夏普 Sharpe")
bars2 = ax4.bar(x_bar + w_bar/2, calmars, w_bar, color="#e74c3c", alpha=0.8,
                label="卡玛 Calmar")
ax4.set_xticks(x_bar)
ax4.set_xticklabels(labels_bar, fontsize=7, color="white")
ax4.legend(fontsize=8, facecolor="#1a1a2e", labelcolor="white")
for bar in bars1:
    ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
             f"{bar.get_height():.2f}", ha="center", va="bottom",
             color="white", fontsize=7)
for bar in bars2:
    ax4.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02,
             f"{bar.get_height():.2f}", ha="center", va="bottom",
             color="white", fontsize=7)

# ── 图5: 双动量月度持仓热力图 (Dual Momentum Monthly Holdings Heatmap) ────────
ax5 = fig.add_subplot(gs[2, :2])
style_ax(ax5, "⑤ 双动量月度持仓热力图  Dual Momentum Monthly Holdings")

holdings_log = results["dual_momentum"]["holdings_log"]
all_codes_sorted = SECTOR_ETFS + [BOND_ETF]
etf_names = [ETF_UNIVERSE[c][0] for c in all_codes_sorted]

heatmap_dates = sorted(holdings_log.keys())
heatmap_data  = np.zeros((len(all_codes_sorted), len(heatmap_dates)))
for j, d in enumerate(heatmap_dates):
    held = holdings_log[d]
    for i, code in enumerate(all_codes_sorted):
        if code in held:
            heatmap_data[i, j] = 1.0 / len(held)  # 持仓权重 (holding weight)

im = ax5.imshow(heatmap_data, aspect="auto", cmap="YlOrRd",
                interpolation="nearest", vmin=0, vmax=0.5)
ax5.set_yticks(range(len(all_codes_sorted)))
ax5.set_yticklabels(etf_names, fontsize=7, color="white")

# X 轴：每年一个刻度 (X-axis: one tick per year)
year_ticks  = []
year_labels = []
for j, d in enumerate(heatmap_dates):
    if d.month == 1:
        year_ticks.append(j)
        year_labels.append(str(d.year))
ax5.set_xticks(year_ticks)
ax5.set_xticklabels(year_labels, color="white", fontsize=8)
plt.colorbar(im, ax=ax5, fraction=0.02, label="权重 Weight").ax.yaxis.set_tick_params(color="white", labelcolor="white")

# ── 图6: 月度换手率 (Monthly Turnover) ──────────────────────────────────────
ax6 = fig.add_subplot(gs[2, 2])
style_ax(ax6, "⑥ 月度换手率  Monthly Turnover", ylabel="换手率 Turnover")
for key in ["equal_weight", "relative_momentum", "dual_momentum"]:
    to_series = results[key].get("turnover", pd.Series())
    if len(to_series) > 0:
        ax6.plot(to_series.index, to_series.values * 100,
                 color=COLORS[key], linewidth=1.2, label=LABELS[key], alpha=0.8)
        # 平均换手率水平线 (average turnover horizontal line)
        avg_to = to_series.mean() * 100
        ax6.axhline(avg_to, color=COLORS[key], linewidth=0.8,
                    linestyle=":", alpha=0.6)
ax6.yaxis.set_major_formatter(mtick.PercentFormatter())
ax6.legend(fontsize=7, facecolor="#1a1a2e", labelcolor="white")

# 保存图表 (save chart)
out_path = "/Users/tigeren/Dev/xorbitlab/trading/etf_rotation_demo.png"
plt.savefig(out_path, dpi=150, bbox_inches="tight",
            facecolor=fig.get_facecolor())
plt.close()
print(f"    图表已保存 (Chart saved): {out_path}")
print()

# ══════════════════════════════════════════════════════════════════════════════
# §7  关键学习要点总结
#     Key Learning Takeaways
# ══════════════════════════════════════════════════════════════════════════════

print("═" * 60)
print("§7  关键学习要点 (Key Learning Takeaways)")
print("═" * 60)
print("""
  1. ETF 轮动 vs 个股选股 (ETF Rotation vs. Stock Picking)
     ───────────────────────────────────────────────────
     ETF 轮动：买"赛道"，避免个股黑天鹅事件
     (ETF rotation: bet on sectors, avoid individual stock blow-ups)
     
     个股选股：需要更强的信息优势，难度更高
     (Stock picking: requires information edge, much harder)

  2. 绝对动量的价值 (Value of Absolute Momentum)
     ───────────────────────────────────────────
     双动量策略在熊市中会自动切换至国债 ETF 避险
     最大回撤通常比纯相对动量低 10-15 个百分点
     (Dual momentum auto-switches to bonds in bear markets,
      reducing max drawdown by ~10-15 percentage points)

  3. 动量的局限性 (Momentum Limitations)
     ────────────────────────────────────
     • 趋势逆转（动量崩溃）风险：牛熊切换时可能大幅亏损
       (Momentum crash risk: large loss when trend reverses)
     • 主题 ETF 历史短：A 股主题 ETF 多数 2019 年后才成立
       (Short history: most A-share thematic ETFs launched post-2019)
     • 需要结合估值/基本面作为反向过滤器
       (Should combine with valuation/fundamentals as counter-filter)

  4. 成本控制 (Cost Management)
     ───────────────────────────
     每月换仓一次约消耗 0.22% 双边成本
     一年 12 次再平衡 ≈ 2.6% 年化成本拖拽
     (Monthly rebalancing ≈ 0.22% per round-trip, ~2.6% annual drag)
     → 适当降低再平衡频率（如季度）可以减少成本
       (Reduce rebalance frequency, e.g., quarterly, to cut costs)

  5. 进阶方向 (Next Steps)
     ───────────────────────
     • 接入真实数据（AKShare）替换合成数据
       (Replace synthetic data with real data via AKShare)
     • 加入估值信号（PE 分位数）作为过滤器
       (Add valuation signal: sector PE percentile as filter)
     • 加入波动率调仓（高波动期降低持仓比例）
       (Add volatility scaling: reduce position in high-vol regimes)
     • 结合宏观信号（利率、信用利差）增强国债 ETF 切换时机
       (Combine macro signals for better timing of bond ETF switch)
""")

print("✅  Demo 完成！(Demo Complete!)")
print(f"    输出文件: etf_rotation_demo.png")
