# -*- coding: utf-8 -*-
"""LOF折溢价"打地鼠"证伪回测: 震荡类基金(非慢性溢价的美股, 非stale-NAV污染)
折价买入/溢价卖出 vs 买入持有。用场内价日收益(你实际能吃的), 溢价=场内价/最近公告净值-1。
每笔0.15%(LOF场内低佣往返~0.3%)。窗口2025-04~2026-06(数据所限, 单regime)。"""
import json, glob, os, numpy as np, pandas as pd
D="/root/cb-allotment/my-app/public/data/lof"
names={f['code']:f['name'] for f in json.load(open(f"{D}/qdii_estimation_map.json"))['funds']}
def is_osc(nm):  # 震荡候选: 排除慢性高溢价美股指数
    if any(k in nm for k in ['纳','标普','美国','道琼','信息科技','纳斯达克']): return False
    return any(k in nm for k in ['中概','互联网','港','黄金','油','原油','能源','商品','印度','REIT','医疗','消费','生物','抗通胀','新兴','亚太','东南亚'])
# 建 溢价 + 场内价日收益
prem={}; pxret={}
for navf in glob.glob(f"{D}/*_nav.csv"):
    code=os.path.basename(navf).split("_")[0]; nm=names.get(code,code)
    if not is_osc(nm): continue
    dailyf=f"{D}/{code}_daily.csv"
    if not os.path.exists(dailyf): continue
    nav=pd.read_csv(navf,dtype={'ann_date':str}).dropna(subset=['unit_nav']).drop_duplicates('ann_date',keep='last').sort_values('ann_date')
    px=pd.read_csv(dailyf,dtype={'trade_date':str}).drop_duplicates('trade_date').sort_values('trade_date')
    nav_a=nav[['ann_date','unit_nav']].values; s={}; rr={}
    prev=None
    for _,r in px.iterrows():
        td=r['trade_date']; cl=r['close']; v=None
        for ad,uv in nav_a:
            if ad<=td: v=uv
            else: break
        if v and v>0 and cl>0:
            s[td]=cl/v-1
            if prev is not None and prev>0: rr[td]=cl/prev-1
            prev=cl
    if len(s)>=200: prem[code]=pd.Series(s); pxret[code]=pd.Series(rr)
codes=list(prem.keys()); print(f"震荡候选 {len(codes)} 只", flush=True)
alld=sorted(set().union(*[set(prem[c].index) for c in codes]))
COST=0.0015
def bt(strategy, buy_th=-0.01, sell_th=0.01):
    # strategy: 'hold' 全程等权持有 | 'wam' 打地鼠(溢价>sell_th空仓, 折价<buy_th或已持有则持)
    nav=1.0; ser=[]; held={c:(strategy=='hold') for c in codes}
    for i,d in enumerate(alld):
        if i==0:
            for c in codes: held[c]=(strategy=='hold')
            ser.append((d,nav)); continue
        # 当日组合收益=在持基金的场内日收益等权
        active=[c for c in codes if held.get(c) and d in pxret[c]]
        r=np.mean([pxret[c][d] for c in active]) if active else 0.0
        # 换仓(按当日溢价, 收盘决策T+1生效简化为当日)
        turn=0
        if strategy=='wam':
            for c in codes:
                if d not in prem[c].index: continue
                p=prem[c][d]; was=held[c]
                if p>=sell_th and was: held[c]=False; turn+=1
                elif p<=buy_th and not was: held[c]=True; turn+=1
        n_active=max(len(active),1)
        r-=COST*turn/len(codes)
        nav*=1+r; ser.append((d,nav))
    return ser
def stat(ser,lo=None,hi=None):
    s=[(d,v) for d,v in ser if (lo is None or d>=lo) and (hi is None or d<hi)]
    nv=np.array([v for _,v in s]); nv=nv/nv[0]; yrs=len(nv)/244; rr=np.diff(nv)/nv[:-1]
    return {"ret":round((nv[-1]-1)*100,1),"cagr":round((nv[-1]**(1/yrs)-1)*100,1),
            "sharpe":round(float(np.mean(rr)/np.std(rr)*np.sqrt(244)),2),"mdd":round(float((nv/np.maximum.accumulate(nv)-1).min())*100,1)}
out={}
out['买入持有(基准)']=stat(bt('hold'))
for bth,sth in [(-0.01,0.01),(-0.02,0.02),(-0.005,0.005),(-0.02,0.03)]:
    out[f'打地鼠 买≤{bth*100:.0f}%卖≥{sth*100:.0f}%']=stat(bt('wam',bth,sth))
for k,v in out.items(): print(f"  {k}: {v}", flush=True)
json.dump(out,open("/root/cb-allotment/research/lof_whackamole_bt.json","w"),ensure_ascii=False,indent=1)
print("DONE",flush=True)

# ========== 成本敏感 + 换手 + 分基金流动性 ==========
def bt_full(buy_th,sell_th,cost):
    nav=1.0; ser=[]; held={c:False for c in codes}; total_turn=0
    for i,d in enumerate(alld):
        if i==0: ser.append((d,nav)); continue
        active=[c for c in codes if held.get(c) and d in pxret[c]]
        r=np.mean([pxret[c][d] for c in active]) if active else 0.0
        turn=0
        for c in codes:
            if d not in prem[c].index: continue
            p=prem[c][d]; was=held[c]
            if p>=sell_th and was: held[c]=False; turn+=1
            elif p<=buy_th and not was: held[c]=True; turn+=1
        total_turn+=turn
        r-=cost*turn/len(codes)
        nav*=1+r; ser.append((d,nav))
    return ser, total_turn
print("\n=== 成本敏感(打地鼠 买≤-1%卖≥1%)===", flush=True)
for cost in (0.0015,0.003,0.005,0.008):
    ser,tt=bt_full(-0.01,0.01,cost); st=stat(ser)
    print(f"  成本{cost*100:.2f}%/边: CAGR {st['cagr']}% Sharpe {st['sharpe']} | 总换手{tt}次(~{tt/len(codes):.0f}次/基金/{len(alld)}日)", flush=True)
# 换手频率 → 年化换手
ser,tt=bt_full(-0.01,0.01,0.0015)
print(f"\n年化换手: {tt/(len(alld)/244):.0f} 次/年 (25只池), 人均 {tt/len(codes)/(len(alld)/244):.1f} 次/基金/年", flush=True)
# 成交额中位: 这些震荡基金够不够大?
import pandas as _pd
liq=[]
for c in codes:
    dd=_pd.read_csv(f"{D}/{c}_daily.csv"); 
    if 'amount' in dd: liq.append((names.get(c,c), round(dd['amount'].median()/1e4,2)))  # 万元→亿? amount单位千元
liq.sort(key=lambda x:x[1])
print("\n=== 日成交额中位(最小5只, 千元)===", flush=True)
for nm,a in liq[:5]: print(f"  {nm}: {a}", flush=True)
print(f"  全池中位: {sorted(a for _,a in liq)[len(liq)//2]} 千元", flush=True)
print("DONE2", flush=True)
