"""M05 教学图：Kenneth French 月频三因子的累计收益与 HML 回撤。

只解析月频块。文件后半的年度块是另一套构造，不能和月频连乘。
原始 zip 缓存在 _cache，不入库。
"""

from __future__ import annotations

import io
import math
import urllib.request
import zipfile
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

URL = (
    "https://mba.tuck.dartmouth.edu/pages/faculty/ken.french/ftp/"
    "F-F_Research_Data_Factors_CSV.zip"
)
CACHE = Path(__file__).resolve().parent / "_cache" / "F-F_Research_Data_Factors_CSV.zip"
OUT = Path(__file__).resolve().parent / "ff_premia.png"


def load_monthly() -> pd.DataFrame:
    if not CACHE.exists():
        CACHE.parent.mkdir(parents=True, exist_ok=True)
        CACHE.write_bytes(urllib.request.urlopen(URL, timeout=60).read())
    raw = zipfile.ZipFile(CACHE).read("F-F_Research_Data_Factors.csv")
    lines = raw.decode("latin1").splitlines()
    start = next(i for i, line in enumerate(lines) if line.startswith(",Mkt-RF"))
    rows: list[list[str]] = []
    for line in lines[start + 1 :]:
        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=["date", "Mkt-RF", "SMB", "HML", "RF"])
    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 classical(series: pd.Series) -> dict[str, float]:
    n = len(series)
    mean = float(series.mean())
    std = float(series.std(ddof=1))
    sharpe = mean / std * math.sqrt(12.0)
    t_stat = mean / (std / math.sqrt(n))
    return {
        "n": float(n),
        "ann": mean * 12.0,
        "vol": std * math.sqrt(12.0),
        "sharpe": sharpe,
        "t": t_stat,
    }


def newey_west_t(series: pd.Series) -> tuple[int, float]:
    """Bartlett 核。滞后用 floor(4*(T/100)^(2/9))。点估计仍是样本均值。"""
    values = series.to_numpy(dtype=float)
    n = len(values)
    mean = float(values.mean())
    demeaned = values - mean
    lag = int(math.floor(4.0 * (n / 100.0) ** (2.0 / 9.0)))
    gamma0 = float(demeaned @ demeaned) / n
    hac = gamma0
    for k in range(1, lag + 1):
        gamma = float(demeaned[k:] @ demeaned[:-k]) / n
        hac += 2.0 * (1.0 - k / (lag + 1.0)) * gamma
    se = math.sqrt(hac / n)
    return lag, mean / se


def toy_capm() -> None:
    market = np.array([0.02, -0.01, 0.03, -0.04])
    stock = np.array([0.04, -0.035, 0.045, -0.05])
    x = market - market.mean()
    y = stock - stock.mean()
    beta = float(np.dot(x, y) / np.dot(x, x))
    alpha = float(stock.mean() - beta * market.mean())
    resid = stock - (alpha + beta * market)
    sigma2 = float(resid @ resid) / (len(stock) - 2)
    se_b = math.sqrt(sigma2 / float(x @ x))
    se_a = math.sqrt(sigma2 * (1.0 / len(stock) + market.mean() ** 2 / float(x @ x)))
    print("toy_beta", beta, "toy_alpha", alpha, "t_beta", beta / se_b, "t_alpha", alpha / se_a)


def sdf_check() -> None:
    m = np.array([1.2, 0.8])
    r = np.array([0.8, 1.3])
    prob = 0.5
    e_m = float(prob * m.sum())
    e_r = float(prob * r.sum())
    cov = float(prob * (m * r).sum() - e_m * e_r)
    rf = 1.0 / e_m
    premium = -rf * cov
    print("sdf_premium", premium, "direct", e_r - rf, "cov", cov)


def main() -> None:
    toy_capm()
    sdf_check()
    factors = load_monthly()
    print(
        "sample",
        factors.index.min().date(),
        factors.index.max().date(),
        "n",
        len(factors),
    )
    windows = {
        "full": factors,
        "y1963_1990": factors.loc["1963-07":"1990-12"],
        "y1991_2025": factors.loc["1991-01":"2025-12"],
    }
    for name, frame in windows.items():
        print("WINDOW", name, frame.index.min().date(), frame.index.max().date())
        for column in ("Mkt-RF", "SMB", "HML"):
            stats = classical(frame[column])
            lag, t_nw = newey_west_t(frame[column])
            print(
                column,
                "ann", round(stats["ann"], 4),
                "vol", round(stats["vol"], 4),
                "sharpe", round(stats["sharpe"], 2),
                "t", round(stats["t"], 2),
                "nw_lag", lag,
                "t_nw", round(t_nw, 2),
                "ac1", round(float(frame[column].autocorr(1)), 3),
            )
    market = factors["Mkt-RF"] + factors["RF"]
    years = len(market) / 12.0
    cagr = float((1.0 + market).prod() ** (1.0 / years) - 1.0)
    print("market_cagr", round(cagr, 4), "mkt_arith", round(float(market.mean() * 12), 4))
    y2022 = factors.loc["2022"]
    for column in ("Mkt-RF", "SMB", "HML"):
        print("c2022", column, round(float((1.0 + y2022[column]).prod() - 1.0), 4))
    print("c2022_market", round(float((1.0 + y2022["Mkt-RF"] + y2022["RF"]).prod() - 1.0), 4))

    hml_nav = (1.0 + factors["HML"]).cumprod()
    drawdown = hml_nav / hml_nav.cummax() - 1.0
    trough = drawdown.idxmin()
    peak = hml_nav.loc[:trough].idxmax()
    print(
        "hml_mdd", round(float(drawdown.min()), 4),
        "peak", peak.date(),
        "trough", trough.date(),
    )

    fig, axes = plt.subplots(1, 2, figsize=(11.2, 4.6))
    ax = axes[0]
    ax.plot((1.0 + market).cumprod(), color="#6c757d", lw=1.2, label="市场总收益")
    ax.plot((1.0 + factors["SMB"]).cumprod(), color="#2a9d8f", lw=1.2, label="SMB 自融资")
    ax.plot(hml_nav, color="#1f4e79", lw=1.5, label="HML 自融资")
    ax.set_yscale("log")
    ax.set_title("月频连乘，未扣交易成本")
    ax.set_ylabel("累计倍数（对数刻度）")
    ax.legend(frameon=False, fontsize=8)
    ax.grid(True, alpha=0.3)

    ax = axes[1]
    ax.fill_between(drawdown.index, drawdown.to_numpy() * 100.0, color="#1f4e79", alpha=0.35)
    ax.plot(drawdown.index, drawdown.to_numpy() * 100.0, color="#1f4e79", lw=1.0)
    marks = [0.0, float(drawdown.min()) * 100.0]
    ax.scatter([peak, trough], marks, color="#c1121f", s=18, zorder=3)
    ax.set_title("HML 自融资组合的回撤")
    ax.set_ylabel("相对前高（%）")
    ax.grid(True, alpha=0.3)
    fig.suptitle("M05 Kenneth French 三因子（月频研究序列）", fontsize=12)
    fig.tight_layout()
    fig.savefig(OUT, dpi=140, bbox_inches="tight")
    print("wrote", OUT)


if __name__ == "__main__":
    main()
