DockerSpace / scripts /compare_versions.py
DennisChan0909's picture
feat(C2): add PTT sentiment scraper and backtest; FAILED — PTT rows=0, features not added to model
cec37e3
Raw
History Blame Contribute Delete
6.79 kB
#!/usr/bin/env python3
"""
Compare prediction confidence between old (dev-main, 40 features) and
new (feat/chip-score, 63 features) model on the same live data.
Usage:
python scripts/compare_versions.py
python scripts/compare_versions.py --stocks 2330 0050 2317 2454
"""
import argparse
import os
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
from sklearn.ensemble import RandomForestClassifier
OLD_FEATURES = [
"return_1d", "return_5d", "return_10d", "return_20d",
"close_ma5_ratio", "close_ma20_ratio", "ma5_ma20_ratio", "ma20_ma60_ratio",
"rsi",
"macd_hist", "macd_signal_ratio", "macd_hist_norm", "macd_hist_delta_1d",
"macd_hist_slope_3d", "macd_cross_up", "macd_cross_down", "macd_above_zero",
"bb_pct_b", "k", "d", "volume_ratio",
"atr_ratio", "high_low_ratio", "obv_trend",
"close_ma60_ratio", "log_volume_ratio",
"volume_zscore", "price_volume_div", "volatility_20d",
"taiex_return_5d", "taiex_ma20_ratio", "usdtwd_return_5d",
"foreign_net_vol_ratio", "trust_net_vol_ratio", "dealer_net_vol_ratio",
"institutional_net_vol_ratio", "institutional_5d_net_vol_ratio",
"institutional_20d_zscore", "foreign_trust_alignment", "institutional_streak",
]
# Always use the live FEATURE_COLUMNS from predictor so this script stays in sync
from models.predictor import FEATURE_COLUMNS as NEW_FEATURES
LABEL_HORIZON = 5
LABEL_THRESHOLD = 0.02
RF_PARAMS = 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: str) -> pd.DataFrame:
from services.predictor_service import _fetch_with_cache
from indicators.technical import add_all_indicators
from data.institutional_flow import add_institutional_flow
from data.margin_flow import add_margin_flow
from data.fetcher import is_us_ticker, fetch_cross_asset_tw
from indicators.technical import add_cross_asset_tw
df = _fetch_with_cache(stock_no, months=24)
if df.empty:
return df
df = add_all_indicators(df)
if not is_us_ticker(stock_no):
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 predict_with_features(feat: pd.DataFrame, cols: list[str]) -> dict:
avail = [c for c in cols if c in feat.columns]
labels = feat["_label"].dropna()
common = feat.index.intersection(labels.index)
X = feat.loc[common, avail].fillna(0).values
y = labels.loc[common].values
# Drop last LABEL_HORIZON rows (no forward return yet)
X_train, y_train = X[:-5], y[:-5]
X_last = X[[-1]]
if len(np.unique(y_train)) < 2:
return {"signal": "N/A", "prob_up": float("nan"),
"prob_dn": float("nan"), "prob_nt": float("nan"),
"n_features": len(avail), "train_rows": len(X_train)}
clf = RandomForestClassifier(**RF_PARAMS)
clf.fit(X_train, y_train)
classes = list(clf.classes_)
proba = clf.predict_proba(X_last)[0]
prob_map = dict(zip(classes, proba))
prob_up = prob_map.get(1, 0.0)
prob_dn = prob_map.get(-1, 0.0)
prob_nt = prob_map.get(0, 0.0)
best = max(prob_up, prob_dn, prob_nt)
signal = "UP" if prob_up == best else ("DOWN" if prob_dn == best else "HOLD")
return {
"signal": signal,
"prob_up": round(prob_up * 100, 1),
"prob_dn": round(prob_dn * 100, 1),
"prob_nt": round(prob_nt * 100, 1),
"n_features": len(avail),
"train_rows": len(X_train),
}
def predict_stock(stock_no: str):
from models.predictor import _build_features
df = fetch_df(stock_no)
if df is None or df.empty:
return None
feat = _build_features(df)
close = df.set_index("date")["close"] if "date" in df.columns else df["close"]
fwd = close.shift(-LABEL_HORIZON)
ret = (fwd - close) / close
labels = pd.Series(
np.where(ret > LABEL_THRESHOLD, 1, np.where(ret < -LABEL_THRESHOLD, -1, 0)),
index=feat.index,
).astype(float)
labels.iloc[-LABEL_HORIZON:] = np.nan
feat["_label"] = labels
return {
f"old ({len(OLD_FEATURES)} feat)": predict_with_features(feat, OLD_FEATURES),
f"new ({len(NEW_FEATURES)} feat)": predict_with_features(feat, NEW_FEATURES),
}
def arrow(old_sig: str, new_sig: str) -> str:
if old_sig == new_sig:
return "="
return f"{old_sig}{new_sig}"
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--stocks", nargs="+", default=["2330", "0050", "2317", "2454", "2881"])
args = parser.parse_args()
hdr = f"{'Stock':>6} {'Version':<18} {'Signal':>5} {'↑%':>6} {'↓%':>6} {'⬄%':>6} {'Feat':>4} {'Rows':>5}"
sep = "-" * len(hdr)
print(f"\n{hdr}\n{sep}")
for stock_no in args.stocks:
print(f" fetching {stock_no}...", end="\r", flush=True)
try:
results = predict_stock(stock_no)
except Exception as e:
print(f"{stock_no:>6} ERROR: {e}")
continue
if results is None:
print(f"{stock_no:>6} No data")
continue
versions = list(results.items())
first = True
for version, r in versions:
prefix = f"{stock_no:>6}" if first else f"{'':>6}"
first = False
print(
f"{prefix} {version:<18} {r['signal']:>5} "
f"{r['prob_up']:>5.1f}% {r['prob_dn']:>5.1f}% {r['prob_nt']:>5.1f}% "
f"{r['n_features']:>4} {r.get('train_rows', 0):>5}"
)
# Summary delta line
if len(versions) == 2:
_, r_old = versions[0]
_, r_new = versions[1]
if r_old["signal"] != "N/A" and r_new["signal"] != "N/A":
delta_up = r_new["prob_up"] - r_old["prob_up"]
delta_dn = r_new["prob_dn"] - r_old["prob_dn"]
sig_change = arrow(r_old["signal"], r_new["signal"])
print(
f"{'':>6} {' delta':<18} {sig_change:>5} "
f"{delta_up:>+5.1f}% {delta_dn:>+5.1f}%"
)
print()
if __name__ == "__main__":
main()