#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""两融杠杆「市场择时」研究 —— A股整体两融余额滚动百分位 → 沪深300 前向收益 + 择时叠加。

与横截面两融报告(margin-signals / margin-crowding, 都是"哪些股票躲")互补:
本文是**总量市场择时**(整个市场何时贵/便宜)。

核心结论(沪深300, 2018-2026, 两融滚动 250 日百分位):
  强不对称 ——
  · 买信号强: 两融≤20 分位后前向 60 日 +7.2%、胜率 70%(仅在去杠杆 washout 出现)。
  · 卖信号弱: 80-100 分位桶前向 60 日仅 +1.1%(非单调, 只有极端≥95 的 fwd20 才略负)
    → 高杠杆是"风险旗"不是做空信号。
  · 但作为风控叠加有效: 只在两融<90 分位持有沪深300, 夏普 0.22→0.94、MDD −46%→−29%;
    <50 分位夏普 1.22。

数据:两融余额本地 my-app/public/data/macro.json(margin.total, 2018+);
      沪深300 收盘 tushare index_daily(env TUSHARE_TOKEN/TUSHARE_API_URL, 无硬编码)。
口径:滚动百分位=过去 250 交易日当前值分位;前向收益=未来 h 日收盘/今日−1;
      择时叠加=昨日两融<阈值分位则今日满仓沪深300, 否则持币(0 收益), 不含成本。
运行:cd /root/cb-allotment && PYTHONPATH=scripts python3 my-app/public/reports/margin-timing/backtest.py
"""
import os
import json
import time

import numpy as np
import pandas as pd
import tushare as ts

WIN = 250


def load_hs300():
    ts.set_token(os.environ["TUSHARE_TOKEN"])
    pro = ts.pro_api(os.environ["TUSHARE_TOKEN"])
    pro._DataApi__token = os.environ["TUSHARE_TOKEN"]
    pro._DataApi__http_url = os.environ["TUSHARE_API_URL"]
    idx = None
    for _ in range(4):
        try:
            idx = pro.index_daily(ts_code="000300.SH", start_date="20180101", end_date="20260701")
            if idx is not None and len(idx):
                break
        except Exception:
            time.sleep(3)
    idx = idx[["trade_date", "close"]].sort_values("trade_date").reset_index(drop=True)
    idx["dt"] = pd.to_datetime(idx["trade_date"], format="%Y%m%d")
    return idx


def main():
    root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
    m = json.load(open(os.path.join(root, "my-app/public/data/macro.json")))
    mg = pd.DataFrame({"dt": pd.to_datetime(m["margin"]["dates"]), "bal": m["margin"]["total"]})
    mg["pct"] = mg["bal"].rolling(WIN, min_periods=120).apply(lambda x: (x.iloc[-1] >= x).mean() * 100, raw=False)

    idx = load_hs300().merge(mg[["dt", "pct"]], on="dt", how="left")
    idx["pct"] = idx["pct"].ffill()
    idx["ret"] = idx["close"].pct_change()
    for h in (20, 60):
        idx[f"fwd{h}"] = idx["close"].shift(-h) / idx["close"] - 1

    print("五分位桶 · 两融滚动百分位 → 沪深300 前向收益:")
    idx["bucket"] = pd.cut(idx["pct"], [0, 20, 40, 60, 80, 100], labels=["0-20", "20-40", "40-60", "60-80", "80-100"])
    for bk in ["0-20", "20-40", "40-60", "60-80", "80-100"]:
        sub = idx[idx["bucket"] == bk]
        r20, r60 = sub["fwd20"].dropna(), sub["fwd60"].dropna()
        print(f"  {bk:8} fwd20 {r20.mean()*100:+.2f}%(胜{(r20>0).mean()*100:.0f}%) "
              f"fwd60 {r60.mean()*100:+.2f}%(胜{(r60>0).mean()*100:.0f}%) n={len(r60)}")

    def timing(th):
        inpos = (idx["pct"] < th).astype(float).shift(1).fillna(0)
        tret = idx["ret"].fillna(0) * inpos
        tnav = (1 + tret).cumprod()
        bnav = (1 + idx["ret"].fillna(0)).cumprod()
        yrs = (idx["dt"].iloc[-1] - idx["dt"].iloc[0]).days / 365.25

        def stat(nav, r):
            cagr = nav.iloc[-1] ** (1 / yrs) - 1
            sh = r.mean() / r.std() * np.sqrt(244) if r.std() > 0 else 0
            mdd = (nav / nav.cummax() - 1).min()
            return cagr * 100, sh, mdd * 100

        tc, tsh, tm = stat(tnav, tret[inpos > 0])
        bc, bsh, bm = stat(bnav, idx["ret"].dropna())
        return tc, tsh, tm, bc, bsh, bm, inpos.mean() * 100

    print("\n择时叠加(只在两融<阈值分位持有沪深300, 否则持币) vs 买入持有:")
    for th in (50, 80, 90):
        tc, tsh, tm, bc, bsh, bm, ip = timing(th)
        print(f"  <{th}分位: 择时 {tc:.1f}%/{tsh:.2f}/{tm:.0f}% 在场{ip:.0f}% | 买持 {bc:.1f}%/{bsh:.2f}/{bm:.0f}%")

    print("\n分年稳健性 · 两融≤20分位 出现天数 + 其 fwd60 均值:")
    idx["yr"] = idx["dt"].dt.year
    for yr, g in idx.groupby("yr"):
        lo = g[g["pct"] <= 20]["fwd60"].dropna()
        if len(lo):
            print(f"  {yr}: ≤20分位 n={len(lo):3} fwd60均 {lo.mean()*100:+.1f}%")

    # 跨指数稳健性: 低杠杆(≤20分位)买信号在中盘/小盘更强(需各拉一次 index_daily)
    print("\n跨指数 · 两融≤20分位 后前向 20/60 日(越小盘越强):")
    ts.set_token(os.environ["TUSHARE_TOKEN"])
    pro = ts.pro_api(os.environ["TUSHARE_TOKEN"])
    pro._DataApi__token = os.environ["TUSHARE_TOKEN"]
    pro._DataApi__http_url = os.environ["TUSHARE_API_URL"]
    for nm, code in [("沪深300", "000300.SH"), ("中证500", "000905.SH"), ("中证1000", "000852.SH")]:
        d = None
        for _ in range(4):
            try:
                d = pro.index_daily(ts_code=code, start_date="20180101", end_date="20260701")
                if d is not None and len(d):
                    break
            except Exception:
                time.sleep(3)
        d = d[["trade_date", "close"]].sort_values("trade_date").reset_index(drop=True)
        d["dt"] = pd.to_datetime(d["trade_date"], format="%Y%m%d")
        d = d.merge(mg[["dt", "pct"]], on="dt", how="left")
        d["pct"] = d["pct"].ffill()
        for h in (20, 60):
            d[f"f{h}"] = d["close"].shift(-h) / d["close"] - 1
        lo = d["pct"] <= 20
        r20, r60 = d.loc[lo, "f20"].dropna(), d.loc[lo, "f60"].dropna()
        print(f"  {nm:8} 前向20 {r20.mean()*100:+.2f}%(胜{(r20>0).mean()*100:.0f}%) "
              f"前向60 {r60.mean()*100:+.2f}%(胜{(r60>0).mean()*100:.0f}%)")


if __name__ == "__main__":
    main()
