"""M14 教学图：Brinson 三项，以及 t = 2 所需的年数。

左图是给定的两组权重和收益。右图是 t = IR × √年数，残差按独立同分布。
"""

from __future__ import annotations

import math
from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

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

OUT = Path(__file__).resolve().parent / "brinson_and_years.png"


def brinson(wp: np.ndarray, wb: np.ndarray, rp: np.ndarray, rb: np.ndarray) -> dict[str, float]:
    port = float(wp @ rp)
    bench = float(wb @ rb)
    active_w = wp - wb
    # Fachler 用行业收益相对基准总收益。Hood 用行业收益本身。权重和相等时，两项的合计相同。
    alloc_bf = float(active_w @ (rb - bench))
    alloc_bhb = float(active_w @ rb)
    selection = float(wb @ (rp - rb))
    interaction = float(active_w @ (rp - rb))
    return {
        "port": port,
        "bench": bench,
        "active": port - bench,
        "alloc_bf": alloc_bf,
        "alloc_bhb": alloc_bhb,
        "selection": selection,
        "interaction": interaction,
        "sum_bf": alloc_bf + selection + interaction,
    }


def carino_link(port: np.ndarray, bench: np.ndarray) -> None:
    """对数收益差除以简单收益差。先连乘再相减，才和各期主动收益的加权和相等。"""

    def gap_log(left: float, right: float) -> float:
        return math.log((1.0 + left) / (1.0 + right)) / (left - right)

    total_p = float(np.prod(1.0 + port) - 1.0)
    total_b = float(np.prod(1.0 + bench) - 1.0)
    scale = gap_log(total_p, total_b)
    linked = [
        gap_log(float(p), float(b)) / scale * (float(p) - float(b))
        for p, b in zip(port, bench, strict=True)
    ]
    print("carino", f"{scale:.6f}", " ".join(f"{item:.6f}" for item in linked))
    print("carino_sum", f"{sum(linked):.6f}", "compound_active", f"{total_p - total_b:.6f}")


def constrained_style(style_a: np.ndarray, style_b: np.ndarray, target: np.ndarray) -> None:
    """系数非负且和为 1。解在这条线段上，不是先回归再把权重归一。"""
    diff = style_a - style_b
    beta = float((target - style_b) @ diff / (diff @ diff))
    beta = min(1.0, max(0.0, beta))
    fitted = beta * style_a + (1.0 - beta) * style_b
    sse = float(np.sum((target - fitted) ** 2))
    naive = np.array([2.0 / 3.0, 1.0 / 3.0])
    naive_fit = naive[0] * style_a + naive[1] * style_b
    naive_sse = float(np.sum((target - naive_fit) ** 2))
    print("style_beta", f"{beta:.6f}", f"{1.0 - beta:.6f}", "sse", f"{sse:.8f}")
    print("naive_sse", f"{naive_sse:.8f}")


def ols(y: np.ndarray, x: np.ndarray) -> None:
    n, k = x.shape
    beta = np.linalg.lstsq(x, y, rcond=None)[0]
    resid = y - x @ beta
    df = n - k
    sigma2 = float(resid @ resid) / df
    xtx_inv = np.linalg.inv(x.T @ x)
    se = math.sqrt(sigma2 * float(xtx_inv[0, 0]))
    t_stat = float(beta[0]) / se
    print("alpha_m", f"{float(beta[0]):.6f}", "ann", f"{float(beta[0]) * 12:.4f}")
    print("betas", " ".join(f"{float(b):.4f}" for b in beta[1:]))
    print("t_alpha", f"{t_stat:.3f}", "df", df, "sigma", f"{math.sqrt(sigma2):.6f}")


def main() -> None:
    full = brinson(
        np.array([0.80, 0.20]),
        np.array([0.60, 0.40]),
        np.array([0.12, 0.01]),
        np.array([0.10, 0.02]),
    )
    print("full", {key: f"{value:.4f}" for key, value in full.items()})
    partial = brinson(
        np.array([0.40, 0.20]),
        np.array([0.60, 0.40]),
        np.array([0.12, 0.01]),
        np.array([0.10, 0.02]),
    )
    print("partial", {key: f"{value:.4f}" for key, value in partial.items()})
    for ir in (0.25, 0.50, 0.60, 0.75, 1.00):
        print("years_t2", f"{ir:.2f}", f"{(2.0 / ir) ** 2:.3f}")
        print("years_t196", f"{ir:.2f}", f"{(1.96 / ir) ** 2:.3f}")

    mkt = np.array([0.02, -0.01, 0.03, 0.00])
    hml = np.array([0.01, 0.02, -0.01, 0.02])
    design = np.column_stack([np.ones(4), mkt, hml])
    noise = np.array([0.004, -0.002, 0.001, -0.003])
    # 先从噪声里去掉能被常数项和因子解释的部分，使回归系数保持手写的 0.001、1、0.5。
    noise = noise - design @ np.linalg.lstsq(design, noise, rcond=None)[0]
    port = 0.001 + mkt + 0.5 * hml + noise
    ols(port, design)
    carino_link(np.array([0.098, 0.025]), np.array([0.068, 0.020]))
    style_a = np.array([0.02, -0.01, 0.03, 0.00])
    style_b = np.array([0.01, 0.02, -0.01, 0.01])
    constrained_style(style_a, style_b, style_a + 0.5 * style_b)

    fig, axes = plt.subplots(1, 2, figsize=(11.2, 4.8))
    ax = axes[0]
    labels = ["配置", "选择", "交互"]
    values = [full["alloc_bf"], full["selection"], full["interaction"]]
    ax.bar(labels, [v * 100 for v in values], color=["#1f4e79", "#5b8e7d", "#c4a35a"])
    ax.set_ylabel("对主动收益的贡献（百分点）")
    ax.set_title("两组都满仓时，三项合计 3.00 个百分点")
    ax = axes[1]
    irs = np.linspace(0.25, 1.0, 60)
    ax.plot(irs, (2.0 / irs) ** 2, color="#1f4e79", label="t = 2")
    ax.plot(irs, (1.96 / irs) ** 2, color="#a33b3b", label="t = 1.96")
    ax.scatter([0.60], [(2.0 / 0.60) ** 2], color="#1f4e79", zorder=3)
    ax.set_xlabel("年化信息比率")
    ax.set_ylabel("所需年数")
    ax.set_title("残差独立同分布时，t = IR × √年数")
    ax.legend(frameon=False)
    fig.suptitle("M14 归因与年数：公式，不是基金业绩")
    fig.tight_layout()
    fig.savefig(OUT, dpi=140)
    print("wrote", OUT)


if __name__ == "__main__":
    main()
