""" One-shot simulation: pick 10 random TW stocks, fetch predictor signal, then compute P&L for two strategies over the past ~1 month: A) Equal weight across ALL 10 (random baseline) B) Equal weight ONLY across stocks currently flagged BUY by the predictor Uses the live Render API so this reflects the same model the product serves. """ import random import time import json import sys from datetime import date, timedelta import requests if sys.stdout.encoding and sys.stdout.encoding.lower() != "utf-8": try: sys.stdout.reconfigure(encoding="utf-8") except Exception: pass BASE = "https://stock-predictor-1oyc.onrender.com" UNIVERSE = [ ("2330", "台積電"), ("2317", "鴻海"), ("2454", "聯發科"), ("2881", "富邦金"), ("2882", "國泰金"), ("2412", "中華電"), ("1301", "台塑"), ("1303", "南亞"), ("2002", "中鋼"), ("2303", "聯電"), ("2308", "台達電"), ("2891", "中信金"), ("3008", "大立光"), ("2603", "長榮"), ("2609", "陽明"), ("2615", "萬海"), ("3034", "聯詠"), ("6505", "台塑化"), ("1216", "統一"), ("2885", "元大金"), ("2884", "玉山金"), ("2886", "兆豐金"), ("2892", "第一金"), ("2357", "華碩"), ("2382", "廣達"), ("6669", "緯穎"), ("3711", "日月光投控"), ("2912", "統一超"), ("1101", "台泥"), ("2880", "華南金"), ("3231", "緯創"), ("1476", "儒鴻"), ("2207", "和泰車"), ("4904", "遠傳"), ("3045", "台灣大"), ("2379", "瑞昱"), ] def pick_random(n: int = 10, seed: int | None = None) -> list[tuple[str, str]]: rng = random.Random(seed) return rng.sample(UNIVERSE, n) def fetch_history(code: str, months: int = 2) -> list[dict]: r = requests.get(f"{BASE}/api/stock/{code}/history", params={"months": months}, timeout=60) r.raise_for_status() return r.json().get("data", []) def fetch_batch_predict(codes: list[str], retries: int = 3) -> list[dict]: last_exc = None for attempt in range(retries): try: r = requests.get(f"{BASE}/api/stock/batch", params={"codes": ",".join(codes)}, timeout=180) r.raise_for_status() return r.json().get("results", []) except Exception as exc: last_exc = exc print(f" retry {attempt + 1}/{retries}: {exc}") time.sleep(5) raise last_exc def get_prices_around(hist: list[dict], target_days_ago: int = 21) -> tuple[float, float, str, str]: """Return (buy_price, sell_price, buy_date, sell_date) using close prices.""" if not hist: raise ValueError("no history") sell = hist[-1] target_idx = max(0, len(hist) - 1 - target_days_ago) buy = hist[target_idx] return float(buy["close"]), float(sell["close"]), buy["date"], sell["date"] def main(): seed_arg = sys.argv[1] if len(sys.argv) > 1 else None seed = int(seed_arg) if seed_arg else int(time.time()) % 10000 picked = pick_random(10, seed=seed) print(f"=== seed={seed} — randomly picked 10 TW stocks ===") for code, name in picked: print(f" {code} {name}") print() # Fetch history for each rows = [] for code, name in picked: try: hist = fetch_history(code, months=2) buy_p, sell_p, buy_d, sell_d = get_prices_around(hist, target_days_ago=21) pct = (sell_p - buy_p) / buy_p * 100 rows.append({ "code": code, "name": name, "buy_date": buy_d, "sell_date": sell_d, "buy_price": buy_p, "sell_price": sell_p, "pct": pct, }) print(f" history {code} {name}: {buy_d} {buy_p:.2f} -> {sell_d} {sell_p:.2f} ({pct:+.2f}%)") except Exception as exc: print(f" history {code} FAILED: {exc}") time.sleep(0.3) print() # Fetch predictor signals (batch, 5 at a time, rate limited) preds: dict[str, dict] = {} codes = [r["code"] for r in rows] for i in range(0, len(codes), 5): chunk = codes[i : i + 5] try: res = fetch_batch_predict(chunk) for p in res: if "error" not in p: preds[p["code"]] = p print(f" predict batch {i//5 + 1}: {[(p['code'], p.get('signal'), p.get('buy_prob')) for p in res]}") except Exception as exc: print(f" predict batch FAILED: {exc}") if i + 5 < len(codes): time.sleep(15) # batch rate limit: 5/min print() # Simulate capital = 1_000_000 # Strategy A: equal weight, all 10 if rows: per = capital / len(rows) total_a = sum(per * (1 + r["pct"] / 100) for r in rows) pnl_a = total_a - capital else: total_a = capital pnl_a = 0 # Strategy B: equal weight, only BUY signals buys = [r for r in rows if preds.get(r["code"], {}).get("signal") == "BUY"] if buys: per_b = capital / len(buys) total_b = sum(per_b * (1 + r["pct"] / 100) for r in buys) pnl_b = total_b - capital else: total_b = capital pnl_b = 0 print("=" * 60) print(f"本金: NT${capital:,}") print() print(f"[A] 隨機等權重 10 檔") for r in rows: per = capital / len(rows) print(f" {r['code']} {r['name']:8} {r['pct']:+6.2f}% 損益 {per * r['pct'] / 100:+10,.0f}") print(f" -> 總市值 NT${total_a:,.0f} 損益 NT${pnl_a:+,.0f} ({pnl_a/capital*100:+.2f}%)") print() print(f"[B] 只買預測 BUY 的 {len(buys)} 檔 (等權重)") if buys: for r in buys: per_b = capital / len(buys) print(f" {r['code']} {r['name']:8} {r['pct']:+6.2f}% 損益 {per_b * r['pct'] / 100:+10,.0f}") print(f" -> 總市值 NT${total_b:,.0f} 損益 NT${pnl_b:+,.0f} ({pnl_b/capital*100:+.2f}%)") else: print(" (預測器目前對這 10 檔都沒有 BUY 訊號 -> 全部持幣)") print(f" -> 總市值 NT${total_b:,.0f} 損益 NT${pnl_b:+,.0f} (0.00%)") print() print("註:B 策略使用「當下」預測器訊號回看一個月,有 look-ahead bias。") print("正統的做法是用 /api/stock/{code}/backtest 做 walk-forward。") if __name__ == "__main__": main()