AlanRex commited on
Commit
9282bf7
·
verified ·
1 Parent(s): c2c2994

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +8 -1472
app.py CHANGED
@@ -256,7 +256,6 @@ app.layout = html.Div([
256
  dcc.Dropdown(
257
  id='taiex-prediction-period',
258
  options=[
259
- {'label': '1日後預測', 'value': 1},
260
  {'label': '5日後預測', 'value': 5},
261
  {'label': '10日後預測', 'value': 10},
262
  {'label': '20日後預測', 'value': 20},
@@ -592,1480 +591,17 @@ def update_taiex_prediction(predict_days):
592
  line=dict(color='#FFA726', width=2)
593
  ))
594
 
595
- # --- 關鍵修正從這裡開始 ---
596
- # 定義所有要顯示的預測天數點
597
- all_predict_days = [1, 5, 10, 20, 60]
598
-
599
- # 過濾出所有小於或等於使用者選擇的預測天數
600
- points_to_show = [d for d in all_predict_days if d <= predict_days]
601
-
602
- # 為每個要顯示的預測點創建圖表軌跡
603
- for d in points_to_show:
604
- # 重新計算每個點的預測值
605
- point_prediction = simple_lstm_predict(data, d)
606
- if point_prediction:
607
- point_predicted_price = point_prediction['predicted_price']
608
- point_future_date = recent_data.index[-1] + timedelta(days=d)
609
-
610
- # 決定點的顏色
611
- point_color = '#00C851' if point_predicted_price >= current_price else '#FF4444'
612
-
613
- # 添加預測點
614
- fig.add_trace(go.Scatter(
615
- x=[point_future_date],
616
- y=[point_predicted_price],
617
- mode='markers',
618
- name=f'{d}日預測點',
619
- marker=dict(size=10, color=point_color)
620
- ))
621
-
622
- # 為使用者選擇的最終天數添加趨勢線
623
- final_future_date = recent_data.index[-1] + timedelta(days=predict_days)
624
- fig.add_trace(go.Scatter(
625
- x=[recent_data.index[-1], final_future_date],
626
- y=[current_price, predicted_price],
627
- mode='lines',
628
- name='最終預測線',
629
- line=dict(color=color, width=3, dash='dash')
630
- ))
631
-
632
- # --- 修正結束 ---
633
-
634
- fig.update_layout(
635
- title=f'台指期 {predict_days}日預測走勢',
636
- xaxis_title='日期',
637
- yaxis_title='指數點位',
638
- height=350,
639
- plot_bgcolor='rgba(0,0,0,0)',
640
- paper_bgcolor='rgba(0,0,0,0)',
641
- font=dict(color='white')
642
- )
643
-
644
- return result_card, fig
645
-
646
- # 更新股價資訊卡片
647
- @app.callback(
648
- dash.dependencies.Output('stock-info-cards', 'children'),
649
- [dash.dependencies.Input('stock-dropdown', 'value')]
650
- )
651
- def update_stock_info(selected_stock):
652
- data = get_stock_data(selected_stock, '5d')
653
- if data.empty:
654
- return html.Div("無法獲取股票資料")
655
-
656
- current_price = data['Close'].iloc[-1]
657
- prev_price = data['Close'].iloc[-2] if len(data) > 1 else current_price
658
- change = current_price - prev_price
659
- change_pct = (change / prev_price) * 100
660
-
661
- # 找出股票中文名稱
662
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
663
-
664
- color = 'green' if change >= 0 else 'red'
665
-
666
- return html.Div([
667
- html.Div([
668
- html.H3(f"{stock_name} ({selected_stock})", style={'margin': '0'}),
669
- html.H2(f"${current_price:.2f}", style={'margin': '5px 0', 'color': color}),
670
- html.P(f"{'▲' if change >= 0 else '▼'} {change:+.2f} ({change_pct:+.2f}%)",
671
- style={'margin': '0', 'color': color, 'font-weight': 'bold'})
672
- ], style={
673
- 'background': 'white',
674
- 'padding': '20px',
675
- 'border-radius': '10px',
676
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)',
677
- 'display': 'inline-block',
678
- 'margin-right': '20px'
679
- }),
680
-
681
- html.Div([
682
- html.H4("今日統計", style={'margin': '0 0 10px 0'}),
683
- html.P(f"最高: ${data['High'].iloc[-1]:.2f}", style={'margin': '5px 0'}),
684
- html.P(f"最低: ${data['Low'].iloc[-1]:.2f}", style={'margin': '5px 0'}),
685
- html.P(f"成交量: {data['Volume'].iloc[-1]:,.0f}", style={'margin': '5px 0'})
686
- ], style={
687
- 'background': 'white',
688
- 'padding': '20px',
689
- 'border-radius': '10px',
690
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)',
691
- 'display': 'inline-block'
692
- })
693
- ])
694
-
695
- # 更新股價圖表
696
- @app.callback(
697
- dash.dependencies.Output('price-chart', 'figure'),
698
- [dash.dependencies.Input('stock-dropdown', 'value'),
699
- dash.dependencies.Input('period-dropdown', 'value'),
700
- dash.dependencies.Input('chart-type', 'value')]
701
- )
702
- def update_price_chart(selected_stock, period, chart_type):
703
- data = get_stock_data(selected_stock, period)
704
- if data.empty:
705
- return {}
706
-
707
- data = calculate_technical_indicators(data)
708
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
709
-
710
- if chart_type == 'candlestick':
711
- fig = go.Figure(data=go.Candlestick(
712
- x=data.index,
713
- open=data['Open'],
714
- high=data['High'],
715
- low=data['Low'],
716
- close=data['Close'],
717
- name=stock_name
718
- ))
719
- else:
720
- fig = px.line(data, y='Close', title=f'{stock_name} 股價走勢')
721
-
722
- # 添加移動平均線
723
- fig.add_trace(go.Scatter(x=data.index, y=data['MA5'], mode='lines', name='MA5', line=dict(color='orange')))
724
- fig.add_trace(go.Scatter(x=data.index, y=data['MA20'], mode='lines', name='MA20', line=dict(color='blue')))
725
-
726
- fig.update_layout(
727
- title=f'{stock_name} 股價走勢',
728
- xaxis_title='日期',
729
- yaxis_title='價格 (TWD)',
730
- height=400
731
- )
732
-
733
- return fig
734
-
735
- # 更新RSI圖表(保持兼容性)
736
- @app.callback(
737
- dash.dependencies.Output('rsi-chart', 'figure'),
738
- [dash.dependencies.Input('stock-dropdown', 'value'),
739
- dash.dependencies.Input('period-dropdown', 'value')]
740
- )
741
- def update_rsi_chart(selected_stock, period):
742
- data = get_stock_data(selected_stock, period)
743
- if data.empty:
744
- return {}
745
-
746
- data = calculate_technical_indicators(data)
747
-
748
- fig = go.Figure()
749
- fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
750
- fig.add_hline(y=70, line_dash="dash", line_color="red", annotation_text="超買線(70)")
751
- fig.add_hline(y=30, line_dash="dash", line_color="green", annotation_text="超賣線(30)")
752
- fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)")
753
-
754
- # 添加超買超賣區域背景
755
- fig.add_hrect(y0=70, y1=100, fillcolor="red", opacity=0.1, annotation_text="超買區")
756
- fig.add_hrect(y0=0, y1=30, fillcolor="green", opacity=0.1, annotation_text="超賣區")
757
-
758
- fig.update_layout(
759
- title='RSI 相對強弱指標',
760
- xaxis_title='日期',
761
- yaxis_title='RSI',
762
- height=400,
763
- yaxis=dict(range=[0, 100])
764
- )
765
-
766
- return fig
767
-
768
- # 新增:進階技術指標圖表
769
- @app.callback(
770
- dash.dependencies.Output('advanced-technical-chart', 'figure'),
771
- [dash.dependencies.Input('technical-indicator-selector', 'value'),
772
- dash.dependencies.Input('stock-dropdown', 'value'),
773
- dash.dependencies.Input('period-dropdown', 'value')]
774
- )
775
- def update_advanced_technical_chart(indicator, selected_stock, period):
776
- data = get_stock_data(selected_stock, period)
777
- if data.empty:
778
- return {}
779
-
780
- data = calculate_technical_indicators(data)
781
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
782
-
783
- if indicator == 'RSI':
784
- fig = go.Figure()
785
- fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
786
- fig.add_hline(y=70, line_dash="dash", line_color="red", annotation_text="超買線(70)")
787
- fig.add_hline(y=30, line_dash="dash", line_color="green", annotation_text="超賣線(30)")
788
- fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)")
789
-
790
- fig.add_hrect(y0=70, y1=100, fillcolor="red", opacity=0.1)
791
- fig.add_hrect(y0=0, y1=30, fillcolor="green", opacity=0.1)
792
-
793
- fig.update_layout(
794
- title=f'{stock_name} - RSI 相對強弱指標',
795
- xaxis_title='日期',
796
- yaxis_title='RSI',
797
- height=450,
798
- yaxis=dict(range=[0, 100])
799
- )
800
-
801
- elif indicator == 'MACD':
802
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
803
- vertical_spacing=0.1,
804
- row_heights=[0.7, 0.3],
805
- subplot_titles=('價格與MACD線', 'MACD柱狀圖'))
806
-
807
- # 上方:價格線
808
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
809
- line=dict(color='black', width=1)), row=1, col=1)
810
-
811
- # MACD線和信號線
812
- fig.add_trace(go.Scatter(x=data.index, y=data['MACD'], mode='lines', name='MACD',
813
- line=dict(color='blue', width=2)), row=1, col=1)
814
- fig.add_trace(go.Scatter(x=data.index, y=data['MACD_Signal'], mode='lines', name='信號線',
815
- line=dict(color='red', width=2)), row=1, col=1)
816
-
817
- # 下方:MACD柱狀圖
818
- colors = ['green' if x >= 0 else 'red' for x in data['MACD_Histogram']]
819
- fig.add_trace(go.Bar(x=data.index, y=data['MACD_Histogram'], name='MACD柱狀圖',
820
- marker_color=colors), row=2, col=1)
821
-
822
- fig.add_hline(y=0, line_dash="dash", line_color="gray", row=1, col=1)
823
- fig.add_hline(y=0, line_dash="dash", line_color="gray", row=2, col=1)
824
-
825
- fig.update_layout(
826
- title=f'{stock_name} - MACD 指數平滑異同移動平均線',
827
- height=500
828
- )
829
-
830
- elif indicator == 'BB':
831
- fig = go.Figure()
832
-
833
- # 價格線
834
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
835
- line=dict(color='black', width=2)))
836
-
837
- # 布林通道上���
838
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines', name='上軌',
839
- line=dict(color='red', width=1, dash='dash')))
840
-
841
- # 布林通道中軌
842
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Middle'], mode='lines', name='中軌(MA20)',
843
- line=dict(color='blue', width=1)))
844
-
845
- # 布林通道下軌
846
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines', name='下軌',
847
- line=dict(color='green', width=1, dash='dash')))
848
-
849
- # 填充通道區域
850
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines',
851
- line=dict(color='rgba(0,0,0,0)'), showlegend=False))
852
- fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines',
853
- fill='tonexty', fillcolor='rgba(173,216,230,0.2)',
854
- line=dict(color='rgba(0,0,0,0)'), name='布林通道', showlegend=False))
855
-
856
- fig.update_layout(
857
- title=f'{stock_name} - 布林通道 (20日, 2σ)',
858
- xaxis_title='日期',
859
- yaxis_title='價格 (TWD)',
860
- height=450
861
- )
862
-
863
- elif indicator == 'KD':
864
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
865
- vertical_spacing=0.1,
866
- row_heights=[0.6, 0.4],
867
- subplot_titles=('價格走勢', 'KD指標'))
868
-
869
- # 上方:價格線
870
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
871
- line=dict(color='black', width=1)), row=1, col=1)
872
-
873
- # 下方:KD線
874
- fig.add_trace(go.Scatter(x=data.index, y=data['K'], mode='lines', name='K線',
875
- line=dict(color='blue', width=2)), row=2, col=1)
876
- fig.add_trace(go.Scatter(x=data.index, y=data['D'], mode='lines', name='D線',
877
- line=dict(color='red', width=2)), row=2, col=1)
878
-
879
- # KD指標參考線
880
- fig.add_hline(y=80, line_dash="dash", line_color="red", annotation_text="超買線(80)", row=2, col=1)
881
- fig.add_hline(y=20, line_dash="dash", line_color="green", annotation_text="超賣線(20)", row=2, col=1)
882
- fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)", row=2, col=1)
883
-
884
- # 超買超賣區域
885
- fig.add_hrect(y0=80, y1=100, fillcolor="red", opacity=0.1, row=2, col=1)
886
- fig.add_hrect(y0=0, y1=20, fillcolor="green", opacity=0.1, row=2, col=1)
887
-
888
- fig.update_layout(
889
- title=f'{stock_name} - KD 隨機指標 (9,3,3)',
890
- height=500
891
- )
892
- fig.update_yaxes(range=[0, 100], row=2, col=1)
893
-
894
- elif indicator == 'WR':
895
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
896
- vertical_spacing=0.1,
897
- row_heights=[0.6, 0.4],
898
- subplot_titles=('價格走勢', '威廉指標 %R'))
899
-
900
- # 上方:價格線
901
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
902
- line=dict(color='black', width=1)), row=1, col=1)
903
-
904
- # 下方:威廉指標
905
- fig.add_trace(go.Scatter(x=data.index, y=data['Williams_R'], mode='lines', name='威廉%R',
906
- line=dict(color='purple', width=2)), row=2, col=1)
907
-
908
- # 威廉指標參考線
909
- fig.add_hline(y=-20, line_dash="dash", line_color="red", annotation_text="超買線(-20)", row=2, col=1)
910
- fig.add_hline(y=-80, line_dash="dash", line_color="green", annotation_text="超賣線(-80)", row=2, col=1)
911
- fig.add_hline(y=-50, line_dash="dot", line_color="gray", annotation_text="中線(-50)", row=2, col=1)
912
-
913
- # 超買超賣區域
914
- fig.add_hrect(y0=-20, y1=0, fillcolor="red", opacity=0.1, row=2, col=1)
915
- fig.add_hrect(y0=-100, y1=-80, fillcolor="green", opacity=0.1, row=2, col=1)
916
-
917
- fig.update_layout(
918
- title=f'{stock_name} - 威廉指標 %R (14日)',
919
- height=500
920
- )
921
- fig.update_yaxes(range=[-100, 0], row=2, col=1)
922
-
923
- return fig
924
-
925
- # 更新成交量圖表
926
- @app.callback(
927
- dash.dependencies.Output('volume-chart', 'figure'),
928
- [dash.dependencies.Input('stock-dropdown', 'value'),
929
- dash.dependencies.Input('period-dropdown', 'value')]
930
- )
931
- def update_volume_chart(selected_stock, period):
932
- data = get_stock_data(selected_stock, period)
933
- if data.empty:
934
- return {}
935
-
936
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
937
-
938
- fig = px.bar(data, y='Volume', title=f'{stock_name} 成交量')
939
- fig.update_layout(
940
- xaxis_title='日期',
941
- yaxis_title='成交量',
942
- height=300
943
- )
944
-
945
- return fig
946
-
947
- # 更新產業分析圖表
948
- @app.callback(
949
- dash.dependencies.Output('industry-analysis', 'figure'),
950
- [dash.dependencies.Input('stock-dropdown', 'value')]
951
- )
952
- def update_industry_analysis(selected_stock):
953
- # 獲取多檔股票資料進行產業比較
954
- industry_data = []
955
-
956
- for symbol in list(TAIWAN_STOCKS.values())[:10]: # 取前10檔做示範
957
- data = get_stock_data(symbol, '1mo')
958
- if not data.empty:
959
- stock_name = [name for name, symbol_code in TAIWAN_STOCKS.items() if symbol_code == symbol][0]
960
- latest_price = data['Close'].iloc[-1]
961
- first_price = data['Close'].iloc[0]
962
- return_pct = ((latest_price - first_price) / first_price) * 100
963
-
964
- industry_data.append({
965
- '股票': stock_name,
966
- '代碼': symbol,
967
- '月報酬率(%)': return_pct,
968
- '產業': INDUSTRY_MAPPING.get(symbol, '其他')
969
- })
970
-
971
- if not industry_data:
972
- return {}
973
-
974
- df_industry = pd.DataFrame(industry_data)
975
-
976
- # 建立產業表現圓餅圖
977
- fig = px.pie(df_industry, values='月報酬率(%)', names='股票',
978
- title='各股票月報酬率比較',
979
- color_discrete_sequence=px.colors.qualitative.Set3)
980
-
981
- fig.update_layout(height=400)
982
- return fig
983
-
984
- # 新增:更新景氣燈號圖表
985
- @app.callback(
986
- dash.dependencies.Output('business-climate-chart', 'figure'),
987
- [dash.dependencies.Input('stock-dropdown', 'value')] # 雖然不會影響圖表,但需要觸發
988
- )
989
- def update_business_climate_chart(selected_stock):
990
- df = get_business_climate_data()
991
-
992
- if df.empty:
993
- # 如果沒有資料,顯示提示圖表
994
- fig = go.Figure()
995
- fig.add_annotation(
996
- x=0.5, y=0.5,
997
- text="無法載入景氣燈號資料<br>請確認 business_climate.csv 檔案是否存在",
998
- xref="paper", yref="paper",
999
- showarrow=False,
1000
- font=dict(size=14)
1001
- )
1002
- fig.update_layout(
1003
- title="台灣景氣燈號",
1004
- height=300,
1005
- showlegend=False
1006
- )
1007
- return fig
1008
-
1009
- # 定義燈號顏色
1010
- def get_light_color(score):
1011
- if score >= 32:
1012
- return 'red' # 紅燈
1013
- elif score >= 24:
1014
- return 'orange' # 黃紅燈
1015
- elif score >= 17:
1016
- return 'yellow' # 黃燈
1017
- elif score >= 10:
1018
- return 'lightgreen' # 黃藍燈
1019
- else:
1020
- return 'blue' # 藍燈
1021
-
1022
- # 為每個點設定顏色
1023
- colors = [get_light_color(score) for score in df['Index']]
1024
-
1025
- fig = go.Figure()
1026
-
1027
- fig.add_trace(go.Scatter(
1028
- x=df['Date'],
1029
- y=df['Index'],
1030
- mode='lines+markers',
1031
- name='景氣燈號',
1032
- line=dict(color='darkblue', width=2),
1033
- marker=dict(
1034
- size=8,
1035
- color=colors,
1036
- line=dict(width=2, color='darkblue')
1037
- )
1038
- ))
1039
-
1040
- # 添加燈號區間線
1041
- fig.add_hline(y=32, line_dash="dash", line_color="red", annotation_text="紅燈(32)")
1042
- fig.add_hline(y=24, line_dash="dash", line_color="orange", annotation_text="黃紅燈(24)")
1043
- fig.add_hline(y=17, line_dash="dash", line_color="yellow", annotation_text="黃燈(17)")
1044
- fig.add_hline(y=10, line_dash="dash", line_color="lightgreen", annotation_text="黃藍燈(10)")
1045
-
1046
- fig.update_layout(
1047
- title="台灣景氣燈號走勢",
1048
- xaxis_title='日期',
1049
- yaxis_title='燈號分數',
1050
- height=300,
1051
- yaxis=dict(range=[0, 40])
1052
- )
1053
-
1054
- return fig
1055
-
1056
- # 新增:更新分析師觀點
1057
- @app.callback(
1058
- [dash.dependencies.Output('technical-analysis-text', 'children'),
1059
- dash.dependencies.Output('fundamental-analysis-text', 'children'),
1060
- dash.dependencies.Output('market-outlook-text', 'children')],
1061
- [dash.dependencies.Input('stock-dropdown', 'value'),
1062
- dash.dependencies.Input('period-dropdown', 'value')]
1063
- )
1064
- def update_analysis_text(selected_stock, period):
1065
- # 獲取股票資料進行分析
1066
- data = get_stock_data(selected_stock, period)
1067
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
1068
-
1069
- if data.empty:
1070
- return "無法獲取資料進行分析", "無法獲取資料進行分析", "無法獲取資料進行分析"
1071
-
1072
- # 計算技術指標
1073
- data = calculate_technical_indicators(data)
1074
-
1075
- # 基本數據
1076
- current_price = data['Close'].iloc[-1]
1077
- price_change = ((current_price - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
1078
- volume_avg = data['Volume'].mean()
1079
- recent_volume = data['Volume'].iloc[-5:].mean()
1080
- rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
1081
-
1082
- # 新增技術指標數據
1083
- macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
1084
- macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
1085
- bb_position = data['BB_Position'].iloc[-1] if not pd.isna(data['BB_Position'].iloc[-1]) else 0.5
1086
- k_current = data['K'].iloc[-1] if not pd.isna(data['K'].iloc[-1]) else 50
1087
- d_current = data['D'].iloc[-1] if not pd.isna(data['D'].iloc[-1]) else 50
1088
-
1089
- # 技術面分析
1090
- technical_text = html.Div([
1091
- html.P([
1092
- html.Strong("價格趨勢:"),
1093
- f"近期{period}期間內,{stock_name}呈現",
1094
- html.Span(f"{'上漲' if price_change > 5 else '下跌' if price_change < -5 else '盤整'}",
1095
- style={'color': 'green' if price_change > 5 else 'red' if price_change < -5 else 'orange', 'font-weight': 'bold'}),
1096
- f"走勢,累計變動{price_change:+.1f}%。"
1097
- ]),
1098
- html.P([
1099
- html.Strong("RSI指標:"),
1100
- f"目前為{rsi_current:.1f},",
1101
- html.Span(
1102
- "處於超買區間" if rsi_current > 70 else "處於超賣區間" if rsi_current < 30 else "在正常範圍內",
1103
- style={'color': 'red' if rsi_current > 70 else 'green' if rsi_current < 30 else 'blue', 'font-weight': 'bold'}
1104
- ),
1105
- "。"
1106
- ]),
1107
- html.P([
1108
- html.Strong("MACD指標:"),
1109
- f"MACD線({macd_current:.3f})",
1110
- html.Span(
1111
- "高於" if macd_current > macd_signal_current else "低於",
1112
- style={'color': 'green' if macd_current > macd_signal_current else 'red', 'font-weight': 'bold'}
1113
- ),
1114
- f"信號線({macd_signal_current:.3f}),",
1115
- f"顯示{'多頭' if macd_current > macd_signal_current else '空頭'}格局。"
1116
- ]),
1117
- html.P([
1118
- html.Strong("布林通道:"),
1119
- f"股價位於通道",
1120
- html.Span(
1121
- "上半部" if bb_position > 0.8 else "下半部" if bb_position < 0.2 else "中段",
1122
- style={'color': 'red' if bb_position > 0.8 else 'green' if bb_position < 0.2 else 'blue', 'font-weight': 'bold'}
1123
- ),
1124
- f"({bb_position*100:.0f}%),",
1125
- f"{'壓力較大' if bb_position > 0.8 else '支撐較強' if bb_position < 0.2 else '整理格局'}。"
1126
- ]),
1127
- html.P([
1128
- html.Strong("KD指標:"),
1129
- f"K值({k_current:.1f})",
1130
- html.Span(
1131
- "高於" if k_current > d_current else "低於",
1132
- style={'color': 'green' if k_current > d_current else 'red', 'font-weight': 'bold'}
1133
- ),
1134
- f"D值({d_current:.1f}),",
1135
- html.Span(
1136
- "超買警戒" if k_current > 80 else "超賣關注" if k_current < 20 else "正常區間",
1137
- style={'color': 'red' if k_current > 80 else 'green' if k_current < 20 else 'blue', 'font-weight': 'bold'}
1138
- ),
1139
- "。"
1140
- ]),
1141
- html.P([
1142
- html.Strong("成交量分析:"),
1143
- f"近期成交量{'放大' if recent_volume > volume_avg * 1.2 else '萎縮' if recent_volume < volume_avg * 0.8 else '平穩'},",
1144
- f"顯示市場{'關注度提升' if recent_volume > volume_avg * 1.2 else '觀望氣氛濃厚' if recent_volume < volume_avg * 0.8 else '交投正常'}。"
1145
- ])
1146
- ])
1147
-
1148
- # 基本面分析
1149
- industry = INDUSTRY_MAPPING.get(selected_stock, '綜合')
1150
- fundamental_text = html.Div([
1151
- html.P([
1152
- html.Strong("產業地位:"),
1153
- f"{stock_name}屬於{industry}產業,在產業鏈中具有",
1154
- html.Span("重要地位" if selected_stock in ['2330.TW', '2454.TW', '2317.TW'] else "一定影響力",
1155
- style={'font-weight': 'bold'}),
1156
- "。"
1157
- ]),
1158
- html.P([
1159
- html.Strong("營運展望:"),
1160
- f"考量{industry}產業前景及公司基本面,建議持續關注季報表現及未來指引。"
1161
- ]),
1162
- html.P([
1163
- html.Strong("風險評估:"),
1164
- "注意產業週期性變化、國際競爭及法規環境變化等風險因子。"
1165
- ])
1166
- ])
1167
-
1168
- # 市場展望
1169
- if price_change > 10:
1170
- outlook_tone = "謹慎樂觀"
1171
- outlook_color = "#28a745"
1172
- elif price_change < -10:
1173
- outlook_tone = "保守觀望"
1174
- outlook_color = "#dc3545"
1175
- else:
1176
- outlook_tone = "中性持平"
1177
- outlook_color = "#ffc107"
1178
-
1179
- market_outlook = html.Div([
1180
- html.P([
1181
- html.Strong("整體評估:", style={'font-size': '16px'}),
1182
- f"基於技術面及基本面分析,對{stock_name}採取",
1183
- html.Span(f"{outlook_tone}", style={'color': outlook_color, 'font-weight': 'bold', 'font-size': '16px'}),
1184
- "態度。"
1185
- ]),
1186
- html.P([
1187
- html.Strong("投資建議:"),
1188
- "建議投資人根據自身風險承受能力,採取適當的資產配置策略。短線操作注意技術指標,長線投資關注基本面變化。"
1189
- ]),
1190
- html.P([
1191
- html.Strong("風險提醒:"),
1192
- "股票投資具有風險,過去績效不代表未來表現,投資前請詳閱公開說明書並審慎評估。"
1193
- ], style={'font-style': 'italic', 'font-size': '13px'})
1194
- ])
1195
-
1196
- return technical_text, fundamental_text, market_outlook
1197
-
1198
- # 新增:更新PMI圖表
1199
- @app.callback(
1200
- dash.dependencies.Output('pmi-chart', 'figure'),
1201
- [dash.dependencies.Input('stock-dropdown', 'value')] # 雖然不會影響圖表,但需要觸發
1202
- )
1203
- def update_pmi_chart(selected_stock):
1204
- df = get_pmi_data()
1205
-
1206
- if df.empty:
1207
- # 如果沒有資料,顯示提示圖表
1208
- fig = go.Figure()
1209
- fig.add_annotation(
1210
- x=0.5, y=0.5,
1211
- text="無法載入PMI資料<br>請確認 taiwan_pmi.csv 檔案是否存在",
1212
- xref="paper", yref="paper",
1213
- showarrow=False,
1214
- font=dict(size=14)
1215
- )
1216
- fig.update_layout(
1217
- title="台灣PMI指數",
1218
- height=300,
1219
- showlegend=False
1220
- )
1221
- return fig
1222
-
1223
- # 定義PMI顏色 (50以上擴張,以下緊縮)
1224
- def get_pmi_color(value):
1225
- return 'green' if value >= 50 else 'red'
1226
-
1227
- colors = [get_pmi_color(value) for value in df['Index']]
1228
-
1229
- fig = go.Figure()
1230
-
1231
- fig.add_trace(go.Scatter(
1232
- x=df['Date'],
1233
- y=df['Index'],
1234
- mode='lines+markers',
1235
- name='PMI指數',
1236
- line=dict(color='darkblue', width=2),
1237
- marker=dict(
1238
- size=8,
1239
- color=colors,
1240
- line=dict(width=2, color='darkblue')
1241
- )
1242
- ))
1243
-
1244
- # 添加榮枯線
1245
- fig.add_hline(y=50, line_dash="dash", line_color="black", annotation_text="榮枯線(50)")
1246
-
1247
- # 添加背景色區域
1248
- fig.add_hrect(
1249
- y0=50, y1=60,
1250
- fillcolor="lightgreen", opacity=0.2,
1251
- annotation_text="擴張區間", annotation_position="top left"
1252
- )
1253
- fig.add_hrect(
1254
- y0=40, y1=50,
1255
- fillcolor="lightcoral", opacity=0.2,
1256
- annotation_text="緊縮區間", annotation_position="bottom left"
1257
- )
1258
-
1259
- fig.update_layout(
1260
- title="台灣PMI指數走勢",
1261
- xaxis_title='日期',
1262
- yaxis_title='PMI指數',
1263
- height=300,
1264
- yaxis=dict(range=[35, 60])
1265
- )
1266
-
1267
- return fig
1268
-
1269
- # 新增:多檔股票比較
1270
- @app.callback(
1271
- [dash.dependencies.Output('comparison-chart', 'figure'),
1272
- dash.dependencies.Output('comparison-table', 'children')],
1273
- [dash.dependencies.Input('comparison-stocks', 'value'),
1274
- dash.dependencies.Input('comparison-period', 'value')]
1275
- )
1276
- def update_comparison_analysis(selected_stocks, period):
1277
- if not selected_stocks:
1278
- return {}, html.Div("請選擇要比較的股票")
1279
-
1280
- # 限制最多5檔
1281
- selected_stocks = selected_stocks[:5]
1282
-
1283
- fig = go.Figure()
1284
- comparison_data = []
1285
-
1286
- for stock in selected_stocks:
1287
- data = get_stock_data(stock, period)
1288
- if not data.empty:
1289
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == stock][0]
1290
-
1291
- # 正規化價格(以期初為基準100)
1292
- normalized_prices = (data['Close'] / data['Close'].iloc[0]) * 100
1293
-
1294
- fig.add_trace(go.Scatter(
1295
- x=data.index,
1296
- y=normalized_prices,
1297
- mode='lines',
1298
- name=stock_name,
1299
- line=dict(width=2)
1300
- ))
1301
-
1302
- # 計算績效數據
1303
- total_return = ((data['Close'].iloc[-1] / data['Close'].iloc[0]) - 1) * 100
1304
- volatility = data['Close'].pct_change().std() * np.sqrt(252) * 100 # 年化波動率
1305
-
1306
- comparison_data.append({
1307
- 'name': stock_name,
1308
- 'return': total_return,
1309
- 'volatility': volatility,
1310
- 'current_price': data['Close'].iloc[-1]
1311
- })
1312
-
1313
- fig.update_layout(
1314
- title=f'股票績效比較 - {period}',
1315
- xaxis_title='日期',
1316
- yaxis_title='相對績效 (基期=100)',
1317
- height=400,
1318
- hovermode='x unified'
1319
- )
1320
-
1321
- # 建立比較表格
1322
- if comparison_data:
1323
- table_rows = []
1324
- for item in sorted(comparison_data, key=lambda x: x['return'], reverse=True):
1325
- color = 'green' if item['return'] > 0 else 'red'
1326
- table_rows.append(
1327
- html.Tr([
1328
- html.Td(item['name'], style={'font-weight': 'bold'}),
1329
- html.Td(f"{item['return']:+.1f}%", style={'color': color, 'font-weight': 'bold'}),
1330
- html.Td(f"{item['volatility']:.1f}%"),
1331
- html.Td(f"${item['current_price']:.2f}")
1332
- ])
1333
- )
1334
-
1335
- table = html.Table([
1336
- html.Thead([
1337
- html.Tr([
1338
- html.Th("股票", style={'text-align': 'center'}),
1339
- html.Th("報酬率", style={'text-align': 'center'}),
1340
- html.Th("波動率", style={'text-align': 'center'}),
1341
- html.Th("現價", style={'text-align': 'center'})
1342
- ])
1343
- ]),
1344
- html.Tbody(table_rows)
1345
- ], style={
1346
- 'width': '100%',
1347
- 'border-collapse': 'collapse',
1348
- 'font-size': '12px'
1349
- })
1350
-
1351
- return fig, table
1352
-
1353
- return fig, html.Div("無可比較資料")
1354
-
1355
- # 新增:市場情緒分析
1356
- @app.callback(
1357
- [dash.dependencies.Output('sentiment-gauge', 'children'),
1358
- dash.dependencies.Output('news-summary', 'children')],
1359
- [dash.dependencies.Input('stock-dropdown', 'value')]
1360
- )
1361
- def update_sentiment_analysis(selected_stock):
1362
- # 模擬情緒指標(實際應用中可接入新聞API或情緒分析服務)
1363
- sentiment_score = np.random.uniform(30, 80) # 模擬情緒分數 0-100
1364
-
1365
- # 建立情緒指標圓形圖
1366
- gauge_fig = go.Figure(go.Indicator(
1367
- mode = "gauge+number+delta",
1368
- value = sentiment_score,
1369
- domain = {'x': [0, 1], 'y': [0, 1]},
1370
- title = {'text': "市場情緒指數"},
1371
- delta = {'reference': 50},
1372
- gauge = {
1373
- 'axis': {'range': [None, 100]},
1374
- 'bar': {'color': "darkblue"},
1375
- 'steps': [
1376
- {'range': [0, 30], 'color': "lightcoral"},
1377
- {'range': [30, 70], 'color': "lightgray"},
1378
- {'range': [70, 100], 'color': "lightgreen"}
1379
- ],
1380
- 'threshold': {
1381
- 'line': {'color': "red", 'width': 4},
1382
- 'thickness': 0.75,
1383
- 'value': 90
1384
- }
1385
- }
1386
- ))
1387
-
1388
- gauge_fig.update_layout(height=200, margin=dict(l=20, r=20, t=40, b=20))
1389
-
1390
- # 模擬新聞摘要
1391
- stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
1392
-
1393
- news_items = [
1394
- f"📈 {stock_name}獲外資調升目標價,看好後續發展前景",
1395
- f"💼 法人預期{stock_name}下季營收將較上季成長5-10%",
1396
- f"🌐 國際市場波動對{stock_name}影響有限,基本面穩健",
1397
- f"⚡ 產業景氣回溫,{stock_name}受惠程度值得關注",
1398
- f"📊 技術面顯示{stock_name}突破關鍵壓力,短線偏多"
1399
- ]
1400
-
1401
- news_content = html.Div([
1402
- html.P(news, style={
1403
- 'margin': '8px 0',
1404
- 'padding': '8px',
1405
- 'background': '#e8f4f8',
1406
- 'border-radius': '5px',
1407
- 'border-left': '3px solid #17a2b8',
1408
- 'font-size': '13px'
1409
- }) for news in news_items[:3] # 顯示前3條
1410
- ])
1411
-
1412
- return dcc.Graph(figure=gauge_fig), news_content
1413
-
1414
- # 在 Colab 中執行的設定
1415
- if __name__ == '__main__':
1416
- # 在執行前先測試檔案讀取
1417
- print("測試檔案讀取...")
1418
- business_data = get_business_climate_data()
1419
- pmi_data = get_pmi_data()
1420
-
1421
- if not business_data.empty:
1422
- print(f"景氣燈號資料預覽:\n{business_data.head()}")
1423
- if not pmi_data.empty:
1424
- print(f"PMI資料預覽:\n{pmi_data.head()}")
1425
-
1426
- # 在 Hugging Face Spaces 中執行
1427
- app.run(host="0.0.0.0", port=7860, debug=False) if data.empty:
1428
- # 最後嘗試使用加權指數
1429
- stock = yf.Ticker('^TWII')
1430
- data = stock.history(period=period)
1431
-
1432
- return data
1433
- except:
1434
- return pd.DataFrame()
1435
-
1436
- def create_lstm_dataset(data, time_step=60):
1437
- """建立LSTM訓練資料集"""
1438
- X, y = [], []
1439
- for i in range(time_step, len(data)):
1440
- X.append(data[i-time_step:i, 0])
1441
- y.append(data[i, 0])
1442
- return np.array(X), np.array(y)
1443
-
1444
- def simple_lstm_predict(data, predict_days=5):
1445
- """簡化的LSTM預測模型 (使用統計方法模擬)"""
1446
- if len(data) < 60:
1447
- return None
1448
-
1449
- # 使用移動平均和趨勢分析來模擬深度學習預測
1450
- prices = data['Close'].values
1451
-
1452
- # 計算短期和長期移動平均
1453
- ma_short = np.mean(prices[-5:])
1454
- ma_medium = np.mean(prices[-20:])
1455
- ma_long = np.mean(prices[-60:])
1456
-
1457
- # 計算價格變化趨勢
1458
- recent_trend = np.polyfit(range(20), prices[-20:], 1)[0]
1459
- volatility = np.std(prices[-20:]) / np.mean(prices[-20:])
1460
-
1461
- # 模擬預測邏輯
1462
- base_change = recent_trend * predict_days
1463
- trend_factor = 1.0
1464
-
1465
- if ma_short > ma_medium > ma_long:
1466
- trend_factor = 1.02 # 上升趨勢
1467
- elif ma_short < ma_medium < ma_long:
1468
- trend_factor = 0.98 # 下降趨勢
1469
- else:
1470
- trend_factor = 1.0 # 盤整
1471
-
1472
- # 加入隨機性模擬市場不確定性
1473
- noise_factor = np.random.normal(1, volatility * 0.1)
1474
-
1475
- predicted_price = prices[-1] * trend_factor + base_change + (prices[-1] * noise_factor * 0.01)
1476
- change_pct = ((predicted_price - prices[-1]) / prices[-1]) * 100
1477
-
1478
- return {
1479
- 'predicted_price': predicted_price,
1480
- 'change_pct': change_pct,
1481
- 'confidence': max(0.6, 1 - volatility * 2) # 基於波動率的信心度
1482
- }
1483
-
1484
- def calculate_technical_indicators(df):
1485
- """計算技術指標"""
1486
- if df.empty:
1487
- return df
1488
-
1489
- # 移動平均線
1490
- df['MA5'] = df['Close'].rolling(window=5).mean()
1491
- df['MA20'] = df['Close'].rolling(window=20).mean()
1492
-
1493
- # RSI
1494
- delta = df['Close'].diff()
1495
- gain = (delta.where(delta > 0, 0)).rolling(window=14).mean()
1496
- loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean()
1497
- rs = gain / loss
1498
- df['RSI'] = 100 - (100 / (1 + rs))
1499
-
1500
- # MACD (12, 26, 9)
1501
- exp1 = df['Close'].ewm(span=12).mean()
1502
- exp2 = df['Close'].ewm(span=26).mean()
1503
- df['MACD'] = exp1 - exp2
1504
- df['MACD_Signal'] = df['MACD'].ewm(span=9).mean()
1505
- df['MACD_Histogram'] = df['MACD'] - df['MACD_Signal']
1506
-
1507
- # 布林通道 (20日, 2倍標準差)
1508
- df['BB_Middle'] = df['Close'].rolling(window=20).mean()
1509
- bb_std = df['Close'].rolling(window=20).std()
1510
- df['BB_Upper'] = df['BB_Middle'] + (bb_std * 2)
1511
- df['BB_Lower'] = df['BB_Middle'] - (bb_std * 2)
1512
- df['BB_Width'] = df['BB_Upper'] - df['BB_Lower']
1513
- df['BB_Position'] = (df['Close'] - df['BB_Lower']) / (df['BB_Upper'] - df['BB_Lower'])
1514
-
1515
- # KD指標 (9, 3, 3)
1516
- low_min = df['Low'].rolling(window=9).min()
1517
- high_max = df['High'].rolling(window=9).max()
1518
- rsv = (df['Close'] - low_min) / (high_max - low_min) * 100
1519
- df['K'] = rsv.ewm(com=2).mean() # com=2 相當於 span=3
1520
- df['D'] = df['K'].ewm(com=2).mean()
1521
-
1522
- # 威廉指標 %R (14日)
1523
- low_min_14 = df['Low'].rolling(window=14).min()
1524
- high_max_14 = df['High'].rolling(window=14).max()
1525
- df['Williams_R'] = -100 * (high_max_14 - df['Close']) / (high_max_14 - low_min_14)
1526
-
1527
- return df
1528
-
1529
- def get_business_climate_data():
1530
- """獲取台灣景氣燈號資料"""
1531
- try:
1532
- # 檢查檔案是否存在
1533
- if not os.path.exists('business_climate.csv'):
1534
- print("business_climate.csv 檔案不存在")
1535
- return pd.DataFrame()
1536
-
1537
- # 讀取CSV檔案,假設列名為 Date 和 Index
1538
- df = pd.read_csv('business_climate.csv')
1539
-
1540
- # 檢查列名並調整
1541
- if 'Date' not in df.columns:
1542
- # 如果第一列是日期,重新命名
1543
- df.columns = ['Date', 'Index'] if len(df.columns) == 2 else df.columns
1544
-
1545
- # 轉換日期格式 (處理 YYYY-MM 格式)
1546
- if 'Date' in df.columns:
1547
- try:
1548
- # 如果是 YYYY-MM 格式,轉換為日期
1549
- df['Date'] = pd.to_datetime(df['Date'] + '-01', format='%Y-%m-%d', errors='coerce')
1550
- except:
1551
- df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
1552
-
1553
- # 移除日期轉換失敗的行
1554
- df = df.dropna(subset=['Date'])
1555
-
1556
- print(f"成功讀取景氣燈號資料:{len(df)} 筆記錄")
1557
- return df
1558
-
1559
- except Exception as e:
1560
- print(f"無法獲取景氣燈號資料: {str(e)}")
1561
- return pd.DataFrame()
1562
-
1563
- def get_pmi_data():
1564
- """獲取台灣 PMI 資料"""
1565
- try:
1566
- # 檢查檔案是否存在
1567
- if not os.path.exists('taiwan_pmi.csv'):
1568
- print("taiwan_pmi.csv 檔案不存在")
1569
- return pd.DataFrame()
1570
-
1571
- # 讀取CSV檔案
1572
- df = pd.read_csv('taiwan_pmi.csv')
1573
-
1574
- # 檢查列名並調整 (處理 DATE/INDEX 或其他可能的列名)
1575
- if 'DATE' in df.columns:
1576
- df = df.rename(columns={'DATE': 'Date', 'INDEX': 'Index'})
1577
- elif len(df.columns) == 2:
1578
- df.columns = ['Date', 'Index']
1579
-
1580
- # 轉換日期格式
1581
- if 'Date' in df.columns:
1582
- try:
1583
- # 如果是 YYYY-MM 格式,轉換為日期
1584
- df['Date'] = pd.to_datetime(df['Date'] + '-01', format='%Y-%m-%d', errors='coerce')
1585
- except:
1586
- df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
1587
-
1588
- # 移除日期轉換失敗的行
1589
- df = df.dropna(subset=['Date'])
1590
-
1591
- print(f"成功讀取 PMI 資料:{len(df)} 筆記錄")
1592
- return df
1593
-
1594
- except Exception as e:
1595
- print(f"無法獲取 PMI 資料: {str(e)}")
1596
- return pd.DataFrame()
1597
-
1598
- # 建立 Dash 應用程式
1599
- app = dash.Dash(__name__, suppress_callback_exceptions=True)
1600
-
1601
- # 應用程式佈局
1602
- app.layout = html.Div([
1603
- html.H1("台股分析儀表板", style={'text-align': 'center', 'margin-bottom': '30px'}),
1604
-
1605
- # 台指期獨立預測區塊 - 置於頂部
1606
- html.Div([
1607
- html.H2("🤖 AI深度學習預測 - 台指期指數", style={
1608
- 'text-align': 'center',
1609
- 'color': '#FFCC22',
1610
- 'margin-bottom': '25px'
1611
- }),
1612
- html.Div([
1613
- html.Div([
1614
- html.Label("預測期間:", style={'font-weight': 'bold', 'color': '#FFCC22'}),
1615
- dcc.Dropdown(
1616
- id='taiex-prediction-period',
1617
- options=[
1618
- {'label': '1日後預測', 'value': 1},
1619
- {'label': '5日後預測', 'value': 5},
1620
- {'label': '10日後預測', 'value': 10},
1621
- {'label': '20日後預測', 'value': 20},
1622
- {'label': '60日後預測', 'value': 60}
1623
- ],
1624
- value=5,
1625
- style={'margin-bottom': '10px', 'color': '#272727'}
1626
- )
1627
- ], style={'width': '30%', 'display': 'inline-block'}),
1628
-
1629
- html.Div(id='taiex-prediction-results', style={'width': '65%', 'display': 'inline-block', 'margin-left': '5%'})
1630
- ]),
1631
-
1632
- html.Div([
1633
- dcc.Graph(id='taiex-prediction-chart')
1634
- ], style={'margin-top': '20px'})
1635
- ], style={
1636
- 'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
1637
- 'padding': '25px',
1638
- 'border-radius': '15px',
1639
- 'box-shadow': '0 8px 25px rgba(0,0,0,0.15)',
1640
- 'color': 'white',
1641
- 'margin-bottom': '40px'
1642
- }),
1643
-
1644
- # 控制面板 (移除台指期選項)
1645
- html.Div([
1646
- html.Div([
1647
- html.Label("選擇股票:"),
1648
- dcc.Dropdown(
1649
- id='stock-dropdown',
1650
- options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
1651
- value='2330.TW', # 預設改為台積電
1652
- style={'margin-bottom': '10px'}
1653
- )
1654
- ], style={'width': '30%', 'display': 'inline-block', 'vertical-align': 'top'}),
1655
-
1656
- html.Div([
1657
- html.Label("時間範圍:"),
1658
- dcc.Dropdown(
1659
- id='period-dropdown',
1660
- options=[
1661
- {'label': '1個月', 'value': '1mo'},
1662
- {'label': '3個月', 'value': '3mo'},
1663
- {'label': '6個月', 'value': '6mo'},
1664
- {'label': '1年', 'value': '1y'},
1665
- {'label': '2年', 'value': '2y'}
1666
- ],
1667
- value='6mo',
1668
- style={'margin-bottom': '10px'}
1669
- )
1670
- ], style={'width': '30%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'}),
1671
-
1672
- html.Div([
1673
- html.Label("圖表類型:"),
1674
- dcc.Dropdown(
1675
- id='chart-type',
1676
- options=[
1677
- {'label': '線圖', 'value': 'line'},
1678
- {'label': '蠟燭圖', 'value': 'candlestick'}
1679
- ],
1680
- value='candlestick',
1681
- style={'margin-bottom': '10px'}
1682
- )
1683
- ], style={'width': '30%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'})
1684
- ], style={'margin-bottom': '30px'}),
1685
-
1686
- # 股價資訊卡片
1687
- html.Div(id='stock-info-cards', style={'margin-bottom': '30px'}),
1688
-
1689
- # 主要圖表區域
1690
- html.Div([
1691
- # 左側:股價走勢圖和技術指標
1692
- html.Div([
1693
- html.Div([
1694
- dcc.Graph(id='price-chart')
1695
- ], style={'margin-bottom': '20px'}),
1696
-
1697
- html.Div([
1698
- dcc.Graph(id='rsi-chart')
1699
- ])
1700
- ], style={'width': '65%', 'display': 'inline-block', 'vertical-align': 'top'}),
1701
-
1702
- # 右側:分析資訊面板
1703
- html.Div([
1704
- html.Div(id='analysis-panel')
1705
- ], style={'width': '33%', 'display': 'inline-block', 'margin-left': '2%', 'vertical-align': 'top'})
1706
- ]),
1707
-
1708
- # 技術指標選擇區域
1709
- html.Div([
1710
- html.H3("📊 進階技術指標分析", style={'margin-bottom': '20px'}),
1711
- html.Div([
1712
- html.Label("選擇技術指標:", style={'font-weight': 'bold', 'margin-right': '10px'}),
1713
- dcc.Dropdown(
1714
- id='technical-indicator-selector',
1715
- options=[
1716
- {'label': 'RSI 相對強弱指標', 'value': 'RSI'},
1717
- {'label': 'MACD 指數平滑異同移動平均線', 'value': 'MACD'},
1718
- {'label': '布林通道 Bollinger Bands', 'value': 'BB'},
1719
- {'label': 'KD 隨機指標', 'value': 'KD'},
1720
- {'label': '威廉指標 %R', 'value': 'WR'}
1721
- ],
1722
- value='RSI',
1723
- style={'width': '100%'}
1724
- )
1725
- ], style={'margin-bottom': '20px'}),
1726
-
1727
- html.Div([
1728
- dcc.Graph(id='advanced-technical-chart')
1729
- ])
1730
- ], style={
1731
- 'margin-top': '20px',
1732
- 'padding': '20px',
1733
- 'background': 'white',
1734
- 'border-radius': '10px',
1735
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)'
1736
- }),
1737
-
1738
- # 成交量圖
1739
- html.Div([
1740
- dcc.Graph(id='volume-chart')
1741
- ], style={'margin-top': '20px'}),
1742
-
1743
- # 產業分析
1744
- html.Div([
1745
- html.H3("產業表現分析"),
1746
- dcc.Graph(id='industry-analysis')
1747
- ], style={'margin-top': '30px'}),
1748
-
1749
- # 分析師觀點區域
1750
- html.Div([
1751
- html.H3("📊 分析師觀點與市場解讀", style={'color': '#2E86AB', 'margin-bottom': '20px'}),
1752
- html.Div([
1753
- # 左側:技術分析觀點
1754
- html.Div([
1755
- html.H4("🔍 技術面分析", style={'color': '#A23B72', 'margin-bottom': '15px'}),
1756
- html.Div(id='technical-analysis-text', style={
1757
- 'background': '#f8f9fa',
1758
- 'padding': '15px',
1759
- 'border-radius': '8px',
1760
- 'border-left': '4px solid #A23B72',
1761
- 'min-height': '150px',
1762
- 'font-size': '14px',
1763
- 'line-height': '1.6'
1764
- })
1765
- ], style={'width': '48%', 'display': 'inline-block', 'vertical-align': 'top'}),
1766
-
1767
- # 右側:基本面分析觀點
1768
- html.Div([
1769
- html.H4("📈 基本面分析", style={'color': '#F18F01', 'margin-bottom': '15px'}),
1770
- html.Div(id='fundamental-analysis-text', style={
1771
- 'background': '#f8f9fa',
1772
- 'padding': '15px',
1773
- 'border-radius': '8px',
1774
- 'border-left': '4px solid #F18F01',
1775
- 'min-height': '150px',
1776
- 'font-size': '14px',
1777
- 'line-height': '1.6'
1778
- })
1779
- ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '4%', 'vertical-align': 'top'})
1780
- ]),
1781
-
1782
- # 底部:市場展望
1783
- html.Div([
1784
- html.H4("🎯 市場展望與投資建議", style={'color': '#C73E1D', 'margin-bottom': '15px', 'margin-top': '25px'}),
1785
- html.Div(id='market-outlook-text', style={
1786
- 'background': 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
1787
- 'color': 'white',
1788
- 'padding': '20px',
1789
- 'border-radius': '10px',
1790
- 'min-height': '100px',
1791
- 'font-size': '15px',
1792
- 'line-height': '1.7',
1793
- 'box-shadow': '0 4px 15px rgba(0,0,0,0.1)'
1794
- })
1795
- ])
1796
- ], style={
1797
- 'margin-top': '30px',
1798
- 'padding': '25px',
1799
- 'background': 'white',
1800
- 'border-radius': '12px',
1801
- 'box-shadow': '0 4px 20px rgba(0,0,0,0.08)',
1802
- 'border': '1px solid #e9ecef'
1803
- }),
1804
-
1805
- # 景氣燈號與 PMI 分析
1806
- html.Div([
1807
- html.H3("景氣燈號與 PMI 分析"),
1808
- html.Div([
1809
- html.Div([
1810
- dcc.Graph(id='business-climate-chart')
1811
- ], style={'width': '48%', 'display': 'inline-block'}),
1812
- html.Div([
1813
- dcc.Graph(id='pmi-chart')
1814
- ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '2%'})
1815
- ])
1816
- ], style={'margin-top': '30px'}),
1817
-
1818
- # 多檔股票比較區域
1819
- html.Div([
1820
- html.H3("📊 多檔股票比較分析", style={'margin-bottom': '20px'}),
1821
- html.Div([
1822
- html.Div([
1823
- html.Label("選擇比較股票(最多5檔):", style={'font-weight': 'bold'}),
1824
- dcc.Dropdown(
1825
- id='comparison-stocks',
1826
- options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
1827
- value=['2330.TW', '2454.TW', '2317.TW'], # 預設選擇
1828
- multi=True,
1829
- style={'margin-bottom': '15px'}
1830
- )
1831
- ], style={'width': '60%', 'display': 'inline-block'}),
1832
-
1833
- html.Div([
1834
- html.Label("比較期間:", style={'font-weight': 'bold'}),
1835
- dcc.Dropdown(
1836
- id='comparison-period',
1837
- options=[
1838
- {'label': '1個月', 'value': '1mo'},
1839
- {'label': '3個月', 'value': '3mo'},
1840
- {'label': '6個月', 'value': '6mo'},
1841
- {'label': '1年', 'value': '1y'}
1842
- ],
1843
- value='3mo'
1844
- )
1845
- ], style={'width': '35%', 'display': 'inline-block', 'margin-left': '5%'})
1846
- ]),
1847
-
1848
- html.Div([
1849
- html.Div([
1850
- dcc.Graph(id='comparison-chart')
1851
- ], style={'width': '65%', 'display': 'inline-block'}),
1852
-
1853
- html.Div([
1854
- html.H4("比較結果", style={'color': '#2E86AB'}),
1855
- html.Div(id='comparison-table')
1856
- ], style={'width': '33%', 'display': 'inline-block', 'margin-left': '2%', 'vertical-align': 'top'})
1857
- ])
1858
- ], style={
1859
- 'margin-top': '30px',
1860
- 'padding': '20px',
1861
- 'background': 'white',
1862
- 'border-radius': '10px',
1863
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)'
1864
- }),
1865
-
1866
- # 新聞情感分析區域(模擬)
1867
- html.Div([
1868
- html.H3("📰 市場情緒與新聞分析", style={'color': '#E74C3C', 'margin-bottom': '20px'}),
1869
- html.Div([
1870
- html.Div([
1871
- html.H4("市場情緒指標", style={'color': '#8E44AD'}),
1872
- html.Div(id='sentiment-gauge')
1873
- ], style={'width': '48%', 'display': 'inline-block'}),
1874
-
1875
- html.Div([
1876
- html.H4("關鍵新聞摘要", style={'color': '#27AE60'}),
1877
- html.Div(id='news-summary', style={
1878
- 'background': '#f8f9fa',
1879
- 'padding': '15px',
1880
- 'border-radius': '8px',
1881
- 'max-height': '200px',
1882
- 'overflow-y': 'auto'
1883
- })
1884
- ], style={'width': '48%', 'display': 'inline-block', 'margin-left': '4%'})
1885
- ])
1886
- ], style={
1887
- 'margin-top': '30px',
1888
- 'padding': '20px',
1889
- 'background': 'white',
1890
- 'border-radius': '10px',
1891
- 'box-shadow': '0 2px 10px rgba(0,0,0,0.1)'
1892
- })
1893
- ])
1894
- # 創建一個列表來儲存所有的預測點數據
1895
- all_predictions_data = []
1896
- # 台指期獨立預測回調函數
1897
- @app.callback(
1898
- [dash.dependencies.Output('taiex-prediction-results', 'children'),
1899
- dash.dependencies.Output('taiex-prediction-chart', 'figure')],
1900
- [dash.dependencies.Input('taiex-prediction-period', 'value')]
1901
- )
1902
- def update_taiex_prediction(predict_days):
1903
- # 獲取台指期歷史資料
1904
- data = get_stock_data('^TWII', '2y')
1905
- if data.empty:
1906
- return html.Div("無法獲取台指期資料"), {}
1907
-
1908
- # 獲取最後的歷史數據點
1909
- last_historical_date = data.index[-1]
1910
- current_price = data['Close'].iloc[-1]
1911
-
1912
- # --- 關鍵修改開始 ---
1913
-
1914
- # 1. 收集所有需要的預測點
1915
- all_predict_options = [1, 5, 10, 20, 60]
1916
-
1917
- # 篩選出使用者選擇天數或更短的所有預測選項
1918
- points_to_calculate = [d for d in all_predict_options if d <= predict_days]
1919
-
1920
- # 清空舊的預測數據,重新計算
1921
- all_predictions_data.clear()
1922
-
1923
- # 添加歷史的最後一個點作為起始點
1924
- all_predictions_data.append({
1925
- 'date': last_historical_date,
1926
- 'price': current_price,
1927
- 'days': 0, # 0天代表歷史數據
1928
- 'is_historical': True
1929
- })
1930
-
1931
- # 計算並儲存每個預測點
1932
- for d in points_to_calculate:
1933
- prediction = simple_lstm_predict(data, d)
1934
- if prediction:
1935
- future_date = last_historical_date + timedelta(days=d)
1936
- all_predictions_data.append({
1937
- 'date': future_date,
1938
- 'price': prediction['predicted_price'],
1939
- 'days': d,
1940
- 'is_historical': False,
1941
- 'change_pct': prediction['change_pct'] # 為了顏色判斷
1942
- })
1943
-
1944
- # 將預測點數據轉換為 DataFrame,方便繪圖
1945
- predictions_df = pd.DataFrame(all_predictions_data)
1946
- predictions_df = predictions_df.sort_values(by='date') # 確保日期排序正確
1947
-
1948
- # 找到最後一個預測點的顏色
1949
- final_prediction_info = predictions_df[predictions_df['days'] == predict_days].iloc[0] if predict_days in predictions_df['days'].values else None
1950
- final_color = '#00C851' if final_prediction_info and final_prediction_info['change_pct'] >= 0 else '#FF4444'
1951
- arrow = '📈' if final_color == '#00C851' else '📉'
1952
-
1953
- # 建立預測趨勢圖
1954
- fig = go.Figure()
1955
-
1956
- # 添加歷史價格線 (最近30天)
1957
- recent_data = data.tail(30)
1958
- fig.add_trace(go.Scatter(
1959
- x=recent_data.index,
1960
- y=recent_data['Close'],
1961
- mode='lines',
1962
- name='歷史價格',
1963
- line=dict(color='#FFA726', width=2)
1964
- ))
1965
-
1966
- # 添加連接所有預測點的趨勢線
1967
- fig.add_trace(go.Scatter(
1968
- x=predictions_df['date'],
1969
- y=predictions_df['price'],
1970
- mode='lines+markers', # 同時顯示線和點
1971
- name='預測趨勢',
1972
- line=dict(color=final_color, width=3, dash='dash'), # 使用最終預測的顏色
1973
- marker=dict(size=8)
1974
- ))
1975
-
1976
- # 為了區分不同天數的點,我們可以再添加一次點,但這次只添加 marker
1977
- # 這樣可以為每個點設置不同的顏色和大小(如果需要)
1978
- for index, row in predictions_df.iterrows():
1979
- if not row['is_historical']:
1980
- point_color = '#00C851' if row['change_pct'] >= 0 else '#FF4444'
1981
- fig.add_trace(go.Scatter(
1982
- x=[row['date']],
1983
- y=[row['price']],
1984
- mode='markers',
1985
- name=f'{row["days"]}日預測點',
1986
- marker=dict(size=10, color=point_color, line=dict(width=2, color='DarkSlateGrey')), # 稍微加點邊框
1987
- legendgroup=f'points', # 將所有點分到同一個圖例組
1988
- showlegend=True if row['days'] in [1, 5, 10, 20, 60] else False # 只顯示主要天數的點在圖例
1989
- ))
1990
-
1991
- # --- 關鍵修改結束 ---
1992
-
1993
- # 預測結果卡片
1994
- color = '#00C851' if change_pct >= 0 else '#FF4444'
1995
- arrow = '📈' if change_pct >= 0 else '📉'
1996
-
1997
- result_card = html.Div([
1998
- html.H4(f"{predict_days}日後預測結果", style={'margin': '0 0 15px 0', 'color': 'white'}),
1999
- html.Div([
2000
- html.Span(f"{arrow} ", style={'font-size': '24px'}),
2001
- html.Span(f"{change_pct:+.2f}%", style={
2002
- 'font-size': '28px',
2003
- 'font-weight': 'bold',
2004
- 'color': color
2005
- })
2006
- ], style={'margin': '10px 0'}),
2007
- html.P(f"目前價格: {current_price:.2f}", style={'margin': '5px 0'}),
2008
- html.P(f"預測價格: {predicted_price:.2f}", style={'margin': '5px 0'}),
2009
- html.P(f"信心度: {confidence:.1%}", style={'margin': '5px 0', 'font-size': '14px'})
2010
- ], style={
2011
- 'background': 'rgba(255,255,255,0.1)',
2012
- 'padding': '20px',
2013
- 'border-radius': '10px',
2014
- 'border': '1px solid rgba(255,255,255,0.2)'
2015
- })
2016
-
2017
- # 建立預測趨勢圖
2018
- fig = go.Figure()
2019
-
2020
- # 歷史價格 (最近30天)
2021
- recent_data = data.tail(30)
2022
- fig.add_trace(go.Scatter(
2023
- x=recent_data.index,
2024
- y=recent_data['Close'],
2025
- mode='lines',
2026
- name='歷史價格',
2027
- line=dict(color='#FFA726', width=2)
2028
- ))
2029
-
2030
- # --- 關鍵修正從這裡開始 ---
2031
- # 定義所有要顯示的預測天數點
2032
- all_predict_days = [1, 5, 10, 20, 60]
2033
-
2034
- # 過濾出所有小於或等於使用者選擇的預測天數
2035
- points_to_show = [d for d in all_predict_days if d <= predict_days]
2036
-
2037
- # 為每個要顯示的預測點創建圖表軌跡
2038
- for d in points_to_show:
2039
- # 重新計算每個點的預測值
2040
- point_prediction = simple_lstm_predict(data, d)
2041
- if point_prediction:
2042
- point_predicted_price = point_prediction['predicted_price']
2043
- point_future_date = recent_data.index[-1] + timedelta(days=d)
2044
-
2045
- # 決定點的顏色
2046
- point_color = '#00C851' if point_predicted_price >= current_price else '#FF4444'
2047
-
2048
- # 添加預測點
2049
- fig.add_trace(go.Scatter(
2050
- x=[point_future_date],
2051
- y=[point_predicted_price],
2052
- mode='markers',
2053
- name=f'{d}日預測點',
2054
- marker=dict(size=10, color=point_color)
2055
- ))
2056
-
2057
- # 為使用者選擇的最終天數添加趨勢線
2058
- final_future_date = recent_data.index[-1] + timedelta(days=predict_days)
2059
  fig.add_trace(go.Scatter(
2060
- x=[recent_data.index[-1], final_future_date],
2061
  y=[current_price, predicted_price],
2062
- mode='lines',
2063
- name='最終預測線',
2064
- line=dict(color=color, width=3, dash='dash')
 
2065
  ))
2066
 
2067
- # --- 修正結束 ---
2068
-
2069
  fig.update_layout(
2070
  title=f'台指期 {predict_days}日預測走勢',
2071
  xaxis_title='日期',
@@ -2859,4 +1395,4 @@ if __name__ == '__main__':
2859
  print(f"PMI資料預覽:\n{pmi_data.head()}")
2860
 
2861
  # 在 Hugging Face Spaces 中執行
2862
- app.run(host="0.0.0.0", port=7860, debug=TRUE)
 
256
  dcc.Dropdown(
257
  id='taiex-prediction-period',
258
  options=[
 
259
  {'label': '5日後預測', 'value': 5},
260
  {'label': '10日後預測', 'value': 10},
261
  {'label': '20日後預測', 'value': 20},
 
591
  line=dict(color='#FFA726', width=2)
592
  ))
593
 
594
+ # 預測點
595
+ future_date = recent_data.index[-1] + timedelta(days=predict_days)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
596
  fig.add_trace(go.Scatter(
597
+ x=[recent_data.index[-1], future_date],
598
  y=[current_price, predicted_price],
599
+ mode='lines+markers',
600
+ name=f'{predict_days}日預測',
601
+ line=dict(color=color, width=3, dash='dash'),
602
+ marker=dict(size=8)
603
  ))
604
 
 
 
605
  fig.update_layout(
606
  title=f'台指期 {predict_days}日預測走勢',
607
  xaxis_title='日期',
 
1395
  print(f"PMI資料預覽:\n{pmi_data.head()}")
1396
 
1397
  # 在 Hugging Face Spaces 中執行
1398
+ app.run(host="0.0.0.0", port=7860, debug=False)