"""Standalone synthetic teaching example. Run locally with Python, not in the HTML."""
import json
import numpy as np
import pandas as pd
from scipy.special import expit

def clean(value):
    if isinstance(value, dict): return {str(k):clean(v) for k,v in value.items()}
    if isinstance(value,(list,tuple,np.ndarray)): return [clean(v) for v in value]
    if isinstance(value,(np.integer,)): return int(value)
    if isinstance(value,(float,np.floating)): return float(value) if np.isfinite(value) else None
    if isinstance(value,(np.bool_,)): return bool(value)
    return value

def print_result(result):
    print(json.dumps(clean(result),ensure_ascii=False,allow_nan=False))


def make_panel(seed=42, T=180, N=24, k=5, mechanism="linear"):
    """Illustrative weekdays, 6 AR factors, future open-to-open k-day label."""
    if mechanism not in {"null","linear","interaction","regime"}:
        raise ValueError("Unknown mechanism")
    rng=np.random.default_rng(seed)
    dates=pd.bdate_range("2020-01-02",periods=T)
    factors=np.empty((T,N,6)); factors[0]=rng.normal(size=(N,6))
    for t in range(1,T):
        factors[t]=.6*factors[t-1]+.8*rng.normal(size=(N,6))
    factors[:,:,3]=.9*factors[:,:,0]+.15*rng.normal(size=(T,N))
    prices=np.full((T,N),100.)
    for t in range(T-1):
        x=factors[max(0,t-1)]
        signal=np.zeros(N)
        if mechanism!="null": signal=.0015*x[:,0]
        if mechanism in {"interaction","regime"}: signal += .0015*x[:,1]*x[:,2]
        if mechanism=="regime" and t>=int(.65*T): signal -= .002*x[:,0]
        noise=.006*rng.standard_t(5,N)/np.sqrt(5/3)+rng.normal(0,.002)
        prices[t+1]=prices[t]*np.exp(signal+noise)
    panel=pd.DataFrame(factors.reshape(T*N,6),columns=[f"x{i}" for i in range(6)])
    panel["date"]=np.repeat(dates,N); panel["stock"]=np.tile([f"S{i:03}" for i in range(N)],T)
    panel["tpos"]=np.repeat(np.arange(T),N); panel["open"]=prices.ravel()
    panel["signal_time"]=panel.date+pd.Timedelta(hours=15)
    panel["feature_available_time"]=panel.signal_time
    labels=panel.groupby("stock").open
    panel["y"]=labels.shift(-(k+1))/labels.shift(-1)-1
    exit_dates=pd.Series(dates).shift(-(k+1)).to_numpy()[panel.tpos]
    panel["label_available_time"]=exit_dates+pd.Timedelta(hours=9,minutes=30)
    return panel

def validate_panel(frame):
    if frame.duplicated(["date","stock"]).any():
        raise ValueError("Duplicate (date,stock) keys")
    if frame[["signal_time","feature_available_time"]].isna().any().any():
        raise ValueError("Missing availability timestamps")
    if (frame.feature_available_time>frame.signal_time).any():
        raise ValueError("Feature was unavailable at decision time")
    if (frame.y.notna() & frame.label_available_time.isna()).any():
        raise ValueError("Known label has unknown availability time")

def time_fold(frame, start, end, train_start=0):
    """Train at prior signal day's 15:00; dates [start,end) are held out."""
    validate_panel(frame)
    dates=frame.date.drop_duplicates().sort_values().reset_index(drop=True)
    if not 1<=start<end<=len(dates) or not 0<=train_start<start:
        raise ValueError("Invalid date fold")
    cutoff=dates.iloc[start-1]+pd.Timedelta(hours=15)
    train=frame[(frame.date>=dates.iloc[train_start]) & (frame.date<dates.iloc[start])
                & frame.y.notna() & (frame.label_available_time<=cutoff)].copy()
    valid=frame[frame.date.isin(dates.iloc[start:end]) & frame.y.notna()].copy()
    if train.empty or valid.empty: raise ValueError("Empty fold")
    return train,valid,cutoff

def keyed_metrics(frame, predictions):
    """predictions is a (date,stock,pred) table; exact keys required, order free."""
    if frame.duplicated(["date","stock"]).any() or predictions.duplicated(["date","stock"]).any():
        raise ValueError("Duplicate prediction/evaluation keys")
    aligned=frame[["date","stock","y"]].merge(predictions[["date","stock","pred"]],
        on=["date","stock"],how="outer",validate="1:1",indicator=True)
    if not aligned._merge.eq("both").all(): raise ValueError("Prediction keys do not match")
    daily=[]
    for date,group in aligned.groupby("date"):
        finite=np.isfinite(group.y)&np.isfinite(group.pred)
        g=group.loc[finite]; ic=rank_ic=spread=np.nan
        if len(g)>=3 and g.y.nunique()>1 and g.pred.nunique()>1:
            ic=g.y.corr(g.pred); rank_ic=g.y.rank().corr(g.pred.rank())
            ordered=g.sort_values(["pred","stock"]); size=max(1,len(g)//5)
            spread=ordered.tail(size).y.mean()-ordered.head(size).y.mean()
        daily.append({"date":date,"n":len(g),"ic":ic,"rank_ic":rank_ic,"spread":spread})
    table=pd.DataFrame(daily)
    valid=np.isfinite(aligned.y)&np.isfinite(aligned.pred)
    mse=float(np.mean((aligned.loc[valid,"y"]-aligned.loc[valid,"pred"])**2)) if valid.any() else np.nan
    return {"mse":mse,"ic":float(table.ic.mean()),"rank_ic":float(table.rank_ic.mean()),
            "coverage":float(table.ic.notna().mean()),"rows":int(valid.sum()),"dates":len(table)},table

def run_research(seed=42):
    """Small predeclared grid; future-date validation; one fixed final holdout."""
    from sklearn.pipeline import make_pipeline
    from sklearn.preprocessing import StandardScaler
    from sklearn.linear_model import Ridge
    from sklearn.ensemble import HistGradientBoostingRegressor
    outputs={}; features=[f"x{i}" for i in range(6)]
    folds=[(80,105),(105,130)]; final=(130,174)
    for mechanism in ["null","linear","interaction","regime"]:
        panel=make_panel(seed=seed,mechanism=mechanism)
        grids={"Ridge":[.01,1.],"GBDT":[15,40]}; selected={}; trial_log=[]
        def build(name,p,n):
            if name=="Ridge":return make_pipeline(StandardScaler(),Ridge(alpha=n*p))
            return HistGradientBoostingRegressor(max_iter=p,max_leaf_nodes=7,
                min_samples_leaf=25,learning_rate=.08,early_stopping=False,random_state=seed)
        for name,grid in grids.items():
            trials=[]
            for parameter in grid:
                scores=[]
                for start,end in folds:
                    tr,va,_=time_fold(panel,start,end)
                    model=build(name,parameter,len(tr)).fit(tr[features],tr.y)
                    pred=va[["date","stock"]].assign(pred=model.predict(va[features]))
                    score,_=keyed_metrics(va,pred);scores.append(score["rank_ic"])
                trials.append((np.mean(scores),parameter))
                trial_log.append({"model":name,"parameter":parameter,"fold_rank_ic":scores})
            selected[name]=max(trials,key=lambda v:v[0])[1]
        tr,test,cutoff=time_fold(panel,*final); results={}
        for name,parameter in selected.items():
            model=build(name,parameter,len(tr)).fit(tr[features],tr.y)
            pred=test[["date","stock"]].assign(pred=model.predict(test[features]))
            results[name]=keyed_metrics(test,pred)[0]
        # A predeclared single-factor and equal-weight baseline, train-only calibration.
        for name,cols in [("单因子",["x0"]),("等权",["x0","x1","x2"])]:
            mean,scale=tr[cols].mean(),tr[cols].std(ddof=0)
            ts=((tr[cols]-mean)/scale).mean(axis=1)
            vs=((test[cols]-mean)/scale).mean(axis=1)
            coef=np.linalg.lstsq(np.column_stack([np.ones(len(ts)),ts]),tr.y,rcond=None)[0]
            pred=test[["date","stock"]].assign(pred=coef[0]+coef[1]*vs)
            results[name]=keyed_metrics(test,pred)[0]
        outputs[mechanism]={"metrics":results,"selected":selected,"trials":trial_log,
            "cutoff":str(cutoff),"test_dates":44,"seed":seed,"data":"synthetic",
            "policy":"Frozen grid; 2 development folds; static final training; no trading NAV"}
    return outputs

if __name__ == "__main__":
    print_result(run_research(seed=42))
