DockerSpace / sim_random10.py
DennisChan0909's picture
feat(metal): Phase 1 backend — gold/silver analysis API
4873e94
Raw
History Blame Contribute Delete
6.3 kB
"""
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()