AlanRex commited on
Commit
6aad597
·
verified ·
1 Parent(s): e7c18ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +111 -1028
app.py CHANGED
@@ -15,13 +15,8 @@ import plotly.graph_objects as go
15
  from plotly.subplots import make_subplots
16
 
17
 
18
-
19
- app = dash.Dash(__name__, suppress_callback_exceptions=True)
20
-
21
-
22
- # 台股代號對應表
23
  TAIWAN_STOCKS = {
24
- '台指期': 'TXF=F', # 台指期貨
25
  '台積電': '2330.TW',
26
  '聯發科': '2454.TW',
27
  '鴻海': '2317.TW',
@@ -80,14 +75,6 @@ def get_stock_data(symbol, period='1y'):
80
  except:
81
  return pd.DataFrame()
82
 
83
- def create_lstm_dataset(data, time_step=60):
84
- """建立LSTM訓練資料集"""
85
- X, y = [], []
86
- for i in range(time_step, len(data)):
87
- X.append(data[i-time_step:i, 0])
88
- y.append(data[i, 0])
89
- return np.array(X), np.array(y)
90
-
91
  def simple_lstm_predict(data, predict_days=5):
92
  """簡化的LSTM預測模型 (使用統計方法模擬)"""
93
  if len(data) < 60:
@@ -249,14 +236,63 @@ app = dash.Dash(__name__, suppress_callback_exceptions=True)
249
  app.layout = html.Div([
250
  html.H1("台股分析儀表板", style={'text-align': 'center', 'margin-bottom': '30px'}),
251
 
252
- # 控制面板
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  html.Div([
254
  html.Div([
255
  html.Label("選擇股票:"),
256
  dcc.Dropdown(
257
  id='stock-dropdown',
258
  options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
259
- value='TXF=F',
260
  style={'margin-bottom': '10px'}
261
  )
262
  ], style={'width': '30%', 'display': 'inline-block', 'vertical-align': 'top'}),
@@ -294,277 +330,38 @@ app.layout = html.Div([
294
  # 股價資訊卡片
295
  html.Div(id='stock-info-cards', style={'margin-bottom': '30px'}),
296
 
297
- # AI預測區塊 (只在選擇台指期時顯示)
298
- html.Div(id='prediction-section', style={'margin-bottom': '30px'}),
299
-
300
- # 主要圖表區域
301
- html.Div([
302
- # 左側:股價走勢圖和技術指標
303
- html.Div([
304
- html.Div([
305
- dcc.Graph(id='price-chart')
306
- ], style={'margin-bottom': '20px'}),
307
-
308
- html.Div([
309
- dcc.Graph(id='rsi-chart')
310
- ])
311
- ], style={'width': '65%', 'display': 'inline-block', 'vertical-align': 'top'}),
312
-
313
- # 右側:分析資訊面板
314
- html.Div([
315
- html.Div(id='analysis-panel')
316
- ], style={'width': '33%', 'display': 'inline-block', 'margin-left': '2%', 'vertical-align': 'top'})
317
- ]),
318
-
319
- # 技術指標選擇區域
320
  html.Div([
321
- html.H3("📊 進階技術指標分析", style={'margin-bottom': '20px'}),
322
- html.Div([
323
- html.Label("選擇技術指標:", style={'font-weight': 'bold', 'margin-right': '10px'}),
324
- dcc.Dropdown(
325
- id='technical-indicator-selector',
326
- options=[
327
- {'label': 'RSI 相對強弱指標', 'value': 'RSI'},
328
- {'label': 'MACD 指數平滑異同移動平均線', 'value': 'MACD'},
329
- {'label': '布林通道 Bollinger Bands', 'value': 'BB'},
330
- {'label': 'KD 隨機指標', 'value': 'KD'},
331
- {'label': '威廉指標 %R', 'value': 'WR'}
332
- ],
333
- value='RSI',
334
- style={'width': '100%'}
335
- )
336
- ], style={'margin-bottom': '20px'}),
337
-
338
- html.Div([
339
- dcc.Graph(id='advanced-technical-chart')
340
- ])
341
- ], style={
342
- 'margin-top': '20px',
343
- 'padding': '20px',
344
- 'background': 'white',
345
- 'border-radius': '10px',
346
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)'
347
- }),
348
 
349
- # 成交量圖
350
  html.Div([
351
  dcc.Graph(id='volume-chart')
352
- ], style={'margin-top': '20px'}),
353
-
354
- # 產業分析
355
- html.Div([
356
- html.H3("產業表現分析"),
357
- dcc.Graph(id='industry-analysis')
358
- ], style={'margin-top': '30px'}),
359
-
360
- # 分析師觀點區域
361
- html.Div([
362
- html.H3("📊 分析師觀點與市場解讀", style={'color': '#2E86AB', 'margin-bottom': '20px'}),
363
- html.Div([
364
- # 左側:技術分析觀點
365
- html.Div([
366
- html.H4("🔍 技術面分析", style={'color': '#A23B72', 'margin-bottom': '15px'}),
367
- html.Div(id='technical-analysis-text', style={
368
- 'background': '#f8f9fa',
369
- 'padding': '15px',
370
- 'border-radius': '8px',
371
- 'border-left': '4px solid #A23B72',
372
- 'min-height': '150px',
373
- 'font-size': '14px',
374
- 'line-height': '1.6'
375
- })
376
- ], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'}),
377
-
378
- # 右側:基本面分析觀點
379
- html.Div([
380
- html.H4("📈 基本面分析", style={'color': '#F18F01', 'margin-bottom': '15px'}),
381
- html.Div(id='fundamental-analysis-text', style={
382
- 'background': '#f8f9fa',
383
- 'padding': '15px',
384
- 'border-radius': '8px',
385
- 'border-left': '4px solid #F18F01',
386
- 'min-height': '150px',
387
- 'font-size': '14px',
388
- 'line-height': '1.6'
389
- })
390
- ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '4%', 'vertical-align': 'top'})
391
- ]),
392
-
393
- # 底部:市場展望
394
- html.Div([
395
- html.H4("🎯 市場展望與投資建議", style={'color': '#C73E1D', 'margin-bottom': '15px', 'margin-top': '25px'}),
396
- html.Div(id='market-outlook-text', style={
397
- 'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
398
- 'color': 'white',
399
- 'padding': '20px',
400
- 'border-radius': '10px',
401
- 'min-height': '100px',
402
- 'font-size': '15px',
403
- 'line-height': '1.7',
404
- 'box-shadow': '0 4px 15px rgba(0,0,0,0.1)'
405
- })
406
- ])
407
- ], style={
408
- 'margin-top': '30px',
409
- 'padding': '25px',
410
- 'background': 'white',
411
- 'border-radius': '12px',
412
- 'box-shadow': '0 4px 20px rgba(0,0,0,0.08)',
413
- 'border': '1px solid #e9ecef'
414
- }),
415
-
416
- # 景氣燈號與 PMI 分析
417
- html.Div([
418
- html.H3("景氣燈號與 PMI 分析"),
419
- html.Div([
420
- html.Div([
421
- dcc.Graph(id='business-climate-chart')
422
- ], style={'width': '48%', 'display': 'inline-block'}),
423
- html.Div([
424
- dcc.Graph(id='pmi-chart')
425
- ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '2%'})
426
- ])
427
- ], style={'margin-top': '30px'}),
428
-
429
- # 多檔股票比較區域
430
- html.Div([
431
- html.H3("📊 多檔股票比較分析", style={'margin-bottom': '20px'}),
432
- html.Div([
433
- html.Div([
434
- html.Label("選擇比較股票(最多5檔):", style={'font-weight': 'bold'}),
435
- dcc.Dropdown(
436
- id='comparison-stocks',
437
- options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
438
- value=['2330.TW', '2454.TW', '2317.TW'], # 預設選擇
439
- multi=True,
440
- style={'margin-bottom': '15px'}
441
- )
442
- ], style={'width': '60%', 'display': 'inline-block'}),
443
-
444
- html.Div([
445
- html.Label("比較期間:", style={'font-weight': 'bold'}),
446
- dcc.Dropdown(
447
- id='comparison-period',
448
- options=[
449
- {'label': '1個月', 'value': '1mo'},
450
- {'label': '3個月', 'value': '3mo'},
451
- {'label': '6個月', 'value': '6mo'},
452
- {'label': '1年', 'value': '1y'}
453
- ],
454
- value='3mo'
455
- )
456
- ], style={'width': '35%', 'display': 'inline-block', 'margin-left': '5%'})
457
- ]),
458
-
459
- html.Div([
460
- html.Div([
461
- dcc.Graph(id='comparison-chart')
462
- ], style={'width': '65%', 'display': 'inline-block'}),
463
-
464
- html.Div([
465
- html.H4("比較結果", style={'color': '#2E86AB'}),
466
- html.Div(id='comparison-table')
467
- ], style={'width': '33%', 'display': 'inline-block', 'margin-left': '2%', 'vertical-align': 'top'})
468
- ])
469
- ], style={
470
- 'margin-top': '30px',
471
- 'padding': '20px',
472
- 'background': 'white',
473
- 'border-radius': '10px',
474
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)'
475
- }),
476
-
477
- # 新聞情感分析區域(模擬)
478
- html.Div([
479
- html.H3("📰 市場情緒與新聞分析", style={'color': '#E74C3C', 'margin-bottom': '20px'}),
480
- html.Div([
481
- html.Div([
482
- html.H4("市場情緒指標", style={'color': '#8E44AD'}),
483
- html.Div(id='sentiment-gauge')
484
- ], style={'width': '48%', 'display': 'inline-block'}),
485
-
486
- html.Div([
487
- html.H4("關鍵新聞摘要", style={'color': '#27AE60'}),
488
- html.Div(id='news-summary', style={
489
- 'background': '#f8f9fa',
490
- 'padding': '15px',
491
- 'border-radius': '8px',
492
- 'max-height': '200px',
493
- 'overflow-y': 'auto'
494
- })
495
- ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '4%'})
496
- ])
497
- ], style={
498
- 'margin-top': '30px',
499
- 'padding': '20px',
500
- 'background': 'white',
501
- 'border-radius': '10px',
502
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)'
503
- })
504
  ])
505
 
506
- # 更新AI預測區塊
507
  @app.callback(
508
- dash.dependencies.Output('prediction-section', 'children'),
509
- [dash.dependencies.Input('stock-dropdown', 'value')],
510
- prevent_initial_call=False
 
511
  )
512
- def update_prediction_section(selected_stock):
513
- if selected_stock != 'TXF=F':
514
- return html.Div() # 只在選擇台指期時顯示預測功能
515
-
516
- return html.Div([
517
- html.H3("🤖 AI深度學習預測 - 台指期指數", style={'text-align': 'center', 'color': '#FFCC22'}),
518
- html.Div([
519
- html.Div([
520
- html.Label("預測期間:", style={'font-weight': 'bold', 'color': '#FFCC22'}),
521
- dcc.Dropdown(
522
- id='prediction-period',
523
- options=[
524
- {'label': '5日後預測', 'value': 5},
525
- {'label': '10日後預測', 'value': 10},
526
- {'label': '20日後預測', 'value': 20},
527
- {'label': '60日後預測', 'value': 60}
528
- ],
529
- value=5,
530
- style={'margin-bottom': '10px', 'color': '#272727'}
531
- )
532
- ], style={'width': '30%', 'display': 'inline-block'}),
533
-
534
- html.Div(id='prediction-results', style={'width': '65%', 'display': 'inline-block', 'margin-left': '5%'})
535
- ]),
536
-
537
- html.Div([
538
- dcc.Graph(id='prediction-chart')
539
- ], style={'margin-top': '20px'})
540
- ], style={
541
- 'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
542
- 'padding': '25px',
543
- 'border-radius': '15px',
544
- 'box-shadow': '0 8px 25px rgba(0,0,0,0.15)',
545
- 'color': 'white'
546
- })
547
-
548
- # 更新預測結果
549
- @app.callback(
550
- [dash.dependencies.Output('prediction-results', 'children'),
551
- dash.dependencies.Output('prediction-chart', 'figure')],
552
- [dash.dependencies.Input('prediction-period', 'value'),
553
- dash.dependencies.Input('stock-dropdown', 'value')]
554
- )
555
- def update_prediction(predict_days, selected_stock):
556
- if selected_stock != 'TXF=F':
557
- return html.Div(), {}
558
-
559
  # 獲取台指期歷史資料
560
- data = get_stock_data('^TWII', '2y')
561
  if data.empty:
562
- return html.Div("無法獲取台指期資料"), {}
 
 
 
 
 
563
 
564
  # 執行預測
565
  prediction = simple_lstm_predict(data, predict_days)
566
  if prediction is None:
567
- return html.Div("資料不足,無法進行預測"), {}
568
 
569
  current_price = data['Close'].iloc[-1]
570
  predicted_price = prediction['predicted_price']
@@ -576,29 +373,43 @@ def update_prediction(predict_days, selected_stock):
576
  arrow = '📈' if change_pct >= 0 else '📉'
577
 
578
  result_card = html.Div([
579
- html.H4(f"{predict_days}日後預測結果", style={'margin': '0 0 15px 0', 'color': 'white'}),
580
  html.Div([
581
  html.Span(f"{arrow} ", style={'font-size': '24px'}),
582
- html.Span(f"{change_pct:+.2f}%", style={
583
- 'font-size': '28px',
584
- 'font-weight': 'bold',
585
- 'color': color
586
- })
587
- ], style={'margin': '10px 0'}),
588
- html.P(f"目前價格: {current_price:.2f}", style={'margin': '5px 0'}),
589
- html.P(f"預測價格: {predicted_price:.2f}", style={'margin': '5px 0'}),
590
- html.P(f"信心度: {confidence:.1%}", style={'margin': '5px 0', 'font-size': '14px'})
591
  ], style={
592
- 'background': 'rgba(255,255,255,0.1)',
593
  'padding': '20px',
594
- 'border-radius': '10px',
595
- 'border': '1px solid rgba(255,255,255,0.2)'
596
  })
597
 
598
- # 建立預測趨勢圖
599
- fig = go.Figure()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
600
 
601
- # 歷史價格 (最近30天)
 
 
 
602
  recent_data = data.tail(30)
603
  fig.add_trace(go.Scatter(
604
  x=recent_data.index,
@@ -621,15 +432,13 @@ def update_prediction(predict_days, selected_stock):
621
 
622
  fig.update_layout(
623
  title=f'台指期 {predict_days}日預測走勢',
624
- xaxis_title='日期',
625
- yaxis_title='指數點位',
626
  height=350,
627
  plot_bgcolor='rgba(0,0,0,0)',
628
  paper_bgcolor='rgba(0,0,0,0)',
629
  font=dict(color='white')
630
  )
631
 
632
- return result_card, fig
633
 
634
  # 更新股價資訊卡片
635
  @app.callback(
@@ -646,39 +455,14 @@ def update_stock_info(selected_stock):
646
  change = current_price - prev_price
647
  change_pct = (change / prev_price) * 100
648
 
649
- # 找出股票中文名稱
650
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
651
-
652
  color = 'green' if change >= 0 else 'red'
653
 
654
  return html.Div([
655
- html.Div([
656
- html.H3(f"{stock_name} ({selected_stock})", style={'margin': '0'}),
657
- html.H2(f"${current_price:.2f}", style={'margin': '5px 0', 'color': color}),
658
- html.P(f"{'' if change >= 0 else ''} {change:+.2f} ({change_pct:+.2f}%)",
659
- style={'margin': '0', 'color': color, 'font-weight': 'bold'})
660
- ], style={
661
- 'background': 'white',
662
- 'padding': '20px',
663
- 'border-radius': '10px',
664
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)',
665
- 'display': 'inline-block',
666
- 'margin-right': '20px'
667
- }),
668
-
669
- html.Div([
670
- html.H4("今日統計", style={'margin': '0 0 10px 0'}),
671
- html.P(f"最高: ${data['High'].iloc[-1]:.2f}", style={'margin': '5px 0'}),
672
- html.P(f"最低: ${data['Low'].iloc[-1]:.2f}", style={'margin': '5px 0'}),
673
- html.P(f"成交量: {data['Volume'].iloc[-1]:,.0f}", style={'margin': '5px 0'})
674
- ], style={
675
- 'background': 'white',
676
- 'padding': '20px',
677
- 'border-radius': '10px',
678
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)',
679
- 'display': 'inline-block'
680
- })
681
- ])
682
 
683
  # 更新股價圖表
684
  @app.callback(
@@ -707,209 +491,7 @@ def update_price_chart(selected_stock, period, chart_type):
707
  else:
708
  fig = px.line(data, y='Close', title=f'{stock_name} 股價走勢')
709
 
710
- # 添加移動平均線
711
- fig.add_trace(go.Scatter(x=data.index, y=data['MA5'], mode='lines', name='MA5', line=dict(color='orange')))
712
- fig.add_trace(go.Scatter(x=data.index, y=data['MA20'], mode='lines', name='MA20', line=dict(color='blue')))
713
-
714
- fig.update_layout(
715
- title=f'{stock_name} 股價走勢',
716
- xaxis_title='日期',
717
- yaxis_title='價格 (TWD)',
718
- height=400
719
- )
720
-
721
- return fig
722
-
723
- # 更新RSI圖表(保持兼容性)
724
- @app.callback(
725
- dash.dependencies.Output('rsi-chart', 'figure'),
726
- [dash.dependencies.Input('stock-dropdown', 'value'),
727
- dash.dependencies.Input('period-dropdown', 'value')]
728
- )
729
- def update_rsi_chart(selected_stock, period):
730
- data = get_stock_data(selected_stock, period)
731
- if data.empty:
732
- return {}
733
-
734
- data = calculate_technical_indicators(data)
735
-
736
- fig = go.Figure()
737
- fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
738
- fig.add_hline(y=70, line_dash="dash", line_color="red", annotation_text="超買線(70)")
739
- fig.add_hline(y=30, line_dash="dash", line_color="green", annotation_text="超賣線(30)")
740
- fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)")
741
-
742
- # 添加超買超賣區域背景
743
- fig.add_hrect(y0=70, y1=100, fillcolor="red", opacity=0.1, annotation_text="超買區")
744
- fig.add_hrect(y0=0, y1=30, fillcolor="green", opacity=0.1, annotation_text="超賣區")
745
-
746
- fig.update_layout(
747
- title='RSI 相對強弱指標',
748
- xaxis_title='日期',
749
- yaxis_title='RSI',
750
- height=400,
751
- yaxis=dict(range=[0, 100])
752
- )
753
-
754
- return fig
755
-
756
-
757
-
758
- # 新增:進階技術指標圖表
759
- @app.callback(
760
- dash.dependencies.Output('advanced-technical-chart', 'figure'),
761
- [dash.dependencies.Input('technical-indicator-selector', 'value'),
762
- dash.dependencies.Input('stock-dropdown', 'value'),
763
- dash.dependencies.Input('period-dropdown', 'value')]
764
- )
765
- def update_advanced_technical_chart(indicator, selected_stock, period):
766
- data = get_stock_data(selected_stock, period)
767
- if data.empty:
768
- return {}
769
-
770
- data = calculate_technical_indicators(data)
771
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
772
-
773
- if indicator == 'RSI':
774
- fig = go.Figure()
775
- fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
776
- fig.add_hline(y=70, line_dash="dash", line_color="red", annotation_text="超買線(70)")
777
- fig.add_hline(y=30, line_dash="dash", line_color="green", annotation_text="超賣線(30)")
778
- fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)")
779
-
780
- fig.add_hrect(y0=70, y1=100, fillcolor="red", opacity=0.1)
781
- fig.add_hrect(y0=0, y1=30, fillcolor="green", opacity=0.1)
782
-
783
- fig.update_layout(
784
- title=f'{stock_name} - RSI 相對強弱指標',
785
- xaxis_title='日期',
786
- yaxis_title='RSI',
787
- height=450,
788
- yaxis=dict(range=[0, 100])
789
- )
790
-
791
- elif indicator == 'MACD':
792
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
793
- vertical_spacing=0.1,
794
- row_heights=[0.7, 0.3],
795
- subplot_titles=('價格與MACD線', 'MACD柱狀圖'))
796
-
797
- # 上方:價格線
798
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
799
- line=dict(color='black', width=1)), row=1, col=1)
800
-
801
- # MACD線和信號線
802
- fig.add_trace(go.Scatter(x=data.index, y=data['MACD'], mode='lines', name='MACD',
803
- line=dict(color='blue', width=2)), row=1, col=1)
804
- fig.add_trace(go.Scatter(x=data.index, y=data['MACD_Signal'], mode='lines', name='信號線',
805
- line=dict(color='red', width=2)), row=1, col=1)
806
-
807
- # 下方:MACD柱狀圖
808
- colors = ['green' if x >= 0 else 'red' for x in data['MACD_Histogram']]
809
- fig.add_trace(go.Bar(x=data.index, y=data['MACD_Histogram'], name='MACD柱狀圖',
810
- marker_color=colors), row=2, col=1)
811
-
812
- fig.add_hline(y=0, line_dash="dash", line_color="gray", row=1, col=1)
813
- fig.add_hline(y=0, line_dash="dash", line_color="gray", row=2, col=1)
814
-
815
- fig.update_layout(
816
- title=f'{stock_name} - MACD 指數平滑異同移動平均線',
817
- height=500
818
- )
819
-
820
- elif indicator == 'BB':
821
- fig = go.Figure()
822
-
823
- # 價格線
824
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
825
- line=dict(color='black', width=2)))
826
-
827
- # 布林通道上軌
828
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines', name='上軌',
829
- line=dict(color='red', width=1, dash='dash')))
830
-
831
- # 布林通道中軌
832
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Middle'], mode='lines', name='中軌(MA20)',
833
- line=dict(color='blue', width=1)))
834
-
835
- # 布林通道下軌
836
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines', name='下軌',
837
- line=dict(color='green', width=1, dash='dash')))
838
-
839
- # 填充通道區域
840
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines',
841
- line=dict(color='rgba(0,0,0,0)'), showlegend=False))
842
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines',
843
- fill='tonexty', fillcolor='rgba(173,216,230,0.2)',
844
- line=dict(color='rgba(0,0,0,0)'), name='布林通道', showlegend=False))
845
-
846
- fig.update_layout(
847
- title=f'{stock_name} - 布林通道 (20日, 2σ)',
848
- xaxis_title='日期',
849
- yaxis_title='價格 (TWD)',
850
- height=450
851
- )
852
-
853
- elif indicator == 'KD':
854
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
855
- vertical_spacing=0.1,
856
- row_heights=[0.6, 0.4],
857
- subplot_titles=('價格走勢', 'KD指標'))
858
-
859
- # 上方:價格線
860
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
861
- line=dict(color='black', width=1)), row=1, col=1)
862
-
863
- # 下方:KD線
864
- fig.add_trace(go.Scatter(x=data.index, y=data['K'], mode='lines', name='K線',
865
- line=dict(color='blue', width=2)), row=2, col=1)
866
- fig.add_trace(go.Scatter(x=data.index, y=data['D'], mode='lines', name='D線',
867
- line=dict(color='red', width=2)), row=2, col=1)
868
-
869
- # KD指標參考線
870
- fig.add_hline(y=80, line_dash="dash", line_color="red", annotation_text="超買線(80)", row=2, col=1)
871
- fig.add_hline(y=20, line_dash="dash", line_color="green", annotation_text="超賣線(20)", row=2, col=1)
872
- fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)", row=2, col=1)
873
-
874
- # 超買超賣區域
875
- fig.add_hrect(y0=80, y1=100, fillcolor="red", opacity=0.1, row=2, col=1)
876
- fig.add_hrect(y0=0, y1=20, fillcolor="green", opacity=0.1, row=2, col=1)
877
-
878
- fig.update_layout(
879
- title=f'{stock_name} - KD 隨機指標 (9,3,3)',
880
- height=500
881
- )
882
- fig.update_yaxes(range=[0, 100], row=2, col=1)
883
-
884
- elif indicator == 'WR':
885
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
886
- vertical_spacing=0.1,
887
- row_heights=[0.6, 0.4],
888
- subplot_titles=('價格走勢', '威廉指標 %R'))
889
-
890
- # 上方:價格線
891
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
892
- line=dict(color='black', width=1)), row=1, col=1)
893
-
894
- # 下方:威廉指標
895
- fig.add_trace(go.Scatter(x=data.index, y=data['Williams_R'], mode='lines', name='威廉%R',
896
- line=dict(color='purple', width=2)), row=2, col=1)
897
-
898
- # 威廉指標參考線
899
- fig.add_hline(y=-20, line_dash="dash", line_color="red", annotation_text="超買線(-20)", row=2, col=1)
900
- fig.add_hline(y=-80, line_dash="dash", line_color="green", annotation_text="超賣線(-80)", row=2, col=1)
901
- fig.add_hline(y=-50, line_dash="dot", line_color="gray", annotation_text="中線(-50)", row=2, col=1)
902
-
903
- # 超買超賣區域
904
- fig.add_hrect(y0=-20, y1=0, fillcolor="red", opacity=0.1, row=2, col=1)
905
- fig.add_hrect(y0=-100, y1=-80, fillcolor="green", opacity=0.1, row=2, col=1)
906
-
907
- fig.update_layout(
908
- title=f'{stock_name} - 威廉指標 %R (14日)',
909
- height=500
910
- )
911
- fig.update_yaxes(range=[-100, 0], row=2, col=1)
912
-
913
  return fig
914
 
915
  # 更新成交量圖表
@@ -924,515 +506,16 @@ def update_volume_chart(selected_stock, period):
924
  return {}
925
 
926
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
927
-
928
  fig = px.bar(data, y='Volume', title=f'{stock_name} 成交量')
929
- fig.update_layout(
930
- xaxis_title='日期',
931
- yaxis_title='成交量',
932
- height=300
933
- )
934
-
935
- return fig
936
-
937
- # 更新產業分析圖表
938
- @app.callback(
939
- dash.dependencies.Output('industry-analysis', 'figure'),
940
- [dash.dependencies.Input('stock-dropdown', 'value')]
941
- )
942
- def update_industry_analysis(selected_stock):
943
- # 獲取多檔股票資料進行產業比較
944
- industry_data = []
945
-
946
- for symbol in list(TAIWAN_STOCKS.values())[:10]: # 取前10檔做示範
947
- data = get_stock_data(symbol, '1mo')
948
- if not data.empty:
949
- stock_name = [name for name, symbol_code in TAIWAN_STOCKS.items() if symbol_code == symbol][0]
950
- latest_price = data['Close'].iloc[-1]
951
- first_price = data['Close'].iloc[0]
952
- return_pct = ((latest_price - first_price) / first_price) * 100
953
-
954
- industry_data.append({
955
- '股票': stock_name,
956
- '代碼': symbol,
957
- '月報酬率(%)': return_pct,
958
- '產業': INDUSTRY_MAPPING.get(symbol, '其他')
959
- })
960
-
961
- if not industry_data:
962
- return {}
963
-
964
- df_industry = pd.DataFrame(industry_data)
965
-
966
- # 建立產業表現圓餅圖
967
- fig = px.pie(df_industry, values='月報酬率(%)', names='股票',
968
- title='各股票月報酬率比較',
969
- color_discrete_sequence=px.colors.qualitative.Set3)
970
-
971
- fig.update_layout(height=400)
972
- return fig
973
-
974
- # 新增:更新景氣燈號圖表
975
- @app.callback(
976
- dash.dependencies.Output('business-climate-chart', 'figure'),
977
- [dash.dependencies.Input('stock-dropdown', 'value')] # 雖然不會影響圖表,但需要觸發
978
- )
979
- def update_business_climate_chart(selected_stock):
980
- df = get_business_climate_data()
981
-
982
- if df.empty:
983
- # 如果沒有資料,顯示提示圖表
984
- fig = go.Figure()
985
- fig.add_annotation(
986
- x=0.5, y=0.5,
987
- text="無法載入景氣燈號資料<br>請確認 business_climate.csv 檔案是否存在",
988
- xref="paper", yref="paper",
989
- showarrow=False,
990
- font=dict(size=14)
991
- )
992
- fig.update_layout(
993
- title="台灣景氣燈號",
994
- height=300,
995
- showlegend=False
996
- )
997
- return fig
998
-
999
- # 定義燈號顏色
1000
- def get_light_color(score):
1001
- if score >= 32:
1002
- return 'red' # 紅燈
1003
- elif score >= 24:
1004
- return 'orange' # 黃紅燈
1005
- elif score >= 17:
1006
- return 'yellow' # 黃燈
1007
- elif score >= 10:
1008
- return 'lightgreen' # 黃藍燈
1009
- else:
1010
- return 'blue' # 藍燈
1011
-
1012
- # 為每個點設定顏色
1013
- colors = [get_light_color(score) for score in df['Index']]
1014
-
1015
- fig = go.Figure()
1016
-
1017
- fig.add_trace(go.Scatter(
1018
- x=df['Date'],
1019
- y=df['Index'],
1020
- mode='lines+markers',
1021
- name='景氣燈號',
1022
- line=dict(color='darkblue', width=2),
1023
- marker=dict(
1024
- size=8,
1025
- color=colors,
1026
- line=dict(width=2, color='darkblue')
1027
- )
1028
- ))
1029
-
1030
- # 添加燈號區間線
1031
- fig.add_hline(y=32, line_dash="dash", line_color="red", annotation_text="紅燈(32)")
1032
- fig.add_hline(y=24, line_dash="dash", line_color="orange", annotation_text="黃紅燈(24)")
1033
- fig.add_hline(y=17, line_dash="dash", line_color="yellow", annotation_text="黃燈(17)")
1034
- fig.add_hline(y=10, line_dash="dash", line_color="lightgreen", annotation_text="黃藍燈(10)")
1035
-
1036
- fig.update_layout(
1037
- title="台灣景氣燈號走勢",
1038
- xaxis_title='日期',
1039
- yaxis_title='燈號分數',
1040
- height=300,
1041
- yaxis=dict(range=[0, 40])
1042
- )
1043
-
1044
- return fig
1045
-
1046
- # 新增:更新分析師觀點
1047
- @app.callback(
1048
- [dash.dependencies.Output('technical-analysis-text', 'children'),
1049
- dash.dependencies.Output('fundamental-analysis-text', 'children'),
1050
- dash.dependencies.Output('market-outlook-text', 'children')],
1051
- [dash.dependencies.Input('stock-dropdown', 'value'),
1052
- dash.dependencies.Input('period-dropdown', 'value')]
1053
- )
1054
- def update_analysis_text(selected_stock, period):
1055
- # 獲取股票資料進行分析
1056
- data = get_stock_data(selected_stock, period)
1057
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
1058
-
1059
- if data.empty:
1060
- return "無法獲取資料進行分析", "無法獲取資料進行分析", "無法獲取資料進行分析"
1061
-
1062
- # 計算技術指標
1063
- data = calculate_technical_indicators(data)
1064
-
1065
- # 基本數據
1066
- current_price = data['Close'].iloc[-1]
1067
- price_change = ((current_price - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
1068
- volume_avg = data['Volume'].mean()
1069
- recent_volume = data['Volume'].iloc[-5:].mean()
1070
- rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
1071
-
1072
- # 新增技術指標數據
1073
- macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
1074
- macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
1075
- bb_position = data['BB_Position'].iloc[-1] if not pd.isna(data['BB_Position'].iloc[-1]) else 0.5
1076
- k_current = data['K'].iloc[-1] if not pd.isna(data['K'].iloc[-1]) else 50
1077
- d_current = data['D'].iloc[-1] if not pd.isna(data['D'].iloc[-1]) else 50
1078
-
1079
- # 技術面分析
1080
- technical_text = html.Div([
1081
- html.P([
1082
- html.Strong("價格趨勢:"),
1083
- f"近期{period}期間內,{stock_name}呈現",
1084
- html.Span(f"{'上漲' if price_change > 5 else '下跌' if price_change < -5 else '盤整'}",
1085
- style={'color': 'green' if price_change > 5 else 'red' if price_change < -5 else 'orange', 'font-weight': 'bold'}),
1086
- f"走勢,累計變動{price_change:+.1f}%。"
1087
- ]),
1088
- html.P([
1089
- html.Strong("RSI指標:"),
1090
- f"目前為{rsi_current:.1f},",
1091
- html.Span(
1092
- "處於超買區間" if rsi_current > 70 else "處於超賣區間" if rsi_current < 30 else "在正常範圍內",
1093
- style={'color': 'red' if rsi_current > 70 else 'green' if rsi_current < 30 else 'blue', 'font-weight': 'bold'}
1094
- ),
1095
- "。"
1096
- ]),
1097
- html.P([
1098
- html.Strong("MACD指標:"),
1099
- f"MACD線({macd_current:.3f})",
1100
- html.Span(
1101
- "高於" if macd_current > macd_signal_current else "低於",
1102
- style={'color': 'green' if macd_current > macd_signal_current else 'red', 'font-weight': 'bold'}
1103
- ),
1104
- f"信號線({macd_signal_current:.3f}),",
1105
- f"顯示{'多頭' if macd_current > macd_signal_current else '空頭'}格局。"
1106
- ]),
1107
- html.P([
1108
- html.Strong("布林通道:"),
1109
- f"股價位於通道",
1110
- html.Span(
1111
- "上半部" if bb_position > 0.8 else "下半部" if bb_position < 0.2 else "中段",
1112
- style={'color': 'red' if bb_position > 0.8 else 'green' if bb_position < 0.2 else 'blue', 'font-weight': 'bold'}
1113
- ),
1114
- f"({bb_position*100:.0f}%),",
1115
- f"{'壓力較大' if bb_position > 0.8 else '支撐較強' if bb_position < 0.2 else '整理格局'}。"
1116
- ]),
1117
- html.P([
1118
- html.Strong("KD指標:"),
1119
- f"K值({k_current:.1f})",
1120
- html.Span(
1121
- "高於" if k_current > d_current else "低於",
1122
- style={'color': 'green' if k_current > d_current else 'red', 'font-weight': 'bold'}
1123
- ),
1124
- f"D值({d_current:.1f}),",
1125
- html.Span(
1126
- "超買警戒" if k_current > 80 else "超賣關注" if k_current < 20 else "正常區間",
1127
- style={'color': 'red' if k_current > 80 else 'green' if k_current < 20 else 'blue', 'font-weight': 'bold'}
1128
- ),
1129
- "。"
1130
- ]),
1131
- html.P([
1132
- html.Strong("成交量分析:"),
1133
- f"近期成交量{'放大' if recent_volume > volume_avg * 1.2 else '萎縮' if recent_volume < volume_avg * 0.8 else '平穩'},",
1134
- f"顯示市場{'關注度提升' if recent_volume > volume_avg * 1.2 else '觀望氣氛濃厚' if recent_volume < volume_avg * 0.8 else '交投正常'}。"
1135
- ])
1136
- ])
1137
-
1138
- # 基本面分析
1139
- industry = INDUSTRY_MAPPING.get(selected_stock, '綜合')
1140
- if selected_stock == 'TXF=F':
1141
- fundamental_text = html.Div([
1142
- html.P([
1143
- html.Strong("總體經濟:"),
1144
- "台指期反映台股整體表現,當前需關注聯準會政策、國際貿易情勢及台灣出口動能。"
1145
- ]),
1146
- html.P([
1147
- html.Strong("產業輪動:"),
1148
- "觀察半導體、電子等權重產業表現,以及傳統產業復甦力道。"
1149
- ]),
1150
- html.P([
1151
- html.Strong("資金面:"),
1152
- "外資動向、匯率變化及市場流動性為主要觀察重點。"
1153
- ])
1154
- ])
1155
- else:
1156
- fundamental_text = html.Div([
1157
- html.P([
1158
- html.Strong("產業地位:"),
1159
- f"{stock_name}屬於{industry}產業,在產業鏈中具有",
1160
- html.Span("重要地位" if selected_stock in ['2330.TW', '2454.TW', '2317.TW'] else "一定影響力",
1161
- style={'font-weight': 'bold'}),
1162
- "。"
1163
- ]),
1164
- html.P([
1165
- html.Strong("營運展望:"),
1166
- f"考量{industry}產業前景及公司基本面,建議持續關注季報表現及未來指引。"
1167
- ]),
1168
- html.P([
1169
- html.Strong("風險評估:"),
1170
- "注意產業週期性變化、國際競爭及法規環境變化等風險因子。"
1171
- ])
1172
- ])
1173
-
1174
- # 市場展望
1175
- if price_change > 10:
1176
- outlook_tone = "謹慎樂觀"
1177
- outlook_color = "#28a745"
1178
- elif price_change < -10:
1179
- outlook_tone = "保守觀望"
1180
- outlook_color = "#dc3545"
1181
- else:
1182
- outlook_tone = "中性持平"
1183
- outlook_color = "#ffc107"
1184
-
1185
- market_outlook = html.Div([
1186
- html.P([
1187
- html.Strong("整體評估:", style={'font-size': '16px'}),
1188
- f"基於技術面及基本面分析,對{stock_name}採取",
1189
- html.Span(f"{outlook_tone}", style={'color': outlook_color, 'font-weight': 'bold', 'font-size': '16px'}),
1190
- "態度。"
1191
- ]),
1192
- html.P([
1193
- html.Strong("投資建議:"),
1194
- "建議投資人根據自身風險承受能力,採取適當的資產配置策略。短線操作注意技術指標,長線投資關注基本面變化。"
1195
- ]),
1196
- html.P([
1197
- html.Strong("風險提醒:"),
1198
- "股票投資具有風險,過去績效不代表未來表現,投資前請詳閱公開說明書並審慎評估。"
1199
- ], style={'font-style': 'italic', 'font-size': '13px'})
1200
- ])
1201
-
1202
- return technical_text, fundamental_text, market_outlook
1203
-
1204
- # 新增:更新PMI圖表
1205
- @app.callback(
1206
- dash.dependencies.Output('pmi-chart', 'figure'),
1207
- [dash.dependencies.Input('stock-dropdown', 'value')] # 雖然不會影響圖表,但需要觸發
1208
- )
1209
- def update_pmi_chart(selected_stock):
1210
- df = get_pmi_data()
1211
-
1212
- if df.empty:
1213
- # 如果沒有資料,顯示提示圖表
1214
- fig = go.Figure()
1215
- fig.add_annotation(
1216
- x=0.5, y=0.5,
1217
- text="無法載入PMI資料<br>請確認 taiwan_pmi.csv 檔案是否存在",
1218
- xref="paper", yref="paper",
1219
- showarrow=False,
1220
- font=dict(size=14)
1221
- )
1222
- fig.update_layout(
1223
- title="台灣PMI指數",
1224
- height=300,
1225
- showlegend=False
1226
- )
1227
- return fig
1228
-
1229
- # 定義PMI顏色 (50以上擴張,以下緊縮)
1230
- def get_pmi_color(value):
1231
- return 'green' if value >= 50 else 'red'
1232
-
1233
- colors = [get_pmi_color(value) for value in df['Index']]
1234
-
1235
- fig = go.Figure()
1236
-
1237
- fig.add_trace(go.Scatter(
1238
- x=df['Date'],
1239
- y=df['Index'],
1240
- mode='lines+markers',
1241
- name='PMI指數',
1242
- line=dict(color='darkblue', width=2),
1243
- marker=dict(
1244
- size=8,
1245
- color=colors,
1246
- line=dict(width=2, color='darkblue')
1247
- )
1248
- ))
1249
-
1250
- # 添加榮枯線
1251
- fig.add_hline(y=50, line_dash="dash", line_color="black", annotation_text="榮枯線(50)")
1252
-
1253
- # 添加背景色區域
1254
- fig.add_hrect(
1255
- y0=50, y1=60,
1256
- fillcolor="lightgreen", opacity=0.2,
1257
- annotation_text="擴張區間", annotation_position="top left"
1258
- )
1259
- fig.add_hrect(
1260
- y0=40, y1=50,
1261
- fillcolor="lightcoral", opacity=0.2,
1262
- annotation_text="緊縮區間", annotation_position="bottom left"
1263
- )
1264
-
1265
- fig.update_layout(
1266
- title="台灣PMI指數走勢",
1267
- xaxis_title='日期',
1268
- yaxis_title='PMI指數',
1269
- height=300,
1270
- yaxis=dict(range=[35, 60])
1271
- )
1272
-
1273
  return fig
1274
 
1275
- # 新增:多檔股票比較
1276
- @app.callback(
1277
- [dash.dependencies.Output('comparison-chart', 'figure'),
1278
- dash.dependencies.Output('comparison-table', 'children')],
1279
- [dash.dependencies.Input('comparison-stocks', 'value'),
1280
- dash.dependencies.Input('comparison-period', 'value')]
1281
- )
1282
- def update_comparison_analysis(selected_stocks, period):
1283
- if not selected_stocks:
1284
- return {}, html.Div("請選擇要比較的股票")
1285
-
1286
- # 限制最多5檔
1287
- selected_stocks = selected_stocks[:5]
1288
-
1289
- fig = go.Figure()
1290
- comparison_data = []
1291
-
1292
- for stock in selected_stocks:
1293
- data = get_stock_data(stock, period)
1294
- if not data.empty:
1295
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == stock][0]
1296
-
1297
- # 正規化價格(以期初為基準100)
1298
- normalized_prices = (data['Close'] / data['Close'].iloc[0]) * 100
1299
-
1300
- fig.add_trace(go.Scatter(
1301
- x=data.index,
1302
- y=normalized_prices,
1303
- mode='lines',
1304
- name=stock_name,
1305
- line=dict(width=2)
1306
- ))
1307
-
1308
- # 計算績效數據
1309
- total_return = ((data['Close'].iloc[-1] / data['Close'].iloc[0]) - 1) * 100
1310
- volatility = data['Close'].pct_change().std() * np.sqrt(252) * 100 # 年化波動率
1311
-
1312
- comparison_data.append({
1313
- 'name': stock_name,
1314
- 'return': total_return,
1315
- 'volatility': volatility,
1316
- 'current_price': data['Close'].iloc[-1]
1317
- })
1318
-
1319
- fig.update_layout(
1320
- title=f'股票績效比較 - {period}',
1321
- xaxis_title='日期',
1322
- yaxis_title='相對績效 (基期=100)',
1323
- height=400,
1324
- hovermode='x unified'
1325
- )
1326
-
1327
- # 建立比較表格
1328
- if comparison_data:
1329
- table_rows = []
1330
- for item in sorted(comparison_data, key=lambda x: x['return'], reverse=True):
1331
- color = 'green' if item['return'] > 0 else 'red'
1332
- table_rows.append(
1333
- html.Tr([
1334
- html.Td(item['name'], style={'font-weight': 'bold'}),
1335
- html.Td(f"{item['return']:+.1f}%", style={'color': color, 'font-weight': 'bold'}),
1336
- html.Td(f"{item['volatility']:.1f}%"),
1337
- html.Td(f"${item['current_price']:.2f}")
1338
- ])
1339
- )
1340
-
1341
- table = html.Table([
1342
- html.Thead([
1343
- html.Tr([
1344
- html.Th("股票", style={'text-align': 'center'}),
1345
- html.Th("報酬率", style={'text-align': 'center'}),
1346
- html.Th("波動率", style={'text-align': 'center'}),
1347
- html.Th("現價", style={'text-align': 'center'})
1348
- ])
1349
- ]),
1350
- html.Tbody(table_rows)
1351
- ], style={
1352
- 'width': '100%',
1353
- 'border-collapse': 'collapse',
1354
- 'font-size': '12px'
1355
- })
1356
-
1357
- return fig, table
1358
-
1359
- return fig, html.Div("無可比較資料")
1360
-
1361
- # 新增:市場情緒分析
1362
- @app.callback(
1363
- [dash.dependencies.Output('sentiment-gauge', 'children'),
1364
- dash.dependencies.Output('news-summary', 'children')],
1365
- [dash.dependencies.Input('stock-dropdown', 'value')]
1366
- )
1367
- def update_sentiment_analysis(selected_stock):
1368
- # 模擬情緒指標(實際應用中可接入新聞API或情緒分析服務)
1369
- sentiment_score = np.random.uniform(30, 80) # 模擬情緒分數 0-100
1370
-
1371
- # 建立情緒指標圓形圖
1372
- gauge_fig = go.Figure(go.Indicator(
1373
- mode = "gauge+number+delta",
1374
- value = sentiment_score,
1375
- domain = {'x': [0, 1], 'y': [0, 1]},
1376
- title = {'text': "市場情緒指數"},
1377
- delta = {'reference': 50},
1378
- gauge = {
1379
- 'axis': {'range': [None, 100]},
1380
- 'bar': {'color': "darkblue"},
1381
- 'steps': [
1382
- {'range': [0, 30], 'color': "lightcoral"},
1383
- {'range': [30, 70], 'color': "lightgray"},
1384
- {'range': [70, 100], 'color': "lightgreen"}
1385
- ],
1386
- 'threshold': {
1387
- 'line': {'color': "red", 'width': 4},
1388
- 'thickness': 0.75,
1389
- 'value': 90
1390
- }
1391
- }
1392
- ))
1393
-
1394
- gauge_fig.update_layout(height=200, margin=dict(l=20, r=20, t=40, b=20))
1395
-
1396
- # 模擬新聞摘要
1397
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
1398
-
1399
- news_items = [
1400
- f"📈 {stock_name}獲外資調升目標價,看好後續發展前景",
1401
- f"💼 法人預期{stock_name}下季營收將較上季成長5-10%",
1402
- f"🌐 國際市場波動對{stock_name}影響有限,基本面穩健",
1403
- f"⚡ 產業景氣回溫,{stock_name}受惠程度值得關注",
1404
- f"📊 技術面顯示{stock_name}突破關鍵壓力,短線偏多"
1405
- ]
1406
-
1407
- news_content = html.Div([
1408
- html.P(news, style={
1409
- 'margin': '8px 0',
1410
- 'padding': '8px',
1411
- 'background': '#e8f4f8',
1412
- 'border-radius': '5px',
1413
- 'border-left': '3px solid #17a2b8',
1414
- 'font-size': '13px'
1415
- }) for news in news_items[:3] # 顯示前3條
1416
- ])
1417
-
1418
- return dcc.Graph(figure=gauge_fig), news_content
1419
-
1420
- # 在 Colab 中執行的設定
1421
  if __name__ == '__main__':
1422
- # 在執行前先測試檔案讀取
1423
  print("測試檔案讀取...")
1424
  business_data = get_business_climate_data()
1425
  pmi_data = get_pmi_data()
1426
 
1427
- if not business_data.empty:
1428
- print(f"景氣燈號資料預覽:\n{business_data.head()}")
1429
- if not pmi_data.empty:
1430
- print(f"PMI資料預覽:\n{pmi_data.head()}")
1431
-
1432
- # 在 Colab 中需要使用以下方式啟動
1433
- app.run(host="0.0.0.0", port=7860, debug=True)
1434
-
1435
-
1436
-
1437
- # 如果在本地環境執行,使用以下方式
1438
- # app.run_server(debug=True)
 
15
  from plotly.subplots import make_subplots
16
 
17
 
18
+ # 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
 
 
 
 
19
  TAIWAN_STOCKS = {
 
20
  '台積電': '2330.TW',
21
  '聯發科': '2454.TW',
22
  '鴻海': '2317.TW',
 
75
  except:
76
  return pd.DataFrame()
77
 
 
 
 
 
 
 
 
 
78
  def simple_lstm_predict(data, predict_days=5):
79
  """簡化的LSTM預測模型 (使用統計方法模擬)"""
80
  if len(data) < 60:
 
236
  app.layout = html.Div([
237
  html.H1("台股分析儀表板", style={'text-align': 'center', 'margin-bottom': '30px'}),
238
 
239
+ # 台指期獨立預測區塊
240
+ html.Div([
241
+ html.H2("🤖 AI深度學習預測 - 台指期指數", style={
242
+ 'text-align': 'center',
243
+ 'color': '#fff',
244
+ 'margin-bottom': '25px',
245
+ 'font-size': '28px',
246
+ 'text-shadow': '2px 2px 4px rgba(0,0,0,0.3)'
247
+ }),
248
+
249
+ html.Div([
250
+ # 左側:預測控制面板
251
+ html.Div([
252
+ html.Label("預測期間:", style={'font-weight': 'bold', 'color': '#fff', 'font-size': '16px'}),
253
+ dcc.Dropdown(
254
+ id='taiex-prediction-period',
255
+ options=[
256
+ {'label': '5日後預測', 'value': 5},
257
+ {'label': '10日後預測', 'value': 10},
258
+ {'label': '20日後預測', 'value': 20},
259
+ {'label': '60日後預測', 'value': 60}
260
+ ],
261
+ value=5,
262
+ style={'margin-bottom': '15px'}
263
+ ),
264
+ html.Div(id='taiex-prediction-results')
265
+ ], style={'width': '35%', 'display': 'inline-block', 'vertical-align': 'top'}),
266
+
267
+ # 右側:台指期基本資訊
268
+ html.Div([
269
+ html.Div(id='taiex-info-card')
270
+ ], style={'width': '60%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'})
271
+ ]),
272
+
273
+ # 預測圖表區域
274
+ html.Div([
275
+ dcc.Graph(id='taiex-prediction-chart')
276
+ ], style={'margin-top': '25px'})
277
+
278
+ ], style={
279
+ 'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
280
+ 'padding': '30px',
281
+ 'border-radius': '15px',
282
+ 'box-shadow': '0 8px 32px rgba(0,0,0,0.2)',
283
+ 'color': 'white',
284
+ 'margin-bottom': '40px',
285
+ 'border': '2px solid rgba(255,255,255,0.1)'
286
+ }),
287
+
288
+ # 原有的控制面板 (移除台指期選項)
289
  html.Div([
290
  html.Div([
291
  html.Label("選擇股票:"),
292
  dcc.Dropdown(
293
  id='stock-dropdown',
294
  options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
295
+ value='2330.TW', # 預設改為台積電
296
  style={'margin-bottom': '10px'}
297
  )
298
  ], style={'width': '30%', 'display': 'inline-block', 'vertical-align': 'top'}),
 
330
  # 股價資訊卡片
331
  html.Div(id='stock-info-cards', style={'margin-bottom': '30px'}),
332
 
333
+ # 其餘區域簡化版本
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  html.Div([
335
+ dcc.Graph(id='price-chart')
336
+ ], style={'margin-bottom': '20px'}),
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
337
 
 
338
  html.Div([
339
  dcc.Graph(id='volume-chart')
340
+ ])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
  ])
342
 
343
+ # 台指期獨立預測回調函數
344
  @app.callback(
345
+ [dash.dependencies.Output('taiex-prediction-results', 'children'),
346
+ dash.dependencies.Output('taiex-prediction-chart', 'figure'),
347
+ dash.dependencies.Output('taiex-info-card', 'children')],
348
+ [dash.dependencies.Input('taiex-prediction-period', 'value')]
349
  )
350
+ def update_taiex_prediction(predict_days):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
351
  # 獲取台指期歷史資料
352
+ data = get_stock_data('^TWII', '1y')
353
  if data.empty:
354
+ data = get_stock_data('TXF=F', '1y')
355
+ if data.empty:
356
+ data = get_stock_data('0050.TW', '1y')
357
+
358
+ if data.empty:
359
+ return html.Div("無法獲取台指期資料"), {}, html.Div("資料載入失敗")
360
 
361
  # 執行預測
362
  prediction = simple_lstm_predict(data, predict_days)
363
  if prediction is None:
364
+ return html.Div("資料不足,無法進行預測"), {}, html.Div("預測失敗")
365
 
366
  current_price = data['Close'].iloc[-1]
367
  predicted_price = prediction['predicted_price']
 
373
  arrow = '📈' if change_pct >= 0 else '📉'
374
 
375
  result_card = html.Div([
376
+ html.H4(f"{predict_days}日後 AI 預測", style={'margin': '0 0 15px 0', 'color': 'white'}),
377
  html.Div([
378
  html.Span(f"{arrow} ", style={'font-size': '24px'}),
379
+ html.Span(f"{change_pct:+.2f}%", style={'font-size': '28px', 'font-weight': 'bold', 'color': color})
380
+ ]),
381
+ html.P(f"預測價格: {predicted_price:.0f} 點"),
382
+ html.P(f"信心度: {confidence:.1%}")
 
 
 
 
 
383
  ], style={
384
+ 'background': 'rgba(255,255,255,0.15)',
385
  'padding': '20px',
386
+ 'border-radius': '12px',
387
+ 'border': '2px solid rgba(255,255,255,0.2)'
388
  })
389
 
390
+ # 台指期資訊卡片
391
+ prev_price = data['Close'].iloc[-2] if len(data) > 1 else current_price
392
+ daily_change = current_price - prev_price
393
+ daily_change_pct = (daily_change / prev_price) * 100
394
+
395
+ info_card = html.Div([
396
+ html.H3("台指期現況", style={'color': 'white', 'margin-bottom': '15px'}),
397
+ html.Div([
398
+ html.H4(f"{current_price:.0f} 點", style={'color': 'white', 'margin': '0'}),
399
+ html.P(f"{'▲' if daily_change >= 0 else '▼'} {daily_change:+.2f} ({daily_change_pct:+.2f}%)",
400
+ style={'color': '#00C851' if daily_change >= 0 else '#FF4444', 'font-weight': 'bold'})
401
+ ])
402
+ ], style={
403
+ 'background': 'rgba(255,255,255,0.15)',
404
+ 'padding': '20px',
405
+ 'border-radius': '12px',
406
+ 'border': '2px solid rgba(255,255,255,0.2)'
407
+ })
408
 
409
+ # 建立預測圖表
410
+ fig = go.Figure()
411
+
412
+ # 歷史價格
413
  recent_data = data.tail(30)
414
  fig.add_trace(go.Scatter(
415
  x=recent_data.index,
 
432
 
433
  fig.update_layout(
434
  title=f'台指期 {predict_days}日預測走勢',
 
 
435
  height=350,
436
  plot_bgcolor='rgba(0,0,0,0)',
437
  paper_bgcolor='rgba(0,0,0,0)',
438
  font=dict(color='white')
439
  )
440
 
441
+ return result_card, fig, info_card
442
 
443
  # 更新股價資訊卡片
444
  @app.callback(
 
455
  change = current_price - prev_price
456
  change_pct = (change / prev_price) * 100
457
 
 
458
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
 
459
  color = 'green' if change >= 0 else 'red'
460
 
461
  return html.Div([
462
+ html.H3(f"{stock_name} ({selected_stock})"),
463
+ html.H2(f"${current_price:.2f}", style={'color': color}),
464
+ html.P(f"{'▲' if change >= 0 else '▼'} {change:+.2f} ({change_pct:+.2f}%)", style={'color': color})
465
+ ], style={'background': 'white', 'padding': '20px', 'border-radius': '10px'})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
466
 
467
  # 更新股價圖表
468
  @app.callback(
 
491
  else:
492
  fig = px.line(data, y='Close', title=f'{stock_name} 股價走勢')
493
 
494
+ fig.update_layout(title=f'{stock_name} 股價走勢', height=400)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
495
  return fig
496
 
497
  # 更新成交量圖表
 
506
  return {}
507
 
508
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
 
509
  fig = px.bar(data, y='Volume', title=f'{stock_name} 成交量')
510
+ fig.update_layout(height=300)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
511
  return fig
512
 
513
+ # 執行應用程式
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
514
  if __name__ == '__main__':
515
+ # 測試檔案讀取
516
  print("測試檔案讀取...")
517
  business_data = get_business_climate_data()
518
  pmi_data = get_pmi_data()
519
 
520
+ # Hugging Face Spaces 中執行
521
+ app.run(host="0.0.0.0", port=7860, debug=False)