from pathlib import Path
from datetime import datetime
from zoneinfo import ZoneInfo
import json,hashlib
import numpy as np
import pandas as pd
OUT=Path(__file__).resolve().parent
def read(p):return json.loads(p.read_text(encoding='utf-8-sig'))
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
cfg=read(OUT/'价格对照实验设定.json');risk=read(OUT/'价格与组合风险诊断-r2.json')
src=Path(risk['source_path']);supp=Path(risk['supplement_path'])
assert sha(src)==risk['source_sha256'] and sha(supp)==risk['supplement_sha256']
cols=['SPY','IEF','GLD','BIL','XLK','XLI','XLE','XLV','XLP','XLU'];universe=cols[4:]
p=pd.read_csv(src).pivot(index='date',columns='symbol',values='adjusted_close').sort_index().loc['2023-10-30':'2026-11-04']
new=pd.read_csv(supp).pivot(index='date',columns='symbol',values='adjusted_close')
for s in ['XLV','XLP']:p[s]=new[s]
p=p[cols];assert len(p)==757 and p.notna().all().all() and np.isfinite(p.values).all()
end_month=[i for i in range(len(p)-1) if p.index[i][:7]!=p.index[i+1][:7] and p.index[i]>='2024-12-01']
assert p.index[end_month[0]]=='2024-12-31'
selection_log=[];targets={};selected=None
for i in end_month:
    if p.index[i][5:7]=='12':
        moment=(p.iloc[i-21][universe]/p.iloc[i-252][universe]-1).to_dict()
        selected=sorted(universe,key=lambda s:(-moment[s],s))[:2]
        selection_log.append({'signal':p.index[i],'execution':p.index[i+1],'ranking':sorted(moment.items(),key=lambda x:(-x[1],x[0])),'selected':selected,'latest_price_used':p.index[i-21],'lookback_start':p.index[i-252]})
    assert selected is not None
    basic={'SPY':.30,'IEF':.20,'GLD':.25,'BIL':.25}
    target={'base_AI8_static':basic,'add_SPY_10pct':basic|{'SPY':.40,'BIL':.15},
      'equal_sector_10pct':basic|{'BIL':.15,**{s:.10/6 for s in universe}},
      'annual_price_top2_10pct':basic|{'BIL':.15,**{s:.05 for s in selected}}}
    for w in target.values():assert abs(sum(w.values())-1)<1e-12
    targets[i+1]={'signal':p.index[i],'weights':target}
assert len(selection_log)==2
def net_trade_nav(holdings,cash,w,c):
    nav=float(holdings.sum()+cash);low,high=nav/(1+2*c),nav
    for _ in range(60):
        mid=(low+high)/2
        if mid+c*np.abs(mid*w-holdings).sum()>nav:high=mid
        else:low=mid
    after=(low+high)/2;notional=float(np.abs(after*w-holdings).sum());fee=notional*c
    assert abs(after+fee-nav)<1e-7
    return after,notional,fee
# Independent closed-form cases: initial purchase and a 100/25 portfolio rebalanced 50/50.
assert abs(net_trade_nav(np.array([0.,0.]),100.,np.array([.5,.5]),.0005)[0]-100/1.0005)<1e-10
assert abs(net_trade_nav(np.array([100.,25.]),0.,np.array([.5,.5]),.001)[0]-124.925)<1e-10
# Future prices must not change the annual selection made at the signal date.
for log in selection_log:
    i=p.index.get_loc(log['signal']);perturbed=p.copy();perturbed.iloc[i+1:]*=7
    score=perturbed.iloc[i-21][universe]/perturbed.iloc[i-252][universe]-1
    assert sorted(universe,key=lambda s:(-score[s],s))[:2]==log['selected']
start=min(targets);results=[];curves={};trades=[]
for bps in cfg['costs_one_way_bps']:
    c=bps/10000
    for strategy in list(next(iter(targets.values()))['weights']):
        holdings=np.zeros(len(cols));cash=100000.;navs=[cash];fees=0.;turnover=0.;orders=[]
        for i in range(start,len(p)):
            if i>start:holdings*=p.iloc[i].values/p.iloc[i-1].values
            nav=float(holdings.sum()+cash)
            if i in targets:
                signal=targets[i]['signal'];assert signal<p.index[i]
                w=pd.Series(targets[i]['weights'][strategy]).reindex(cols,fill_value=0).values
                after,notional,fee=net_trade_nav(holdings,cash,w,c)
                assert abs(w.sum()-1)<1e-12 and np.all(w>=0)
                holdings=after*w;cash=0.;fees+=fee;turnover+=notional/nav
                orders.append({'signal':signal,'execution':p.index[i],'fee':fee,'traded_notional':notional,'weights':dict(zip(cols,w.tolist()))})
            navs.append(float(holdings.sum()+cash))
        arr=np.array(navs);dd=1-arr/np.maximum.accumulate(arr);rets=arr[1:]/arr[:-1]-1
        years=(pd.Timestamp(p.index[-1])-pd.Timestamp(p.index[start])).days/365.25
        record={'strategy':strategy,'one_way_bps':bps,'first_trade':p.index[start],'last_valuation':p.index[-1],'sessions':len(arr)-1,'annual_decisions':len(selection_log) if strategy=='annual_price_top2_10pct' else 0,'cumulative_return':arr[-1]/100000-1,'short_sample_cagr':(arr[-1]/100000)**(1/years)-1,'max_drawdown':float(dd.max()),'realized_volatility':float(np.std(rets[1:],ddof=1)*np.sqrt(252)),'fees_USD':fees,'summed_two_way_traded_notional_over_nav':turnover,'rebalance_events':len(orders)}
        results.append(record);curves[strategy+'_'+str(bps)]={'seed_cash':100000,'dates':p.index[start:].tolist(),'nav':arr[1:].tolist()}
        if bps==5:trades.append({'strategy':strategy,'orders':orders})
# Check event alignment and equal-equity comparator risk budgets, without reading future prices in selection.
assert all(sum(o['weights'][s] for s in ['SPY']+universe)<=.40000000001 for t in trades for o in t['orders'])
for strategy in set(r['strategy'] for r in results):
    seq=[r for r in results if r['strategy']==strategy]
    assert seq[0]['cumulative_return']>=seq[1]['cumulative_return']>=seq[2]['cumulative_return']
out={'created_at':datetime.now(ZoneInfo('Asia/Taipei')).isoformat(),'experiment':cfg,'settings_sha256':sha(OUT/'价格对照实验设定.json'),'source_sha256':sha(src),'supplement_sha256':sha(supp),'selection_log':selection_log,'results':results,'AI_strategy_performance':None,'account_action':False,'verification':{'annual_signal_count':2,'signal_strictly_before_trade':True,'budget_and_cash_checks':True,'cost_monotonicity':True,'closed_form_fee_tests':True,'future_price_perturbation_invariant':True,'parameter_search':False}}
(OUT/'价格对照实验结果.json').write_text(json.dumps(out,ensure_ascii=False,indent=2),encoding='utf-8')
(OUT/'价格对照实验路径.json').write_text(json.dumps({'curves':curves,'trades_5bps':trades},ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({'selection':selection_log,'base_cost_results':[r for r in results if r['one_way_bps']==5]},ensure_ascii=False,indent=2))
