Spaces:
Sleeping
Sleeping
File size: 6,685 Bytes
70307c7 | 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 | #!/usr/bin/env python3
"""C29: MetaLabel confidence threshold search.
C4 introduced MetaLabelClassifier (threshold=0.55 in production) as a secondary
filter: suppress signals where the meta-model's P(primary correct) < threshold.
Under C19 adaptive labels, the label quality is higher so the meta-signal may be
stronger. This experiment sweeps threshold in [0.50..0.65] to find the optimal
operating point against the C19+CURRENT_FEATURES baseline.
Walk-forward: same windows as harness. Primary RF trained on train window;
MetaLabel trained in-sample on primary's own predictions (consistent with C4).
"""
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
from sklearn.ensemble import RandomForestClassifier
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,
LABEL_HORIZON, MIN_TRAIN, STEP, RF_PARAMS,
)
from models.predictor import MetaLabelClassifier, _build_features
THRESHOLDS = [0.50, 0.52, 0.55, 0.58, 0.60, 0.65]
def walk_forward_meta(feat_df, label_arr, cols, threshold):
avail = [c for c in cols if c in feat_df.columns]
X_all = feat_df[avail].fillna(0).values
n = len(feat_df)
y_true_all, y_pred_all = [], []
cutoff = MIN_TRAIN
while cutoff + STEP + LABEL_HORIZON <= n:
train_end = cutoff - LABEL_HORIZON
y_tr = label_arr[:train_end]
valid = ~np.isnan(y_tr)
y_v = y_tr[valid]
if len(y_v) < 10 or len(np.unique(y_v)) < 2:
cutoff += STEP; continue
X_tr = X_all[:train_end][valid]
clf = RandomForestClassifier(**RF_PARAMS)
clf.fit(X_tr, y_v.astype(int))
train_pred = clf.predict(X_tr)
train_proba = clf.predict_proba(X_tr)
meta = MetaLabelClassifier(threshold=threshold)
meta.fit(X_tr, train_pred, train_proba, y_v.astype(int))
test_end = min(cutoff + STEP, n - LABEL_HORIZON)
y_te = label_arr[cutoff:test_end]
valid_te = ~np.isnan(y_te)
if valid_te.sum() == 0:
cutoff += STEP; continue
X_te = X_all[cutoff:test_end][valid_te]
y_pred = clf.predict(X_te)
y_prob = clf.predict_proba(X_te)
y_filt = meta.filter(X_te, y_pred, y_prob)
y_true_all.extend(y_te[valid_te].tolist())
y_pred_all.extend(y_filt.tolist())
cutoff += STEP
if not y_true_all:
return {}
return compute_metrics(np.array(y_true_all), np.array(y_pred_all))
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")
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=== C29: MetaLabel threshold search [{tag}] ===")
print(f" Thresholds: {THRESHOLDS}\n")
# Pre-load stock data once
stock_data = {}
for s in stocks:
df = fetch_df(s)
if df is None or df.empty:
continue
feat = _build_features(df)
close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values
labels = build_triple_barrier_labels(close)
stock_data[s] = (feat, labels)
# baseline (no MetaLabel — standard walk_forward)
base_results = []
for s, (feat, labels) in stock_data.items():
base_results.append(walk_forward(feat, labels, CURRENT_FEATURES))
base_agg = {
"dir_accuracy": _avg(base_results, "dir_accuracy"),
"up_precision": _avg(base_results, "up_precision"),
}
print(f" Baseline (no meta): dir={base_agg['dir_accuracy']}% ↑prec={base_agg['up_precision']}%\n")
threshold_results = {}
best_thresh = None
best_agg = None
best_score = -999
for thresh in THRESHOLDS:
results = []
stock_lines = []
for s, (feat, labels) in stock_data.items():
m = walk_forward_meta(feat, labels, CURRENT_FEATURES, thresh)
results.append(m)
d = m.get("dir_accuracy", float("nan"))
u = m.get("up_precision", float("nan"))
n = m.get("n_signals", 0)
stock_lines.append(f"{s}: dir={d}% ↑prec={u}% sigs={n}")
agg = {
"dir_accuracy": _avg(results, "dir_accuracy"),
"up_precision": _avg(results, "up_precision"),
"n_signals": _avg(results, "n_signals"),
}
passed = agg["dir_accuracy"] >= PASS_DIR_ACC and agg["up_precision"] >= PASS_UP_PREC
flag = "PASS ✓" if passed else "FAIL ✗"
print(f" thresh={thresh}: dir={agg['dir_accuracy']}% ↑prec={agg['up_precision']}% sigs={agg['n_signals']} {flag}")
for line in stock_lines:
print(f" {line}")
print()
threshold_results[thresh] = {"agg": agg, "passed": passed}
score = agg["up_precision"] if agg["dir_accuracy"] >= PASS_DIR_ACC else agg["up_precision"] - 10
if score > best_score:
best_score = score
best_thresh = thresh
best_agg = agg
passed = best_agg["dir_accuracy"] >= PASS_DIR_ACC and best_agg["up_precision"] >= PASS_UP_PREC
print(f" Best threshold: {best_thresh}")
print(f" Best agg: dir={best_agg['dir_accuracy']}% ↑prec={best_agg['up_precision']}%")
print(f" Gate (dir≥{PASS_DIR_ACC}% AND ↑prec≥{PASS_UP_PREC}%): {'PASS ✓' if passed else 'FAIL ✗'}")
result = {
"experiment": "C29",
"description": "MetaLabel confidence threshold search under C19 labels",
"thresholds": THRESHOLDS,
"best_threshold": best_thresh,
"stocks": stocks,
"aggregate": {"baseline": base_agg, "best_c29": best_agg},
"all_thresholds": {str(t): threshold_results[t] for t in THRESHOLDS},
"passed": passed,
"pass_gate": {"dir_accuracy": PASS_DIR_ACC, "up_precision": PASS_UP_PREC},
}
out = ROOT / f"docs/c29_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())
|