Spaces:
Sleeping
Sleeping
File size: 15,443 Bytes
8e3b455 eda6a09 8e3b455 b7459ba 8e3b455 b7459ba 8e3b455 b7459ba 8e3b455 fb54c7f 8e3b455 b7459ba 8e3b455 16c2273 8e3b455 b7ae03b 8e3b455 5d3f8c9 8e3b455 e9f536d 8e3b455 a4f0f1c 8e3b455 5d3f8c9 8e3b455 e9f536d 8e3b455 a4f0f1c 8e3b455 05ccbbe 8e3b455 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 |
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) |