File size: 5,863 Bytes
cbde68f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
#!/usr/bin/env python3
"""C1 validation: 40f baseline vs 40f + fracdiff features."""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))

try:
    from dotenv import load_dotenv; load_dotenv(ROOT / ".env")
except ImportError: pass

import warnings; warnings.filterwarnings("ignore")
import numpy as np
import pandas as pd
import json
from sklearn.ensemble import RandomForestClassifier

from models.predictor import FEATURE_COLUMNS

BASELINE = FEATURE_COLUMNS  # 40 features
FRACDIFF = FEATURE_COLUMNS + ["fracdiff_close_35", "fracdiff_close_40"]

STOCKS = ["2330", "0050", "2317", "2454", "2881"]
LABEL_HORIZON = 5
LABEL_THRESH = 0.02
MIN_TRAIN = 240
STEP = 21
RF = dict(n_estimators=200, max_depth=6, min_samples_leaf=10,
          class_weight="balanced", random_state=42, n_jobs=-1)

def fetch_df(stock_no):
    from services.predictor_service import _fetch_with_cache
    from indicators.technical import add_all_indicators, add_cross_asset_tw
    from data.institutional_flow import add_institutional_flow
    from data.margin_flow import add_margin_flow
    from data.fetcher import fetch_cross_asset_tw
    df = _fetch_with_cache(stock_no, months=24)
    if df is None or df.empty: return None
    df = add_all_indicators(df)
    df = add_institutional_flow(df, stock_no)
    df = add_margin_flow(df, stock_no)
    start, end = str(df["date"].min()), str(df["date"].max())
    result = fetch_cross_asset_tw(start, end)
    taiex, usdtwd = result[0], result[1]
    sox = result[2] if len(result) > 2 else None
    tnx = result[3] if len(result) > 3 else None
    df = add_cross_asset_tw(df, taiex, usdtwd, sox_close=sox, tnx_close=tnx)
    return df

def make_labels(close):
    fwd = close.shift(-LABEL_HORIZON)
    ret = (fwd - close) / close
    return np.where(ret > LABEL_THRESH, 1, np.where(ret < -LABEL_THRESH, -1, 0))

def walk_forward(feat, cols):
    avail = [c for c in cols if c in feat.columns]
    labels = feat["_label"].values
    X_all = feat[avail].fillna(0).values
    n = len(feat)
    y_true_all, y_pred_all = [], []
    cutoff = MIN_TRAIN
    while cutoff + STEP + LABEL_HORIZON <= n:
        X_tr, y_tr = X_all[:cutoff], labels[:cutoff]
        test_end = min(cutoff + STEP, n - LABEL_HORIZON)
        X_te, y_te = X_all[cutoff:test_end], labels[cutoff:test_end]
        if len(np.unique(y_tr)) < 2 or len(X_te) == 0:
            cutoff += STEP; continue
        clf = RandomForestClassifier(**RF)
        clf.fit(X_tr, y_tr)
        y_pred = clf.predict(X_te)
        y_true_all.extend(y_te.tolist())
        y_pred_all.extend(y_pred.tolist())
        cutoff += STEP
    if not y_true_all: return {}
    y_true = np.array(y_true_all)
    y_pred = np.array(y_pred_all)
    dir_mask = y_pred != 0
    acc = (y_true == y_pred).mean() * 100
    dir_acc = (y_true[dir_mask] == y_pred[dir_mask]).mean() * 100 if dir_mask.sum() > 0 else float("nan")
    up_mask = y_pred == 1
    up_prec = (y_true[up_mask] == 1).mean() * 100 if up_mask.sum() > 0 else float("nan")
    return {"accuracy": round(acc, 1), "dir_accuracy": round(dir_acc, 1),
            "up_precision": round(up_prec, 1), "n_predictions": len(y_true),
            "n_features": len(avail)}

def main():
    from models.predictor import _build_features
    results = {}
    hdr = f"{'Stock':>6}  {'Set':>10}  {'Acc%':>5}  {'Dir%':>5}  {'UpPrec%':>7}  {'Feat':>4}"
    print(f"\n{hdr}\n{'-'*len(hdr)}")
    agg = {"baseline": [], "fracdiff": []}
    for stock_no in STOCKS:
        print(f"  computing {stock_no}...", end="\r", flush=True)
        df = fetch_df(stock_no)
        if df is None or df.empty: continue
        feat = _build_features(df)
        # Pass fracdiff columns through from df (not built by _build_features)
        for col in ("fracdiff_close_35", "fracdiff_close_40"):
            if col in df.columns:
                feat[col] = df[col].values
        close = df.set_index("date")["close"] if "date" in df.columns else df["close"]
        feat["_label"] = make_labels(close)
        results[stock_no] = {}
        for name, cols in [("baseline", BASELINE), ("fracdiff", FRACDIFF)]:
            r = walk_forward(feat, cols)
            results[stock_no][name] = r
            if r:
                print(f"{stock_no:>6}  {name:>10}  {r['accuracy']:>5.1f}  {r['dir_accuracy']:>5.1f}  {r['up_precision']:>7.1f}  {r['n_features']:>4}")
                agg[name].append(r)
        if results[stock_no].get("baseline") and results[stock_no].get("fracdiff"):
            rb, rf = results[stock_no]["baseline"], results[stock_no]["fracdiff"]
            dd = rf["dir_accuracy"] - rb["dir_accuracy"]
            print(f"{'':>6}  {'delta':>10}  {'':>5}  {dd:>+5.1f}")
        print()

    def mean_field(rows, f):
        vals = [r[f] for r in rows if not (isinstance(r.get(f), float) and np.isnan(r.get(f, float("nan"))))]
        return round(sum(vals)/len(vals), 1) if vals else float("nan")

    print("=== AGGREGATE ===")
    agg_summary = {}
    for name in ("baseline", "fracdiff"):
        rows = agg[name]
        s = {k: mean_field(rows, k) for k in ("accuracy", "dir_accuracy", "up_precision")}
        agg_summary[name] = s
        print(f"  {name:>10}: acc={s['accuracy']}%  dir={s['dir_accuracy']}%  up_prec={s['up_precision']}%")

    winner = "fracdiff" if agg_summary["fracdiff"]["dir_accuracy"] > agg_summary["baseline"]["dir_accuracy"] else "baseline"
    passed = agg_summary["fracdiff"]["dir_accuracy"] >= 31.5
    print(f"\n  Winner: {winner}  |  passed={passed}")

    out = {"results": results, "aggregate": agg_summary, "winner": winner,
           "pass_threshold": 31.5, "passed": passed}
    Path("docs").mkdir(exist_ok=True)
    with open("docs/c1_fracdiff_result.json", "w") as f:
        json.dump(out, f, indent=2)
    return out

if __name__ == "__main__":
    main()