F114126123's picture
Upload app.py
0a62590 verified
Raw
History Blame Contribute Delete
11 kB
"""
大宗商品即期價格 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"""
<div style="background:{BG_CARD};border:1px solid {C_BORDER};border-radius:10px;
padding:20px;font-family:Arial">
<div style="color:{C_TEXT};font-size:20px;font-weight:700">{name}</div>
<div style="color:{C_MUTED};font-size:15px;margin-top:8px">資料暫時無法取得</div>
</div>"""
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"""
<div style="background:{BG_INNER};border:1px solid {C_BORDER};border-radius:6px;
padding:10px 14px;margin:10px 0 0;font-size:15px;color:{C_MUTED}">
💱 每台兩換算:
<b style="color:{C_TEXT}">USD ${tael_usd:,.2f}</b>
&nbsp;=&nbsp;
<b style="color:#B8860B">NT${tael_twd:,.0f}</b>
</div>"""
return f"""
<div style="background:{BG_CARD};border:1px solid {C_BORDER};border-radius:10px;
border-top:4px solid {color};padding:20px;font-family:Arial">
<div style="display:flex;align-items:baseline;gap:10px;margin-bottom:14px">
<span style="color:{C_TEXT};font-size:22px;font-weight:700">{name}</span>
<span style="color:{C_MUTED};font-size:14px">{unit}</span>
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px">
<div style="background:{BG_INNER};border:1px solid {C_BORDER};border-radius:8px;padding:12px 14px">
<div style="color:{C_MUTED};font-size:14px;margin-bottom:4px">💵 美元 USD</div>
<div style="color:{C_TEXT};font-size:26px;font-weight:700;margin-bottom:4px">{usd_str}</div>
<div style="color:{chg_color};font-size:15px;font-weight:600">{cusd_str}</div>
</div>
<div style="background:{BG_INNER};border:1px solid {C_BORDER};border-radius:8px;padding:12px 14px">
<div style="color:{C_MUTED};font-size:14px;margin-bottom:4px">🇹🇼 台幣 TWD</div>
<div style="color:#7B4F00;font-size:22px;font-weight:700;margin-bottom:4px">{twd_str}</div>
<div style="color:{chg_color};font-size:15px;font-weight:600">{ctwd_str}</div>
</div>
</div>
{convert_html}
<div style="color:{C_MUTED};font-size:13px;text-align:right;margin-top:10px">
匯率:1 USD = {usdtwd:.2f} TWD | {now.strftime('%m/%d %H:%M')} 台灣時間
</div>
</div>"""
def fetch_all():
tw_tz = pytz.timezone("Asia/Taipei")
now = datetime.now(tw_tz)
usdtwd = get_usdtwd()
grid_html = f"""
<style>
.cgrid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:14px;padding:4px}}
</style>
<div class="cgrid">"""
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 += "</div>"
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"""
<div style="background:{BG_HEADER};border-bottom:3px solid #DAA520;padding:18px 24px">
<div style="display:flex;align-items:center;gap:14px">
<span style="font-size:32px">📦</span>
<div>
<p style="color:{C_TITLE};font-size:28px;font-weight:700;margin:0">大宗商品即期價格</p>
<p style="color:{C_SUB};font-size:15px;margin:5px 0 0">
黃金 · 白銀 · 石油 · 比特幣 · 以太幣
&nbsp;|&nbsp; USD + TWD 雙幣顯示
&nbsp;|&nbsp; 7日走勢圖
</p>
</div>
</div>
</div>
""")
refresh_btn = gr.Button("🔄 立即更新價格", variant="primary", size="lg")
gr.HTML(f'<p style="color:{C_TEXT};font-size:18px;font-weight:600;margin:14px 4px 6px">📊 即期價格</p>')
cards_out = gr.HTML()
gr.HTML(f'<p style="color:{C_TEXT};font-size:18px;font-weight:600;margin:18px 4px 8px">📈 7日走勢圖</p>')
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"""
<div style="padding:20px 10px;color:{C_MUTED};font-size:14px;line-height:1.9">
· 走勢圖為近 7 個交易日收盤價<br>
· 黃金/白銀附每台兩換算<br>
&nbsp;&nbsp;(1 台兩 = 1.20337 盎司)<br>
· 匯率即時取自 Yahoo Finance
</div>""")
update_out = gr.Textbox(label="", interactive=False,
value="⬆️ 點擊上方按鈕載入最新價格",
elem_id="upd")
gr.HTML(f'<style>#upd textarea{{font-size:16px!important;color:{C_MUTED}!important;background:{BG_INNER}!important}}</style>')
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)