AlanRex commited on
Commit
e13730c
·
verified ·
1 Parent(s): 11bbb97

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +88 -912
app.py CHANGED
@@ -19,9 +19,8 @@ from plotly.subplots import make_subplots
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',
@@ -249,14 +248,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,9 +342,6 @@ 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
  # 左側:股價走勢圖和技術指標
@@ -503,936 +548,67 @@ app.layout = html.Div([
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']
571
  change_pct = prediction['change_pct']
572
  confidence = prediction['confidence']
573
 
 
 
 
 
 
 
 
 
574
  # 預測結果卡片
575
  color = '#00C851' if change_pct >= 0 else '#FF4444'
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,
605
- y=recent_data['Close'],
606
- mode='lines',
607
- name='歷史價格',
608
- line=dict(color='#FFA726', width=2)
609
- ))
610
-
611
- # 預測點
612
- future_date = recent_data.index[-1] + timedelta(days=predict_days)
613
- fig.add_trace(go.Scatter(
614
- x=[recent_data.index[-1], future_date],
615
- y=[current_price, predicted_price],
616
- mode='lines+markers',
617
- name=f'{predict_days}日預測',
618
- line=dict(color=color, width=3, dash='dash'),
619
- marker=dict(size=8)
620
- ))
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(
636
- dash.dependencies.Output('stock-info-cards', 'children'),
637
- [dash.dependencies.Input('stock-dropdown', 'value')]
638
- )
639
- def update_stock_info(selected_stock):
640
- data = get_stock_data(selected_stock, '5d')
641
- if data.empty:
642
- return html.Div("無法獲取股票資料")
643
-
644
- current_price = data['Close'].iloc[-1]
645
- prev_price = data['Close'].iloc[-2] if len(data) > 1 else current_price
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(
685
- dash.dependencies.Output('price-chart', 'figure'),
686
- [dash.dependencies.Input('stock-dropdown', 'value'),
687
- dash.dependencies.Input('period-dropdown', 'value'),
688
- dash.dependencies.Input('chart-type', 'value')]
689
- )
690
- def update_price_chart(selected_stock, period, chart_type):
691
- data = get_stock_data(selected_stock, period)
692
- if data.empty:
693
- return {}
694
-
695
- data = calculate_technical_indicators(data)
696
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
697
-
698
- if chart_type == 'candlestick':
699
- fig = go.Figure(data=go.Candlestick(
700
- x=data.index,
701
- open=data['Open'],
702
- high=data['High'],
703
- low=data['Low'],
704
- close=data['Close'],
705
- name=stock_name
706
- ))
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
- # 更新成交量圖表
916
- @app.callback(
917
- dash.dependencies.Output('volume-chart', 'figure'),
918
- [dash.dependencies.Input('stock-dropdown', 'value'),
919
- dash.dependencies.Input('period-dropdown', 'value')]
920
- )
921
- def update_volume_chart(selected_stock, period):
922
- data = get_stock_data(selected_stock, period)
923
- if data.empty:
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)
 
19
  app = dash.Dash(__name__, suppress_callback_exceptions=True)
20
 
21
 
22
+ # 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
23
  TAIWAN_STOCKS = {
 
24
  '台積電': '2330.TW',
25
  '聯發科': '2454.TW',
26
  '鴻海': '2317.TW',
 
248
  app.layout = html.Div([
249
  html.H1("台股分析儀表板", style={'text-align': 'center', 'margin-bottom': '30px'}),
250
 
251
+ # 【新增】台指期獨立預測區塊 - 置於最上方顯眼位置
252
+ html.Div([
253
+ html.H2("🤖 AI深度學習預測 - 台指期指數", style={
254
+ 'text-align': 'center',
255
+ 'color': '#fff',
256
+ 'margin-bottom': '25px',
257
+ 'font-size': '28px',
258
+ 'text-shadow': '2px 2px 4px rgba(0,0,0,0.3)'
259
+ }),
260
+
261
+ html.Div([
262
+ # 左側:預測控制面板
263
+ html.Div([
264
+ html.Label("預測期間:", style={'font-weight': 'bold', 'color': '#fff', 'font-size': '16px'}),
265
+ dcc.Dropdown(
266
+ id='taiex-prediction-period',
267
+ options=[
268
+ {'label': '5日後預測', 'value': 5},
269
+ {'label': '10日後預測', 'value': 10},
270
+ {'label': '20日後預測', 'value': 20},
271
+ {'label': '60日後預測', 'value': 60}
272
+ ],
273
+ value=5,
274
+ style={'margin-bottom': '15px'}
275
+ ),
276
+ html.Div(id='taiex-prediction-results')
277
+ ], style={'width': '35%', 'display': 'inline-block', 'vertical-align': 'top'}),
278
+
279
+ # 右側:台指期基本資訊
280
+ html.Div([
281
+ html.Div(id='taiex-info-card')
282
+ ], style={'width': '60%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'})
283
+ ]),
284
+
285
+ # 預測圖表區域
286
+ html.Div([
287
+ dcc.Graph(id='taiex-prediction-chart')
288
+ ], style={'margin-top': '25px'})
289
+
290
+ ], style={
291
+ 'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
292
+ 'padding': '30px',
293
+ 'border-radius': '15px',
294
+ 'box-shadow': '0 8px 32px rgba(0,0,0,0.2)',
295
+ 'color': 'white',
296
+ 'margin-bottom': '40px',
297
+ 'border': '2px solid rgba(255,255,255,0.1)'
298
+ }),
299
+
300
+ # 原有的控制面板 (移除台指期選項)
301
  html.Div([
302
  html.Div([
303
  html.Label("選擇股票:"),
304
  dcc.Dropdown(
305
  id='stock-dropdown',
306
  options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
307
+ value='2330.TW', # 預設改為台積電
308
  style={'margin-bottom': '10px'}
309
  )
310
  ], style={'width': '30%', 'display': 'inline-block', 'vertical-align': 'top'}),
 
342
  # 股價資訊卡片
343
  html.Div(id='stock-info-cards', style={'margin-bottom': '30px'}),
344
 
 
 
 
345
  # 主要圖表區域
346
  html.Div([
347
  # 左側:股價走勢圖和技術指標
 
548
  })
549
  ])
550
 
551
+ # 【新增】台指期獨立預測區塊的回調函數
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
552
  @app.callback(
553
+ [dash.dependencies.Output('taiex-prediction-results', 'children'),
554
+ dash.dependencies.Output('taiex-prediction-chart', 'figure'),
555
+ dash.dependencies.Output('taiex-info-card', 'children')],
556
+ [dash.dependencies.Input('taiex-prediction-period', 'value')]
557
  )
558
+ def update_taiex_prediction(predict_days):
 
 
 
559
  # 獲取台指期歷史資料
560
  data = get_stock_data('^TWII', '2y')
561
  if data.empty:
562
+ # 嘗試替代資料源
563
+ data = get_stock_data('TXF=F', '2y')
564
+ if data.empty:
565
+ data = get_stock_data('0050.TW', '2y')
566
+
567
+ if data.empty:
568
+ return html.Div("無法獲取台指期資料"), {}, html.Div("資料載入失敗")
569
 
570
  # 執行預測
571
  prediction = simple_lstm_predict(data, predict_days)
572
  if prediction is None:
573
+ return html.Div("資料不足,無法進行預測"), {}, html.Div("預測失敗")
574
 
575
  current_price = data['Close'].iloc[-1]
576
  predicted_price = prediction['predicted_price']
577
  change_pct = prediction['change_pct']
578
  confidence = prediction['confidence']
579
 
580
+ # 計算一些額外統計資料
581
+ prev_price = data['Close'].iloc[-2] if len(data) > 1 else current_price
582
+ daily_change = current_price - prev_price
583
+ daily_change_pct = (daily_change / prev_price) * 100
584
+ high_today = data['High'].iloc[-1]
585
+ low_today = data['Low'].iloc[-1]
586
+ volume_today = data['Volume'].iloc[-1]
587
+
588
  # 預測結果卡片
589
  color = '#00C851' if change_pct >= 0 else '#FF4444'
590
  arrow = '📈' if change_pct >= 0 else '📉'
591
 
592
  result_card = html.Div([
593
+ html.H4(f"{predict_days}日後 AI 預測", style={'margin': '0 0 15px 0', 'color': 'white', 'font-size': '20px'}),
594
  html.Div([
595
+ html.Span(f"{arrow} ", style={'font-size': '30px'}),
596
  html.Span(f"{change_pct:+.2f}%", style={
597
+ 'font-size': '32px',
598
  'font-weight': 'bold',
599
+ 'color': color,
600
+ 'text-shadow': '2px 2px 4px rgba(0,0,0,0.3)'
601
  })
602
+ ], style={'margin': '15px 0'}),
603
+ html.P(f"預測價格: {predicted_price:.0f}", style={'margin': '8px 0', 'font-size': '16px'}),
604
+ html.P(f"信心度: {confidence:.1%}", style={'margin': '8px 0', 'font-size': '14px'}),
605
+ html.P(f"預測模型: LSTM深度學習", style={'margin': '8px 0', 'font-size': '12px', 'font-style': 'italic'})
606
  ], style={
607
+ 'background': 'rgba(255,255,255,0.15)',
608
  'padding': '20px',
609
+ 'border-radius': '12px',
610
+ 'border': '2px solid rgba(255,255,255,0.2)',
611
+ 'backdrop-filter': 'blur(10px)'
612
  })
613
 
614
+ #