Spaces:
Sleeping
Sleeping
File size: 6,628 Bytes
ff75b25 | 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 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 | #!/usr/bin/env python3
"""C30: Sector peer return features.
Hypothesis: Stocks within the same sector exhibit lead-lag relationships.
TSMC often leads UMC by 1 trading day; Fubon leads Cathay and Mega.
Adding yesterday's average peer return as a feature lets the model capture
this intra-sector momentum signal.
New features (non-leaking — all use shift(1)):
peer_ret_1d — prior-day mean return of same-sector peers
peer_ret_5d — 5-day return of peers ending yesterday
All 12 stocks are loaded to compute peer returns even for the 5-stock eval set.
Stocks with no peers (2412 telecom) get 0.0 fill.
"""
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
import warnings; warnings.filterwarnings("ignore")
import json
import argparse
import numpy as np
import pandas as pd
from scripts.improvement_harness import (
fetch_df, build_triple_barrier_labels, walk_forward, compute_metrics,
CURRENT_FEATURES, DEFAULT_STOCKS, EXTENDED_STOCKS,
PASS_DIR_ACC, PASS_UP_PREC,
)
from models.predictor import _build_features
SECTOR_MAP = {
"semis": ["2330", "2454", "2303"],
"electronics": ["2317", "2382", "2308"],
"financials": ["2881", "2882", "2886"],
"etfs": ["0050", "0056"],
"telecom": ["2412"],
}
STOCK_SECTOR = {s: sec for sec, members in SECTOR_MAP.items() for s in members}
C30_FEATURES = ["peer_ret_1d", "peer_ret_5d"]
NEW_FEATURES = CURRENT_FEATURES + C30_FEATURES
ALL_STOCKS = [s for members in SECTOR_MAP.values() for s in members]
def _build_peer_close(stock_dfs: dict[str, pd.DataFrame]) -> dict[str, pd.Series]:
"""Return {stock_no: close Series indexed by YYYY-MM-DD string}."""
out = {}
for s, df in stock_dfs.items():
if df is not None and not df.empty and "date" in df.columns:
out[s] = df.set_index("date")["close"].astype(float)
return out
def _add_peer_features(feat: pd.DataFrame, df: pd.DataFrame,
stock_no: str, peer_close: dict[str, pd.Series]) -> pd.DataFrame:
sector = STOCK_SECTOR.get(stock_no, "")
peers = [p for p in SECTOR_MAP.get(sector, []) if p != stock_no and p in peer_close]
if not peers or "date" not in df.columns:
feat["peer_ret_1d"] = 0.0
feat["peer_ret_5d"] = 0.0
return feat
date_col = df["date"].astype(str).reset_index(drop=True)
r1_cols, r5_cols = [], []
for p in peers:
s = peer_close[p]
r1 = s.pct_change(1).shift(1) # prior-day 1d return — non-leaking
r5 = s.pct_change(5).shift(1) # 5d return ending yesterday — non-leaking
r1_cols.append(date_col.map(r1.to_dict()).astype(float))
r5_cols.append(date_col.map(r5.to_dict()).astype(float))
feat = feat.reset_index(drop=True)
feat["peer_ret_1d"] = pd.concat(r1_cols, axis=1).mean(axis=1).ffill().bfill().fillna(0.0).values
feat["peer_ret_5d"] = pd.concat(r5_cols, axis=1).mean(axis=1).ffill().bfill().fillna(0.0).values
return feat
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--extended", action="store_true")
args = parser.parse_args()
stocks = EXTENDED_STOCKS if args.extended else DEFAULT_STOCKS
tag = "12-stock" if args.extended else "5-stock"
suffix = "_12stock" if args.extended else ""
print(f"\n=== C30: Sector peer return features [{tag}] ===")
print(f" New features: {C30_FEATURES}\n")
# Load all 12 stocks for peer close prices
print(" Loading all stock data for peer returns...")
all_dfs: dict[str, pd.DataFrame] = {}
for s in ALL_STOCKS:
df = fetch_df(s)
if df is not None and not df.empty:
all_dfs[s] = df
peer_close = _build_peer_close(all_dfs)
print(f" Loaded {len(peer_close)} stocks\n")
def _avg(results, key):
vals = [m[key] for m in results if m and not np.isnan(m.get(key, float("nan")))]
return round(float(np.mean(vals)), 1) if vals else float("nan")
per_stock = {}
base_results, c30_results = [], []
for stock_no in stocks:
print(f" {stock_no}...", end=" ", flush=True)
df = all_dfs.get(stock_no)
if df is None or df.empty:
print("no data"); continue
feat = _build_features(df)
feat = _add_peer_features(feat, df, stock_no, peer_close)
close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values
labels = build_triple_barrier_labels(close)
sector = STOCK_SECTOR.get(stock_no, "?")
peers = [p for p in SECTOR_MAP.get(sector, []) if p != stock_no and p in peer_close]
m_base = walk_forward(feat, labels, CURRENT_FEATURES)
m_c30 = walk_forward(feat, labels, NEW_FEATURES)
per_stock[stock_no] = {"baseline": m_base, "c30": m_c30, "peers": peers}
base_results.append(m_base)
c30_results.append(m_c30)
b_dir = m_base.get("dir_accuracy", float("nan"))
b_up = m_base.get("up_precision", float("nan"))
c_dir = m_c30.get("dir_accuracy", float("nan"))
c_up = m_c30.get("up_precision", float("nan"))
print(f"peers={peers} base dir={b_dir}% ↑prec={b_up}% → c30 dir={c_dir}% ↑prec={c_up}%")
base_agg = {"dir_accuracy": _avg(base_results, "dir_accuracy"),
"up_precision": _avg(base_results, "up_precision")}
c30_agg = {"dir_accuracy": _avg(c30_results, "dir_accuracy"),
"up_precision": _avg(c30_results, "up_precision")}
passed = c30_agg["dir_accuracy"] >= PASS_DIR_ACC and c30_agg["up_precision"] >= PASS_UP_PREC
print(f"\n Baseline avg: dir={base_agg['dir_accuracy']}% ↑prec={base_agg['up_precision']}%")
print(f" C30 avg: dir={c30_agg['dir_accuracy']}% ↑prec={c30_agg['up_precision']}%")
print(f" Gate (dir≥{PASS_DIR_ACC}% AND ↑prec≥{PASS_UP_PREC}%): {'PASS ✓' if passed else 'FAIL ✗'}")
result = {
"experiment": "C30",
"description": "Sector peer return features (peer_ret_1d, peer_ret_5d)",
"new_features": C30_FEATURES,
"stocks": stocks,
"aggregate": {"baseline": base_agg, "c30": c30_agg},
"passed": passed,
"pass_gate": {"dir_accuracy": PASS_DIR_ACC, "up_precision": PASS_UP_PREC},
"per_stock": per_stock,
}
out = ROOT / f"docs/c30_result{suffix}.json"
out.parent.mkdir(exist_ok=True)
out.write_text(json.dumps(result, indent=2))
print(f"\n Saved: {out}")
return 0 if passed else 1
if __name__ == "__main__":
sys.exit(main())
|