Spaces:
Sleeping
Sleeping
File size: 6,896 Bytes
5ae1928 | 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 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 | #!/usr/bin/env python3
"""C9 validation: single-stock training vs multi-stock training universe."""
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 json
import numpy as np
import pandas as pd
from sklearn.ensemble import RandomForestClassifier
from scripts.improvement_harness import (
BASELINE_FEATURES, fetch_df, build_triple_barrier_labels,
compute_metrics, RF_PARAMS, MIN_TRAIN, STEP, LABEL_HORIZON,
walk_forward,
)
from models.predictor import _build_features
TEST_STOCKS = ["2330", "0050", "2317", "2454", "2881"]
UNIVERSE = list(dict.fromkeys([
"2330", "0050", "2317", "2454", "2881",
"2382", "2303", "2308", "3034", "2412",
"6505", "6669", "2886", "2891", "3008",
"4904", "2882", "2885", "1301", "2912"
]))
def walk_forward_multi(
test_feat_df, test_labels,
all_pool_rows,
test_dates,
cols=BASELINE_FEATURES,
):
"""Walk-forward where training pulls from full universe pool."""
avail = [c for c in cols if c in test_feat_df.columns]
X_test_all = test_feat_df.reindex(columns=avail, fill_value=0).fillna(0).values
n = len(test_feat_df)
y_true_all, y_pred_all = [], []
cutoff = MIN_TRAIN
while cutoff + STEP + LABEL_HORIZON <= n:
cutoff_date = test_dates[cutoff] if cutoff < len(test_dates) else None
X_pool, y_pool = [], []
for (pool_feat_df, pool_labels, pool_dates) in all_pool_rows:
pool_avail = [c for c in cols if c in pool_feat_df.columns]
if len(pool_avail) < len(avail) * 0.8:
continue
X_p = pool_feat_df.reindex(columns=avail, fill_value=0).fillna(0).values
if cutoff_date is not None:
mask_date = pool_dates < cutoff_date
else:
mask_date = np.ones(len(pool_labels), dtype=bool)
valid = ~np.isnan(pool_labels) & mask_date
if valid.sum() < 5:
continue
X_pool.append(X_p[valid])
y_pool.append(pool_labels[valid])
if not X_pool:
cutoff += STEP
continue
X_tr = np.vstack(X_pool)
y_tr = np.concatenate(y_pool).astype(int)
if len(np.unique(y_tr)) < 2:
cutoff += STEP
continue
clf = RandomForestClassifier(**RF_PARAMS)
clf.fit(X_tr, y_tr)
test_end = min(cutoff + STEP, n - LABEL_HORIZON)
y_te = test_labels[cutoff:test_end]
valid_te = ~np.isnan(y_te)
if valid_te.sum() == 0:
cutoff += STEP
continue
y_pred = clf.predict(X_test_all[cutoff:test_end][valid_te])
y_true_all.extend(y_te[valid_te].tolist())
y_pred_all.extend(y_pred.tolist())
cutoff += STEP
if not y_true_all:
return {}
return compute_metrics(np.array(y_true_all), np.array(y_pred_all))
def main():
print("Fetching universe data...")
pool_data = {} # stock_no -> (feat_df, labels, dates_array)
for stock_no in UNIVERSE:
print(f" {stock_no}...", end=" ", flush=True)
try:
df = fetch_df(stock_no)
if df is None or df.empty:
print("no data")
continue
feat = _build_features(df)
date_col = df["date"] if "date" in df.columns else df.index.to_series()
dates = pd.to_datetime(date_col).values # numpy datetime64 array
close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values
labels = build_triple_barrier_labels(close)
pool_data[stock_no] = (feat, labels, dates)
print(f"rows={len(feat)}")
except Exception as e:
print(f"error: {e}")
stocks_fetched = len(pool_data)
print(f"\nFetched {stocks_fetched}/{len(UNIVERSE)} stocks")
all_pool_rows = list(pool_data.values())
print("\nRunning walk-forward...")
results = {}
for stock_no in TEST_STOCKS:
if stock_no not in pool_data:
print(f" {stock_no}: no data, skipping")
continue
feat_df, labels, dates = pool_data[stock_no]
print(f" {stock_no} single...", end=" ", flush=True)
single = walk_forward(feat_df, labels, BASELINE_FEATURES)
print(f"dir={single.get('dir_accuracy','?')} up={single.get('up_precision','?')}", end=" ")
print(f"multi...", end=" ", flush=True)
multi = walk_forward_multi(feat_df, labels, all_pool_rows, dates)
print(f"dir={multi.get('dir_accuracy','?')} up={multi.get('up_precision','?')}")
results[stock_no] = {"single": single, "multi": multi}
# Aggregate
def _mean(dicts, key):
vals = [d[key] for d in dicts if isinstance(d.get(key), (int, float)) and not np.isnan(d.get(key, float("nan")))]
return round(sum(vals) / len(vals), 1) if vals else float("nan")
single_rows = [v["single"] for v in results.values() if v["single"]]
multi_rows = [v["multi"] for v in results.values() if v["multi"]]
agg_keys = ["accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals"]
agg = {
"single": {k: _mean(single_rows, k) for k in agg_keys},
"multi": {k: _mean(multi_rows, k) for k in agg_keys},
}
print("\n=== AGGREGATE ===")
for mode in ("single", "multi"):
s = agg[mode]
print(f" {mode:>6}: acc={s['accuracy']}% dir={s['dir_accuracy']}% "
f"↑prec={s['up_precision']}% signals={s['n_signals']}")
multi_dir = agg["multi"]["dir_accuracy"]
multi_prec = agg["multi"]["up_precision"]
passed = (
isinstance(multi_dir, float) and multi_dir >= 44.0 and
isinstance(multi_prec, float) and multi_prec >= 54.0
)
print(f"\n Pass (dir≥44.0 AND up_prec≥54.0): {'YES' if passed else 'NO'}")
output = {
"universe_size": len(UNIVERSE),
"stocks_fetched": stocks_fetched,
"results": results,
"aggregate": agg,
"passed": passed,
"pass_criterion": "dir_accuracy >= 44.0 AND up_precision >= 54.0",
}
out_path = ROOT / "docs" / "c9_multi_result.json"
class _NumpyEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, (np.integer,)): return int(obj)
if isinstance(obj, (np.floating,)): return float(obj)
if isinstance(obj, (np.bool_,)): return bool(obj)
if isinstance(obj, np.ndarray): return obj.tolist()
return super().default(obj)
with open(out_path, "w") as f:
json.dump(output, f, indent=2, cls=_NumpyEncoder)
print(f"\nWrote {out_path}")
return output
if __name__ == "__main__":
main()
|