File size: 5,878 Bytes
55298f4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
#!/usr/bin/env python3
"""C12: 1-day vs 5-day label horizon comparison."""
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 (
    fetch_df, build_triple_barrier_labels, walk_forward,
    compute_metrics, BASELINE_FEATURES, DEFAULT_STOCKS,
    RF_PARAMS, MIN_TRAIN, STEP, LABEL_HORIZON,
)
from models.predictor import _build_features

PASS_DIR = 44.0
PASS_UP  = 54.0


def build_1day_labels(close: np.ndarray, vol_lookback: int = 20, pt_sl_ratio: float = 1.0) -> np.ndarray:
    """Triple barrier with 1-day vertical barrier instead of 5-day."""
    n = len(close)
    log_ret = np.diff(np.log(close + 1e-9))
    labels = np.full(n, np.nan)
    for i in range(n - 1):  # horizon=1
        start_v = max(0, i - vol_lookback)
        window = log_ret[start_v:i]
        vol = float(np.std(window)) if len(window) >= 5 else 0.015
        vol = max(vol, 0.001)
        upper = close[i] * (1.0 + vol * pt_sl_ratio)
        lower = close[i] * (1.0 - vol * pt_sl_ratio)
        c = close[i + 1]
        if c >= upper:
            labels[i] = 1
        elif c <= lower:
            labels[i] = -1
        else:
            labels[i] = 0
    return labels


def walk_forward_1d(feat_df: pd.DataFrame, label_arr: np.ndarray, cols: list) -> dict:
    LABEL_HORIZON_1D = 1
    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_1D <= n:
        y_tr = label_arr[:cutoff]
        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
        clf = RandomForestClassifier(**RF_PARAMS)
        clf.fit(X_all[:cutoff][valid], y_v.astype(int))
        test_end = min(cutoff + STEP, n - LABEL_HORIZON_1D)
        y_te = label_arr[cutoff:test_end]
        valid_te = ~np.isnan(y_te)
        if valid_te.sum() == 0:
            cutoff += STEP; continue
        y_pred = clf.predict(X_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():
    hdr = f"{'Stock':>6}  {'5day_dir%':>9}  {'1day_dir%':>9}  {'Δdir':>5}  {'5day_↑prec%':>11}  {'1day_↑prec%':>11}  {'Δprec':>6}  {'1day_sigs':>9}"
    print(f"\n{hdr}\n{'-'*len(hdr)}")

    per_stock = {}
    rows_5d, rows_1d = [], []

    for stock_no in DEFAULT_STOCKS:
        print(f"  computing {stock_no}...", end="\r", flush=True)
        df = fetch_df(stock_no)
        if df is None or df.empty:
            print(f"{stock_no:>6}  no data")
            continue

        feat = _build_features(df)
        close = (df.set_index("date")["close"] if "date" in df.columns else df["close"]).values

        labels_5d = build_triple_barrier_labels(close)
        r5 = walk_forward(feat, labels_5d, BASELINE_FEATURES)

        labels_1d = build_1day_labels(close)
        r1 = walk_forward_1d(feat, labels_1d, BASELINE_FEATURES)

        per_stock[stock_no] = {"5day": r5, "1day": r1}
        if r5:
            rows_5d.append(r5)
        if r1:
            rows_1d.append(r1)

        d5 = r5.get("dir_accuracy", float("nan"))
        d1 = r1.get("dir_accuracy", float("nan"))
        p5 = r5.get("up_precision", float("nan"))
        p1 = r1.get("up_precision", float("nan"))
        dd = d1 - d5 if not (np.isnan(d1) or np.isnan(d5)) else float("nan")
        dp = p1 - p5 if not (np.isnan(p1) or np.isnan(p5)) else float("nan")
        sigs = r1.get("n_signals", 0)
        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}")

    def _mean(rows, k):
        vals = [r[k] for r in rows if isinstance(r.get(k), (int, float)) and not np.isnan(r.get(k, float("nan")))]
        return round(sum(vals) / len(vals), 1) if vals else float("nan")

    agg5 = {k: _mean(rows_5d, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")}
    agg1 = {k: _mean(rows_1d, k) for k in ("accuracy", "dir_accuracy", "up_precision", "dn_precision", "n_signals")}

    dd_agg = agg1["dir_accuracy"] - agg5["dir_accuracy"]
    dp_agg = agg1["up_precision"] - agg5["up_precision"]

    print(f"\n{'=== AGGREGATE ==='}")
    print(f"  5day: dir={agg5['dir_accuracy']}%  ↑prec={agg5['up_precision']}%  signals={agg5['n_signals']}")
    print(f"  1day: dir={agg1['dir_accuracy']}%  ↑prec={agg1['up_precision']}%  signals={agg1['n_signals']}")
    print(f"  Δ   : dir={dd_agg:+.1f}pp  ↑prec={dp_agg:+.1f}pp")

    passed = bool((agg1["dir_accuracy"] >= PASS_DIR) and (agg1["up_precision"] >= PASS_UP))
    status = "PASSED" if passed else "FAILED"
    print(f"\n  Pass criterion: 1day dir ≥ {PASS_DIR} AND ↑prec ≥ {PASS_UP}")
    print(f"  C12 {status}")

    result = {
        "results": per_stock,
        "aggregate": {"5day": agg5, "1day": agg1},
        "passed": passed,
        "pass_criterion": f"1day dir_accuracy >= {PASS_DIR} AND up_precision >= {PASS_UP}",
    }

    out = ROOT / "docs" / "c12_1day_result.json"
    out.parent.mkdir(exist_ok=True)
    with open(out, "w") as f:
        json.dump(result, f, indent=2)
    print(f"\n  Written: {out}")

    print(f"\nC12 {status} — 1day dir: {agg1['dir_accuracy']}%, ↑prec: {agg1['up_precision']}%, signals/stock: {agg1['n_signals']}")


if __name__ == "__main__":
    main()