DennisChan0909 Claude Sonnet 4.6 commited on
Commit
df2c658
·
1 Parent(s): 9fa1ee6

Add full TW universe precompute + midnight LaunchAgent

Browse files

- scripts/precompute_all_tw_stocks.py: runs ML on all 2279 TWSE/TPEX
stocks+ETFs sequentially with --resume support and incremental saves
every 50 stocks
- scripts/precompute_all_tw_stocks_cron.sh: shell wrapper with lock file
and logging
- LaunchAgent: com.dennis.stock-predictor-precompute-all fires at 00:00

Estimated runtime: ~2h on warm cache. Output: data/precomputed_predictions_all.json

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

frontend/.env.local CHANGED
@@ -4,3 +4,4 @@ VITE_FIREBASE_PROJECT_ID=dennisstocktest
4
  VITE_FIREBASE_STORAGE_BUCKET=dennisstocktest.firebasestorage.app
5
  VITE_FIREBASE_MESSAGING_SENDER_ID=715584494213
6
  VITE_FIREBASE_APP_ID=1:715584494213:web:ce343ed485b8afa44b27ec
 
 
4
  VITE_FIREBASE_STORAGE_BUCKET=dennisstocktest.firebasestorage.app
5
  VITE_FIREBASE_MESSAGING_SENDER_ID=715584494213
6
  VITE_FIREBASE_APP_ID=1:715584494213:web:ce343ed485b8afa44b27ec
7
+ VITE_SKIP_LOGIN=1
scripts/precompute_all_tw_stocks.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Precompute ML predictions for the entire TW listed stock universe.
3
+
4
+ Runs sequentially with incremental saves every --save-interval stocks so
5
+ an interrupted run can be resumed. Designed to run overnight (midnight cron).
6
+
7
+ Usage:
8
+ venv/bin/python scripts/precompute_all_tw_stocks.py
9
+ venv/bin/python scripts/precompute_all_tw_stocks.py --limit 20 # smoke
10
+ venv/bin/python scripts/precompute_all_tw_stocks.py --resume # skip already-done
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import argparse
15
+ import json
16
+ import sys
17
+ import time
18
+ from datetime import datetime, timezone
19
+ from pathlib import Path
20
+
21
+ ROOT = Path(__file__).resolve().parent.parent
22
+ sys.path.insert(0, str(ROOT))
23
+
24
+ try:
25
+ from dotenv import load_dotenv; load_dotenv(ROOT / ".env")
26
+ except ImportError:
27
+ pass
28
+
29
+ import os
30
+ # Disable heavy optional enrichments during batch run — saves ~3x time
31
+ os.environ.setdefault("ENABLE_REVENUE_FEATURES", "0")
32
+ os.environ.setdefault("ENABLE_SECURITIES_LENDING", "0")
33
+ os.environ.setdefault("ENABLE_BLOCK_TRADE_FEATURES", "0")
34
+ os.environ.setdefault("ENABLE_LARGE_HOLDER_FEATURES", "0")
35
+ os.environ.setdefault("ENABLE_NEWS_OVERLAY", "0")
36
+
37
+ import warnings; warnings.filterwarnings("ignore")
38
+ import twstock
39
+
40
+ OUTPUT_PATH = ROOT / "data" / "precomputed_predictions_all.json"
41
+ SAVE_INTERVAL = 50 # write to disk every N stocks
42
+
43
+ # Only TWSE / TPEX listed stocks and ETFs — skip warrants, TDRs, REITs, etc.
44
+ VALID_MARKETS = {"上市", "上櫃"}
45
+ VALID_TYPES = {"股票", "ETF", "ETN"}
46
+
47
+
48
+ def _build_stock_list(limit: int | None = None) -> list[str]:
49
+ codes = []
50
+ for raw_code, info in twstock.codes.items():
51
+ market = getattr(info, "market", "") or ""
52
+ stype = getattr(info, "type", "") or ""
53
+ if market not in VALID_MARKETS or stype not in VALID_TYPES:
54
+ continue
55
+ code = str(raw_code).replace(".TW", "").replace(".TWO", "").strip()
56
+ if code:
57
+ codes.append(code)
58
+ codes.sort()
59
+ return codes[:limit] if limit else codes
60
+
61
+
62
+ def _run_stock(code: str) -> dict:
63
+ """Run full ML pipeline for one stock. Returns status dict."""
64
+ from services.predictor_service import _fetch_with_cache, load_predictor
65
+ from indicators.technical import add_all_indicators, add_cross_asset_tw
66
+ from data.institutional_flow import add_institutional_flow
67
+ from data.margin_flow import add_margin_flow
68
+ from data.fetcher import fetch_cross_asset_tw
69
+ from models.predictor import _build_features
70
+
71
+ df = _fetch_with_cache(code, months=24)
72
+ if df is None or df.empty:
73
+ return {"status": "no_data"}
74
+ if len(df) < 30:
75
+ return {"status": "insufficient_data", "rows": int(len(df))}
76
+
77
+ df = add_all_indicators(df)
78
+ df = add_institutional_flow(df, code)
79
+ df = add_margin_flow(df, code)
80
+ start, end = str(df["date"].min()), str(df["date"].max())
81
+ taiex, usdtwd, sox, tnx = fetch_cross_asset_tw(start, end)
82
+ df = add_cross_asset_tw(df, taiex, usdtwd, sox_close=sox, tnx_close=tnx)
83
+
84
+ features = _build_features(df)
85
+ predictor = load_predictor(code, df, raise_http=False, precomputed_features=features)
86
+ result = predictor.predict(df, precomputed_features=features)
87
+ result["optimized"] = True
88
+ result["source"] = "mac_precompute_all"
89
+
90
+ return {
91
+ "status": "done",
92
+ "completed_at": datetime.now(timezone.utc).isoformat(),
93
+ "source": "mac_precompute",
94
+ "result": result,
95
+ }
96
+
97
+
98
+ def main() -> int:
99
+ parser = argparse.ArgumentParser()
100
+ parser.add_argument("--limit", type=int, default=None, help="Cap number of stocks (smoke test)")
101
+ parser.add_argument("--save-interval", type=int, default=SAVE_INTERVAL)
102
+ parser.add_argument("--output", default=str(OUTPUT_PATH))
103
+ parser.add_argument("--resume", action="store_true", help="Skip stocks already in output file")
104
+ args = parser.parse_args()
105
+
106
+ output = Path(args.output)
107
+ output.parent.mkdir(parents=True, exist_ok=True)
108
+
109
+ # Load existing results if resuming
110
+ cache: dict = {}
111
+ if args.resume and output.exists():
112
+ try:
113
+ cache = json.loads(output.read_text(encoding="utf-8"))
114
+ print(f"Resume: {len(cache)} stocks already done")
115
+ except Exception:
116
+ pass
117
+
118
+ codes = _build_stock_list(args.limit)
119
+ todo = [c for c in codes if c not in cache] if args.resume else codes
120
+
121
+ print(f"Universe: {len(codes)} stocks | To process: {len(todo)}", flush=True)
122
+ started_at = datetime.now(timezone.utc).isoformat()
123
+
124
+ ok = err = skipped = 0
125
+ t0 = time.monotonic()
126
+
127
+ for i, code in enumerate(todo, 1):
128
+ t_stock = time.monotonic()
129
+ try:
130
+ entry = _run_stock(code)
131
+ except Exception as exc:
132
+ entry = {"status": "error", "error": str(exc),
133
+ "completed_at": datetime.now(timezone.utc).isoformat()}
134
+
135
+ cache[code] = entry
136
+ status = entry.get("status", "?")
137
+
138
+ if status == "done":
139
+ ok += 1
140
+ elif status in ("no_data", "insufficient_data"):
141
+ skipped += 1
142
+ else:
143
+ err += 1
144
+
145
+ elapsed = time.monotonic() - t_stock
146
+ done_total = ok + err + skipped
147
+ eta_s = (time.monotonic() - t0) / done_total * (len(todo) - done_total) if done_total else 0
148
+ print(
149
+ f"[{i}/{len(todo)}] {code:6s} {status:20s} {elapsed:.1f}s "
150
+ f"ok={ok} err={err} skip={skipped} ETA={int(eta_s//60)}m{int(eta_s%60):02d}s",
151
+ flush=True,
152
+ )
153
+
154
+ if i % args.save_interval == 0:
155
+ output.write_text(json.dumps(cache, ensure_ascii=False, default=str), encoding="utf-8")
156
+ print(f" [saved {len(cache)} entries → {output}]", flush=True)
157
+
158
+ # Final save
159
+ meta = {
160
+ "__meta__": {
161
+ "generated_at": started_at,
162
+ "completed_at": datetime.now(timezone.utc).isoformat(),
163
+ "universe_size": len(codes),
164
+ "processed": len(todo),
165
+ "ok": ok,
166
+ "errors": err,
167
+ "skipped": skipped,
168
+ }
169
+ }
170
+ cache.update(meta)
171
+ output.write_text(json.dumps(cache, ensure_ascii=False, default=str), encoding="utf-8")
172
+
173
+ elapsed_total = time.monotonic() - t0
174
+ print(
175
+ f"\nDone in {int(elapsed_total//60)}m{int(elapsed_total%60):02d}s "
176
+ f"ok={ok} err={err} skip={skipped} → {output}",
177
+ flush=True,
178
+ )
179
+ return 0 if err == 0 else 1
180
+
181
+
182
+ if __name__ == "__main__":
183
+ sys.exit(main())
scripts/precompute_all_tw_stocks_cron.sh ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/zsh
2
+ # Nightly full-universe TW stock precompute — called by LaunchAgent at midnight.
3
+ set -uo pipefail
4
+
5
+ REPO_DIR="/Users/po-chengchan/Documents/Codex/2026-05-09/help-me-to-download-the-stock/stock-predictor"
6
+ PY="$REPO_DIR/venv/bin/python"
7
+ LOG_DIR="$REPO_DIR/logs"
8
+ LOG_FILE="$LOG_DIR/precompute_all_tw_stocks.log"
9
+ LOCK_DIR="$HOME/.cache/stock-predictor-precompute-all.lock"
10
+
11
+ mkdir -p "$LOG_DIR" "$HOME/.cache"
12
+
13
+ # Prevent overlapping runs
14
+ if ! mkdir "$LOCK_DIR" 2>/dev/null; then
15
+ echo "$(date '+%Y-%m-%d %H:%M:%S %Z') skipped: another run is active" >> "$LOG_FILE"
16
+ exit 0
17
+ fi
18
+ trap 'rmdir "$LOCK_DIR" 2>/dev/null || true' EXIT
19
+
20
+ cd "$REPO_DIR" || exit 1
21
+ [[ -f "$REPO_DIR/.env" ]] && { set -a; source "$REPO_DIR/.env"; set +a; }
22
+
23
+ {
24
+ echo "==== $(date '+%Y-%m-%d %H:%M:%S %Z') precompute_all_tw_stocks start ===="
25
+ "$PY" scripts/precompute_all_tw_stocks.py --resume
26
+ echo "==== $(date '+%Y-%m-%d %H:%M:%S %Z') precompute_all_tw_stocks done ===="
27
+ } >> "$LOG_FILE" 2>&1