"""M17 教学图：2022 年的因子连乘，以及 60/40 在两种相关下的假设波动。

因子来自已缓存的 French 月频。60/40 的波动是给定的 16% 与 6%，不是某只债券基金的净值。
"""

from __future__ import annotations

import math
import zipfile
from io import BytesIO
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

plt.rcParams["font.sans-serif"] = ["Microsoft YaHei", "SimHei", "DejaVu Sans"]
plt.rcParams["axes.unicode_minus"] = False

HERE = Path(__file__).resolve().parent
FF5_ZIP = HERE.parent / "M07" / "_cache" / "F-F_Research_Data_5_Factors_2x3_CSV.zip"
MOM_ZIP = HERE.parent / "M06" / "_cache" / "F-F_Momentum_Factor_CSV.zip"
OUT = HERE / "timing_and_sixty_forty.png"
FACTORS = ["HML", "RMW", "CMA", "Mom"]


def monthly_block(text: str, marker: str) -> pd.DataFrame:
    lines = text.splitlines()
    start = next(i for i, line in enumerate(lines) if marker in line)
    if lines[start].startswith(","):
        header_line = lines[start]
        data_start = start + 1
    else:
        header_line = lines[start + 1]
        data_start = start + 2
    header = [part.strip() for part in header_line.split(",")]
    rows: list[list[str]] = []
    for line in lines[data_start:]:
        if not line.strip():
            break
        parts = [part.strip() for part in line.split(",")]
        if len(parts[0]) != 6:
            break
        rows.append(parts)
    frame = pd.DataFrame(rows, columns=header)
    frame = frame.rename(columns={header[0]: "date"})
    for column in frame.columns[1:]:
        frame[column] = pd.to_numeric(frame[column]) / 100.0
    frame["date"] = pd.to_datetime(frame["date"], format="%Y%m")
    return frame.set_index("date")


def compound(series: pd.Series) -> float:
    values = series.dropna()
    return float((1.0 + values).prod() - 1.0)


def performance(series: pd.Series) -> tuple[float, float, float]:
    values = series.dropna()
    mean = float(values.mean())
    std = float(values.std(ddof=1))
    # 因子收益已经是多空。不再减无风险利率。夏普用月频 √12。
    return mean * 12.0, mean / std * math.sqrt(12.0), compound(values)


def port_vol(weight_stock: float, vol_stock: float, vol_bond: float, corr: float) -> float:
    weight_bond = 1.0 - weight_stock
    var = (
        weight_stock**2 * vol_stock**2
        + weight_bond**2 * vol_bond**2
        + 2.0 * weight_stock * weight_bond * vol_stock * vol_bond * corr
    )
    return math.sqrt(var)


def main() -> None:
    ff5_raw = zipfile.ZipFile(BytesIO(FF5_ZIP.read_bytes())).read(
        "F-F_Research_Data_5_Factors_2x3.csv"
    )
    mom_raw = zipfile.ZipFile(BytesIO(MOM_ZIP.read_bytes())).read(
        "F-F_Momentum_Factor.csv"
    )
    ff5 = monthly_block(ff5_raw.decode("latin1"), ",Mkt-RF")
    mom = monthly_block(mom_raw.decode("latin1"), ",Mom")
    data = ff5.join(mom.rename(columns={mom.columns[0]: "Mom"}), how="inner")
    print("sample", data.index.min().date(), data.index.max().date(), len(data))
    year = data.loc["2022-01":"2022-12"]
    print("n_2022", len(year))
    for column in ["Mkt-RF", *FACTORS]:
        print("c2022", column, f"{compound(year[column]):.6f}")

    # 过去 12 个月连乘，再 shift(1)。月 t 的仓位看不到月 t 的收益。
    trail = (1.0 + data[FACTORS]).rolling(12).apply(np.prod, raw=True) - 1.0
    signal = trail.shift(1)
    keep = signal.notna().all(axis=1)
    panel = data.loc[keep, FACTORS]
    sign = signal.loc[keep]
    print("timed", panel.index.min().date(), panel.index.max().date(), len(panel))

    base_w = np.full(panel.shape, 0.25)
    # 过去 12 个月为负的因子留在现金，不把权重摊给其余因子。
    cash_w = np.where(sign.to_numpy() > 0.0, 0.25, 0.0)
    positive = sign.to_numpy() > 0.0
    counts = positive.sum(axis=1, keepdims=True)
    # 摊回只作为对照：正号因子平分 1。全负的月份权重为 0。
    renorm = np.divide(
        positive.astype(float),
        counts,
        out=np.zeros(panel.shape, dtype=float),
        where=counts > 0,
    )
    returns = panel.to_numpy()
    base = pd.Series(np.sum(base_w * returns, axis=1), index=panel.index)
    timed = pd.Series(np.sum(cash_w * returns, axis=1), index=panel.index)
    scaled = pd.Series(np.sum(renorm * returns, axis=1), index=panel.index)
    for name, series in ("equal", base), ("cash", timed), ("renorm", scaled):
        ann, sharpe, _ = performance(series)
        y2022 = compound(series.loc["2022-01":"2022-12"])
        print(name, f"{ann:.6f}", f"{sharpe:.4f}", f"{y2022:.6f}")

    turnover = 0.5 * np.abs(np.diff(cash_w, axis=0)).sum(axis=1)
    print("cash_one_way_month", f"{float(turnover.mean()):.6f}")
    annual_one_way = float(turnover.mean()) * 12.0
    drag = 2.0 * annual_one_way * 0.0015
    print("cash_one_way_year", f"{annual_one_way:.6f}", "drag_15bps", f"{drag:.6f}")
    print("cash_gross", f"{float(cash_w.sum(axis=1).mean()):.6f}")
    print("months_all_cash", int(np.sum(cash_w.sum(axis=1) == 0.0)))

    pair = data[["HML", "Mom"]].dropna()
    corr = float(pair["HML"].corr(pair["Mom"]))
    half = 0.5 * pair["HML"] + 0.5 * pair["Mom"]
    ann, sharpe, _ = performance(half)
    vol = float(half.std(ddof=1) * math.sqrt(12.0))
    print("hml_mom", pair.index.min().date(), pair.index.max().date(), len(pair))
    print("corr", f"{corr:.6f}", "ann", f"{ann:.6f}", "vol", f"{vol:.6f}", "sr", f"{sharpe:.4f}")

    for corr_ab in (-0.2, 0.4):
        vol_p = port_vol(0.60, 0.16, 0.06, corr_ab)
        print("sixty_forty", corr_ab, f"{vol_p:.6f}")
    inv = np.array([1.0 / 0.16, 1.0 / 0.06])
    print("inv_vol", " ".join(f"{w:.4f}" for w in inv / inv.sum()))

    start = np.array([0.60, 0.40])
    grown = start * np.array([1.30, 1.0])
    nav = float(grown.sum())
    drifted = grown / nav
    print("drift_stock", f"{drifted[0]:.6f}", "gap_pp", f"{(drifted[0] - 0.60) * 100:.4f}")
    target = np.array([0.60, 0.40]) * nav
    traded = float(np.abs(target - grown).sum())
    one_way = traded / 2.0 / nav
    print("rebalance_one_way", f"{one_way:.6f}", "nav", f"{nav:.4f}")
    # 偏离超过 5 个百分点才交易。涨 30% 之后的缺口大于这条带。
    gap = abs(float(drifted[0]) - 0.60)
    print("band_trigger", gap > 0.05, "gap", f"{gap:.6f}")
    print("breakeven_15", f"{0.0015 / (2.0 * 0.0015):.4f}")
    print("breakeven_25", f"{0.0015 / (2.0 * 0.0025):.4f}")

    active = 0.30 * 0.015
    tracking = 0.30 * 0.05
    print(
        "sleeve_excess", f"{active:.6f}",
        "te", f"{tracking:.6f}",
        "ir", f"{active / tracking:.4f}",
    )
    print("fee_on_nav", f"{0.70 * 0.0015:.6f}")
    print("ir_one_bet", f"{0.05 * math.sqrt(1.0):.6f}")
    print("ir_twelve", f"{0.05 * math.sqrt(12.0):.6f}")

    fig, axes = plt.subplots(1, 2, figsize=(11.2, 4.8))
    ax = axes[0]
    labels = ["Mkt-RF", "HML", "Mom", "CMA", "RMW"]
    heights = [compound(year[name]) * 100.0 for name in labels]
    colors = ["#a33b3b" if value < 0 else "#1f4e79" for value in heights]
    ax.bar(labels, heights, color=colors)
    ax.axhline(0.0, color="#333333", linewidth=0.8)
    ax.set_ylabel("2022 年连乘（%）")
    ax.set_title("French 月频连乘，不是基金净值")
    ax = axes[1]
    vols = [port_vol(0.60, 0.16, 0.06, corr_ab) * 100.0 for corr_ab in (-0.2, 0.4)]
    ax.bar(["相关 −0.2", "相关 +0.4"], vols, color=["#5b8e7d", "#c4a35a"])
    ax.set_ylabel("组合波动（%）")
    ax.set_title("股票 16%、债券 6%、权重 60/40")
    fig.suptitle("M17：因子连乘与假设相关；右图不是 2022 年的净值")
    fig.tight_layout()
    fig.savefig(OUT, dpi=140)
    print("wrote", OUT)


if __name__ == "__main__":
    main()
