File size: 6,298 Bytes
4873e94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
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()