Last active
September 18, 2025 16:15
-
-
Save Ray901/2f8afc162373ad05416822db2da9cf83 to your computer and use it in GitHub Desktop.
Python FinMind Stock kline
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import requests | |
| import pandas as pd | |
| import plotly.graph_objects as go | |
| from plotly.subplots import make_subplots | |
| from datetime import datetime | |
| # ====== 設定區 ====== | |
| token = "" # ←請填入你的 FinMind API token | |
| start_date = "2025-01-01" | |
| end_date = datetime.today().strftime('%Y-%m-%d') | |
| stock_ids = [ | |
| "2330", "2317", "2454", "2344", | |
| "3036", "3260", "2408", "3231", | |
| "3293", "2337", "6531", "4973" | |
| ] | |
| stock_names = { | |
| "2330": "台積電", | |
| "2317": "鴻海", | |
| "2454": "聯發科", | |
| "2344": "華邦電", | |
| "3036": "文曄", | |
| "3260": "威剛", | |
| "2408": "南亞科", | |
| "3231": "緯創", | |
| "3293": "鈊象", | |
| "2337": "旺宏", | |
| "6531": "AP Memory", | |
| "4973": "廣穎電通" | |
| } | |
| # ====== 建立 3x4 子圖 ====== | |
| rows, cols = 3, 4 | |
| fig = make_subplots( | |
| rows=rows, cols=cols, | |
| shared_xaxes=False, | |
| shared_yaxes=False, | |
| subplot_titles=[stock_names.get(sid, sid) for sid in stock_ids], | |
| column_widths=[1/cols]*cols, | |
| row_heights=[1/rows]*rows, | |
| vertical_spacing=0.07, | |
| horizontal_spacing=0.05 | |
| ) | |
| # ====== 抓資料並畫圖 ====== | |
| valid_stocks = [] | |
| url = "https://api.finmindtrade.com/api/v4/data" | |
| for idx, stock_id in enumerate(stock_ids): | |
| params = { | |
| "dataset": "TaiwanStockPrice", | |
| "data_id": stock_id, | |
| "start_date": start_date, | |
| "end_date": end_date, | |
| "token": token | |
| } | |
| resp = requests.get(url, params=params, verify=False) | |
| data = resp.json() | |
| df = pd.DataFrame(data.get("data", [])) | |
| df.dropna(subset=["open", "max", "min", "close"], inplace=True) | |
| df = df[(df["open"] > 0) & (df["close"] > 0)] | |
| if df.empty or "date" not in df.columns: | |
| print(f"[!] {stock_id} 無資料或格式錯誤") | |
| continue | |
| # 計算均線 | |
| df["MA5"] = df["close"].rolling(window=5).mean() | |
| df["MA20"] = df["close"].rolling(window=20).mean() | |
| row = idx // cols + 1 | |
| col = idx % cols + 1 | |
| # K 線 | |
| fig.add_trace( | |
| go.Candlestick( | |
| x=df['date'], | |
| open=df['open'], | |
| high=df['max'], | |
| low=df['min'], | |
| close=df['close'], | |
| name=stock_names.get(stock_id, stock_id), | |
| showlegend=False, | |
| increasing_line_color="red", # 收漲 → 紅色 | |
| decreasing_line_color="green" # 收跌 → 綠色 | |
| ), | |
| row=row, | |
| col=col | |
| ) | |
| # 5日均線 | |
| fig.add_trace( | |
| go.Scatter( | |
| x=df['date'], | |
| y=df['MA5'], | |
| mode="lines", | |
| line=dict(color="orange", width=1), | |
| name="MA5", | |
| showlegend=False | |
| ), | |
| row=row, | |
| col=col | |
| ) | |
| # 20日均線 | |
| fig.add_trace( | |
| go.Scatter( | |
| x=df['date'], | |
| y=df['MA20'], | |
| mode="lines", | |
| line=dict(color="blue", width=1), | |
| name="MA20", | |
| showlegend=False | |
| ), | |
| row=row, | |
| col=col | |
| ) | |
| valid_stocks.append(stock_id) | |
| # ====== 關閉所有有效子圖的 Range Slider ====== | |
| for i in range(1, len(valid_stocks) + 1): | |
| fig.layout[f'xaxis{i}'].rangeslider.visible = False | |
| # ====== 圖表美化 ====== | |
| fig.update_layout( | |
| height=900, | |
| width=1600, | |
| title_text="台股 12 檔股票 K 線圖 + MA5/MA20(資料來源:FinMind)", | |
| showlegend=False, | |
| margin=dict(t=100, b=50) | |
| ) | |
| fig.write_html("stock_kline_3x4.html", auto_open=True) | |
| # ====== 抓資料並計算週漲幅 ====== | |
| weekly_changes = pd.DataFrame() | |
| for stock_id in stock_ids: | |
| params = { | |
| "dataset": "TaiwanStockPrice", | |
| "data_id": stock_id, | |
| "start_date": start_date, | |
| "end_date": end_date, | |
| "token": token | |
| } | |
| resp = requests.get(url, params=params, verify=False) | |
| data = resp.json() | |
| df = pd.DataFrame(data.get("data", [])) | |
| if df.empty or "date" not in df.columns: | |
| print(f"[!] {stock_id} 無資料或格式錯誤") | |
| continue | |
| df["date"] = pd.to_datetime(df["date"]) | |
| df = df.set_index("date") | |
| # 取每週最後一個收盤價 | |
| weekly_close = df["close"].resample("W").last() | |
| # 計算週漲幅 (%) | |
| weekly_return = weekly_close.pct_change() * 100 | |
| weekly_return.name = stock_names.get(stock_id, stock_id) | |
| # 合併到表格 | |
| weekly_changes = pd.concat([weekly_changes, weekly_return], axis=1) | |
| # ====== 美化輸出 ====== | |
| weekly_changes = weekly_changes.round(2) | |
| weekly_changes.index.name = "date" # 🔹 指定 index 名稱,避免 KeyError | |
| # 彩色 HTML 表格函式 | |
| def highlight_table(val): | |
| try: | |
| num = float(val) | |
| num_fmt = f"{num:.2f}" | |
| if num > 0: | |
| return f'<td style="color:red;font-weight:bold;">{num_fmt}</td>' | |
| elif num < 0: | |
| return f'<td style="color:green;font-weight:bold;">{num_fmt}</td>' | |
| else: | |
| return f'<td style="color:black;">{num_fmt}</td>' | |
| except: | |
| return f"<td>{val}</td>" | |
| # 建立 HTML 表格 | |
| html_rows = [] | |
| # 加上表頭 | |
| header = ["日期"] + list(weekly_changes.columns) | |
| html_header = "<tr>" + "".join([f"<th>{col}</th>" for col in header]) + "</tr>" | |
| html_rows.append(html_header) | |
| # 內容 | |
| for idx, row in weekly_changes.reset_index().iterrows(): | |
| html_cells = [f"<td>{row['date'].strftime('%Y-%m-%d')}</td>"] | |
| for val in row[1:]: | |
| html_cells.append(highlight_table(val)) | |
| html_rows.append("<tr>" + "".join(html_cells) + "</tr>") | |
| # 組合 HTML | |
| html_custom = ( | |
| "<table border='1' style='border-collapse:collapse; text-align:center;'>" | |
| "<caption style='caption-side:top; font-weight:bold; font-size:16px;'>每週漲幅 (%)</caption>" | |
| + "".join(html_rows) + | |
| "</table>" | |
| ) | |
| # 輸出 HTML 檔 | |
| html_path = "weekly_changes.html" | |
| with open(html_path, "w", encoding="utf-8") as f: | |
| f.write(html_custom) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment