Spaces:
Sleeping
Sleeping
import gradio as gr | |
import yfinance as yf | |
import pandas as pd | |
import numpy as np | |
import matplotlib.pyplot as plt | |
from google import genai | |
import os | |
from datetime import datetime | |
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY") | |
client = genai.Client(api_key=GOOGLE_API_KEY) | |
MODEL_NAME = "gemini-2.5-flash-lite-preview-06-17" | |
DEVELOPER_NAMES = "蔡勝軒、蔡柏鴻、巫丞喆、張凱崴" | |
# 支援的美國公司列表 | |
US_STOCKS = { | |
"Apple (AAPL)": "AAPL", | |
"Microsoft (MSFT)": "MSFT", | |
"Google (GOOG)": "GOOG", | |
"Amazon (AMZN)": "AMZN", | |
"NVIDIA (NVDA)": "NVDA", | |
"Meta Platforms (META)": "META", | |
"Tesla (TSLA)": "TSLA", | |
"Berkshire Hathaway (BRK-B)": "BRK-B", | |
"JPMorgan Chase & Co. (JPM)": "JPM", | |
"Johnson & Johnson (JNJ)": "JNJ", | |
"Samsung Electronics Co., Ltd. (005930.KS)": "005930.KS" | |
} | |
# 支援的台灣公司列表 | |
TW_STOCKS = { | |
"台積電": "2330.TW", | |
"鴻海": "2317.TW", | |
"聯發科": "2454.TW", | |
"富邦金": "2881.TW", | |
"台達電": "2308.TW", | |
"國泰金": "2882.TW", | |
"中華電": "2412.TW", | |
"日月光投控": "3711.TW", | |
"廣達": "2382.TW", | |
"台塑化": "6505.TW", | |
"聯電": "2303.TW", | |
"中信金": "2891.TW", | |
"兆豐金": "2886.TW", | |
"南亞": "1303.TW", | |
"統一": "1216.TW", | |
"台塑": "1301.TW", | |
"玉山金": "2884.TW", | |
"世芯‑KY": "3661.TW", | |
"華碩": "2357.TW", | |
"長榮": "2603.TW", | |
"智邦": "2345.TW", | |
"元大金": "2885.TW", | |
"第一金": "2892.TW", | |
"華南金": "2880.TW", | |
"合庫金": "5880.TW", | |
"台灣大": "3045.TW", | |
"緯創": "3231.TW", | |
"聯詠": "3034.TW", | |
"遠傳": "4904.TW", | |
"和泰車": "2207.TW", | |
"台光電": "2383.TW", | |
"大立光": "3008.TW", | |
"永豐金": "2890.TW", | |
"瑞昱": "2379.TW", | |
"奇鋐": "3017.TW", | |
"研華": "2395.TW", | |
"中鋼": "2002.TW", | |
"統一超": "2912.TW", | |
"萬海": "2615.TW", | |
"元太": "8069.TWO", | |
"國巨": "2327.TW", | |
"陽明": "2609.TW", | |
"光寶科": "2301.TW", | |
"凱基金": "2883.TW", | |
"鈊象": "3293.TWO", | |
"上海商銀": "5876.TW", | |
"長榮航": "2618.TW", | |
"中租‑KY": "5871.TW", | |
"彰銀": "2801.TW", | |
"健策": "3653.TW", | |
"台新金": "2887.TW", | |
"和碩": "4938.TW", | |
"新光金": "2888.TW", | |
"藥華藥": "6446.TW", | |
"川湖": "2059.TW", | |
"台泥": "1101.TW", | |
"信驊": "5274.TWO", | |
"技嘉": "2376.TW", | |
"世界": "5347.TWO", | |
"致茂": "2360.TW", | |
"力旺": "3529.TWO", | |
"遠東新": "1402.TW", | |
"創意": "3443.TW", | |
"亞德客‑KY": "1590.TW", | |
"欣興": "3037.TW", | |
"華城": "1519.TW", | |
"台灣高鐵": "2633.TW", | |
"貿聯‑KY": "3665.TW", | |
"英業達": "2356.TW", | |
"南亞科": "2408.TW", | |
"亞泥": "1102.TW", | |
"嘉澤": "3533.TW", | |
"文曄": "3036.TW", | |
"環球晶": "6488.TWO", | |
"臺企銀": "2834.TW", | |
"金像電": "2368.TW", | |
"可成": "2474.TW", | |
"祥碩": "5269.TW", | |
"矽力‑KY": "6415.TW", | |
"台化": "1326.TW", | |
"華航": "2610.TW", | |
"健鼎": "3044.TW", | |
"京元電子": "2449.TW", | |
"仁寶": "2324.TW", | |
"正新": "2105.TW", | |
"大聯大": "3702.TW", | |
"台中銀": "2812.TW", | |
"微星": "2377.TW", | |
"漢唐": "2404.TW", | |
"億豐": "8464.TW", | |
"豐泰": "9910.TW", | |
"儒鴻": "1476.TW", | |
"旭隼": "6409.TW", | |
"聯強": "2347.TW", | |
"寶成": "9904.TW", | |
"東元": "1504.TW", | |
"群聯": "8299.TWO" | |
} | |
# 合併公司列表 | |
COMPANY_TO_TICKER = {**US_STOCKS, **TW_STOCKS} | |
COMPANY_CHOICES = list(COMPANY_TO_TICKER.keys()) | |
# --- 核心功能函式 (Core Functions) --- | |
def get_stock_data(company_name: str, period: str = "1y") -> tuple[pd.DataFrame | None, dict | None, str | None]: | |
""" | |
獲取並處理指定公司的股票數據與財務指標。 | |
Args: | |
company_name: 公司顯示名稱或代號。 | |
period: 數據期間 (例如 "1y", "6mo")。 | |
Returns: | |
一個包含 (DataFrame, dict, str) 的元組。 | |
- DataFrame: 包含歷史股價和移動平均線的 pandas DataFrame。 | |
- dict: 包含關鍵財務指標的字典。 | |
- str: 用於顯示的公司名稱。 | |
如果失敗則回傳 (None, None, None)。 | |
""" | |
ticker_symbol = COMPANY_TO_TICKER.get(company_name, None) | |
display_name = company_name | |
if not ticker_symbol: | |
# 處理使用者手動輸入的代號 | |
if "." in company_name.upper() or len(company_name) <= 6: | |
ticker_symbol = company_name.upper() | |
else: | |
return None, None, None | |
try: | |
ticker = yf.Ticker(ticker_symbol) | |
info = ticker.info | |
# 檢查是否能成功獲取資訊,若否,則此代號無效 | |
if not info or info.get('regularMarketPrice') is None: | |
return None, None, None | |
hist_data = ticker.history(period=period) | |
if hist_data.empty: | |
return None, None, None | |
# 如果是手動輸入代號,則更新顯示名稱 | |
if company_name not in COMPANY_TO_TICKER: | |
short_name = info.get('shortName', company_name) | |
display_name = f"{short_name} ({ticker_symbol})" | |
# 計算移動平均線 | |
hist_data['SMA_20'] = hist_data['Close'].rolling(window=20).mean() | |
hist_data['SMA_60'] = hist_data['Close'].rolling(window=60).mean() | |
# 獲取財務指標 | |
financial_metrics = { | |
'市值 (Market Cap)': info.get('marketCap'), | |
'本益比 (P/E)': info.get('trailingPE'), | |
'股價淨值比 (P/B)': info.get('priceToBook'), | |
'每股盈餘 (EPS)': info.get('trailingEps'), | |
'毛利率 (Gross Margin)': info.get('grossMargins'), | |
'營業利益率 (Operating Margin)': info.get('operatingMargins'), | |
'股東權益報酬率 (ROE)': info.get('returnOnEquity'), | |
'資產報酬率 (ROA)': info.get('returnOnAssets'), | |
'自由現金流 (Free Cash Flow)': info.get('freeCashflow'), | |
'營運現金流 (Operating Cash Flow)': info.get('operatingCashflow'), | |
'殖利率 (Yield)': info.get('dividendYield'), | |
} | |
return hist_data, financial_metrics, display_name | |
except Exception as e: | |
print(f"Error fetching data for {company_name}: {e}") | |
return None, None, None | |
def plot_stock_chart(df: pd.DataFrame, company_name: str) -> plt.Figure: | |
""" | |
根據 DataFrame 繪製股價走勢圖。 | |
""" | |
fig, ax = plt.subplots(figsize=(10, 6)) | |
x = np.arange(df.shape[0]) | |
ticks = [ | |
tick.strftime('%Y/%m/%d') if i % max(int(df.shape[0] / 6), 1) == 0 else '' | |
for i, tick in enumerate(df.index) | |
] | |
ax.plot(x, df['Close'], label='Close Price', color='royalblue', linewidth=2) | |
ax.plot(x, df['SMA_20'], label='20-Day SMA', color='orange', linestyle='--', alpha=0.8) | |
ax.plot(x, df['SMA_60'], label='60-Day SMA', color='green', linestyle='--', alpha=0.8) | |
ax.set_title(f'{COMPANY_TO_TICKER.get(company_name, company_name)}', fontsize=16) | |
ax.set_xlabel('Date', fontsize=12) | |
ax.set_ylabel('Price', fontsize=12) | |
ax.legend() | |
ax.grid(True, axis='y', linestyle='--', alpha=0.6) | |
plt.xticks(x, ticks, rotation=30) | |
plt.tight_layout() | |
return fig | |
def format_metrics_display(metrics: dict) -> str: | |
""" | |
將財務指標格式化為易於閱讀的 Markdown 字串。 | |
""" | |
if not metrics: | |
return "無法獲取財務指標。" | |
display_text = "" | |
for key, value in metrics.items(): | |
if value is None or pd.isna(value): | |
formatted_value = "N/A" | |
elif key in ['股東權益報酬率 (ROE)', '資產報酬率 (ROA)', '毛利率 (Gross Margin)', '營業利益率 (Operating Margin)']: | |
formatted_value = f"{value:.2%}" | |
elif key in ['市值 (Market Cap)', '自由現金流 (Free Cash Flow)', '營運現金流 (Operating Cash Flow)']: | |
formatted_value = f"{value:,.0f}" | |
else: | |
formatted_value = f"{value:.2f}" | |
display_text += f"- **{key}**: {formatted_value}\n" | |
return display_text | |
def generate_trend_description(company_name: str, df: pd.DataFrame, metrics: dict) -> str: | |
""" | |
使用 Gemini API 生成趨勢描述。 | |
""" | |
if not GOOGLE_API_KEY: | |
return "### ⚠️ AI 分析失敗\nGoogle API 金鑰未設定。請在應用程式的環境變數中設定 GOOGLE_API_KEY。" | |
try: | |
latest_data = df.iloc[-1] | |
ticker_symbol = COMPANY_TO_TICKER.get(company_name, None) | |
prompt = f""" | |
你是一位專業的金融市場分析師。請根據以下關於「{company_name}{"" if not ticker_symbol else " (%s)" % ticker_symbol }」的近期歷史數據和提供的財務指標,提供一份深入淺出的**趨勢分析**。 | |
**近期股價數據:** | |
- 最新收盤價: {latest_data['Close']:.2f} | |
- 20日移動平均線: {latest_data['SMA_20']:.2f} | |
- 60日移動平均線: {latest_data['SMA_60']:.2f} | |
**關鍵財務指標:** | |
{format_metrics_display(metrics)} | |
請根據以上所有資訊,以繁體中文,詳細分析股價趨勢,例如目前股價與移動平均線的關係,是處於多頭還是空頭排列,並結合財務指標解釋其技術面與基本面的意義,描述可能對未來趨勢的潛在影響。請將分析內容分點說明,使其清晰易讀,並直接輸出**僅包含分析的內容**即可,**不要**加入多餘的訊息。 | |
重要提醒:此部分僅為基於數據的趨勢**描述性文字**,請勿提供具體的價格預測或投資建議。 | |
""" | |
response = client.models.generate_content( | |
model=MODEL_NAME, | |
contents=prompt | |
) | |
return response.text | |
except Exception as e: | |
return f"### ⚠️ AI 分析失敗\n呼叫 AI 模型時發生錯誤:{e}" | |
def generate_recommendation(company_name: str, metrics: dict) -> str: | |
""" | |
使用 Gemini API 生成投資建議。 | |
""" | |
if not GOOGLE_API_KEY: | |
return "### ⚠️ 建議生成失敗\nGoogle API 金鑰未設定。" | |
try: | |
ticker_symbol = COMPANY_TO_TICKER.get(company_name, None) | |
prompt = f""" | |
您是一位專業的金融市場分析師。請僅根據以下關於「{company_name}{"" if not ticker_symbol else " (%s)" % ticker_symbol }」的**關鍵財務指標**,提供一個簡潔的投資潛力評估。 | |
**關鍵財務指標:** | |
{format_metrics_display(metrics)} | |
請根據這些指標,給出一個總結性的建議,例如「建議觀望」、「建議長期持有」或「建議謹慎評估」。並在建議下方,簡要說明您是基於哪些指標(例如高 ROE、低 P/E 等)做出此判斷的。 | |
請以繁體中文回覆,直接輸出**包含評估報告的內容**即可,**不要**加入多餘的文字,並強調這僅是基於提供數據的**初步建議**,非投資決策依據。 | |
""" | |
response = client.models.generate_content( | |
model=MODEL_NAME, | |
contents=prompt | |
) | |
return response.text | |
except Exception as e: | |
return f"### ⚠️ 建議生成失敗\n呼叫 AI 模型時發生錯誤:{e}" | |
# --- Gradio 介面 (Gradio Interface) --- | |
def create_ui(): | |
"""建立並回傳 Gradio 應用程式介面。""" | |
custom_css = """ | |
footer {display: none !important;} | |
.gradio-container {font-family: 'IBM Plex Sans', sans-serif;} | |
#student_footer {text-align: center; margin-top: 20px; color: #888;} | |
""" | |
APP_DESCRIPTION = f""" | |
# AI 股市趨勢探索家 📈 | |
**Designed by: {DEVELOPER_NAMES}** | |
歡迎來到 AI 股市趨勢探索家!這是一個結合股票數據獲取、分析與 Google Gemini AI 技術的互動應用。 | |
**如何使用:** | |
1. 從下拉式選單中選擇一家您感興趣的公司或者輸入您想查詢的公司名稱。例如:台積電。 | |
2. 點擊「分析!」按鈕。 | |
3. 應用程式將會為您呈現該公司近期的股票走勢圖、關鍵財務指標,並由 AI 分析與見解! | |
""" | |
with gr.Blocks(theme=gr.themes.Monochrome(), css=custom_css) as app: | |
gr.Markdown(APP_DESCRIPTION) | |
with gr.Row(): | |
with gr.Column(scale=3): | |
company_input = gr.Dropdown( | |
choices=COMPANY_CHOICES, | |
label="選擇或輸入公司名稱/代號", | |
value=COMPANY_CHOICES[0], | |
allow_custom_value=True | |
) | |
with gr.Column(scale=1): | |
analyze_button = gr.Button("分析!", variant="primary") | |
with gr.Row(): | |
with gr.Column(scale=3): | |
output_plot = gr.Plot(label="股價走勢圖") | |
with gr.Column(scale=3): | |
with gr.Tabs(): | |
with gr.TabItem("🤖 AI 趨勢洞察"): | |
output_ai_description = gr.Markdown() | |
with gr.TabItem("💡 AI 投資建議"): | |
output_ai_recommendation = gr.Markdown() | |
with gr.TabItem("📊 關鍵財務指標"): | |
output_metrics = gr.Markdown() | |
gr.Markdown(f"**Designed by: {DEVELOPER_NAMES}**", elem_id="student_footer") | |
def run_analysis(company_name): | |
if not company_name: | |
gr.Warning("請輸入或選擇一家公司!") | |
return None, "請先選擇一家公司。", "請先選擇一家公司。", "請先選擇一家公司。" | |
gr.Info("正在獲取數據並進行 AI 分析,請稍候...") | |
# 1. 獲取數據 | |
stock_df, metrics, display_name = get_stock_data(company_name) | |
if stock_df is None: | |
err_msg = f"找不到「{company_name}」的相關數據,請確認公司名稱或代號是否正確。" | |
gr.Error(err_msg) | |
return None, err_msg, err_msg, err_msg | |
# 2. 繪製圖表 | |
plot = plot_stock_chart(stock_df, display_name) | |
# 3. 格式化指標 | |
metrics_display = format_metrics_display(metrics) | |
# 4. 進行 AI 分析 (拆分為兩步驟) | |
description = generate_trend_description(display_name, stock_df, metrics) | |
recommendation = generate_recommendation(display_name, metrics) | |
# 加上免責聲明 | |
disclaimer = "\n\n<br>\n*免責聲明:本分析由 AI 模型生成,僅供參考,不構成任何投資建議。*" | |
description += disclaimer | |
recommendation += disclaimer | |
return plot, description, recommendation, metrics_display | |
analyze_button.click( | |
fn=run_analysis, | |
inputs=[company_input], | |
outputs=[output_plot, output_ai_description, output_ai_recommendation, output_metrics] | |
) | |
app.load( | |
fn=run_analysis, | |
inputs=[gr.Textbox(value=COMPANY_CHOICES[0], visible=False)], | |
outputs=[output_plot, output_ai_description, output_ai_recommendation, output_metrics] | |
) | |
return app | |
stock_analyzer_app = create_ui() | |
stock_analyzer_app.launch(show_error=True, show_api=False) |