# -*- coding: utf-8 -*-
"""传统封基时代(2004-2017)深折价策略回测 — 点位时点宇宙, 无幸存者偏差。

宇宙: tushare fund_basic market='E' 代码段 500xxx.SH/184xxx.SZ 共 54 只传统封基
(剔 184801 REITs), 全带 list_date/delist_date/due_date; 每个月初只用当时在市的。
净值周频(封基每周五公布) → 折价 = 场内价 / 最近一期净值 − 1 (staleness ≤5 交易日)。
策略: 月初按折价排序, 深 1/3 / 全部 / 浅 1/3 等权持有到下月, 场内价收益, 0.3%/边×换手。
基准: 沪深300(2005-04 起有日线); beta/alpha 回归 + 0.5x/1x 对冲(2010-04 IF 上市前无对冲工具, 诚实标注)。
数据缓存 research/cef_early_cache/ 免重复拉取。
"""
import json, os, time
import numpy as np
import pandas as pd
import tushare as ts

ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
os.chdir(ROOT)
CACHE = "research/cef_early_cache"
os.makedirs(CACHE, exist_ok=True)
pro = ts.pro_api(os.environ["TUSHARE_TOKEN"], timeout=90)
pro._DataApi__token = os.environ["TUSHARE_TOKEN"]
pro._DataApi__http_url = os.environ["TUSHARE_API_URL"]

START, END = "20040101", "20171231"
COST = 0.003
HEDGE_COST = 0.0  # 2010 前无工具; 2010 后 IF 基差成本低, 毛口径为主另注


def cached(name, fetch):
    fp = f"{CACHE}/{name}.csv"
    if os.path.exists(fp):
        return pd.read_csv(fp, dtype=str)
    for k in range(3):
        try:
            df = fetch()
            break
        except Exception as e:
            if k == 2:
                raise
            time.sleep(5)
    df.to_csv(fp, index=False)
    time.sleep(0.4)
    return pd.read_csv(fp, dtype=str)


fb = cached("fund_basic", lambda: pro.fund_basic(
    market="E", fields="ts_code,name,list_date,delist_date,due_date"))
trad = fb[fb["ts_code"].str.match(r"(500\d{3}\.SH|184\d{3}\.SZ)")]
trad = trad[trad["ts_code"] != "184801.SZ"]  # REITs 非传统封基
print(f"传统封基宇宙: {len(trad)} 只")

funds = {}
for _, f in trad.iterrows():
    c = f["ts_code"]
    try:
        d = cached(f"daily_{c}", lambda c=c: pro.fund_daily(
            ts_code=c, start_date=START, end_date=END, fields="trade_date,close"))
        n = cached(f"nav_{c}", lambda c=c: pro.fund_nav(
            ts_code=c, start_date=START, end_date=END, fields="nav_date,unit_nav"))
    except Exception as e:
        print(f"  {c} 拉取失败: {str(e)[:60]}")
        continue
    if d is None or n is None or len(d) < 60 or len(n) < 10:
        continue
    px = d.dropna().astype({"close": float}).sort_values("trade_date").set_index("trade_date")["close"]
    nav = n.dropna().astype({"unit_nav": float}).sort_values("nav_date").set_index("nav_date")["unit_nav"]
    funds[c] = {"px": px, "nav": nav,
                "list": f["list_date"], "delist": f["delist_date"] or "20991231"}
print(f"有价格+净值的: {len(funds)} 只")

# 沪深300
hs = cached("index_hs300", lambda: pro.index_daily(
    ts_code="000300.SH", start_date=START, end_date=END, fields="trade_date,close"))
hs = hs.dropna().astype({"close": float}).sort_values("trade_date").set_index("trade_date")["close"]
print(f"沪深300: {len(hs)} 日 ({hs.index.min()}~{hs.index.max()})")

# 月初交易日节点(用沪深300日历; 2004 年沪深300未发布, 用所有基金价格日期并集)
alld = sorted(set().union(*[set(f["px"].index) for f in funds.values()]))
months = pd.Series(alld).groupby(pd.Series(alld).str[:6]).first().tolist()
print(f"月初节点: {len(months)} ({months[0]}~{months[-1]})")


def disc_at(c, d):
    """折价 = 场内价(d) / 最近一期净值(≤d) − 1; 净值周频, 允许 ≤10 日 stale。"""
    f = funds[c]
    if d not in f["px"].index:
        return None
    navs = f["nav"][f["nav"].index <= d]
    if navs.empty or (pd.Timestamp(d) - pd.Timestamp(navs.index[-1])).days > 14:
        return None
    v = navs.iloc[-1]
    return (f["px"].loc[d] / v - 1) * 100 if v > 0 else None


def px_at(c, d):
    f = funds[c]
    return f["px"].loc[d] if d in f["px"].index else None


def alive(c, d):
    f = funds[c]
    return f["list"] <= d < f["delist"]


def run(bucket, cost=COST):
    rets, dates, prev, nsel = [], [], set(), []
    for i in range(len(months) - 1):
        d0, d1 = months[i], months[i + 1]
        cand = [(c, disc_at(c, d0)) for c in funds
                if alive(c, d0) and alive(c, d1)]
        cand = [(c, x) for c, x in cand if x is not None and px_at(c, d0) and px_at(c, d1)]
        if len(cand) < 9:
            continue
        cand.sort(key=lambda x: x[1])
        n = len(cand) // 3
        if bucket == "deep":
            sel = set(c for c, _ in cand[:n])
        elif bucket == "shallow":
            sel = set(c for c, _ in cand[-n:])
        else:
            sel = set(c for c, _ in cand)
        r = float(np.mean([px_at(c, d1) / px_at(c, d0) - 1 for c in sel]))
        turn = len(sel ^ prev) / max(len(sel), 1)
        r -= cost * turn
        prev = sel
        rets.append(r); dates.append(d1); nsel.append(len(cand))
    return pd.Series(rets, index=dates), np.mean(nsel)


def stats(rets, label):
    rets = np.asarray(rets, dtype=float)
    nav = np.cumprod(1 + rets)
    yrs = len(rets) / 12
    cagr = (nav[-1] ** (1 / yrs) - 1) * 100
    sharpe = rets.mean() / rets.std() * np.sqrt(12) if rets.std() > 0 else 0
    mdd = ((nav / np.maximum.accumulate(nav)) - 1).min() * 100
    print(f"  {label:24s} CAGR {cagr:+6.1f}%  Sharpe {sharpe:5.2f}  MDD {mdd:6.1f}%  ({len(rets)}月)")
    return dict(cagr=round(float(cagr), 1), sharpe=round(float(sharpe), 2),
                mdd=round(float(mdd), 1), nav=[round(float(x), 4) for x in nav])


print("\n=== 传统封基时代(含0.3%/边成本) ===")
deep, avg_n = run("deep")
sh_, _ = run("shallow")
al_, _ = run("all")
print(f"平均可选池 {avg_n:.0f} 只/月")
out = {"months": deep.index.tolist(), "series": {}}
out["series"]["deep"] = stats(deep.values, "折价最深1/3")
out["series"]["all"] = stats(al_.reindex(deep.index).fillna(0).values, "全部等权")
out["series"]["shallow"] = stats(sh_.reindex(deep.index).fillna(0).values, "折价最浅1/3")

# 基准月收益(同节点)
def idx_ret(d0, d1):
    s = hs[(hs.index >= d0) & (hs.index <= d1)]
    return float(s.iloc[-1] / s.iloc[0] - 1) if len(s) >= 2 else None


bench = []
for i in range(len(months) - 1):
    d1 = months[i + 1]
    if d1 in deep.index:
        bench.append((d1, idx_ret(months[i], d1)))
bench = pd.Series(dict(bench)).dropna()
common = deep.index.intersection(bench.index)
db, bb = deep[common].values, bench[common].values
out["series"]["hs300"] = stats(bb, "沪深300(同窗)")
out["series"]["deep_common"] = stats(db, "深组(沪深300同窗)")

beta = np.cov(db, bb)[0, 1] / np.var(bb)
alpha_m = db.mean() - beta * bb.mean()
resid = db - (alpha_m + beta * bb)
t = alpha_m / (resid.std(ddof=2) / np.sqrt(len(db)))
print(f"\n回归 vs 沪深300: beta {beta:.2f}  年化alpha {((1+alpha_m)**12-1)*100:+.1f}%  t={t:.2f}  R²={1-resid.var()/np.var(db):.2f}")
out["reg"] = {"beta": round(float(beta), 2), "alpha_ann": round(float(((1 + alpha_m) ** 12 - 1) * 100), 1),
              "t": round(float(t), 2), "r2": round(float(1 - resid.var() / np.var(db)), 2)}

print("\n=== 对冲(毛; IF 2010-04 上市, 之前无工具) ===")
for h in (0.5, 1.0):
    out["hedged"] = out.get("hedged", {})
    out["hedged"][f"h{h}"] = stats(db - h * bb, f"0.5x1x对冲 ×{h}")
json.dump(out, open("research/cef_early_backtest.json", "w"), ensure_ascii=False)
print("\nDONE -> research/cef_early_backtest.json")
