""" 大宗商品即期價格 v3 配色:米白暖灰(護眼) 字體:全面放大 """ import yfinance as yf import pandas as pd import numpy as np import matplotlib matplotlib.use("Agg") import matplotlib.pyplot as plt import gradio as gr from datetime import datetime, timedelta import pytz, io, warnings warnings.filterwarnings("ignore") # 中文字型 import subprocess try: subprocess.run(["apt-get","install","-y","-q","fonts-noto-cjk"], capture_output=True) import matplotlib.font_manager as fm fm._load_fontmanager(try_read_cache=False) plt.rcParams["font.family"] = ["Noto Sans CJK TC","DejaVu Sans"] except Exception: pass # ── 配色 ── BG_PAGE = "#F8F5F0" BG_HEADER = "#4A3728" BG_CARD = "#FFFFFF" BG_INNER = "#F8F5F0" C_BORDER = "#D4C5A9" C_TITLE = "#F5DEB3" C_SUB = "#C4A882" C_TEXT = "#2C1810" C_MUTED = "#8B7355" C_UP = "#1A7A1A" C_DOWN = "#CC3300" COMMODITIES = { "黃金": {"ticker":"GC=F", "unit":"USD / 盎司", "color":"#DAA520"}, "白銀": {"ticker":"SI=F", "unit":"USD / 盎司", "color":"#888888"}, "石油(WTI)": {"ticker":"CL=F", "unit":"USD / 桶", "color":"#8B4513"}, "比特幣": {"ticker":"BTC-USD", "unit":"USD", "color":"#F7931A"}, "以太幣": {"ticker":"ETH-USD", "unit":"USD", "color":"#627EEA"}, } def get_usdtwd(): try: h = yf.Ticker("USDTWD=X").history(period="5d") if not h.empty: return float(h["Close"].iloc[-1]) except Exception: pass return 31.5 def get_data(ticker, days=10): try: end = datetime.now() start = end - timedelta(days=days+7) df = yf.download(ticker, start=start, end=end, auto_adjust=True, progress=False) if df.empty: return None if isinstance(df.columns, pd.MultiIndex): df.columns = df.columns.get_level_values(0) return df[["Close"]].dropna().tail(days) except Exception: return None def make_spark(df, color, name): fig, ax = plt.subplots(figsize=(5, 2.2)) fig.patch.set_facecolor(BG_CARD) ax.set_facecolor(BG_INNER) if df is not None and len(df) >= 2: prices = df["Close"].squeeze().values xs = list(range(len(prices))) ax.plot(xs, prices, color=color, linewidth=2.8, zorder=3) ax.fill_between(xs, prices, prices.min()*0.998, alpha=0.15, color=color, zorder=2) ax.annotate(f"{prices[-1]:,.2f}", xy=(xs[-1], prices[-1]), xytext=(-55, 8), textcoords="offset points", color=color, fontsize=12, fontweight="bold") ax.set_xlim(xs[0], xs[-1]) ax.set_ylim(prices.min()*0.994, prices.max()*1.006) else: ax.text(0.5, 0.5, "資料載入中", ha="center", va="center", color=C_MUTED, transform=ax.transAxes, fontsize=13) ax.set_title(name, color=color, fontsize=14, fontweight="bold", pad=5, loc="left") ax.tick_params(colors=C_MUTED, labelsize=9) for sp in ax.spines.values(): sp.set_edgecolor(C_BORDER) ax.grid(axis="y", color=C_BORDER, linewidth=0.6, alpha=0.7) ax.set_xlabel("近 7 交易日", color=C_MUTED, fontsize=9) plt.tight_layout(pad=0.6) buf = io.BytesIO() fig.savefig(buf, format="png", dpi=140, facecolor=BG_CARD) buf.seek(0) plt.close(fig) from PIL import Image return Image.open(buf) def build_card(name, price, chg_abs, chg_pct, unit, color, usdtwd, now): if price is None: return f"""
{name}
資料暫時無法取得
""" price_twd = price * usdtwd up = chg_pct >= 0 arrow = "▲" if up else "▼" chg_color = C_UP if up else C_DOWN if price >= 10000: usd_str = f"${price:,.0f}" twd_str = f"NT${price_twd:,.0f}" cusd_str = f"{arrow} ${abs(chg_abs):,.0f} ({abs(chg_pct):.2f}%)" ctwd_str = f"{arrow} NT${abs(chg_abs*usdtwd):,.0f}" elif price >= 100: usd_str = f"${price:,.2f}" twd_str = f"NT${price_twd:,.0f}" cusd_str = f"{arrow} ${abs(chg_abs):.2f} ({abs(chg_pct):.2f}%)" ctwd_str = f"{arrow} NT${abs(chg_abs*usdtwd):,.0f}" else: usd_str = f"${price:.3f}" twd_str = f"NT${price_twd:.2f}" cusd_str = f"{arrow} ${abs(chg_abs):.3f} ({abs(chg_pct):.2f}%)" ctwd_str = f"{arrow} NT${abs(chg_abs*usdtwd):.2f}" convert_html = "" if name in ["黃金", "白銀"]: tael_usd = price * 1.20337 tael_twd = tael_usd * usdtwd convert_html = f"""
💱 每台兩換算: USD ${tael_usd:,.2f}  =  NT${tael_twd:,.0f}
""" return f"""
{name} {unit}
💵 美元 USD
{usd_str}
{cusd_str}
🇹🇼 台幣 TWD
{twd_str}
{ctwd_str}
{convert_html}
匯率:1 USD = {usdtwd:.2f} TWD | {now.strftime('%m/%d %H:%M')} 台灣時間
""" def fetch_all(): tw_tz = pytz.timezone("Asia/Taipei") now = datetime.now(tw_tz) usdtwd = get_usdtwd() grid_html = f"""
""" images = {} for name, cfg in COMMODITIES.items(): df = get_data(cfg["ticker"], days=8) price = chg_abs = chg_pct = None if df is not None and len(df) >= 2: price = float(df["Close"].squeeze().iloc[-1]) prev = float(df["Close"].squeeze().iloc[-2]) chg_abs = price - prev chg_pct = chg_abs / prev * 100 if prev else 0 grid_html += build_card(name, price, chg_abs, chg_pct, cfg["unit"], cfg["color"], usdtwd, now) images[name] = make_spark(df, cfg["color"], name) grid_html += "
" update_str = (f"✅ 更新完成:{now.strftime('%Y-%m-%d %H:%M:%S')} 台灣時間" f" | 1 USD = {usdtwd:.2f} TWD") return (grid_html, images.get("黃金"), images.get("白銀"), images.get("石油(WTI)"), images.get("比特幣"), images.get("以太幣"), update_str) # ── CSS ── CSS = f""" body, .gradio-container {{ background:{BG_PAGE} !important; font-family:Arial,sans-serif }} button.primary {{ background:{BG_HEADER} !important; color:{C_TITLE} !important; font-size:18px !important; font-weight:700 !important; border:none !important; border-radius:8px !important; padding:14px !important; }} button.primary:hover {{ background:#6B5040 !important; }} label span, .label-wrap span {{ font-size:16px !important; color:{C_MUTED} !important; }} """ with gr.Blocks(css=CSS, title="大宗商品即期價格") as demo: gr.HTML(f"""
📦

大宗商品即期價格

黃金 · 白銀 · 石油 · 比特幣 · 以太幣  |  USD + TWD 雙幣顯示  |  7日走勢圖

""") refresh_btn = gr.Button("🔄 立即更新價格", variant="primary", size="lg") gr.HTML(f'

📊 即期價格

') cards_out = gr.HTML() gr.HTML(f'

📈 7日走勢圖

') with gr.Row(): gold_img = gr.Image(label="🥇 黃金", show_label=True, height=200) silver_img = gr.Image(label="🥈 白銀", show_label=True, height=200) oil_img = gr.Image(label="🛢️ 石油(WTI)", show_label=True, height=200) with gr.Row(): btc_img = gr.Image(label="₿ 比特幣", show_label=True, height=200) eth_img = gr.Image(label="◈ 以太幣", show_label=True, height=200) gr.HTML(f"""
· 走勢圖為近 7 個交易日收盤價
· 黃金/白銀附每台兩換算
  (1 台兩 = 1.20337 盎司)
· 匯率即時取自 Yahoo Finance
""") update_out = gr.Textbox(label="", interactive=False, value="⬆️ 點擊上方按鈕載入最新價格", elem_id="upd") gr.HTML(f'') refresh_btn.click( fn=fetch_all, inputs=[], outputs=[cards_out, gold_img, silver_img, oil_img, btc_img, eth_img, update_out], ) demo.launch(server_name="0.0.0.0", server_port=7860)