# -*- coding: utf-8 -*-
"""现代封基/定开时代(2017-2026)深折价策略回测 — 点位时点宇宙。

宇宙: 场内权益基金现名含 定开/封闭/持有 特征 89 只(2022前上市, 剔分级/ETF/FOF/债)
+ 2018 战略配售六只(转型改名逃过名字过滤, 手动找回: 161131/501189/161728/160142/501186/501188)。
已知局限: 其它转型改名的历史封基无法系统找回(fund_basic 无名字史), 候选池偏少、结论偏保守。
定开净值日频(LOF 披露); 折价=场内价/最近净值−1(容忍≤7日)。
策略同证据三/五: 月初深 1/3 等权持有到下月, 0.3%/边×换手; 基准沪深300, beta/alpha+0.5x/1x对冲。
缓存 research/cef_modern_cache/。
"""
import json, os, sys, 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_modern_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 = "20170101", "20260706"
COST = 0.003
STRATEGIC = ["161131.SZ", "501189.SH", "161728.SZ", "160142.SZ", "501186.SH", "501188.SH"]


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:
            if k == 2:
                raise
            time.sleep(5)
    df.to_csv(fp, index=False)
    time.sleep(0.3)
    return pd.read_csv(fp, dtype=str)


fb = pd.read_csv("research/cef_early_cache/fund_basic_full.csv", dtype=str)
eq = fb[fb["fund_type"].isin(["股票型", "混合型"])]
pat = eq["name"].str.contains("定开|定期开放|封闭|战略配售|配售|三年持有|两年持有", na=False)
bad = eq["name"].str.contains("分级|ETF|FOF|债|货币", na=False)
cand = eq[pat & ~bad]
cand = cand[cand["list_date"].fillna("9") < "20220101"]
codes = sorted(set(cand["ts_code"]) | set(STRATEGIC))
meta = fb.set_index("ts_code")
print(f"宇宙: {len(codes)} 只", flush=True)

funds = {}
for i, c in enumerate(codes):
    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)[:50]}", flush=True)
        continue
    if d is None or n is None or len(d) < 120 or len(n) < 30:
        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"]
    m = meta.loc[c]
    funds[c] = {"px": px, "nav": nav,
                "list": m["list_date"] if pd.notna(m["list_date"]) else "20170101",
                "delist": m["delist_date"] if pd.notna(m["delist_date"]) else "20991231"}
    if (i + 1) % 20 == 0:
        print(f"  进度 {i+1}/{len(codes)}", flush=True)
print(f"有数据: {len(funds)} 只", flush=True)

hs = cached("index_hs300_17", 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"]

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]})", flush=True)


def disc_at(c, d):
    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 > 7:
        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):
    rets, dates, prev, pool = [], [], 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
        sel = set(c for c, _ in (cand[:n] if bucket == "deep" else cand[-n:] if bucket == "shallow" else cand))
        r = float(np.mean([px_at(c, d1) / px_at(c, d0) - 1 for c in sel]))
        r -= COST * (len(sel ^ prev) / max(len(sel), 1))
        prev = sel
        rets.append(r); dates.append(d1); pool.append(len(cand))
    return pd.Series(rets, index=dates), np.mean(pool)


def stats(r, label):
    r = np.asarray(r, dtype=float)
    nav = np.cumprod(1 + r)
    yrs = len(r) / 12
    cagr = (nav[-1] ** (1 / yrs) - 1) * 100
    sharpe = r.mean() / r.std() * np.sqrt(12)
    mdd = ((nav / np.maximum.accumulate(nav)) - 1).min() * 100
    print(f"  {label:26s} CAGR {cagr:+6.1f}%  Sharpe {sharpe:5.2f}  MDD {mdd:6.1f}%  ({len(r)}月)", flush=True)
    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=== 2017-2026 现代封基/定开(含0.3%/边) ===", flush=True)
deep, avg_pool = run("deep")
al_, _ = run("all")
sh_, _ = run("shallow")
print(f"平均候选池 {avg_pool:.0f} 只/月", flush=True)
out = {"months": deep.index.tolist(), "avg_pool": round(float(avg_pool)), "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 = pd.Series({months[i + 1]: idx_ret(months[i], months[i + 1])
                   for i in range(len(months) - 1)}).dropna()
common = deep.index.intersection(bench.index)
db, bb = deep[common].values, bench[common].values
out["series"]["hs300"] = stats(bb, "沪深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)))
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(f"\n回归 vs 沪深300: beta {out['reg']['beta']} alpha {out['reg']['alpha_ann']:+.1f}%/年 t={out['reg']['t']} R²={out['reg']['r2']}", flush=True)

print("\n=== 对冲(毛 / 净扣3%/年: 2017+可用IF/IM基差近似) ===", flush=True)
out["hedged"] = {}
for h in (0.5, 1.0):
    out["hedged"][f"h{h}"] = stats(db - h * bb, f"对冲沪深300 ×{h} 毛")
    out["hedged"][f"h{h}_net"] = stats(db - h * bb - 0.03 * h / 12, f"对冲 ×{h} 净(扣3%基差)")
json.dump(out, open("research/cef_modern_backtest.json", "w"), ensure_ascii=False)
print("\nDONE", flush=True)
