DennisChan0909 Claude Sonnet 4.6 commited on
Commit
55298f4
·
1 Parent(s): e3117db

feat(C12): 1-day horizon; FAILED

Browse files

1-day triple barrier labels: dir_acc 18.0% (-23.3pp), up_prec 20.4% (-31.8pp).
5-day horizon is essential — shorter horizon can't overcome next-day noise.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

docs/c12_1day_result.json ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "results": {
3
+ "2330": {
4
+ "5day": {
5
+ "accuracy": 33.3,
6
+ "dir_accuracy": 36.7,
7
+ "up_precision": 52.9,
8
+ "dn_precision": 27.8,
9
+ "n_signals": 196,
10
+ "n_predictions": 231
11
+ },
12
+ "1day": {
13
+ "accuracy": 55.4,
14
+ "dir_accuracy": 24.6,
15
+ "up_precision": 29.4,
16
+ "dn_precision": 19.4,
17
+ "n_signals": 65,
18
+ "n_predictions": 231
19
+ }
20
+ },
21
+ "0050": {
22
+ "5day": {
23
+ "accuracy": 38.1,
24
+ "dir_accuracy": 43.1,
25
+ "up_precision": 62.8,
26
+ "dn_precision": 29.9,
27
+ "n_signals": 195,
28
+ "n_predictions": 231
29
+ },
30
+ "1day": {
31
+ "accuracy": 56.3,
32
+ "dir_accuracy": 24.6,
33
+ "up_precision": 30.0,
34
+ "dn_precision": 6.7,
35
+ "n_signals": 65,
36
+ "n_predictions": 231
37
+ }
38
+ },
39
+ "2317": {
40
+ "5day": {
41
+ "accuracy": 39.0,
42
+ "dir_accuracy": 40.5,
43
+ "up_precision": 50.8,
44
+ "dn_precision": 28.4,
45
+ "n_signals": 220,
46
+ "n_predictions": 231
47
+ },
48
+ "1day": {
49
+ "accuracy": 55.4,
50
+ "dir_accuracy": 11.9,
51
+ "up_precision": 8.7,
52
+ "dn_precision": 13.9,
53
+ "n_signals": 59,
54
+ "n_predictions": 231
55
+ }
56
+ },
57
+ "2454": {
58
+ "5day": {
59
+ "accuracy": 39.4,
60
+ "dir_accuracy": 41.6,
61
+ "up_precision": 41.3,
62
+ "dn_precision": 41.8,
63
+ "n_signals": 190,
64
+ "n_predictions": 231
65
+ },
66
+ "1day": {
67
+ "accuracy": 51.5,
68
+ "dir_accuracy": 11.9,
69
+ "up_precision": 16.1,
70
+ "dn_precision": 8.3,
71
+ "n_signals": 67,
72
+ "n_predictions": 231
73
+ }
74
+ },
75
+ "2881": {
76
+ "5day": {
77
+ "accuracy": 40.3,
78
+ "dir_accuracy": 44.8,
79
+ "up_precision": 53.0,
80
+ "dn_precision": 36.9,
81
+ "n_signals": 203,
82
+ "n_predictions": 231
83
+ },
84
+ "1day": {
85
+ "accuracy": 54.5,
86
+ "dir_accuracy": 17.2,
87
+ "up_precision": 17.9,
88
+ "dn_precision": 16.0,
89
+ "n_signals": 64,
90
+ "n_predictions": 231
91
+ }
92
+ }
93
+ },
94
+ "aggregate": {
95
+ "5day": {
96
+ "accuracy": 38.0,
97
+ "dir_accuracy": 41.3,
98
+ "up_precision": 52.2,
99
+ "dn_precision": 33.0,
100
+ "n_signals": 200.8
101
+ },
102
+ "1day": {
103
+ "accuracy": 54.6,
104
+ "dir_accuracy": 18.0,
105
+ "up_precision": 20.4,
106
+ "dn_precision": 12.9,
107
+ "n_signals": 64.0
108
+ }
109
+ },
110
+ "passed": false,
111
+ "pass_criterion": "1day dir_accuracy >= 44.0 AND up_precision >= 54.0"
112
+ }
docs/improvements_log.md CHANGED
@@ -13,3 +13,4 @@
13
  | C9 | multi-stock training universe (20 stocks) | dir_acc: 41.3 → 40.4, up_prec: 52.2 → 52.9 | FAILED — cross-stock label noise hurts more than extra data helps |
14
  | C10 | asymmetric triple barrier grid (pt_ratio=1.0..3.0, sl fixed 1.0) | up_prec: 52.2 @ pt=1.0, 43.5 @ pt=1.5, 37.2 @ pt=2.0 — monotonically worse | FAILED — wider UP barrier degrades precision; symmetric pt=1.0 remains best |
15
  | C11 | full ensemble walk-forward (RF + XGB + LGBM) | dir_acc: 41.3 → 43.1 (+1.8pp), up_prec: 52.2 → 52.2 (flat) | FAILED — dir misses 43.5 threshold; RF already representative; ↑prec flat |
 
 
13
  | C9 | multi-stock training universe (20 stocks) | dir_acc: 41.3 → 40.4, up_prec: 52.2 → 52.9 | FAILED — cross-stock label noise hurts more than extra data helps |
14
  | C10 | asymmetric triple barrier grid (pt_ratio=1.0..3.0, sl fixed 1.0) | up_prec: 52.2 @ pt=1.0, 43.5 @ pt=1.5, 37.2 @ pt=2.0 — monotonically worse | FAILED — wider UP barrier degrades precision; symmetric pt=1.0 remains best |
15
  | C11 | full ensemble walk-forward (RF + XGB + LGBM) | dir_acc: 41.3 → 43.1 (+1.8pp), up_prec: 52.2 → 52.2 (flat) | FAILED — dir misses 43.5 threshold; RF already representative; ↑prec flat |
16
+ | C12 | 1-day label horizon (vs 5-day triple barrier) | dir_acc: 41.3 → 18.0% (-23.3pp), up_prec: 52.2 → 20.4% (-31.8pp) | FAILED — 1-day horizon catastrophically degrades; model can't predict next-day direction; 5-day horizon absorbs noise better |
scripts/backtest_c12.py ADDED
@@ -0,0 +1,157 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """C12: 1-day vs 5-day label horizon comparison."""
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ ROOT = Path(__file__).resolve().parent.parent
7
+ sys.path.insert(0, str(ROOT))
8
+
9
+ try:
10
+ from dotenv import load_dotenv; load_dotenv(ROOT / ".env")
11
+ except ImportError:
12
+ pass
13
+
14
+ import warnings; warnings.filterwarnings("ignore")
15
+ import json
16
+ import numpy as np
17
+ import pandas as pd
18
+ from sklearn.ensemble import RandomForestClassifier
19
+
20
+ from scripts.improvement_harness import (
21
+ fetch_df, build_triple_barrier_labels, walk_forward,
22
+ compute_metrics, BASELINE_FEATURES, DEFAULT_STOCKS,
23
+ RF_PARAMS, MIN_TRAIN, STEP, LABEL_HORIZON,
24
+ )
25
+ from models.predictor import _build_features
26
+
27
+ PASS_DIR = 44.0
28
+ PASS_UP = 54.0
29
+
30
+
31
+ def build_1day_labels(close: np.ndarray, vol_lookback: int = 20, pt_sl_ratio: float = 1.0) -> np.ndarray:
32
+ """Triple barrier with 1-day vertical barrier instead of 5-day."""
33
+ n = len(close)
34
+ log_ret = np.diff(np.log(close + 1e-9))
35
+ labels = np.full(n, np.nan)
36
+ for i in range(n - 1): # horizon=1
37
+ start_v = max(0, i - vol_lookback)
38
+ window = log_ret[start_v:i]
39
+ vol = float(np.std(window)) if len(window) >= 5 else 0.015
40
+ vol = max(vol, 0.001)
41
+ upper = close[i] * (1.0 + vol * pt_sl_ratio)
42
+ lower = close[i] * (1.0 - vol * pt_sl_ratio)
43
+ c = close[i + 1]
44
+ if c >= upper:
45
+ labels[i] = 1
46
+ elif c <= lower:
47
+ labels[i] = -1
48
+ else:
49
+ labels[i] = 0
50
+ return labels
51
+
52
+
53
+ def walk_forward_1d(feat_df: pd.DataFrame, label_arr: np.ndarray, cols: list) -> dict:
54
+ LABEL_HORIZON_1D = 1
55
+ avail = [c for c in cols if c in feat_df.columns]
56
+ X_all = feat_df[avail].fillna(0).values
57
+ n = len(feat_df)
58
+ y_true_all, y_pred_all = [], []
59
+ cutoff = MIN_TRAIN
60
+ while cutoff + STEP + LABEL_HORIZON_1D <= n:
61
+ y_tr = label_arr[:cutoff]
62
+ valid = ~np.isnan(y_tr)
63
+ y_v = y_tr[valid]
64
+ if len(y_v) < 10 or len(np.unique(y_v)) < 2:
65
+ cutoff += STEP; continue
66
+ clf = RandomForestClassifier(**RF_PARAMS)
67
+ clf.fit(X_all[:cutoff][valid], y_v.astype(int))
68
+ test_end = min(cutoff + STEP, n - LABEL_HORIZON_1D)
69
+ y_te = label_arr[cutoff:test_end]
70
+ valid_te = ~np.isnan(y_te)
71
+ if valid_te.sum() == 0:
72
+ cutoff += STEP; continue
73
+ y_pred = clf.predict(X_all[cutoff:test_end][valid_te])
74
+ y_true_all.extend(y_te[valid_te].tolist())
75
+ y_pred_all.extend(y_pred.tolist())
76
+ cutoff += STEP
77
+ if not y_true_all:
78
+ return {}
79
+ return compute_metrics(np.array(y_true_all), np.array(y_pred_all))
80
+
81
+
82
+ def main():
83
+ hdr = f"{'Stock':>6} {'5day_dir%':>9} {'1day_dir%':>9} {'Δdir':>5} {'5day_↑prec%':>11} {'1day_↑prec%':>11} {'Δprec':>6} {'1day_sigs':>9}"
84
+ print(f"\n{hdr}\n{'-'*len(hdr)}")
85
+
86
+ per_stock = {}
87
+ rows_5d, rows_1d = [], []
88
+
89
+ for stock_no in DEFAULT_STOCKS:
90
+ print(f" computing {stock_no}...", end="\r", flush=True)
91
+ df = fetch_df(stock_no)
92
+ if df is None or df.empty:
93
+ print(f"{stock_no:>6} no data")
94
+ continue
95
+
96
+ feat = _build_features(df)
97
+ close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values
98
+
99
+ labels_5d = build_triple_barrier_labels(close)
100
+ r5 = walk_forward(feat, labels_5d, BASELINE_FEATURES)
101
+
102
+ labels_1d = build_1day_labels(close)
103
+ r1 = walk_forward_1d(feat, labels_1d, BASELINE_FEATURES)
104
+
105
+ per_stock[stock_no] = {"5day": r5, "1day": r1}
106
+ if r5:
107
+ rows_5d.append(r5)
108
+ if r1:
109
+ rows_1d.append(r1)
110
+
111
+ d5 = r5.get("dir_accuracy", float("nan"))
112
+ d1 = r1.get("dir_accuracy", float("nan"))
113
+ p5 = r5.get("up_precision", float("nan"))
114
+ p1 = r1.get("up_precision", float("nan"))
115
+ dd = d1 - d5 if not (np.isnan(d1) or np.isnan(d5)) else float("nan")
116
+ dp = p1 - p5 if not (np.isnan(p1) or np.isnan(p5)) else float("nan")
117
+ sigs = r1.get("n_signals", 0)
118
+ print(f"{stock_no:>6} {d5:>9.1f} {d1:>9.1f} {dd:>+5.1f} {p5:>11.1f} {p1:>11.1f} {dp:>+6.1f} {sigs:>9}")
119
+
120
+ def _mean(rows, k):
121
+ vals = [r[k] for r in rows if isinstance(r.get(k), (int, float)) and not np.isnan(r.get(k, float("nan")))]
122
+ return round(sum(vals) / len(vals), 1) if vals else float("nan")
123
+
124
+ agg5 = {k: _mean(rows_5d, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")}
125
+ agg1 = {k: _mean(rows_1d, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")}
126
+
127
+ dd_agg = agg1["dir_accuracy"] - agg5["dir_accuracy"]
128
+ dp_agg = agg1["up_precision"] - agg5["up_precision"]
129
+
130
+ print(f"\n{'=== AGGREGATE ==='}")
131
+ print(f" 5day: dir={agg5['dir_accuracy']}% ↑prec={agg5['up_precision']}% signals={agg5['n_signals']}")
132
+ print(f" 1day: dir={agg1['dir_accuracy']}% ↑prec={agg1['up_precision']}% signals={agg1['n_signals']}")
133
+ print(f" Δ : dir={dd_agg:+.1f}pp ↑prec={dp_agg:+.1f}pp")
134
+
135
+ passed = bool((agg1["dir_accuracy"] >= PASS_DIR) and (agg1["up_precision"] >= PASS_UP))
136
+ status = "PASSED" if passed else "FAILED"
137
+ print(f"\n Pass criterion: 1day dir ≥ {PASS_DIR} AND ↑prec ≥ {PASS_UP}")
138
+ print(f" C12 {status}")
139
+
140
+ result = {
141
+ "results": per_stock,
142
+ "aggregate": {"5day": agg5, "1day": agg1},
143
+ "passed": passed,
144
+ "pass_criterion": f"1day dir_accuracy >= {PASS_DIR} AND up_precision >= {PASS_UP}",
145
+ }
146
+
147
+ out = ROOT / "docs" / "c12_1day_result.json"
148
+ out.parent.mkdir(exist_ok=True)
149
+ with open(out, "w") as f:
150
+ json.dump(result, f, indent=2)
151
+ print(f"\n Written: {out}")
152
+
153
+ print(f"\nC12 {status} — 1day dir: {agg1['dir_accuracy']}%, ↑prec: {agg1['up_precision']}%, signals/stock: {agg1['n_signals']}")
154
+
155
+
156
+ if __name__ == "__main__":
157
+ main()