#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""turn-of-month(月末月初效应)A股 7 宽基实测 —— 报告复现脚本。

口径(与报告一致):
  样本 2018-01-01 ~ 至今;7 个宽基指数;不含成本(择时换手极低)。
  窗口(TOM) = 每月「最后 1 个交易日」+ 下月「头 3 个交易日」。
  择时 = 仅在窗口内持有指数、窗口外持币(日收益 0)。
  买入持有 = 全程持有指数。
  年化 = nav^(252/交易日数)-1;夏普 = 日收益均值/标准差×√252(含窗口外的 0);
  回撤 = min(nav/累计峰值 - 1)。窗口内/外日均 = 各自子集的日收益均值。

数据源:tushare index_daily(env TUSHARE_TOKEN / TUSHARE_API_URL,无硬编码)。
运行:cd /root/cb-allotment && PYTHONPATH=scripts python3 my-app/public/reports/turn-of-month/backtest.py

复现勘误(2026-07):本脚本重建时发现,报告初版「创业板指」一行误用了 2015 起始
(其余 6 个宽基均 2018 起),导致 12.9%/夏普0.88/买持回撤−70% 等数字与「样本 2018–2026」
口径不一致。本脚本统一 2018 起,创业板正确值为:窗口内 +0.187%、窗外 +0.029%、
择时 8.5%/0.61/−22%、买持 11.7%/0.53/−57%(窗外并非≈0,是 7 个宽基里月历效应最弱的)。
报告表格与结论已据此更正。其余 6 个宽基与报告初版吻合(微差来自数据更新)。
"""
import os
import numpy as np
import pandas as pd
import tushare as ts

TOKEN = os.environ["TUSHARE_TOKEN"]
API_URL = os.environ["TUSHARE_API_URL"]
ts.set_token(TOKEN)
pro = ts.pro_api(TOKEN)
pro._DataApi__token = TOKEN
pro._DataApi__http_url = API_URL

START, END = "20180101", "20260701"
INDICES = [
    ("沪深300", "000300.SH", "大盘"),
    ("中证500", "000905.SH", "中盘"),
    ("中证1000", "000852.SH", "小盘"),
    ("中证2000", "932000.CSI", "微盘"),
    ("中证全指", "000985.CSI", "全A"),
    ("创业板指", "399006.SZ", "成长"),
    ("中证红利", "000922.CSI", "价值"),
]
TOM_HEAD = 3  # 月初头 N 个交易日


def load(code):
    df = pro.index_daily(ts_code=code, start_date=START, end_date=END)
    if df is None or not len(df):
        return None
    df = df[["trade_date", "close"]].dropna().sort_values("trade_date").reset_index(drop=True)
    df["dt"] = pd.to_datetime(df["trade_date"], format="%Y%m%d")
    df["ret"] = df["close"].pct_change()
    return df.dropna().reset_index(drop=True)


def tom_mask(df):
    """窗口内=每月最后 1 个交易日 或 每月头 3 个交易日。"""
    ym = df["dt"].dt.to_period("M")
    mask = np.zeros(len(df), dtype=bool)
    for _, idx in df.groupby(ym).groups.items():
        idx = list(idx)
        for k in idx[:TOM_HEAD]:
            mask[k] = True          # 月初头 3
        mask[idx[-1]] = True        # 月末最后 1
    return mask


def stats(nav, rets_for_sharpe):
    nav = np.asarray(nav, float)
    n = len(nav) - 1
    cagr = nav[-1] ** (252.0 / n) - 1 if n > 0 and nav[-1] > 0 else float("nan")
    r = np.asarray(rets_for_sharpe, float)
    sharpe = r.mean() / r.std() * np.sqrt(252) if r.std() > 0 else float("nan")
    peak = np.maximum.accumulate(nav)
    mdd = (nav / peak - 1).min()
    return cagr * 100, sharpe, mdd * 100


def main():
    print(f"{'指数':8} {'窗口内日均':>9} {'窗外日均':>9} | "
          f"{'择时CAGR':>8} {'夏普':>5} {'回撤':>6} | {'买持CAGR':>8} {'夏普':>5} {'回撤':>6}")
    for nm, code, tag in INDICES:
        df = load(code)
        if df is None:
            print(f"{nm:8} 无数据"); continue
        m = tom_mask(df)
        ret = df["ret"].values
        in_avg = ret[m].mean() * 100
        out_avg = ret[~m].mean() * 100
        # 择时:窗口外收益置 0
        tret = np.where(m, ret, 0.0)
        tnav = np.concatenate([[1.0], np.cumprod(1 + tret)])
        bnav = np.concatenate([[1.0], np.cumprod(1 + ret)])
        tc, ts_, tm = stats(tnav, tret)
        bc, bs, bm = stats(bnav, ret)
        print(f"{nm:8} {in_avg:>+8.3f}% {out_avg:>+8.3f}% | "
              f"{tc:>7.1f}% {ts_:>5.2f} {tm:>5.0f}% | {bc:>7.1f}% {bs:>5.2f} {bm:>5.0f}%")


if __name__ == "__main__":
    main()
