AlanRex commited on
Commit
cbe4085
·
verified ·
1 Parent(s): 03029b1

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +283 -100
app.py CHANGED
@@ -1,7 +1,9 @@
 
 
1
  # 系統套件
2
  import os
3
  from datetime import datetime, timedelta
4
- import google.generativeai as genai # <--- 【新增】引用 Gemini
5
  import pandas as pd
6
  import numpy as np
7
  import yfinance as yf
@@ -13,24 +15,84 @@ from plotly.subplots import make_subplots
13
  import re
14
  from bs4 import BeautifulSoup
15
  import requests
 
16
 
 
17
  # 引用您組員的預測器程式
18
  from Bert_predict import BertPredictor
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
21
  TAIWAN_STOCKS = {
22
- '元大台灣50': '0050.TW', # 新增
23
  '台積電': '2330.TW',
24
  '聯發科': '2454.TW',
25
  '鴻海': '2317.TW',
26
- '台塑': '1301.TW',
27
- '中華電': '2412.TW',
28
  '富邦金': '2881.TW',
 
29
  '國泰金': '2882.TW',
30
- '台達電': '2308.TW',
 
 
 
 
 
31
  '統一': '1216.TW',
32
- '日月光': '3711.TW',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  '長榮': '2603.TW',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  '慧洋-KY': '2637.TW',
35
  '上銀': '2049.TW',
36
  '台泥': '1101.TW',
@@ -44,24 +106,62 @@ TAIWAN_STOCKS = {
44
 
45
  # 產業分類
46
  INDUSTRY_MAPPING = {
47
- '0050.TW': 'ETF', # 新增
48
  '2330.TW': '半導體',
49
  '2454.TW': '半導體',
50
  '2317.TW': '電子組件',
51
- '1301.TW': '塑膠',
52
- '2412.TW': '電信',
53
  '2881.TW': '金融',
 
54
  '2882.TW': '金融',
55
- '2308.TW': '電子',
56
- '1216.TW': '食品',
 
 
57
  '3711.TW': '半導體',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  '2603.TW': '航運',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  '2637.TW': '散裝航運',
60
  '2049.TW': '工具機',
61
  '1101.TW': '營建',
62
  '2408.TW': 'DRAM',
63
  '2337.TW': 'NFLSH',
64
- '1101.TW': '營建',
65
  '4966.TWO': '高速傳輸',
66
  '3665.TW': '連接器',
67
  '6870.TWO': '軟體整合',
@@ -83,10 +183,9 @@ def get_stock_data(symbol, period='1y'):
83
  except:
84
  return pd.DataFrame()
85
 
86
- def simple_lstm_predict(data, predict_days=5):
87
- """簡化的LSTM預測模型 (使用統計方法模擬)"""
88
- if len(data) < 60:
89
- return None
90
  prices = data['Close'].values
91
  ma_short = np.mean(prices[-5:])
92
  ma_medium = np.mean(prices[-20:])
@@ -94,21 +193,29 @@ def simple_lstm_predict(data, predict_days=5):
94
  recent_trend = np.polyfit(range(20), prices[-20:], 1)[0]
95
  volatility = np.std(prices[-20:]) / np.mean(prices[-20:])
96
  base_change = recent_trend * predict_days
97
- trend_factor = 1.0
98
- if ma_short > ma_medium > ma_long:
99
- trend_factor = 1.02
100
- elif ma_short < ma_medium < ma_long:
101
- trend_factor = 0.98
102
- else:
103
- trend_factor = 1.0
104
  noise_factor = np.random.normal(1, volatility * 0.1)
105
  predicted_price = prices[-1] * trend_factor + base_change + (prices[-1] * noise_factor * 0.01)
106
  change_pct = ((predicted_price - prices[-1]) / prices[-1]) * 100
107
- return {
108
- 'predicted_price': predicted_price,
109
- 'change_pct': change_pct,
110
- 'confidence': max(0.6, 1 - volatility * 2)
111
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  def calculate_technical_indicators(df):
114
  """計算技術指標"""
@@ -188,10 +295,70 @@ def get_pmi_data():
188
  print(f"無法獲取 PMI 資料: {str(e)}")
189
  return pd.DataFrame()
190
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
  # 建立 Dash 應用程式
192
  app = dash.Dash(__name__, suppress_callback_exceptions=True)
193
 
194
- # --- 【新增】在程式啟動時,初始化 BERT 新聞預測器 ---
195
  try:
196
  print("正在初始化新聞情緒分析模型...")
197
  predictor = BertPredictor(max_news_per_keyword=5)
@@ -220,7 +387,6 @@ app.layout = html.Div([
220
  html.Div([dcc.Graph(id='taiex-prediction-chart')], style={'margin-top': '20px'})
221
  ], style={'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)','padding': '25px','border-radius': '15px','box-shadow': '0 8px 25px rgba(0,0,0,0.15)','color': 'white','margin-bottom': '40px'}),
222
 
223
- # 新聞情感分析區域
224
  html.Div([
225
  html.H3("📰 市場情緒與新聞分析", style={'color': '#E74C3C', 'margin-bottom': '20px'}),
226
  html.Div([
@@ -252,7 +418,7 @@ app.layout = html.Div([
252
  html.Label("時間範圍:"),
253
  dcc.Dropdown(id='period-dropdown',
254
  options=[{'label': '1個月', 'value': '1mo'},{'label': '3個月', 'value': '3mo'},{'label': '6個月', 'value': '6mo'},{'label': '1年', 'value': '1y'},{'label': '2年', 'value': '2y'}],
255
- value='6mo', style={'margin-bottom': '10px'})
256
  ], style={'width': '30%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'}),
257
  html.Div([
258
  html.Label("圖表類型:"),
@@ -283,12 +449,12 @@ app.layout = html.Div([
283
  html.Div(id='technical-analysis-text', style={'background': '#f8f9fa','padding': '15px','border-radius': '8px','border-left': '4px solid #A23B72','min-height': '150px','font-size': '14px','line-height': '1.6'})
284
  ], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'}),
285
  html.Div([
286
- html.H4("📈 基本面分析", style={'color': '#F18F01', 'margin-bottom': '15px'}),
287
  html.Div(id='fundamental-analysis-text', style={'background': '#f8f9fa','padding': '15px','border-radius': '8px','border-left': '4px solid #F18F01','min-height': '150px','font-size': '14px','line-height': '1.6'})
288
  ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '4%', 'vertical-align': 'top'})
289
  ]),
290
  html.Div([
291
- html.H4("🎯 市場展望與投資建議", style={'color': '#C73E1D', 'margin-bottom': '15px', 'margin-top': '25px'}),
292
  html.Div(id='market-outlook-text', style={'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)','color': 'white','padding': '20px','border-radius': '10px','min-height': '100px','font-size': '15px','line-height': '1.7','box-shadow': '0 4px 15px rgba(0,0,0,0.1)'})
293
  ])
294
  ], style={'margin-top': '30px','padding': '25px','background': 'white','border-radius': '12px','box-shadow': '0 4px 20px rgba(0,0,0,0.08)','border': '1px solid #e9ecef'}),
@@ -312,7 +478,6 @@ app.layout = html.Div([
312
  ], style={'margin-top': '30px','padding': '20px','background': 'white','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)'}),
313
  ])
314
 
315
- # 台指期獨立預測回調函數
316
  @app.callback(
317
  [dash.dependencies.Output('taiex-prediction-results', 'children'),
318
  dash.dependencies.Output('taiex-prediction-chart', 'figure')],
@@ -321,18 +486,26 @@ app.layout = html.Div([
321
  def update_taiex_prediction(predict_days):
322
  data = get_stock_data('^TWII', '2y')
323
  if data.empty: return html.Div("無法獲取台指期資料"), {}
324
- final_prediction = simple_lstm_predict(data, predict_days)
 
 
 
325
  if final_prediction is None: return html.Div("資料不足,無法進行預測"), {}
326
  current_price, last_date = data['Close'].iloc[-1], data.index[-1]
327
  predicted_price, change_pct, confidence = final_prediction['predicted_price'], final_prediction['change_pct'], final_prediction['confidence']
 
328
  prediction_paths = {1: [1], 5: [1, 5], 10: [1, 5, 10], 20: [1, 10, 20], 60: [1, 10, 20, 60]}
329
  intervals_to_predict = prediction_paths.get(predict_days, [predict_days])
330
  prediction_dates, prediction_prices = [last_date], [current_price]
 
331
  for days in intervals_to_predict:
332
- interim_prediction = simple_lstm_predict(data, days)
 
333
  if interim_prediction:
334
  prediction_dates.append(last_date + timedelta(days=days))
335
  prediction_prices.append(interim_prediction['predicted_price'])
 
 
336
  color, arrow = ('red', '📈') if change_pct >= 0 else ('green', '📉')
337
  result_card = html.Div([
338
  html.H4(f"{predict_days}日後預測結果", style={'margin': '0 0 15px 0', 'color': 'white'}),
@@ -347,7 +520,6 @@ def update_taiex_prediction(predict_days):
347
  fig.update_layout(title=f'台指期 {predict_days}日預測走勢', xaxis_title='日期', yaxis_title='指數點位', height=350, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='white'))
348
  return result_card, fig
349
 
350
- # 更新股價資訊卡片
351
  @app.callback(
352
  dash.dependencies.Output('stock-info-cards', 'children'),
353
  [dash.dependencies.Input('stock-dropdown', 'value')]
@@ -375,7 +547,6 @@ def update_stock_info(selected_stock):
375
  ], style={'background': 'white','padding': '20px','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)','display': 'inline-block'})
376
  ])
377
 
378
- # 更新主要圖表 (股價與成交量分佈)
379
  @app.callback(
380
  dash.dependencies.Output('price-chart', 'figure'),
381
  [dash.dependencies.Input('stock-dropdown', 'value'),
@@ -400,7 +571,6 @@ def update_price_chart(selected_stock, period, chart_type):
400
  fig.update_layout(title_text=f'{stock_name} 股價走勢與成交量分佈', height=500, showlegend=True, xaxis1=dict(title='日期', type='date', rangeslider_visible=False), yaxis1=dict(title='價格 (TWD)'), xaxis2=dict(title='成交量', showticklabels=True), yaxis2=dict(showticklabels=False), bargap=0.05)
401
  return fig
402
 
403
- # 更新進階技術指標圖表
404
  @app.callback(
405
  dash.dependencies.Output('advanced-technical-chart', 'figure'),
406
  [dash.dependencies.Input('technical-indicator-selector', 'value'),
@@ -412,7 +582,7 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
412
  if data.empty: return {}
413
  data = calculate_technical_indicators(data)
414
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
415
- fig = go.Figure() # Fallback
416
  if indicator == 'RSI':
417
  fig = go.Figure()
418
  fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
@@ -460,7 +630,6 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
460
  fig.update_layout(title=f'{stock_name} - DMI 動向指標 (14日)', height=500, showlegend=True, yaxis2_range=[0, 100])
461
  return fig
462
 
463
- # 更新成交量圖表
464
  @app.callback(
465
  dash.dependencies.Output('volume-chart', 'figure'),
466
  [dash.dependencies.Input('stock-dropdown', 'value'),
@@ -475,26 +644,43 @@ def update_volume_chart(selected_stock, period):
475
  fig.update_layout(title=f'{stock_name} 成交量', xaxis_title='日期', yaxis_title='成交量', height=300)
476
  return fig
477
 
478
- # 更新產業分析圖表
479
  @app.callback(
480
  dash.dependencies.Output('industry-analysis', 'figure'),
481
  [dash.dependencies.Input('stock-dropdown', 'value')]
482
  )
483
  def update_industry_analysis(selected_stock):
484
- industry_data = []
485
- for symbol in list(TAIWAN_STOCKS.values())[:10]:
486
  data = get_stock_data(symbol, '1mo')
487
- if not data.empty:
488
- stock_name = [name for name, symbol_code in TAIWAN_STOCKS.items() if symbol_code == symbol][0]
489
  return_pct = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
490
- industry_data.append({'股票': stock_name, '代碼': symbol, '月報酬率(%)': return_pct, '產業': INDUSTRY_MAPPING.get(symbol, '其他')})
491
- if not industry_data: return {}
492
- df_industry = pd.DataFrame(industry_data)
493
- fig = px.pie(df_industry, values='月報酬率(%)', names='股票', title='各股票月報酬率比較', color_discrete_sequence=px.colors.qualitative.Set3)
494
- fig.update_layout(height=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  return fig
496
 
497
- # 更新景氣燈號圖表
498
  @app.callback(
499
  dash.dependencies.Output('business-climate-chart', 'figure'),
500
  [dash.dependencies.Input('stock-dropdown', 'value')]
@@ -519,7 +705,7 @@ def update_business_climate_chart(selected_stock):
519
  fig.update_layout(title="台灣景氣燈號走勢", xaxis_title='日期', yaxis_title='燈號分數', height=300, yaxis=dict(range=[0, 40]))
520
  return fig
521
 
522
- # 更新分析師觀點
523
  @app.callback(
524
  [dash.dependencies.Output('technical-analysis-text', 'children'),
525
  dash.dependencies.Output('fundamental-analysis-text', 'children'),
@@ -528,33 +714,54 @@ def update_business_climate_chart(selected_stock):
528
  dash.dependencies.Input('period-dropdown', 'value')]
529
  )
530
  def update_analysis_text(selected_stock, period):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
531
  data = get_stock_data(selected_stock, period)
532
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
533
- if data.empty: return "無法獲取資料", "無法獲取資料", "無法獲取資料"
 
 
534
  data = calculate_technical_indicators(data)
535
- current_price = data['Close'].iloc[-1]
536
- price_change = ((current_price - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
 
537
  rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
538
  macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
539
  macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
 
540
  technical_text = html.Div([
541
- html.P([html.Strong("價格趨勢:"), f"近期{period}期間內,{stock_name}呈現", html.Span(f"{'上漲' if price_change > 5 else '下跌' if price_change < -5 else '盤整'}", style={'color': 'red' if price_change > 5 else 'green' if price_change < -5 else 'orange', 'font-weight': 'bold'}), f"走勢,累計變動{price_change:+.1f}%。"]),
542
- html.P([html.Strong("RSI指標:"), f"目前為{rsi_current:.1f},", html.Span("處於超買區間" if rsi_current > 70 else "處於超賣區間" if rsi_current < 30 else "在正常範圍內", style={'color': 'green' if rsi_current > 70 else 'red' if rsi_current < 30 else 'blue', 'font-weight': 'bold'}), "。"]),
543
- html.P([html.Strong("MACD指標:"), f"MACD({macd_current:.3f})", html.Span("高於" if macd_current > macd_signal_current else "低於", style={'color': 'red' if macd_current > macd_signal_current else 'green', 'font-weight': 'bold'}), f"信號線({macd_signal_current:.3f}),", f"顯示{'多頭' if macd_current > macd_signal_current else '空頭'}格局。"]),
544
  ])
545
- industry = INDUSTRY_MAPPING.get(selected_stock, '綜合')
546
- fundamental_text = html.Div([
547
- html.P([html.Strong("產業地位:"), f"{stock_name}屬於{industry}產業,在產業鏈中具有", html.Span("重要地位" if selected_stock in ['2330.TW', '2454.TW', '2317.TW'] else "一定影響力", style={'font-weight': 'bold'}), "。"]),
548
- html.P([html.Strong("營運展望:"), f"建議持續關注季報表現及未來指引。"]),
549
- ])
550
- outlook_tone = "謹慎樂觀" if price_change > 10 else "保守觀望" if price_change < -10 else "中性持平"
551
- market_outlook = html.Div([
552
- html.P([html.Strong("整體評估:"), f"基於技術面及基本面分析,對{stock_name}採取", html.Span(f"{outlook_tone}", style={'font-weight': 'bold'}), "態度。"]),
553
- html.P([html.Strong("投資建議:"), "短線操作注意技術指標,長線投資關注基本面變化。"]),
554
- ])
555
- return technical_text, fundamental_text, market_outlook
 
 
 
556
 
557
- # 更新PMI圖表
558
  @app.callback(
559
  dash.dependencies.Output('pmi-chart', 'figure'),
560
  [dash.dependencies.Input('stock-dropdown', 'value')]
@@ -575,26 +782,17 @@ def update_pmi_chart(selected_stock):
575
  def summarize_news_with_gemini(news_list: list) -> str:
576
  """
577
  使用 Gemini API 將英文新聞標題列表摘要成一段繁體中文。
578
-
579
- Args:
580
- news_list (list): 包含英文新聞標題字串的列表。
581
-
582
- Returns:
583
- str: Gemini 生成的繁體中文摘要,或在發生錯誤時回傳錯誤訊息。
584
  """
585
- # 從 Hugging Face Secrets 安全地讀取 API 金鑰
586
  api_key = os.getenv("GEMINI_API_KEY")
587
  if not api_key:
588
  return "錯誤:找不到 GEMINI_API_KEY。請在 Hugging Face Secrets 中設定。"
589
 
590
  try:
591
  genai.configure(api_key=api_key)
592
- model = genai.GenerativeModel('gemini-1.5-flash') # 使用速度較快的 Flash 模型
593
 
594
- # 將新聞列表格式化,方便 AI 閱讀
595
  formatted_news = "\n".join([f"- {news}" for news in news_list])
596
 
597
- # 這就是您對 AI 下的指令 (Prompt)
598
  prompt = f"""
599
  請扮演一位專業的金融市場分析師。
600
  以下是幾則最新的英文財經新聞標題,請將它們整合成一段簡潔、流暢、約 200 字的繁體中文市場動態摘要,與利多哪些產業,利空哪些產業。
@@ -612,9 +810,6 @@ def summarize_news_with_gemini(news_list: list) -> str:
612
  print(f"呼叫 Gemini API 時發生錯誤: {e}")
613
  return f"無法生成新聞摘要,請稍後再試。錯誤訊息:{e}"
614
 
615
-
616
-
617
- # 更新多檔股票比較
618
  @app.callback(
619
  [dash.dependencies.Output('comparison-chart', 'figure'),
620
  dash.dependencies.Output('comparison-table', 'children')],
@@ -647,26 +842,19 @@ def update_comparison_analysis(selected_stocks, period):
647
  return fig, table
648
  return fig, html.Div("無可比較資料")
649
 
650
-
651
- # ==============================================================================
652
- # ===== 【修改】市場情緒與新聞分析 (使用真實 BERT 模型) =====
653
- # ==============================================================================
654
  @app.callback(
655
  [dash.dependencies.Output('sentiment-gauge', 'children'),
656
  dash.dependencies.Output('news-summary', 'children')],
657
  [dash.dependencies.Input('stock-dropdown', 'value')]
658
  )
659
  def update_sentiment_analysis(selected_stock):
660
- # 檢查 predictor 是否成功初始化 (這部分邏輯不變)
661
  if predictor is None:
662
  error_fig = go.Figure().add_annotation(text="情緒指標模型載入失敗", showarrow=False)
663
  error_fig.update_layout(height=200)
664
  return dcc.Graph(figure=error_fig), html.P("新聞分析模型載入失敗,請檢查後台日誌。")
665
 
666
- # --- 1. 從 predictor 獲取新聞情緒平均分數 (不變) ---
667
  sentiment_score_raw = predictor.get_news_index()
668
 
669
- # --- 2. 建立情緒指標儀表板 (不變) ---
670
  if sentiment_score_raw is not None:
671
  sentiment_score_normalized = (sentiment_score_raw + 1) * 50
672
  sentiment_score_normalized = max(0, min(100, sentiment_score_normalized))
@@ -692,27 +880,22 @@ def update_sentiment_analysis(selected_stock):
692
  error_fig.update_layout(height=200)
693
  gauge_content = dcc.Graph(figure=error_fig)
694
 
695
-
696
- # --- 3. 【核心修改】獲取新聞並使用 Gemini 進行摘要 ---
697
  top_news_list = predictor.get_news()
698
- news_content = None # 先初始化
699
 
700
- if top_news_list and isinstance(top_news_list, list): # ���保列表不為空且格式正確
701
- # *** 呼叫我們的新函式來生成中文摘要 ***
702
  summary_text = summarize_news_with_gemini(top_news_list)
703
- # 使用 dcc.Markdown 來顯示,這樣如果摘要包含換行等格式會更好看
704
  news_content = dcc.Markdown(summary_text, style={
705
  'margin': '8px 0', 'padding-left': '5px',
706
  'font-size': '15px', 'line-height': '1.7'
707
  })
708
- elif top_news_list == []: # 如果是空列表
709
  news_content = html.P("昨日無重大相關新聞。", style={'text-align': 'center', 'padding-top': '50px'})
710
- else: # 如果是 None 或其他錯誤
711
  news_content = html.P("讀取新聞時發生錯誤。", style={'text-align': 'center', 'padding-top': '50px'})
712
 
713
  return gauge_content, news_content
714
 
715
  # 主程式執行
716
  if __name__ == '__main__':
717
- # 在 Hugging Face Spaces 中執行
718
  app.run(host="0.0.0.0", port=7860, debug=False)
 
1
+ # HUGING_FACE_V3.1.2.py (整合 Bert_predict 版本)
2
+
3
  # 系統套件
4
  import os
5
  from datetime import datetime, timedelta
6
+ import google.generativeai as genai
7
  import pandas as pd
8
  import numpy as np
9
  import yfinance as yf
 
15
  import re
16
  from bs4 import BeautifulSoup
17
  import requests
18
+ import time # 引用 time 模組以處理時間戳
19
 
20
+ # ========================= 引用外部模組 START =========================
21
  # 引用您組員的預測器程式
22
  from Bert_predict import BertPredictor
23
 
24
+ # 引用新的模型預測器
25
+ from model_predictor import advanced_lstm_predict
26
+ # ========================== 引用外部模組 END ==========================
27
+
28
+ # ========================= 全域設定 START =========================
29
+ # 【【【模型切換開關】】】
30
+ # False: 使用簡易統計模型 (預設)
31
+ # True: 使用 model_predictor.py 中的進階 LSTM 模型 (未來啟用)
32
+ USE_ADVANCED_MODEL = False
33
+
34
+
35
+ # ========================= CACHE 設定 START =========================
36
+ # 分析結果的快取字典
37
+ ANALYSIS_CACHE = {}
38
+ # 快取有效時間(秒),例如:4 小時 = 4 * 60 * 60 = 14400 秒
39
+ CACHE_DURATION_SECONDS = 8 * 60 * 60
40
+ # ========================== CACHE 設定 END ==========================
41
+ # ========================== 全域設定 END ==========================
42
+
43
  # 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
44
  TAIWAN_STOCKS = {
45
+ '元大台灣50': '0050.TW',
46
  '台積電': '2330.TW',
47
  '聯發科': '2454.TW',
48
  '鴻海': '2317.TW',
49
+ '台達電': '2308.TW',
50
+ '廣達': '2382.TW',
51
  '富邦金': '2881.TW',
52
+ '中信金': '2891.TW',
53
  '國泰金': '2882.TW',
54
+ '聯電': '2303.TW',
55
+ '中華電': '2412.TW',
56
+ '玉山金': '2884.TW',
57
+ '兆豐金': '2886.TW',
58
+ '日月光投控': '3711.TW',
59
+ '華碩': '2357.TW',
60
  '統一': '1216.TW',
61
+ '元大金': '2885.TW',
62
+ '智邦': '2345.TW',
63
+ '緯創': '3231.TW',
64
+ '聯詠': '3034.TW',
65
+ '第一金': '2892.TW',
66
+ '瑞昱': '2379.TW',
67
+ '緯穎': '6669.TWO',
68
+ '永豐金': '2890.TW',
69
+ '合庫金': '5880.TW',
70
+ '華南金': '2880.TW',
71
+ '台光電': '2383.TW',
72
+ '世芯-KY': '3661.TWO',
73
+ '奇鋐': '3017.TW',
74
+ '凱基金': '2883.TW',
75
+ '大立光': '3008.TW',
76
  '長榮': '2603.TW',
77
+ '光寶科': '2301.TW',
78
+ '中鋼': '2002.TW',
79
+ '中租-KY': '5871.TW',
80
+ '國巨': '2327.TW',
81
+ '台新金': '2887.TW',
82
+ '上海商銀': '5876.TW',
83
+ '台泥': '1101.TW',
84
+ '台灣大': '3045.TW',
85
+ '和碩': '4938.TW',
86
+ '遠傳': '4904.TW',
87
+ '和泰車': '2207.TW',
88
+ '研華': '2395.TW',
89
+ '台塑': '1301.TW',
90
+ '統一超': '2912.TW',
91
+ '藥華藥': '6446.TWO',
92
+ '南亞': '1303.TW',
93
+ '陽明': '2609.TW',
94
+ '萬海': '2615.TW',
95
+ '台塑化': '6505.TW',
96
  '慧洋-KY': '2637.TW',
97
  '上銀': '2049.TW',
98
  '台泥': '1101.TW',
 
106
 
107
  # 產業分類
108
  INDUSTRY_MAPPING = {
109
+ '0050.TW': 'ETF',
110
  '2330.TW': '半導體',
111
  '2454.TW': '半導體',
112
  '2317.TW': '電子組件',
113
+ '2308.TW': '電子',
114
+ '2382.TW': '電子',
115
  '2881.TW': '金融',
116
+ '2891.TW': '金融',
117
  '2882.TW': '金融',
118
+ '2303.TW': '半導體',
119
+ '2412.TW': '電信',
120
+ '2884.TW': '金融',
121
+ '2886.TW': '金融',
122
  '3711.TW': '半導體',
123
+ '2357.TW': '電子',
124
+ '1216.TW': '食品',
125
+ '2885.TW': '金融',
126
+ '2345.TW': '網通設備',
127
+ '3231.TW': '電子',
128
+ '3034.TW': '半導體',
129
+ '2892.TW': '金融',
130
+ '2379.TW': '半導體',
131
+ '6669.TWO': '電子',
132
+ '2890.TW': '金融',
133
+ '5880.TW': '金融',
134
+ '2880.TW': '金融',
135
+ '2383.TW': '電子',
136
+ '3661.TWO': '半導體',
137
+ '3017.TW': '電子',
138
+ '2883.TW': '金融',
139
+ '3008.TW': '光學',
140
  '2603.TW': '航運',
141
+ '2301.TW': '電子',
142
+ '2002.TW': '鋼鐵',
143
+ '5871.TW': '金融',
144
+ '2327.TW': '電子被動元件',
145
+ '2887.TW': '金融',
146
+ '5876.TW': '金融',
147
+ '1101.TW': '營建',
148
+ '3045.TW': '電信',
149
+ '4938.TW': '電子',
150
+ '4904.TW': '電信',
151
+ '2207.TW': '汽車',
152
+ '2395.TW': '電腦周邊',
153
+ '1301.TW': '塑膠',
154
+ '2912.TW': '百貨',
155
+ '6446.TWO': '生技',
156
+ '1303.TW': '塑膠',
157
+ '2609.TW': '航運',
158
+ '2615.TW': '航運',
159
+ '6505.TW': '塑膠',
160
  '2637.TW': '散裝航運',
161
  '2049.TW': '工具機',
162
  '1101.TW': '營建',
163
  '2408.TW': 'DRAM',
164
  '2337.TW': 'NFLSH',
 
165
  '4966.TWO': '高速傳輸',
166
  '3665.TW': '連接器',
167
  '6870.TWO': '軟體整合',
 
183
  except:
184
  return pd.DataFrame()
185
 
186
+ def simple_statistical_predict(data, predict_days=5):
187
+ """【備用模型】簡化的統計預測模型。"""
188
+ if len(data) < 60: return None
 
189
  prices = data['Close'].values
190
  ma_short = np.mean(prices[-5:])
191
  ma_medium = np.mean(prices[-20:])
 
193
  recent_trend = np.polyfit(range(20), prices[-20:], 1)[0]
194
  volatility = np.std(prices[-20:]) / np.mean(prices[-20:])
195
  base_change = recent_trend * predict_days
196
+ trend_factor = 1.0 + (0.02 if ma_short > ma_medium > ma_long else -0.02 if ma_short < ma_medium < ma_long else 0)
 
 
 
 
 
 
197
  noise_factor = np.random.normal(1, volatility * 0.1)
198
  predicted_price = prices[-1] * trend_factor + base_change + (prices[-1] * noise_factor * 0.01)
199
  change_pct = ((predicted_price - prices[-1]) / prices[-1]) * 100
200
+ return {'predicted_price': predicted_price, 'change_pct': change_pct, 'confidence': max(0.6, 1 - volatility * 2)}
201
+
202
+ def get_prediction(data, predict_days=5):
203
+ """
204
+ 【【模型預測控制器】】
205
+ 根據 USE_ADVANCED_MODEL 的設定,呼叫對應的預測模型。
206
+ """
207
+ if USE_ADVANCED_MODEL:
208
+ print(f"模式: 進階LSTM模型 | 預測天期: {predict_days}天")
209
+ prediction = advanced_lstm_predict(predict_days)
210
+ # 如果進階模型預測失敗,則自動降級使用簡易模型
211
+ if prediction is not None:
212
+ return prediction
213
+ else:
214
+ print("進階模型預測失敗,自動降級為簡易統計模型。")
215
+
216
+ # 預設或降級時執行簡易模型
217
+ print(f"模式: 簡易統計模型 | 預測天期: {predict_days}天")
218
+ return simple_statistical_predict(data, predict_days)
219
 
220
  def calculate_technical_indicators(df):
221
  """計算技術指標"""
 
295
  print(f"無法獲取 PMI 資料: {str(e)}")
296
  return pd.DataFrame()
297
 
298
+ def generate_gemini_analysis(stock_name, stock_symbol, period, data):
299
+ """
300
+ 使用 Gemini API 生成基本面和市場展望分析。
301
+ """
302
+ api_key = os.getenv("GEMINI_API_KEY")
303
+ if not api_key:
304
+ return "無法讀取 GEMINI API 金鑰", "請在系統環境變數中設定您的金鑰"
305
+
306
+ try:
307
+ genai.configure(api_key=api_key)
308
+ model = genai.GenerativeModel('gemini-1.5-flash')
309
+
310
+ price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
311
+ rsi_current = data['RSI'].iloc[-1]
312
+ macd_current = data['MACD'].iloc[-1]
313
+ macd_signal_current = data['MACD_Signal'].iloc[-1]
314
+ industry = INDUSTRY_MAPPING.get(stock_symbol, '綜合')
315
+
316
+ prompt = f"""
317
+ 請扮演一位專業、資深的台灣股市金融分析師。
318
+ 我將提供一檔台股的即時技術指標數據,請你基於這些數據,結合你對這家公司、其所在產業以及當前市場趨勢的理解,為我生成一段專業的「基本面分析」和一段「市場展望與投資建議」。
319
+
320
+ **股票資訊:**
321
+ - **公司名稱:** {stock_name} ({stock_symbol})
322
+ - **分析期間:** 最近 {period}
323
+ - **所屬產業:** {industry}
324
+ - **期間價格變動:** {price_change:+.2f}%
325
+ - **目前 RSI 指標:** {rsi_current:.2f}
326
+ - **目前 MACD 指標:** MACD線為 {macd_current:.3f}, 信號線為 {macd_signal_current:.3f}
327
+
328
+ **你的任務:**
329
+ 1. **基本面分析 (約 150 字):**
330
+ - 評論這家公司的產業地位、近期營運亮點或挑戰。
331
+ - 提及任何可能影響其基本面的關鍵因素 (例如:財報、法說會、政策、供應鏈變化等)。
332
+ - 請用專業、客觀的語氣撰寫。
333
+
334
+ 2. **市場展望與投資建議 (約 150 字):**
335
+ - 基於上述所有資訊,提供對該股票的短期和中期市場展望。
336
+ - 提出具體的投資建議,例如:適合何種類型的投資人、潛在的風險點。
337
+ - 請直接提供分析內容,不要包含任何問候語。
338
+
339
+ **輸出格式:**
340
+ 請嚴格按照以下格式回傳,使用"$$"作為兩個段落之間的分隔符:
341
+ [基本面分析內容]$$[市場展望與投資建議內容]
342
+ """
343
+
344
+ response = model.generate_content(prompt)
345
+ parts = response.text.split('$$')
346
+ if len(parts) == 2:
347
+ fundamental_analysis = parts[0].strip()
348
+ market_outlook = parts[1].strip()
349
+ return dcc.Markdown(fundamental_analysis), dcc.Markdown(market_outlook)
350
+ else:
351
+ # Fallback for unexpected response format
352
+ return dcc.Markdown("無法解析 Gemini 回應,請稍後再試。"), dcc.Markdown(response.text)
353
+
354
+ except Exception as e:
355
+ error_message = f"呼叫 Gemini API 時發生錯誤: {str(e)}"
356
+ print(error_message)
357
+ return dcc.Markdown(error_message), dcc.Markdown("請檢查後台日誌或 API 金鑰設定")
358
+
359
  # 建立 Dash 應用程式
360
  app = dash.Dash(__name__, suppress_callback_exceptions=True)
361
 
 
362
  try:
363
  print("正在初始化新聞情緒分析模型...")
364
  predictor = BertPredictor(max_news_per_keyword=5)
 
387
  html.Div([dcc.Graph(id='taiex-prediction-chart')], style={'margin-top': '20px'})
388
  ], style={'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)','padding': '25px','border-radius': '15px','box-shadow': '0 8px 25px rgba(0,0,0,0.15)','color': 'white','margin-bottom': '40px'}),
389
 
 
390
  html.Div([
391
  html.H3("📰 市場情緒與新聞分析", style={'color': '#E74C3C', 'margin-bottom': '20px'}),
392
  html.Div([
 
418
  html.Label("時間範圍:"),
419
  dcc.Dropdown(id='period-dropdown',
420
  options=[{'label': '1個月', 'value': '1mo'},{'label': '3個月', 'value': '3mo'},{'label': '6個月', 'value': '6mo'},{'label': '1年', 'value': '1y'},{'label': '2年', 'value': '2y'}],
421
+ value='1mo', style={'margin-bottom': '10px'})
422
  ], style={'width': '30%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'}),
423
  html.Div([
424
  html.Label("圖表類型:"),
 
449
  html.Div(id='technical-analysis-text', style={'background': '#f8f9fa','padding': '15px','border-radius': '8px','border-left': '4px solid #A23B72','min-height': '150px','font-size': '14px','line-height': '1.6'})
450
  ], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'}),
451
  html.Div([
452
+ html.H4("📈 基本面分析 (AI 生成)", style={'color': '#F18F01', 'margin-bottom': '15px'}),
453
  html.Div(id='fundamental-analysis-text', style={'background': '#f8f9fa','padding': '15px','border-radius': '8px','border-left': '4px solid #F18F01','min-height': '150px','font-size': '14px','line-height': '1.6'})
454
  ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '4%', 'vertical-align': 'top'})
455
  ]),
456
  html.Div([
457
+ html.H4("🎯 市場展望與投資建議 (AI 生成)", style={'color': '#C73E1D', 'margin-bottom': '15px', 'margin-top': '25px'}),
458
  html.Div(id='market-outlook-text', style={'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)','color': 'white','padding': '20px','border-radius': '10px','min-height': '100px','font-size': '15px','line-height': '1.7','box-shadow': '0 4px 15px rgba(0,0,0,0.1)'})
459
  ])
460
  ], style={'margin-top': '30px','padding': '25px','background': 'white','border-radius': '12px','box-shadow': '0 4px 20px rgba(0,0,0,0.08)','border': '1px solid #e9ecef'}),
 
478
  ], style={'margin-top': '30px','padding': '20px','background': 'white','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)'}),
479
  ])
480
 
 
481
  @app.callback(
482
  [dash.dependencies.Output('taiex-prediction-results', 'children'),
483
  dash.dependencies.Output('taiex-prediction-chart', 'figure')],
 
486
  def update_taiex_prediction(predict_days):
487
  data = get_stock_data('^TWII', '2y')
488
  if data.empty: return html.Div("無法獲取台指期資料"), {}
489
+
490
+ # === 修改點:統一呼叫 get_prediction 控制器 ===
491
+ final_prediction = get_prediction(data, predict_days)
492
+
493
  if final_prediction is None: return html.Div("資料不足,無法進行預測"), {}
494
  current_price, last_date = data['Close'].iloc[-1], data.index[-1]
495
  predicted_price, change_pct, confidence = final_prediction['predicted_price'], final_prediction['change_pct'], final_prediction['confidence']
496
+
497
  prediction_paths = {1: [1], 5: [1, 5], 10: [1, 5, 10], 20: [1, 10, 20], 60: [1, 10, 20, 60]}
498
  intervals_to_predict = prediction_paths.get(predict_days, [predict_days])
499
  prediction_dates, prediction_prices = [last_date], [current_price]
500
+
501
  for days in intervals_to_predict:
502
+ # === 修改點:迴圈內也使用統一的預測控制器 ===
503
+ interim_prediction = get_prediction(data, days)
504
  if interim_prediction:
505
  prediction_dates.append(last_date + timedelta(days=days))
506
  prediction_prices.append(interim_prediction['predicted_price'])
507
+
508
+ # (後續繪圖邏輯不變)
509
  color, arrow = ('red', '📈') if change_pct >= 0 else ('green', '📉')
510
  result_card = html.Div([
511
  html.H4(f"{predict_days}日後預測結果", style={'margin': '0 0 15px 0', 'color': 'white'}),
 
520
  fig.update_layout(title=f'台指期 {predict_days}日預測走勢', xaxis_title='日期', yaxis_title='指數點位', height=350, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='white'))
521
  return result_card, fig
522
 
 
523
  @app.callback(
524
  dash.dependencies.Output('stock-info-cards', 'children'),
525
  [dash.dependencies.Input('stock-dropdown', 'value')]
 
547
  ], style={'background': 'white','padding': '20px','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)','display': 'inline-block'})
548
  ])
549
 
 
550
  @app.callback(
551
  dash.dependencies.Output('price-chart', 'figure'),
552
  [dash.dependencies.Input('stock-dropdown', 'value'),
 
571
  fig.update_layout(title_text=f'{stock_name} 股價走勢與成交量分佈', height=500, showlegend=True, xaxis1=dict(title='日期', type='date', rangeslider_visible=False), yaxis1=dict(title='價格 (TWD)'), xaxis2=dict(title='成交量', showticklabels=True), yaxis2=dict(showticklabels=False), bargap=0.05)
572
  return fig
573
 
 
574
  @app.callback(
575
  dash.dependencies.Output('advanced-technical-chart', 'figure'),
576
  [dash.dependencies.Input('technical-indicator-selector', 'value'),
 
582
  if data.empty: return {}
583
  data = calculate_technical_indicators(data)
584
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
585
+ fig = go.Figure()
586
  if indicator == 'RSI':
587
  fig = go.Figure()
588
  fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
 
630
  fig.update_layout(title=f'{stock_name} - DMI 動向指標 (14日)', height=500, showlegend=True, yaxis2_range=[0, 100])
631
  return fig
632
 
 
633
  @app.callback(
634
  dash.dependencies.Output('volume-chart', 'figure'),
635
  [dash.dependencies.Input('stock-dropdown', 'value'),
 
644
  fig.update_layout(title=f'{stock_name} 成交量', xaxis_title='日期', yaxis_title='成交量', height=300)
645
  return fig
646
 
 
647
  @app.callback(
648
  dash.dependencies.Output('industry-analysis', 'figure'),
649
  [dash.dependencies.Input('stock-dropdown', 'value')]
650
  )
651
  def update_industry_analysis(selected_stock):
652
+ performance_data = []
653
+ for name, symbol in TAIWAN_STOCKS.items():
654
  data = get_stock_data(symbol, '1mo')
655
+ if not data.empty and len(data) > 1:
 
656
  return_pct = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
657
+ performance_data.append({
658
+ '股票': name,
659
+ '代碼': symbol,
660
+ '月報酬率(%)': return_pct,
661
+ '絕對波動': abs(return_pct)
662
+ })
663
+ if not performance_data:
664
+ fig = go.Figure().add_annotation(text="無法計算產業資料", showarrow=False)
665
+ fig.update_layout(title="近一月市場波動最大標的", height=400)
666
+ return fig
667
+ df_performance = pd.DataFrame(performance_data)
668
+ df_top_movers = df_performance.sort_values(by='絕對波動', ascending=False).head(10)
669
+ fig = px.pie(
670
+ df_top_movers,
671
+ values='絕對波動',
672
+ names='股票',
673
+ title='近一月市場波動最大 Top 10 標的',
674
+ hover_data={'月報酬率(%)': ':.2f'}
675
+ )
676
+ fig.update_traces(
677
+ textposition='inside',
678
+ textinfo='percent+label',
679
+ hovertemplate="<b>%{label}</b><br>月報酬率: %{customdata[0]:.2f}%<extra></extra>"
680
+ )
681
+ fig.update_layout(height=400, showlegend=False)
682
  return fig
683
 
 
684
  @app.callback(
685
  dash.dependencies.Output('business-climate-chart', 'figure'),
686
  [dash.dependencies.Input('stock-dropdown', 'value')]
 
705
  fig.update_layout(title="台灣景氣燈號走勢", xaxis_title='日期', yaxis_title='燈號分數', height=300, yaxis=dict(range=[0, 40]))
706
  return fig
707
 
708
+ # ========================= MODIFIED SECTION START (CACHE INTEGRATED) =========================
709
  @app.callback(
710
  [dash.dependencies.Output('technical-analysis-text', 'children'),
711
  dash.dependencies.Output('fundamental-analysis-text', 'children'),
 
714
  dash.dependencies.Input('period-dropdown', 'value')]
715
  )
716
  def update_analysis_text(selected_stock, period):
717
+ # 建立快取的唯一鍵值
718
+ cache_key = f"{selected_stock}-{period}"
719
+ current_time = time.time()
720
+
721
+ # 1. 檢查快取
722
+ if cache_key in ANALYSIS_CACHE:
723
+ cached_data = ANALYSIS_CACHE[cache_key]
724
+ if current_time - cached_data['timestamp'] < CACHE_DURATION_SECONDS:
725
+ print(f"從快取載入分析: {cache_key}")
726
+ # 直接回傳快取的內容
727
+ return cached_data['technical'], cached_data['fundamental'], cached_data['outlook']
728
+
729
+ print(f"重新生成分析: {cache_key}")
730
+ # --- 如果快取沒有,才繼續執行以下程式 ---
731
+
732
  data = get_stock_data(selected_stock, period)
733
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
734
+ if data.empty or len(data) < 20:
735
+ return "資料不足,無法分析", "資料不足,無法分析", "資料不足,無法分析"
736
+
737
  data = calculate_technical_indicators(data)
738
+
739
+ # 2. 技術面分析
740
+ price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
741
  rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
742
  macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
743
  macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
744
+
745
  technical_text = html.Div([
746
+ html.P([html.Strong("價格趨勢:"), f"在最近 {period} 期間內,{stock_name} 股價呈現", html.Span(f"{'上漲' if price_change > 5 else '下跌' if price_change < -5 else '盤整'}", style={'color': 'red' if price_change > 5 else 'green' if price_change < -5 else 'orange', 'font-weight': 'bold'}), f"走勢,累計變動 {price_change:+.1f}%。"]),
747
+ html.P([html.Strong("RSI 指標:"), f"目前的 RSI 值為 {rsi_current:.1f},", html.Span("處於超買區(>70)" if rsi_current > 70 else "處於超賣區(<30)" if rsi_current < 30 else "在正常範圍內", style={'color': 'green' if rsi_current > 70 else 'red' if rsi_current < 30 else 'blue', 'font-weight': 'bold'}), "。"]),
748
+ html.P([html.Strong("MACD 指標:"), f"MACD 快線 ({macd_current:.3f}) 目前", html.Span("高於" if macd_current > macd_signal_current else "低於", style={'color': 'red' if macd_current > macd_signal_current else 'green', 'font-weight': 'bold'}), f" Signal 慢線 ({macd_signal_current:.3f}),", f"顯示市場動能偏向{'多頭' if macd_current > macd_signal_current else '空頭'}"]),
749
  ])
750
+
751
+ # 3. 基本面與展望分析 (呼叫 Gemini)
752
+ fundamental_text, market_outlook_text = generate_gemini_analysis(stock_name, selected_stock, period, data)
753
+
754
+ # 4. 將新產生的結果存入快取
755
+ ANALYSIS_CACHE[cache_key] = {
756
+ 'technical': technical_text,
757
+ 'fundamental': fundamental_text,
758
+ 'outlook': market_outlook_text,
759
+ 'timestamp': current_time
760
+ }
761
+
762
+ return technical_text, fundamental_text, market_outlook_text
763
+ # ========================== MODIFIED SECTION END ==========================
764
 
 
765
  @app.callback(
766
  dash.dependencies.Output('pmi-chart', 'figure'),
767
  [dash.dependencies.Input('stock-dropdown', 'value')]
 
782
  def summarize_news_with_gemini(news_list: list) -> str:
783
  """
784
  使用 Gemini API 將英文新聞標題列表摘要成一段繁體中文。
 
 
 
 
 
 
785
  """
 
786
  api_key = os.getenv("GEMINI_API_KEY")
787
  if not api_key:
788
  return "錯誤:找不到 GEMINI_API_KEY。請在 Hugging Face Secrets 中設定。"
789
 
790
  try:
791
  genai.configure(api_key=api_key)
792
+ model = genai.GenerativeModel('gemini-1.5-flash')
793
 
 
794
  formatted_news = "\n".join([f"- {news}" for news in news_list])
795
 
 
796
  prompt = f"""
797
  請扮演一位專業的金融市場分析師。
798
  以下是幾則最新的英文財經新聞標題,請將它們整合成一段簡潔、流暢、約 200 字的繁體中文市場動態摘要,與利多哪些產業,利空哪些產業。
 
810
  print(f"呼叫 Gemini API 時發生錯誤: {e}")
811
  return f"無法生成新聞摘要,請稍後再試。錯誤訊息:{e}"
812
 
 
 
 
813
  @app.callback(
814
  [dash.dependencies.Output('comparison-chart', 'figure'),
815
  dash.dependencies.Output('comparison-table', 'children')],
 
842
  return fig, table
843
  return fig, html.Div("無可比較資料")
844
 
 
 
 
 
845
  @app.callback(
846
  [dash.dependencies.Output('sentiment-gauge', 'children'),
847
  dash.dependencies.Output('news-summary', 'children')],
848
  [dash.dependencies.Input('stock-dropdown', 'value')]
849
  )
850
  def update_sentiment_analysis(selected_stock):
 
851
  if predictor is None:
852
  error_fig = go.Figure().add_annotation(text="情緒指標模型載入失敗", showarrow=False)
853
  error_fig.update_layout(height=200)
854
  return dcc.Graph(figure=error_fig), html.P("新聞分析模型載入失敗,請檢查後台日誌。")
855
 
 
856
  sentiment_score_raw = predictor.get_news_index()
857
 
 
858
  if sentiment_score_raw is not None:
859
  sentiment_score_normalized = (sentiment_score_raw + 1) * 50
860
  sentiment_score_normalized = max(0, min(100, sentiment_score_normalized))
 
880
  error_fig.update_layout(height=200)
881
  gauge_content = dcc.Graph(figure=error_fig)
882
 
 
 
883
  top_news_list = predictor.get_news()
884
+ news_content = None
885
 
886
+ if top_news_list and isinstance(top_news_list, list):
 
887
  summary_text = summarize_news_with_gemini(top_news_list)
 
888
  news_content = dcc.Markdown(summary_text, style={
889
  'margin': '8px 0', 'padding-left': '5px',
890
  'font-size': '15px', 'line-height': '1.7'
891
  })
892
+ elif top_news_list == []:
893
  news_content = html.P("昨日無重大相關新聞。", style={'text-align': 'center', 'padding-top': '50px'})
894
+ else:
895
  news_content = html.P("讀取新聞時發生錯誤。", style={'text-align': 'center', 'padding-top': '50px'})
896
 
897
  return gauge_content, news_content
898
 
899
  # 主程式執行
900
  if __name__ == '__main__':
 
901
  app.run(host="0.0.0.0", port=7860, debug=False)