Spaces:
Running
Running
File size: 5,072 Bytes
c789230 | 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 | #!/usr/bin/env python3
"""research/augment_features.py — add the ML_EXTRA_FEATURES columns to an existing
training CSV WITHOUT re-running the slow per-date compute_features.
The base CSV (ml_predictor/training_data.csv) already carries the 37 production features
+ forward labels for every sampled (ticker, date). This script reloads each ticker's OHLCV
from the same SQLite cache dataset.py uses, computes ONLY the extra indicator columns
(vectorized, full series) + the per-date Monte-Carlo path features via the SHARED helpers in
ml_predictor.features (so live inference and training stay identical), and merges them onto
the base rows by (ticker, date). Writes an augmented CSV.
Usage:
python research/augment_features.py \
--in ml_predictor/training_data.csv \
--out ml_predictor/training_data_extra.csv
"""
from __future__ import annotations
import argparse
import os
import sys
from concurrent.futures import ThreadPoolExecutor, as_completed
import numpy as np
import pandas as pd
_PROJ_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if _PROJ_ROOT not in sys.path:
sys.path.insert(0, _PROJ_ROOT)
from ml_predictor.features import EXTRA_FEATURE_COLS, _extra_feature_series, _mc_features # noqa: E402
from ml_predictor.dataset import _load_ticker # noqa: E402
DEFAULT_IN = os.path.join(_PROJ_ROOT, "ml_predictor", "training_data.csv")
DEFAULT_OUT = os.path.join(_PROJ_ROOT, "ml_predictor", "training_data_extra.csv")
_INDICATOR_COLS = [c for c in EXTRA_FEATURE_COLS if not c.startswith("mc_")]
_MC_COLS = [c for c in EXTRA_FEATURE_COLS if c.startswith("mc_")]
def _rows_for_ticker(ticker: str, want_dates: set) -> list[dict]:
loaded = _load_ticker(ticker)
if loaded is None:
return []
c, h, l, v = (s.dropna() for s in loaded)
if len(c) < 30:
return []
ser = _extra_feature_series(c, h, l, v) # vectorized indicator Series
rets_full = c.pct_change().to_numpy()
idx_by_ts = {pd.Timestamp(d).strftime("%Y-%m-%d"): i for i, d in enumerate(c.index)}
rows = []
for dstr in want_dates:
i = idx_by_ts.get(dstr)
if i is None:
continue
row = {"date": dstr, "ticker": ticker}
for col, s in ser.items():
try:
val = float(s.iloc[i])
except Exception:
val = np.nan
row[col] = val if np.isfinite(val) else np.nan
# Monte-Carlo on the trailing 63 returns up to (and including) this bar.
up_prob, exp_up, exp_dn = _mc_features(rets_full[max(0, i - 62): i + 1])
row["mc_up_prob_3d"] = up_prob
row["mc_exp_maxup_3d"] = exp_up
row["mc_exp_maxdn_3d"] = exp_dn
rows.append(row)
return rows
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--in", dest="inp", default=DEFAULT_IN)
ap.add_argument("--out", default=DEFAULT_OUT)
ap.add_argument("--workers", type=int, default=6)
args = ap.parse_args()
base = pd.read_csv(args.inp)
base["date"] = base["date"].astype(str)
print(f" Base CSV: {len(base):,} rows · {base['ticker'].nunique()} tickers")
want = {tk: set(g["date"]) for tk, g in base.groupby("ticker")}
tickers = list(want.keys())
all_rows: list[dict] = []
done = 0
with ThreadPoolExecutor(max_workers=args.workers) as ex:
futs = {ex.submit(_rows_for_ticker, tk, want[tk]): tk for tk in tickers}
for fut in as_completed(futs):
try:
all_rows.extend(fut.result())
except Exception as e:
print(f" ! {futs[fut]}: {e}")
done += 1
if done % 100 == 0 or done == len(tickers):
print(f" {done}/{len(tickers)} tickers · {len(all_rows):,} extra rows")
extra = pd.DataFrame(all_rows)
if extra.empty:
raise SystemExit("No extra rows produced — is ohlcv_cache.db populated?")
merged = base.merge(extra, on=["date", "ticker"], how="left")
# Reorder so extra columns sit before the label columns (labels stay last).
label_cols = [c for c in merged.columns if c in (
"up_INTRADAY", "dn_INTRADAY", "up_1D", "dn_1D", "up_3D", "dn_3D",
"ret_1D", "ret_3D", "dir_INTRADAY", "dir_1D", "dir_3D")]
front = [c for c in merged.columns if c not in label_cols and c not in EXTRA_FEATURE_COLS]
merged = merged[front + EXTRA_FEATURE_COLS + label_cols]
n_missing = merged[EXTRA_FEATURE_COLS].isna().any(axis=1).sum()
merged.to_csv(args.out, index=False)
print(f"\n ✓ Wrote {len(merged):,} rows × {merged.shape[1]} cols → {args.out}")
print(f" Extra cols: {EXTRA_FEATURE_COLS}")
print(f" Rows with any missing extra feature: {n_missing:,} ({n_missing / len(merged):.1%})")
for col in EXTRA_FEATURE_COLS:
s = merged[col].dropna()
if len(s):
print(f" {col:<18} mean={s.mean():>8.3f} p50={s.median():>8.3f} "
f"p10={s.quantile(.1):>8.3f} p90={s.quantile(.9):>8.3f}")
if __name__ == "__main__":
main()
|