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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +73 -128
app.py CHANGED
@@ -1,7 +1,7 @@
1
  # 系統套件
2
  import os
3
  from datetime import datetime, timedelta
4
- import google.generativeai as genai
5
  import pandas as pd
6
  import numpy as np
7
  import yfinance as yf
@@ -19,7 +19,7 @@ from Bert_predict import BertPredictor
19
 
20
  # 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
21
  TAIWAN_STOCKS = {
22
- '元大台灣50': '0050.TW',
23
  '台積電': '2330.TW',
24
  '聯發科': '2454.TW',
25
  '鴻海': '2317.TW',
@@ -44,7 +44,7 @@ TAIWAN_STOCKS = {
44
 
45
  # 產業分類
46
  INDUSTRY_MAPPING = {
47
- '0050.TW': 'ETF',
48
  '2330.TW': '半導體',
49
  '2454.TW': '半導體',
50
  '2317.TW': '電子組件',
@@ -61,6 +61,7 @@ INDUSTRY_MAPPING = {
61
  '1101.TW': '營建',
62
  '2408.TW': 'DRAM',
63
  '2337.TW': 'NFLSH',
 
64
  '4966.TWO': '高速傳輸',
65
  '3665.TW': '連接器',
66
  '6870.TWO': '軟體整合',
@@ -187,72 +188,10 @@ def get_pmi_data():
187
  print(f"無法獲取 PMI 資料: {str(e)}")
188
  return pd.DataFrame()
189
 
190
- # ========================= GEMINI 整合 START =========================
191
- def generate_gemini_analysis(stock_name, stock_symbol, period, data):
192
- """
193
- 使用 Gemini API 生成基本面和市場展望分析。
194
- """
195
- api_key = os.getenv("GEMINI_API_KEY")
196
- if not api_key:
197
- return "無法讀取 GEMINI API 金鑰", "請在系統環境變數中設定您的金鑰"
198
-
199
- try:
200
- genai.configure(api_key=api_key)
201
- model = genai.GenerativeModel('gemini-1.5-flash')
202
-
203
- # 準備傳送給模型的數據
204
- price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
205
- rsi_current = data['RSI'].iloc[-1]
206
- macd_current = data['MACD'].iloc[-1]
207
- macd_signal_current = data['MACD_Signal'].iloc[-1]
208
- industry = INDUSTRY_MAPPING.get(stock_symbol, '綜合')
209
-
210
- prompt = f"""
211
- 請扮演一位專業、資深的台灣股市金融分析師。
212
- 我將提供一檔台股的即時技術指標數據,請你基於這些數據,結合你對這家公司、其所在產業以及當前市場趨勢的理解,為我生成一段專業的「基本面分析」和一段「市場展望與投資建議」。
213
-
214
- **股票資訊:**
215
- - **公司名稱:** {stock_name} ({stock_symbol})
216
- - **分析期間:** 最近 {period}
217
- - **所屬產業:** {industry}
218
- - **期間價格變動:** {price_change:+.2f}%
219
- - **目前 RSI 指標:** {rsi_current:.2f}
220
- - **目前 MACD 指標:** MACD線為 {macd_current:.3f}, 信號線為 {macd_signal_current:.3f}
221
-
222
- **你的任務:**
223
- 1. **基本面分析 (約 150 字):**
224
- - 評論這家公司的產業地位、近期營運亮點或挑戰。
225
- - 提及任何可能影響其基本面的關鍵因素 (例如:財報、法說會、政策、供應鏈變化等)。
226
- - 請用專業、客觀的語氣撰寫。
227
-
228
- 2. **市場展望與投資建議 (約 150 字):**
229
- - 基於上述所有資訊,提供對該股票的短期和中期市場展望。
230
- - 提出具體的投資建議,例如:適合何種類型的投資人、潛在的風險點。
231
- - 請直接提供分析內容,不要包含任何問候語。
232
-
233
- **輸出格式:**
234
- 請嚴格按照以下格式回傳,使用"$$"作為兩個段落之間的分隔符:
235
- [基本面分析內容]$$[市場展望與投資建議內容]
236
- """
237
-
238
- response = model.generate_content(prompt)
239
- parts = response.text.split('$$')
240
- if len(parts) == 2:
241
- fundamental_analysis = parts[0].strip()
242
- market_outlook = parts[1].strip()
243
- return dcc.Markdown(fundamental_analysis), dcc.Markdown(market_outlook)
244
- else:
245
- return "無法解析 Gemini 回應", response.text
246
-
247
- except Exception as e:
248
- error_message = f"呼叫 Gemini API 時發生錯誤: {str(e)}"
249
- print(error_message)
250
- return error_message, "請檢查後台日誌或 API 金鑰設定"
251
- # ========================== GEMINI 整合 END ==========================
252
-
253
  # 建立 Dash 應用程式
254
  app = dash.Dash(__name__, suppress_callback_exceptions=True)
255
 
 
256
  try:
257
  print("正在初始化新聞情緒分析模型...")
258
  predictor = BertPredictor(max_news_per_keyword=5)
@@ -281,6 +220,7 @@ app.layout = html.Div([
281
  html.Div([dcc.Graph(id='taiex-prediction-chart')], style={'margin-top': '20px'})
282
  ], 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'}),
283
 
 
284
  html.Div([
285
  html.H3("📰 市場情緒與新聞分析", style={'color': '#E74C3C', 'margin-bottom': '20px'}),
286
  html.Div([
@@ -312,7 +252,7 @@ app.layout = html.Div([
312
  html.Label("時間範圍:"),
313
  dcc.Dropdown(id='period-dropdown',
314
  options=[{'label': '1個月', 'value': '1mo'},{'label': '3個月', 'value': '3mo'},{'label': '6個月', 'value': '6mo'},{'label': '1年', 'value': '1y'},{'label': '2年', 'value': '2y'}],
315
- value='1mo', style={'margin-bottom': '10px'}) # 預設改為 1mo
316
  ], style={'width': '30%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'}),
317
  html.Div([
318
  html.Label("圖表類型:"),
@@ -343,12 +283,12 @@ app.layout = html.Div([
343
  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'})
344
  ], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'}),
345
  html.Div([
346
- html.H4("📈 基本面分析 (AI 生成)", style={'color': '#F18F01', 'margin-bottom': '15px'}),
347
  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'})
348
  ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '4%', 'vertical-align': 'top'})
349
  ]),
350
  html.Div([
351
- html.H4("🎯 市場展望與投資建議 (AI 生成)", style={'color': '#C73E1D', 'margin-bottom': '15px', 'margin-top': '25px'}),
352
  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)'})
353
  ])
354
  ], 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'}),
@@ -372,6 +312,7 @@ app.layout = html.Div([
372
  ], style={'margin-top': '30px','padding': '20px','background': 'white','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)'}),
373
  ])
374
 
 
375
  @app.callback(
376
  [dash.dependencies.Output('taiex-prediction-results', 'children'),
377
  dash.dependencies.Output('taiex-prediction-chart', 'figure')],
@@ -406,6 +347,7 @@ def update_taiex_prediction(predict_days):
406
  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'))
407
  return result_card, fig
408
 
 
409
  @app.callback(
410
  dash.dependencies.Output('stock-info-cards', 'children'),
411
  [dash.dependencies.Input('stock-dropdown', 'value')]
@@ -433,6 +375,7 @@ def update_stock_info(selected_stock):
433
  ], style={'background': 'white','padding': '20px','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)','display': 'inline-block'})
434
  ])
435
 
 
436
  @app.callback(
437
  dash.dependencies.Output('price-chart', 'figure'),
438
  [dash.dependencies.Input('stock-dropdown', 'value'),
@@ -457,6 +400,7 @@ def update_price_chart(selected_stock, period, chart_type):
457
  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)
458
  return fig
459
 
 
460
  @app.callback(
461
  dash.dependencies.Output('advanced-technical-chart', 'figure'),
462
  [dash.dependencies.Input('technical-indicator-selector', 'value'),
@@ -468,7 +412,7 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
468
  if data.empty: return {}
469
  data = calculate_technical_indicators(data)
470
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
471
- fig = go.Figure()
472
  if indicator == 'RSI':
473
  fig = go.Figure()
474
  fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
@@ -516,6 +460,7 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
516
  fig.update_layout(title=f'{stock_name} - DMI 動向指標 (14日)', height=500, showlegend=True, yaxis2_range=[0, 100])
517
  return fig
518
 
 
519
  @app.callback(
520
  dash.dependencies.Output('volume-chart', 'figure'),
521
  [dash.dependencies.Input('stock-dropdown', 'value'),
@@ -530,43 +475,26 @@ def update_volume_chart(selected_stock, period):
530
  fig.update_layout(title=f'{stock_name} 成交量', xaxis_title='日期', yaxis_title='成交量', height=300)
531
  return fig
532
 
 
533
  @app.callback(
534
  dash.dependencies.Output('industry-analysis', 'figure'),
535
  [dash.dependencies.Input('stock-dropdown', 'value')]
536
  )
537
  def update_industry_analysis(selected_stock):
538
- performance_data = []
539
- for name, symbol in TAIWAN_STOCKS.items():
540
  data = get_stock_data(symbol, '1mo')
541
- if not data.empty and len(data) > 1:
 
542
  return_pct = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
543
- performance_data.append({
544
- '股票': name,
545
- '代碼': symbol,
546
- '月報酬率(%)': return_pct,
547
- '絕對波動': abs(return_pct)
548
- })
549
- if not performance_data:
550
- fig = go.Figure().add_annotation(text="無法計算產業資料", showarrow=False)
551
- fig.update_layout(title="近一月市場波動最大標的", height=400)
552
- return fig
553
- df_performance = pd.DataFrame(performance_data)
554
- df_top_movers = df_performance.sort_values(by='絕對波動', ascending=False).head(10)
555
- fig = px.pie(
556
- df_top_movers,
557
- values='絕對波動',
558
- names='股票',
559
- title='近一月市場波動最大 Top 10 標的',
560
- hover_data={'月報酬率(%)': ':.2f'}
561
- )
562
- fig.update_traces(
563
- textposition='inside',
564
- textinfo='percent+label',
565
- hovertemplate="<b>%{label}</b><br>月報酬率: %{customdata[0]:.2f}%<extra></extra>"
566
- )
567
- fig.update_layout(height=400, showlegend=False)
568
  return fig
569
 
 
570
  @app.callback(
571
  dash.dependencies.Output('business-climate-chart', 'figure'),
572
  [dash.dependencies.Input('stock-dropdown', 'value')]
@@ -591,7 +519,7 @@ def update_business_climate_chart(selected_stock):
591
  fig.update_layout(title="台灣景氣燈號走勢", xaxis_title='日期', yaxis_title='燈號分數', height=300, yaxis=dict(range=[0, 40]))
592
  return fig
593
 
594
- # ========================= MODIFIED SECTION START =========================
595
  @app.callback(
596
  [dash.dependencies.Output('technical-analysis-text', 'children'),
597
  dash.dependencies.Output('fundamental-analysis-text', 'children'),
@@ -602,38 +530,31 @@ def update_business_climate_chart(selected_stock):
602
  def update_analysis_text(selected_stock, period):
603
  data = get_stock_data(selected_stock, period)
604
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
605
- if data.empty or len(data) < 20: # 確保有足夠資料計算指標
606
- return "資料不足,無法分析", "資料不足,無法分析", "資料不足,無法分析"
607
-
608
  data = calculate_technical_indicators(data)
609
-
610
- # 1. 技術面分析 (保留客觀數據呈現)
611
- price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
612
  rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
613
  macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
614
  macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
615
-
616
  technical_text = html.Div([
617
- 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}%。"]),
618
- 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'}), "。"]),
619
- 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 '空頭'}"]),
620
  ])
621
-
622
- # 2. 基本面與展望分析 (呼叫 Gemini)
623
- # 顯示“正在生成…”提示,改善使用者體驗
624
- loading_text = html.Div([
625
- dcc.Loading(id="loading-analysis", type="dots", children=[html.Div(id="loading-output")])
626
  ])
627
-
628
- try:
629
- fundamental_text, market_outlook_text = generate_gemini_analysis(stock_name, selected_stock, period, data)
630
- except Exception as e:
631
- fundamental_text = f"生成分析時發生錯誤: {e}"
632
- market_outlook_text = "請檢查 API 金鑰或網路連線。"
633
-
634
- return technical_text, fundamental_text, market_outlook_text
635
- # ========================== MODIFIED SECTION END ==========================
636
 
 
637
  @app.callback(
638
  dash.dependencies.Output('pmi-chart', 'figure'),
639
  [dash.dependencies.Input('stock-dropdown', 'value')]
@@ -654,17 +575,26 @@ def update_pmi_chart(selected_stock):
654
  def summarize_news_with_gemini(news_list: list) -> str:
655
  """
656
  使用 Gemini API 將英文新聞標題列表摘要成一段繁體中文。
 
 
 
 
 
 
657
  """
 
658
  api_key = os.getenv("GEMINI_API_KEY")
659
  if not api_key:
660
  return "錯誤:找不到 GEMINI_API_KEY。請在 Hugging Face Secrets 中設定。"
661
 
662
  try:
663
  genai.configure(api_key=api_key)
664
- model = genai.GenerativeModel('gemini-1.5-flash')
665
 
 
666
  formatted_news = "\n".join([f"- {news}" for news in news_list])
667
 
 
668
  prompt = f"""
669
  請扮演一位專業的金融市場分析師。
670
  以下是幾則最新的英文財經新聞標題,請將它們整合成一段簡潔、流暢、約 200 字的繁體中文市場動態摘要,與利多哪些產業,利空哪些產業。
@@ -682,6 +612,9 @@ def summarize_news_with_gemini(news_list: list) -> str:
682
  print(f"呼叫 Gemini API 時發生錯誤: {e}")
683
  return f"無法生成新聞摘要,請稍後再試。錯誤訊息:{e}"
684
 
 
 
 
685
  @app.callback(
686
  [dash.dependencies.Output('comparison-chart', 'figure'),
687
  dash.dependencies.Output('comparison-table', 'children')],
@@ -714,19 +647,26 @@ def update_comparison_analysis(selected_stocks, period):
714
  return fig, table
715
  return fig, html.Div("無可比較資料")
716
 
 
 
 
 
717
  @app.callback(
718
  [dash.dependencies.Output('sentiment-gauge', 'children'),
719
  dash.dependencies.Output('news-summary', 'children')],
720
  [dash.dependencies.Input('stock-dropdown', 'value')]
721
  )
722
  def update_sentiment_analysis(selected_stock):
 
723
  if predictor is None:
724
  error_fig = go.Figure().add_annotation(text="情緒指標模型載入失敗", showarrow=False)
725
  error_fig.update_layout(height=200)
726
  return dcc.Graph(figure=error_fig), html.P("新聞分析模型載入失敗,請檢查後台日誌。")
727
 
 
728
  sentiment_score_raw = predictor.get_news_index()
729
 
 
730
  if sentiment_score_raw is not None:
731
  sentiment_score_normalized = (sentiment_score_raw + 1) * 50
732
  sentiment_score_normalized = max(0, min(100, sentiment_score_normalized))
@@ -752,22 +692,27 @@ def update_sentiment_analysis(selected_stock):
752
  error_fig.update_layout(height=200)
753
  gauge_content = dcc.Graph(figure=error_fig)
754
 
 
 
755
  top_news_list = predictor.get_news()
756
- news_content = None
757
 
758
- if top_news_list and isinstance(top_news_list, list):
 
759
  summary_text = summarize_news_with_gemini(top_news_list)
 
760
  news_content = dcc.Markdown(summary_text, style={
761
  'margin': '8px 0', 'padding-left': '5px',
762
  'font-size': '15px', 'line-height': '1.7'
763
  })
764
- elif top_news_list == []:
765
  news_content = html.P("昨日無重大相關新聞。", style={'text-align': 'center', 'padding-top': '50px'})
766
- else:
767
  news_content = html.P("讀取新聞時發生錯誤。", style={'text-align': 'center', 'padding-top': '50px'})
768
 
769
  return gauge_content, news_content
770
 
771
  # 主程式執行
772
  if __name__ == '__main__':
 
773
  app.run(host="0.0.0.0", port=7860, debug=False)
 
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
 
19
 
20
  # 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
21
  TAIWAN_STOCKS = {
22
+ '元大台灣50': '0050.TW', # 新增
23
  '台積電': '2330.TW',
24
  '聯發科': '2454.TW',
25
  '鴻海': '2317.TW',
 
44
 
45
  # 產業分類
46
  INDUSTRY_MAPPING = {
47
+ '0050.TW': 'ETF', # 新增
48
  '2330.TW': '半導體',
49
  '2454.TW': '半導體',
50
  '2317.TW': '電子組件',
 
61
  '1101.TW': '營建',
62
  '2408.TW': 'DRAM',
63
  '2337.TW': 'NFLSH',
64
+ '1101.TW': '營建',
65
  '4966.TWO': '高速傳輸',
66
  '3665.TW': '連接器',
67
  '6870.TWO': '軟體整合',
 
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
  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
  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
  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
  ], 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')],
 
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
  ], 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
  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
  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
  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
  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
  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'),
 
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
  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
  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
  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
  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)