AlanRex commited on
Commit
0e2b2bc
·
1 Parent(s): add640d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +233 -101
app.py CHANGED
@@ -6,7 +6,6 @@ from datetime import datetime, timedelta
6
  import pandas as pd
7
  import numpy as np
8
  import yfinance as yf
9
- import pandas_ta as ta # 新增:技術分析套件
10
 
11
  # Dash & Plotly
12
  from dash import Dash, dcc, html, callback
@@ -15,9 +14,9 @@ import plotly.express as px
15
  import plotly.graph_objects as go
16
  from plotly.subplots import make_subplots
17
 
18
- # 台股代號對應表
19
  TAIWAN_STOCKS = {
20
- '元大台灣50': '0050.TW',
21
  '台積電': '2330.TW',
22
  '聯發科': '2454.TW',
23
  '鴻海': '2317.TW',
@@ -32,6 +31,8 @@ TAIWAN_STOCKS = {
32
  '慧洋-KY': '2637.TW',
33
  '上銀': '2049.TW',
34
  '台泥': '1101.TW',
 
 
35
  '譜瑞-KY': '4966.TWO',
36
  '貿聯-KY': '3665.TW',
37
  '騰雲': '6870.TWO',
@@ -40,7 +41,7 @@ TAIWAN_STOCKS = {
40
 
41
  # 產業分類
42
  INDUSTRY_MAPPING = {
43
- '0050.TW': 'ETF',
44
  '2330.TW': '半導體',
45
  '2454.TW': '半導體',
46
  '2317.TW': '電子組件',
@@ -55,6 +56,9 @@ INDUSTRY_MAPPING = {
55
  '2637.TW': '散裝航運',
56
  '2049.TW': '工具機',
57
  '1101.TW': '營建',
 
 
 
58
  '4966.TWO': '高速傳輸',
59
  '3665.TW': '連接器',
60
  '6870.TWO': '軟體整合',
@@ -172,10 +176,6 @@ def calculate_technical_indicators(df):
172
  high_max_14 = df['High'].rolling(window=14).max()
173
  df['Williams_R'] = -100 * (high_max_14 - df['Close']) / (high_max_14 - low_min_14)
174
 
175
- # 新增:DMI 指標 (使用 pandas_ta)
176
- # 這會自動新增 'DMP_14', 'DMN_14', 'ADX_14' 欄位
177
- df.ta.dmi(append=True)
178
-
179
  return df
180
 
181
  def get_business_climate_data():
@@ -401,8 +401,7 @@ app.layout = html.Div([
401
  {'label': 'MACD 指數平滑異同移動平均線', 'value': 'MACD'},
402
  {'label': '布林通道 Bollinger Bands', 'value': 'BB'},
403
  {'label': 'KD 隨機指標', 'value': 'KD'},
404
- {'label': '威廉指標 %R', 'value': 'WR'},
405
- {'label': 'DMI 動向指標', 'value': 'DMI'} # 新增
406
  ],
407
  value='RSI',
408
  style={'width': '100%'}
@@ -500,7 +499,9 @@ app.layout = html.Div([
500
  ])
501
  ], style={'margin-top': '30px'}),
502
 
503
- # 多檔股票比較區域
 
 
504
  html.Div([
505
  html.H3("📊 多檔股票比較分析", style={'margin-bottom': '20px'}),
506
  html.Div([
@@ -509,10 +510,11 @@ app.layout = html.Div([
509
  dcc.Dropdown(
510
  id='comparison-stocks',
511
  options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
512
- value=['0050.TW', '2330.TW', '2454.TW'],
513
  multi=True,
514
- style={'margin-bottom': '5px'}
515
  ),
 
516
  html.Small(
517
  '(元大台灣50 (0050.TW) 為固定比較基準,不可移除)',
518
  style={'display': 'block', 'font-style': 'italic', 'color': 'gray'}
@@ -740,7 +742,9 @@ def update_stock_info(selected_stock):
740
  })
741
  ])
742
 
743
- # 主要圖表回呼函式 (合併股價與成交量分佈)
 
 
744
  @app.callback(
745
  dash.dependencies.Output('price-chart', 'figure'),
746
  [dash.dependencies.Input('stock-dropdown', 'value'),
@@ -756,15 +760,17 @@ def update_price_chart(selected_stock, period, chart_type):
756
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
757
 
758
  # --- 1. 建立共享 Y 軸的子圖 ---
 
759
  fig = make_subplots(
760
  rows=1, cols=2,
761
  shared_yaxes=True,
762
- column_widths=[0.8, 0.2],
763
- horizontal_spacing=0.01
764
  )
765
 
766
  # --- 2. 在左側子圖 (col=1) 繪製股價圖 ---
767
  if chart_type == 'candlestick':
 
768
  fig.add_trace(go.Candlestick(
769
  x=data.index,
770
  open=data['Open'],
@@ -772,12 +778,13 @@ def update_price_chart(selected_stock, period, chart_type):
772
  low=data['Low'],
773
  close=data['Close'],
774
  name=stock_name,
775
- increasing_line_color='red',
776
- decreasing_line_color='green'
777
  ), row=1, col=1)
778
  else:
779
  fig.add_trace(px.line(data, y='Close').data[0], row=1, col=1)
780
 
 
781
  fig.add_trace(go.Scatter(
782
  x=data.index, y=data['MA5'], mode='lines',
783
  name='MA5', line=dict(color='orange')
@@ -788,15 +795,17 @@ def update_price_chart(selected_stock, period, chart_type):
788
  ), row=1, col=1)
789
 
790
  # --- 3. 在右側子圖 (col=2) 繪製成交量分佈圖 ---
 
791
  bin_edges, volume_per_bin, price_centers = calculate_volume_profile(data, num_bins=50)
792
 
793
  if volume_per_bin is not None:
 
794
  fig.add_trace(go.Bar(
795
  orientation='h',
796
  y=price_centers,
797
  x=volume_per_bin,
798
  name='Volume Profile',
799
- text=[f'{vol/1000:.0f}k' for vol in volume_per_bin],
800
  textposition='auto',
801
  marker=dict(
802
  color='rgba(173, 216, 230, 0.6)',
@@ -809,28 +818,33 @@ def update_price_chart(selected_stock, period, chart_type):
809
  title_text=f'{stock_name} 股價走勢與成交量分佈',
810
  height=500,
811
  showlegend=True,
 
 
812
  xaxis1=dict(
813
  title='日期',
814
  type='date',
815
- rangeslider_visible=False
816
  ),
817
  yaxis1=dict(
818
  title='價格 (TWD)'
819
  ),
 
 
820
  xaxis2=dict(
821
  title='成交量',
822
- showticklabels=True
823
  ),
824
  yaxis2=dict(
825
- showticklabels=False
826
  ),
827
- bargap=0.05
 
828
  )
829
 
830
  return fig
831
 
832
 
833
- # 進階技術指標圖表
834
  @app.callback(
835
  dash.dependencies.Output('advanced-technical-chart', 'figure'),
836
  [dash.dependencies.Input('technical-indicator-selector', 'value'),
@@ -848,11 +862,15 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
848
  if indicator == 'RSI':
849
  fig = go.Figure()
850
  fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
 
851
  fig.add_hline(y=70, line_dash="dash", line_color="green", annotation_text="超買線(70)")
852
  fig.add_hline(y=30, line_dash="dash", line_color="red", annotation_text="超賣線(30)")
853
  fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)")
 
 
854
  fig.add_hrect(y0=70, y1=100, fillcolor="green", opacity=0.1)
855
  fig.add_hrect(y0=0, y1=30, fillcolor="red", opacity=0.1)
 
856
  fig.update_layout(
857
  title=f'{stock_name} - RSI 相對強弱指標',
858
  xaxis_title='日期',
@@ -862,10 +880,13 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
862
  )
863
 
864
  elif indicator == 'MACD':
 
865
  fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
866
- vertical_spacing=0.1,
867
- row_heights=[0.7, 0.3],
868
- subplot_titles=('價格走勢', 'MACD 指標'))
 
 
869
  fig.add_trace(go.Scatter(
870
  x=data.index,
871
  y=data['Close'],
@@ -873,6 +894,9 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
873
  name='收盤價',
874
  line=dict(color='black', width=1.5)
875
  ), row=1, col=1)
 
 
 
876
  fig.add_trace(go.Scatter(
877
  x=data.index,
878
  y=data['MACD'],
@@ -880,6 +904,8 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
880
  name='MACD (快線)',
881
  line=dict(color='blue', width=2)
882
  ), row=2, col=1)
 
 
883
  fig.add_trace(go.Scatter(
884
  x=data.index,
885
  y=data['MACD_Signal'],
@@ -887,6 +913,9 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
887
  name='Signal (慢線)',
888
  line=dict(color='red', width=2)
889
  ), row=2, col=1)
 
 
 
890
  colors = ['red' if x >= 0 else 'green' for x in data['MACD_Histogram']]
891
  fig.add_trace(go.Bar(
892
  x=data.index,
@@ -894,30 +923,46 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
894
  name='MACD柱狀圖',
895
  marker_color=colors
896
  ), row=2, col=1)
 
 
897
  fig.add_hline(y=0, line_dash="dash", line_color="gray", row=2, col=1)
 
 
898
  fig.update_layout(
899
  title_text=f'{stock_name} - MACD 指數平滑異同移動平均線',
900
- height=550,
901
  legend_title_text='圖例',
902
- showlegend=True
903
  )
 
904
  fig.update_traces(showlegend=False, selector=dict(type='bar'))
905
 
906
  elif indicator == 'BB':
907
  fig = go.Figure()
 
 
908
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
909
  line=dict(color='black', width=2)))
 
 
910
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines', name='上軌',
911
  line=dict(color='red', width=1, dash='dash')))
 
 
912
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Middle'], mode='lines', name='中軌(MA20)',
913
  line=dict(color='blue', width=1)))
 
 
914
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines', name='下軌',
915
  line=dict(color='green', width=1, dash='dash')))
 
 
916
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines',
917
  line=dict(color='rgba(0,0,0,0)'), showlegend=False))
918
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines',
919
  fill='tonexty', fillcolor='rgba(173,216,230,0.2)',
920
  line=dict(color='rgba(0,0,0,0)'), name='布林通道', showlegend=False))
 
921
  fig.update_layout(
922
  title=f'{stock_name} - 布林通道 (20日, 2σ)',
923
  xaxis_title='日期',
@@ -930,17 +975,28 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
930
  vertical_spacing=0.1,
931
  row_heights=[0.6, 0.4],
932
  subplot_titles=('價格走勢', 'KD指標'))
 
 
933
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
934
  line=dict(color='black', width=1)), row=1, col=1)
 
 
935
  fig.add_trace(go.Scatter(x=data.index, y=data['K'], mode='lines', name='K線',
936
  line=dict(color='blue', width=2)), row=2, col=1)
937
  fig.add_trace(go.Scatter(x=data.index, y=data['D'], mode='lines', name='D線',
938
  line=dict(color='red', width=2)), row=2, col=1)
 
 
 
939
  fig.add_hline(y=80, line_dash="dash", line_color="green", annotation_text="超買線(80)", row=2, col=1)
940
  fig.add_hline(y=20, line_dash="dash", line_color="red", annotation_text="超賣線(20)", row=2, col=1)
941
  fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)", row=2, col=1)
 
 
 
942
  fig.add_hrect(y0=80, y1=100, fillcolor="green", opacity=0.1, row=2, col=1)
943
  fig.add_hrect(y0=0, y1=20, fillcolor="red", opacity=0.1, row=2, col=1)
 
944
  fig.update_layout(
945
  title=f'{stock_name} - KD 隨機指標 (9,3,3)',
946
  height=500
@@ -952,56 +1008,32 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
952
  vertical_spacing=0.1,
953
  row_heights=[0.6, 0.4],
954
  subplot_titles=('價格走勢', '威廉指標 %R'))
 
 
955
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
956
  line=dict(color='black', width=1)), row=1, col=1)
 
 
957
  fig.add_trace(go.Scatter(x=data.index, y=data['Williams_R'], mode='lines', name='威廉%R',
958
  line=dict(color='purple', width=2)), row=2, col=1)
 
 
 
959
  fig.add_hline(y=-20, line_dash="dash", line_color="green", annotation_text="超買線(-20)", row=2, col=1)
960
  fig.add_hline(y=-80, line_dash="dash", line_color="red", annotation_text="超賣線(-80)", row=2, col=1)
961
  fig.add_hline(y=-50, line_dash="dot", line_color="gray", annotation_text="中線(-50)", row=2, col=1)
 
 
 
962
  fig.add_hrect(y0=-20, y1=0, fillcolor="green", opacity=0.1, row=2, col=1)
963
  fig.add_hrect(y0=-100, y1=-80, fillcolor="red", opacity=0.1, row=2, col=1)
 
964
  fig.update_layout(
965
  title=f'{stock_name} - 威廉指標 %R (14日)',
966
  height=500
967
  )
968
  fig.update_yaxes(range=[-100, 0], row=2, col=1)
969
 
970
- # ==============================================================================
971
- # ===== 新增 DMI 圖表繪製邏輯 =====
972
- # ==============================================================================
973
- elif indicator == 'DMI':
974
- fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
975
- vertical_spacing=0.1,
976
- row_heights=[0.6, 0.4],
977
- subplot_titles=('價格走勢', 'DMI 動向指標 (14日)'))
978
-
979
- # 上方:價格線
980
- fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
981
- line=dict(color='black', width=1)), row=1, col=1)
982
-
983
- # 下方:DMI 指標線
984
- # +DI (pandas-ta 欄位為 DMP_14)
985
- fig.add_trace(go.Scatter(x=data.index, y=data['DMP_14'], mode='lines', name='+DI (上升趨向)',
986
- line=dict(color='green', width=2)), row=2, col=1)
987
- # -DI (pandas-ta 欄位為 DMN_14)
988
- fig.add_trace(go.Scatter(x=data.index, y=data['DMN_14'], mode='lines', name='-DI (下降趨向)',
989
- line=dict(color='red', width=2)), row=2, col=1)
990
- # ADX (pandas-ta 欄位為 ADX_14)
991
- fig.add_trace(go.Scatter(x=data.index, y=data['ADX_14'], mode='lines', name='ADX (趨勢強度)',
992
- line=dict(color='blue', width=1.5, dash='dash')), row=2, col=1)
993
-
994
- # ADX 趨勢強度參考線
995
- fig.add_hline(y=25, line_dash="dot", line_color="gray",
996
- annotation_text="趨勢增強線(25)", row=2, col=1)
997
-
998
- fig.update_layout(
999
- title=f'{stock_name} - DMI 動向指標',
1000
- height=500
1001
- )
1002
- fig.update_yaxes(title_text="指標值", row=2, col=1)
1003
-
1004
-
1005
  return fig
1006
 
1007
  # 更新成交量圖表
@@ -1077,12 +1109,13 @@ def update_industry_analysis(selected_stock):
1077
  # 新增:更新景氣燈號圖表
1078
  @app.callback(
1079
  dash.dependencies.Output('business-climate-chart', 'figure'),
1080
- [dash.dependencies.Input('stock-dropdown', 'value')]
1081
  )
1082
  def update_business_climate_chart(selected_stock):
1083
  df = get_business_climate_data()
1084
 
1085
  if df.empty:
 
1086
  fig = go.Figure()
1087
  fig.add_annotation(
1088
  x=0.5, y=0.5,
@@ -1098,16 +1131,24 @@ def update_business_climate_chart(selected_stock):
1098
  )
1099
  return fig
1100
 
 
1101
  def get_light_color(score):
1102
- if score >= 32: return 'red'
1103
- elif score >= 24: return 'orange'
1104
- elif score >= 17: return 'yellow'
1105
- elif score >= 10: return 'lightgreen'
1106
- else: return 'blue'
1107
-
 
 
 
 
 
 
1108
  colors = [get_light_color(score) for score in df['Index']]
1109
 
1110
  fig = go.Figure()
 
1111
  fig.add_trace(go.Scatter(
1112
  x=df['Date'],
1113
  y=df['Index'],
@@ -1120,10 +1161,13 @@ def update_business_climate_chart(selected_stock):
1120
  line=dict(width=2, color='darkblue')
1121
  )
1122
  ))
 
 
1123
  fig.add_hline(y=32, line_dash="dash", line_color="red", annotation_text="紅燈(32)")
1124
  fig.add_hline(y=24, line_dash="dash", line_color="orange", annotation_text="黃紅燈(24)")
1125
  fig.add_hline(y=17, line_dash="dash", line_color="yellow", annotation_text="黃燈(17)")
1126
  fig.add_hline(y=10, line_dash="dash", line_color="lightgreen", annotation_text="黃藍燈(10)")
 
1127
  fig.update_layout(
1128
  title="台灣景氣燈號走勢",
1129
  xaxis_title='日期',
@@ -1131,6 +1175,7 @@ def update_business_climate_chart(selected_stock):
1131
  height=300,
1132
  yaxis=dict(range=[0, 40])
1133
  )
 
1134
  return fig
1135
 
1136
  # 新增:更新分析師觀點
@@ -1142,23 +1187,32 @@ def update_business_climate_chart(selected_stock):
1142
  dash.dependencies.Input('period-dropdown', 'value')]
1143
  )
1144
  def update_analysis_text(selected_stock, period):
 
1145
  data = get_stock_data(selected_stock, period)
1146
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
 
1147
  if data.empty:
1148
  return "無法獲取資料進行分析", "無法獲取資料進行分析", "無法獲取資料進行分析"
1149
 
 
1150
  data = calculate_technical_indicators(data)
 
 
1151
  current_price = data['Close'].iloc[-1]
1152
  price_change = ((current_price - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
1153
  volume_avg = data['Volume'].mean()
1154
  recent_volume = data['Volume'].iloc[-5:].mean()
1155
  rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
 
 
1156
  macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
1157
  macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
1158
  bb_position = data['BB_Position'].iloc[-1] if not pd.isna(data['BB_Position'].iloc[-1]) else 0.5
1159
  k_current = data['K'].iloc[-1] if not pd.isna(data['K'].iloc[-1]) else 50
1160
  d_current = data['D'].iloc[-1] if not pd.isna(data['D'].iloc[-1]) else 50
1161
 
 
 
1162
  technical_text = html.Div([
1163
  html.P([
1164
  html.Strong("價格趨勢:"),
@@ -1173,7 +1227,8 @@ def update_analysis_text(selected_stock, period):
1173
  html.Span(
1174
  "處於超買區間" if rsi_current > 70 else "處於超賣區間" if rsi_current < 30 else "在正常範圍內",
1175
  style={'color': 'green' if rsi_current > 70 else 'red' if rsi_current < 30 else 'blue', 'font-weight': 'bold'}
1176
- ), "。"
 
1177
  ]),
1178
  html.P([
1179
  html.Strong("MACD指標:"),
@@ -1181,7 +1236,9 @@ def update_analysis_text(selected_stock, period):
1181
  html.Span(
1182
  "高於" if macd_current > macd_signal_current else "低於",
1183
  style={'color': 'red' if macd_current > macd_signal_current else 'green', 'font-weight': 'bold'}
1184
- ), f"信號線({macd_signal_current:.3f}),", f"顯示{'多頭' if macd_current > macd_signal_current else '空頭'}格局。"
 
 
1185
  ]),
1186
  html.P([
1187
  html.Strong("布林通道:"),
@@ -1189,7 +1246,9 @@ def update_analysis_text(selected_stock, period):
1189
  html.Span(
1190
  "上半部" if bb_position > 0.8 else "下半部" if bb_position < 0.2 else "中段",
1191
  style={'color': 'green' if bb_position > 0.8 else 'red' if bb_position < 0.2 else 'blue', 'font-weight': 'bold'}
1192
- ), f"({bb_position*100:.0f}%),", f"{'壓力較大' if bb_position > 0.8 else '支撐較強' if bb_position < 0.2 else '整理格局'}。"
 
 
1193
  ]),
1194
  html.P([
1195
  html.Strong("KD指標:"),
@@ -1197,11 +1256,13 @@ def update_analysis_text(selected_stock, period):
1197
  html.Span(
1198
  "高於" if k_current > d_current else "低於",
1199
  style={'color': 'red' if k_current > d_current else 'green', 'font-weight': 'bold'}
1200
- ), f"D值({d_current:.1f}),",
 
1201
  html.Span(
1202
  "超買警戒" if k_current > 80 else "超賣關注" if k_current < 20 else "正常區間",
1203
  style={'color': 'green' if k_current > 80 else 'red' if k_current < 20 else 'blue', 'font-weight': 'bold'}
1204
- ), "。"
 
1205
  ]),
1206
  html.P([
1207
  html.Strong("成交量分析:"),
@@ -1210,13 +1271,15 @@ def update_analysis_text(selected_stock, period):
1210
  ])
1211
  ])
1212
 
 
1213
  industry = INDUSTRY_MAPPING.get(selected_stock, '綜合')
1214
  fundamental_text = html.Div([
1215
  html.P([
1216
  html.Strong("產業地位:"),
1217
  f"{stock_name}屬於{industry}產業,在產業鏈中具有",
1218
  html.Span("重要地位" if selected_stock in ['2330.TW', '2454.TW', '2317.TW'] else "一定影響力",
1219
- style={'font-weight': 'bold'}), "。"
 
1220
  ]),
1221
  html.P([
1222
  html.Strong("營運展望:"),
@@ -1228,18 +1291,24 @@ def update_analysis_text(selected_stock, period):
1228
  ])
1229
  ])
1230
 
 
 
1231
  if price_change > 10:
1232
- outlook_tone = "謹慎樂觀"; outlook_color = "#dc3545"
 
1233
  elif price_change < -10:
1234
- outlook_tone = "保守觀望"; outlook_color = "#28a745"
 
1235
  else:
1236
- outlook_tone = "中性持平"; outlook_color = "#ffc107"
 
1237
 
1238
  market_outlook = html.Div([
1239
  html.P([
1240
  html.Strong("整體評估:", style={'font-size': '16px'}),
1241
  f"基於技術面及基本面分析,對{stock_name}採取",
1242
- html.Span(f"{outlook_tone}", style={'color': outlook_color, 'font-weight': 'bold', 'font-size': '16px'}), "態度。"
 
1243
  ]),
1244
  html.P([
1245
  html.Strong("投資建議:"),
@@ -1256,11 +1325,13 @@ def update_analysis_text(selected_stock, period):
1256
  # 新增:更新PMI圖表
1257
  @app.callback(
1258
  dash.dependencies.Output('pmi-chart', 'figure'),
1259
- [dash.dependencies.Input('stock-dropdown', 'value')]
1260
  )
1261
  def update_pmi_chart(selected_stock):
1262
  df = get_pmi_data()
 
1263
  if df.empty:
 
1264
  fig = go.Figure()
1265
  fig.add_annotation(
1266
  x=0.5, y=0.5,
@@ -1276,26 +1347,44 @@ def update_pmi_chart(selected_stock):
1276
  )
1277
  return fig
1278
 
 
 
1279
  def get_pmi_color(value):
1280
  return 'red' if value >= 50 else 'green'
1281
 
1282
  colors = [get_pmi_color(value) for value in df['Index']]
1283
 
1284
  fig = go.Figure()
 
1285
  fig.add_trace(go.Scatter(
1286
- x=df['Date'], y=df['Index'], mode='lines+markers', name='PMI指數',
 
 
 
1287
  line=dict(color='darkblue', width=2),
1288
- marker=dict(size=8, color=colors, line=dict(width=2, color='darkblue'))
 
 
 
 
1289
  ))
 
 
1290
  fig.add_hline(y=50, line_dash="dash", line_color="black", annotation_text="榮枯線(50)")
 
 
 
1291
  fig.add_hrect(
1292
- y0=50, y1=60, fillcolor="lightcoral", opacity=0.2,
 
1293
  annotation_text="擴張區間", annotation_position="top left"
1294
  )
1295
  fig.add_hrect(
1296
- y0=40, y1=50, fillcolor="lightgreen", opacity=0.2,
 
1297
  annotation_text="緊縮區間", annotation_position="bottom left"
1298
  )
 
1299
  fig.update_layout(
1300
  title="台灣PMI指數走勢",
1301
  xaxis_title='日期',
@@ -1303,9 +1392,13 @@ def update_pmi_chart(selected_stock):
1303
  height=300,
1304
  yaxis=dict(range=[35, 60])
1305
  )
 
1306
  return fig
1307
 
1308
- # 多檔股票比較回呼函式
 
 
 
1309
  @app.callback(
1310
  [dash.dependencies.Output('comparison-chart', 'figure'),
1311
  dash.dependencies.Output('comparison-table', 'children')],
@@ -1313,12 +1406,20 @@ def update_pmi_chart(selected_stock):
1313
  dash.dependencies.Input('comparison-period', 'value')]
1314
  )
1315
  def update_comparison_analysis(selected_stocks, period):
 
1316
  fixed_stock = '0050.TW'
 
1317
  if not selected_stocks:
1318
  selected_stocks = [fixed_stock]
 
1319
  elif fixed_stock not in selected_stocks:
1320
  selected_stocks.insert(0, fixed_stock)
 
 
 
 
1321
 
 
1322
  selected_stocks = selected_stocks[:5]
1323
 
1324
  fig = go.Figure()
@@ -1327,8 +1428,12 @@ def update_comparison_analysis(selected_stocks, period):
1327
  for stock in selected_stocks:
1328
  data = get_stock_data(stock, period)
1329
  if not data.empty:
 
1330
  stock_name = next((name for name, symbol in TAIWAN_STOCKS.items() if symbol == stock), stock)
 
 
1331
  normalized_prices = (data['Close'] / data['Close'].iloc[0]) * 100
 
1332
  fig.add_trace(go.Scatter(
1333
  x=data.index,
1334
  y=normalized_prices,
@@ -1336,8 +1441,11 @@ def update_comparison_analysis(selected_stocks, period):
1336
  name=stock_name,
1337
  line=dict(width=2)
1338
  ))
 
 
1339
  total_return = ((data['Close'].iloc[-1] / data['Close'].iloc[0]) - 1) * 100
1340
- volatility = data['Close'].pct_change().std() * np.sqrt(252) * 100
 
1341
  comparison_data.append({
1342
  'name': stock_name,
1343
  'return': total_return,
@@ -1353,9 +1461,11 @@ def update_comparison_analysis(selected_stocks, period):
1353
  hovermode='x unified'
1354
  )
1355
 
 
1356
  if comparison_data:
1357
  table_rows = []
1358
  for item in sorted(comparison_data, key=lambda x: x['return'], reverse=True):
 
1359
  color = 'red' if item['return'] > 0 else 'green'
1360
  table_rows.append(
1361
  html.Tr([
@@ -1365,27 +1475,39 @@ def update_comparison_analysis(selected_stocks, period):
1365
  html.Td(f"${item['current_price']:.2f}")
1366
  ])
1367
  )
 
1368
  table = html.Table([
1369
- html.Thead(html.Tr([
1370
- html.Th("股票", style={'text-align': 'center'}),
1371
- html.Th("報酬率", style={'text-align': 'center'}),
1372
- html.Th("波動率", style={'text-align': 'center'}),
1373
- html.Th("現價", style={'text-align': 'center'})
1374
- ])),
 
 
1375
  html.Tbody(table_rows)
1376
- ], style={'width': '100%', 'border-collapse': 'collapse', 'font-size': '12px'})
 
 
 
 
 
1377
  return fig, table
1378
 
1379
  return fig, html.Div("無可比較資料")
1380
 
1381
- # 市場情緒分析
1382
  @app.callback(
1383
  [dash.dependencies.Output('sentiment-gauge', 'children'),
1384
  dash.dependencies.Output('news-summary', 'children')],
1385
  [dash.dependencies.Input('stock-dropdown', 'value')]
1386
  )
1387
  def update_sentiment_analysis(selected_stock):
1388
- sentiment_score = np.random.uniform(30, 80)
 
 
 
 
1389
  gauge_fig = go.Figure(go.Indicator(
1390
  mode = "gauge+number+delta",
1391
  value = sentiment_score,
@@ -1407,9 +1529,12 @@ def update_sentiment_analysis(selected_stock):
1407
  }
1408
  }
1409
  ))
 
1410
  gauge_fig.update_layout(height=200, margin=dict(l=20, r=20, t=40, b=20))
1411
 
 
1412
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
 
1413
  news_items = [
1414
  f"📈 {stock_name}獲外資調升目標價,看好後續發展前景",
1415
  f"💼 法人預期{stock_name}下季營收將較上季成長5-10%",
@@ -1417,17 +1542,23 @@ def update_sentiment_analysis(selected_stock):
1417
  f"⚡ 產業景氣回溫,{stock_name}受惠程度值得關注",
1418
  f"📊 技術面顯示{stock_name}突破關鍵壓力,短線偏多"
1419
  ]
 
1420
  news_content = html.Div([
1421
  html.P(news, style={
1422
- 'margin': '8px 0', 'padding': '8px', 'background': '#f8f9fa',
1423
- 'border-radius': '5px', 'border-left': '3px solid #17a2b8', 'font-size': '13px'
1424
- }) for news in news_items[:3]
 
 
 
 
1425
  ])
1426
 
1427
  return dcc.Graph(figure=gauge_fig), news_content
1428
 
1429
- # 主程式執行
1430
  if __name__ == '__main__':
 
1431
  print("測試檔案讀取...")
1432
  business_data = get_business_climate_data()
1433
  pmi_data = get_pmi_data()
@@ -1437,4 +1568,5 @@ if __name__ == '__main__':
1437
  if not pmi_data.empty:
1438
  print(f"PMI資料預覽:\n{pmi_data.head()}")
1439
 
 
1440
  app.run(host="0.0.0.0", port=7860, debug=False)
 
6
  import pandas as pd
7
  import numpy as np
8
  import yfinance as yf
 
9
 
10
  # Dash & Plotly
11
  from dash import Dash, dcc, html, callback
 
14
  import plotly.graph_objects as go
15
  from plotly.subplots import make_subplots
16
 
17
+ # 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
18
  TAIWAN_STOCKS = {
19
+ '元大台灣50': '0050.TW', # 新增
20
  '台積電': '2330.TW',
21
  '聯發科': '2454.TW',
22
  '鴻海': '2317.TW',
 
31
  '慧洋-KY': '2637.TW',
32
  '上銀': '2049.TW',
33
  '台泥': '1101.TW',
34
+ '南亞科': '2408.TW',
35
+ '旺宏': '2337.TW',
36
  '譜瑞-KY': '4966.TWO',
37
  '貿聯-KY': '3665.TW',
38
  '騰雲': '6870.TWO',
 
41
 
42
  # 產業分類
43
  INDUSTRY_MAPPING = {
44
+ '0050.TW': 'ETF', # 新增
45
  '2330.TW': '半導體',
46
  '2454.TW': '半導體',
47
  '2317.TW': '電子組件',
 
56
  '2637.TW': '散裝航運',
57
  '2049.TW': '工具機',
58
  '1101.TW': '營建',
59
+ '2408.TW': 'DRAM',
60
+ '2337.TW': 'NFLSH',
61
+ '1101.TW': '營建',
62
  '4966.TWO': '高速傳輸',
63
  '3665.TW': '連接器',
64
  '6870.TWO': '軟體整合',
 
176
  high_max_14 = df['High'].rolling(window=14).max()
177
  df['Williams_R'] = -100 * (high_max_14 - df['Close']) / (high_max_14 - low_min_14)
178
 
 
 
 
 
179
  return df
180
 
181
  def get_business_climate_data():
 
401
  {'label': 'MACD 指數平滑異同移動平均線', 'value': 'MACD'},
402
  {'label': '布林通道 Bollinger Bands', 'value': 'BB'},
403
  {'label': 'KD 隨機指標', 'value': 'KD'},
404
+ {'label': '威廉指標 %R', 'value': 'WR'}
 
405
  ],
406
  value='RSI',
407
  style={'width': '100%'}
 
499
  ])
500
  ], style={'margin-top': '30px'}),
501
 
502
+ # ==============================================================================
503
+ # ===== 修改後的多檔股票比較區域 =====
504
+ # ==============================================================================
505
  html.Div([
506
  html.H3("📊 多檔股票比較分析", style={'margin-bottom': '20px'}),
507
  html.Div([
 
510
  dcc.Dropdown(
511
  id='comparison-stocks',
512
  options=[{'label': name, 'value': symbol} for name, symbol in TAIWAN_STOCKS.items()],
513
+ value=['0050.TW', '2330.TW', '2454.TW'], # 修改:預設包含0050
514
  multi=True,
515
+ style={'margin-bottom': '5px'} # 調整間距
516
  ),
517
+ # 新增:提示文字
518
  html.Small(
519
  '(元大台灣50 (0050.TW) 為固定比較基準,不可移除)',
520
  style={'display': 'block', 'font-style': 'italic', 'color': 'gray'}
 
742
  })
743
  ])
744
 
745
+ # ==============================================================================
746
+ # ===== 修改後的主要圖表回呼函式 (合併股價與成交量分佈) =====
747
+ # ==============================================================================
748
  @app.callback(
749
  dash.dependencies.Output('price-chart', 'figure'),
750
  [dash.dependencies.Input('stock-dropdown', 'value'),
 
760
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
761
 
762
  # --- 1. 建立共享 Y 軸的子圖 ---
763
+ # 建立一個 1x2 的網格,設定欄位寬度比例,並共享 Y 軸
764
  fig = make_subplots(
765
  rows=1, cols=2,
766
  shared_yaxes=True,
767
+ column_widths=[0.8, 0.2], # 左側圖佔80%,右側圖佔20%
768
+ horizontal_spacing=0.01 # 子圖間的水平間距
769
  )
770
 
771
  # --- 2. 在左側子圖 (col=1) 繪製股價圖 ---
772
  if chart_type == 'candlestick':
773
+ # 根據台股慣例修改顏色
774
  fig.add_trace(go.Candlestick(
775
  x=data.index,
776
  open=data['Open'],
 
778
  low=data['Low'],
779
  close=data['Close'],
780
  name=stock_name,
781
+ increasing_line_color='red', # 上漲為紅色
782
+ decreasing_line_color='green' # 下跌為綠色
783
  ), row=1, col=1)
784
  else:
785
  fig.add_trace(px.line(data, y='Close').data[0], row=1, col=1)
786
 
787
+ # 添加移動平均線到左側子圖
788
  fig.add_trace(go.Scatter(
789
  x=data.index, y=data['MA5'], mode='lines',
790
  name='MA5', line=dict(color='orange')
 
795
  ), row=1, col=1)
796
 
797
  # --- 3. 在右側子圖 (col=2) 繪製成交量分佈圖 ---
798
+ # 計算 Volume Profile 數據
799
  bin_edges, volume_per_bin, price_centers = calculate_volume_profile(data, num_bins=50)
800
 
801
  if volume_per_bin is not None:
802
+ # 繪製水平長條圖
803
  fig.add_trace(go.Bar(
804
  orientation='h',
805
  y=price_centers,
806
  x=volume_per_bin,
807
  name='Volume Profile',
808
+ text=[f'{vol/1000:.0f}k' for vol in volume_per_bin], # 顯示成交量
809
  textposition='auto',
810
  marker=dict(
811
  color='rgba(173, 216, 230, 0.6)',
 
818
  title_text=f'{stock_name} 股價走勢與成交量分佈',
819
  height=500,
820
  showlegend=True,
821
+
822
+ # 左側子圖的座標軸設定
823
  xaxis1=dict(
824
  title='日期',
825
  type='date',
826
+ rangeslider_visible=False # 隱藏範圍滑桿,避免干擾佈局
827
  ),
828
  yaxis1=dict(
829
  title='價格 (TWD)'
830
  ),
831
+
832
+ # 右側子圖的座標軸設定
833
  xaxis2=dict(
834
  title='成交量',
835
+ showticklabels=True # 顯示刻度
836
  ),
837
  yaxis2=dict(
838
+ showticklabels=False # 因為共享Y軸,所以隱藏右側的Y軸標籤
839
  ),
840
+
841
+ bargap=0.05 # 長條圖間的間隙
842
  )
843
 
844
  return fig
845
 
846
 
847
+ # 新增:進階技術指標圖表
848
  @app.callback(
849
  dash.dependencies.Output('advanced-technical-chart', 'figure'),
850
  [dash.dependencies.Input('technical-indicator-selector', 'value'),
 
862
  if indicator == 'RSI':
863
  fig = go.Figure()
864
  fig.add_trace(go.Scatter(x=data.index, y=data['RSI'], mode='lines', name='RSI', line=dict(color='purple', width=2)))
865
+ # 根據台股慣例修改顏色
866
  fig.add_hline(y=70, line_dash="dash", line_color="green", annotation_text="超買線(70)")
867
  fig.add_hline(y=30, line_dash="dash", line_color="red", annotation_text="超賣線(30)")
868
  fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)")
869
+
870
+ # 根據台股慣例修改顏色
871
  fig.add_hrect(y0=70, y1=100, fillcolor="green", opacity=0.1)
872
  fig.add_hrect(y0=0, y1=30, fillcolor="red", opacity=0.1)
873
+
874
  fig.update_layout(
875
  title=f'{stock_name} - RSI 相對強弱指標',
876
  xaxis_title='日期',
 
880
  )
881
 
882
  elif indicator == 'MACD':
883
+ # 建立兩個垂直排列的子圖,並共享X軸
884
  fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
885
+ vertical_spacing=0.1, # 子圖間的垂直間距
886
+ row_heights=[0.7, 0.3], # 上方圖佔70%,下方圖佔30%
887
+ subplot_titles=('價格走勢', 'MACD 指標')) # 設定子圖標題
888
+
889
+ # --- 上方子圖 (row=1):只繪製價格走勢 ---
890
  fig.add_trace(go.Scatter(
891
  x=data.index,
892
  y=data['Close'],
 
894
  name='收盤價',
895
  line=dict(color='black', width=1.5)
896
  ), row=1, col=1)
897
+
898
+ # --- 下方子圖 (row=2):繪製所有MACD相關指標 ---
899
+ # 1. MACD 快線 (DIF)
900
  fig.add_trace(go.Scatter(
901
  x=data.index,
902
  y=data['MACD'],
 
904
  name='MACD (快線)',
905
  line=dict(color='blue', width=2)
906
  ), row=2, col=1)
907
+
908
+ # 2. Signal 慢線 (MACD)
909
  fig.add_trace(go.Scatter(
910
  x=data.index,
911
  y=data['MACD_Signal'],
 
913
  name='Signal (慢線)',
914
  line=dict(color='red', width=2)
915
  ), row=2, col=1)
916
+
917
+ # 3. Histogram 柱狀圖
918
+ # 根據台股慣例修改顏色
919
  colors = ['red' if x >= 0 else 'green' for x in data['MACD_Histogram']]
920
  fig.add_trace(go.Bar(
921
  x=data.index,
 
923
  name='MACD柱狀圖',
924
  marker_color=colors
925
  ), row=2, col=1)
926
+
927
+ # 在MACD子圖中添加一條零軸水平線,方便觀察
928
  fig.add_hline(y=0, line_dash="dash", line_color="gray", row=2, col=1)
929
+
930
+ # 更新整個圖表的佈局
931
  fig.update_layout(
932
  title_text=f'{stock_name} - MACD 指數平滑異同移動平均線',
933
+ height=550, # 可以適當增加圖表高度以容納兩個子圖
934
  legend_title_text='圖例',
935
+ showlegend=True # 確保圖例顯示
936
  )
937
+ # 隱藏柱狀圖的圖例,因為顏色已經表達了正負值
938
  fig.update_traces(showlegend=False, selector=dict(type='bar'))
939
 
940
  elif indicator == 'BB':
941
  fig = go.Figure()
942
+
943
+ # 價格線
944
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
945
  line=dict(color='black', width=2)))
946
+
947
+ # 布林通道上軌
948
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines', name='上軌',
949
  line=dict(color='red', width=1, dash='dash')))
950
+
951
+ # 布林通道中軌
952
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Middle'], mode='lines', name='中軌(MA20)',
953
  line=dict(color='blue', width=1)))
954
+
955
+ # 布林通道下軌
956
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines', name='下軌',
957
  line=dict(color='green', width=1, dash='dash')))
958
+
959
+ # 填充通道區域
960
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Upper'], mode='lines',
961
  line=dict(color='rgba(0,0,0,0)'), showlegend=False))
962
  fig.add_trace(go.Scatter(x=data.index, y=data['BB_Lower'], mode='lines',
963
  fill='tonexty', fillcolor='rgba(173,216,230,0.2)',
964
  line=dict(color='rgba(0,0,0,0)'), name='布林通道', showlegend=False))
965
+
966
  fig.update_layout(
967
  title=f'{stock_name} - 布林通道 (20日, 2σ)',
968
  xaxis_title='日期',
 
975
  vertical_spacing=0.1,
976
  row_heights=[0.6, 0.4],
977
  subplot_titles=('價格走勢', 'KD指標'))
978
+
979
+ # 上方:價格線
980
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
981
  line=dict(color='black', width=1)), row=1, col=1)
982
+
983
+ # 下方:KD線
984
  fig.add_trace(go.Scatter(x=data.index, y=data['K'], mode='lines', name='K線',
985
  line=dict(color='blue', width=2)), row=2, col=1)
986
  fig.add_trace(go.Scatter(x=data.index, y=data['D'], mode='lines', name='D線',
987
  line=dict(color='red', width=2)), row=2, col=1)
988
+
989
+ # KD指標參考線
990
+ # 根據台股慣例修改顏色
991
  fig.add_hline(y=80, line_dash="dash", line_color="green", annotation_text="超買線(80)", row=2, col=1)
992
  fig.add_hline(y=20, line_dash="dash", line_color="red", annotation_text="超賣線(20)", row=2, col=1)
993
  fig.add_hline(y=50, line_dash="dot", line_color="gray", annotation_text="中線(50)", row=2, col=1)
994
+
995
+ # 超買超賣區域
996
+ # 根據台股慣例修改顏色
997
  fig.add_hrect(y0=80, y1=100, fillcolor="green", opacity=0.1, row=2, col=1)
998
  fig.add_hrect(y0=0, y1=20, fillcolor="red", opacity=0.1, row=2, col=1)
999
+
1000
  fig.update_layout(
1001
  title=f'{stock_name} - KD 隨機指標 (9,3,3)',
1002
  height=500
 
1008
  vertical_spacing=0.1,
1009
  row_heights=[0.6, 0.4],
1010
  subplot_titles=('價格走勢', '威廉指標 %R'))
1011
+
1012
+ # 上方:價格線
1013
  fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
1014
  line=dict(color='black', width=1)), row=1, col=1)
1015
+
1016
+ # 下方:威廉指標
1017
  fig.add_trace(go.Scatter(x=data.index, y=data['Williams_R'], mode='lines', name='威廉%R',
1018
  line=dict(color='purple', width=2)), row=2, col=1)
1019
+
1020
+ # 威廉指標參考線
1021
+ # 根據台股慣例修改顏色
1022
  fig.add_hline(y=-20, line_dash="dash", line_color="green", annotation_text="超買線(-20)", row=2, col=1)
1023
  fig.add_hline(y=-80, line_dash="dash", line_color="red", annotation_text="超賣線(-80)", row=2, col=1)
1024
  fig.add_hline(y=-50, line_dash="dot", line_color="gray", annotation_text="中線(-50)", row=2, col=1)
1025
+
1026
+ # 超買超賣區域
1027
+ # 根據台股慣例修改顏色
1028
  fig.add_hrect(y0=-20, y1=0, fillcolor="green", opacity=0.1, row=2, col=1)
1029
  fig.add_hrect(y0=-100, y1=-80, fillcolor="red", opacity=0.1, row=2, col=1)
1030
+
1031
  fig.update_layout(
1032
  title=f'{stock_name} - 威廉指標 %R (14日)',
1033
  height=500
1034
  )
1035
  fig.update_yaxes(range=[-100, 0], row=2, col=1)
1036
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1037
  return fig
1038
 
1039
  # 更新成交量圖表
 
1109
  # 新增:更新景氣燈號圖表
1110
  @app.callback(
1111
  dash.dependencies.Output('business-climate-chart', 'figure'),
1112
+ [dash.dependencies.Input('stock-dropdown', 'value')] # 雖然不會影響圖表,但需要觸發
1113
  )
1114
  def update_business_climate_chart(selected_stock):
1115
  df = get_business_climate_data()
1116
 
1117
  if df.empty:
1118
+ # 如果沒有資料,顯示提示圖表
1119
  fig = go.Figure()
1120
  fig.add_annotation(
1121
  x=0.5, y=0.5,
 
1131
  )
1132
  return fig
1133
 
1134
+ # 定義燈號顏色
1135
  def get_light_color(score):
1136
+ if score >= 32:
1137
+ return 'red' # 紅燈
1138
+ elif score >= 24:
1139
+ return 'orange' # 黃紅燈
1140
+ elif score >= 17:
1141
+ return 'yellow' # 黃燈
1142
+ elif score >= 10:
1143
+ return 'lightgreen' # 黃藍燈
1144
+ else:
1145
+ return 'blue' # 藍燈
1146
+
1147
+ # 為每個點設定顏色
1148
  colors = [get_light_color(score) for score in df['Index']]
1149
 
1150
  fig = go.Figure()
1151
+
1152
  fig.add_trace(go.Scatter(
1153
  x=df['Date'],
1154
  y=df['Index'],
 
1161
  line=dict(width=2, color='darkblue')
1162
  )
1163
  ))
1164
+
1165
+ # 添加燈號區間線
1166
  fig.add_hline(y=32, line_dash="dash", line_color="red", annotation_text="紅燈(32)")
1167
  fig.add_hline(y=24, line_dash="dash", line_color="orange", annotation_text="黃紅燈(24)")
1168
  fig.add_hline(y=17, line_dash="dash", line_color="yellow", annotation_text="黃燈(17)")
1169
  fig.add_hline(y=10, line_dash="dash", line_color="lightgreen", annotation_text="黃藍燈(10)")
1170
+
1171
  fig.update_layout(
1172
  title="台灣景氣燈號走勢",
1173
  xaxis_title='日期',
 
1175
  height=300,
1176
  yaxis=dict(range=[0, 40])
1177
  )
1178
+
1179
  return fig
1180
 
1181
  # 新增:更新分析師觀點
 
1187
  dash.dependencies.Input('period-dropdown', 'value')]
1188
  )
1189
  def update_analysis_text(selected_stock, period):
1190
+ # 獲取股票資料進行分析
1191
  data = get_stock_data(selected_stock, period)
1192
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
1193
+
1194
  if data.empty:
1195
  return "無法獲取資料進行分析", "無法獲取資料進行分析", "無法獲取資料進行分析"
1196
 
1197
+ # 計算技術指標
1198
  data = calculate_technical_indicators(data)
1199
+
1200
+ # 基本數據
1201
  current_price = data['Close'].iloc[-1]
1202
  price_change = ((current_price - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
1203
  volume_avg = data['Volume'].mean()
1204
  recent_volume = data['Volume'].iloc[-5:].mean()
1205
  rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
1206
+
1207
+ # 新增技術指標數據
1208
  macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
1209
  macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
1210
  bb_position = data['BB_Position'].iloc[-1] if not pd.isna(data['BB_Position'].iloc[-1]) else 0.5
1211
  k_current = data['K'].iloc[-1] if not pd.isna(data['K'].iloc[-1]) else 50
1212
  d_current = data['D'].iloc[-1] if not pd.isna(data['D'].iloc[-1]) else 50
1213
 
1214
+ # 技術面分析
1215
+ # 根據台股慣例修改顏色
1216
  technical_text = html.Div([
1217
  html.P([
1218
  html.Strong("價格趨勢:"),
 
1227
  html.Span(
1228
  "處於超買區間" if rsi_current > 70 else "處於超賣區間" if rsi_current < 30 else "在正常範圍內",
1229
  style={'color': 'green' if rsi_current > 70 else 'red' if rsi_current < 30 else 'blue', 'font-weight': 'bold'}
1230
+ ),
1231
+ "。"
1232
  ]),
1233
  html.P([
1234
  html.Strong("MACD指標:"),
 
1236
  html.Span(
1237
  "高於" if macd_current > macd_signal_current else "低於",
1238
  style={'color': 'red' if macd_current > macd_signal_current else 'green', 'font-weight': 'bold'}
1239
+ ),
1240
+ f"信號線({macd_signal_current:.3f}),",
1241
+ f"顯示{'多頭' if macd_current > macd_signal_current else '空頭'}格局。"
1242
  ]),
1243
  html.P([
1244
  html.Strong("布林通道:"),
 
1246
  html.Span(
1247
  "上半部" if bb_position > 0.8 else "下半部" if bb_position < 0.2 else "中段",
1248
  style={'color': 'green' if bb_position > 0.8 else 'red' if bb_position < 0.2 else 'blue', 'font-weight': 'bold'}
1249
+ ),
1250
+ f"({bb_position*100:.0f}%),",
1251
+ f"{'壓力較大' if bb_position > 0.8 else '支撐較強' if bb_position < 0.2 else '整理格局'}。"
1252
  ]),
1253
  html.P([
1254
  html.Strong("KD指標:"),
 
1256
  html.Span(
1257
  "高於" if k_current > d_current else "低於",
1258
  style={'color': 'red' if k_current > d_current else 'green', 'font-weight': 'bold'}
1259
+ ),
1260
+ f"D值({d_current:.1f}),",
1261
  html.Span(
1262
  "超買警戒" if k_current > 80 else "超賣關注" if k_current < 20 else "正常區間",
1263
  style={'color': 'green' if k_current > 80 else 'red' if k_current < 20 else 'blue', 'font-weight': 'bold'}
1264
+ ),
1265
+ "。"
1266
  ]),
1267
  html.P([
1268
  html.Strong("成交量分析:"),
 
1271
  ])
1272
  ])
1273
 
1274
+ # 基本面分析
1275
  industry = INDUSTRY_MAPPING.get(selected_stock, '綜合')
1276
  fundamental_text = html.Div([
1277
  html.P([
1278
  html.Strong("產業地位:"),
1279
  f"{stock_name}屬於{industry}產業,在產業鏈中具有",
1280
  html.Span("重要地位" if selected_stock in ['2330.TW', '2454.TW', '2317.TW'] else "一定影響力",
1281
+ style={'font-weight': 'bold'}),
1282
+ "。"
1283
  ]),
1284
  html.P([
1285
  html.Strong("營運展望:"),
 
1291
  ])
1292
  ])
1293
 
1294
+ # 市場展望
1295
+ # 根據台股慣例修改顏色
1296
  if price_change > 10:
1297
+ outlook_tone = "謹慎樂觀"
1298
+ outlook_color = "#dc3545"
1299
  elif price_change < -10:
1300
+ outlook_tone = "保守觀望"
1301
+ outlook_color = "#28a745"
1302
  else:
1303
+ outlook_tone = "中性持平"
1304
+ outlook_color = "#ffc107"
1305
 
1306
  market_outlook = html.Div([
1307
  html.P([
1308
  html.Strong("整體評估:", style={'font-size': '16px'}),
1309
  f"基於技術面及基本面分析,對{stock_name}採取",
1310
+ html.Span(f"{outlook_tone}", style={'color': outlook_color, 'font-weight': 'bold', 'font-size': '16px'}),
1311
+ "態度。"
1312
  ]),
1313
  html.P([
1314
  html.Strong("投資建議:"),
 
1325
  # 新增:更新PMI圖表
1326
  @app.callback(
1327
  dash.dependencies.Output('pmi-chart', 'figure'),
1328
+ [dash.dependencies.Input('stock-dropdown', 'value')] # 雖然不會影響圖表,但需要觸發
1329
  )
1330
  def update_pmi_chart(selected_stock):
1331
  df = get_pmi_data()
1332
+
1333
  if df.empty:
1334
+ # 如果沒有資料,顯示提示圖表
1335
  fig = go.Figure()
1336
  fig.add_annotation(
1337
  x=0.5, y=0.5,
 
1347
  )
1348
  return fig
1349
 
1350
+ # 定義PMI顏色 (50以上擴張,以下緊縮)
1351
+ # 根據台股慣例修改顏色
1352
  def get_pmi_color(value):
1353
  return 'red' if value >= 50 else 'green'
1354
 
1355
  colors = [get_pmi_color(value) for value in df['Index']]
1356
 
1357
  fig = go.Figure()
1358
+
1359
  fig.add_trace(go.Scatter(
1360
+ x=df['Date'],
1361
+ y=df['Index'],
1362
+ mode='lines+markers',
1363
+ name='PMI指數',
1364
  line=dict(color='darkblue', width=2),
1365
+ marker=dict(
1366
+ size=8,
1367
+ color=colors,
1368
+ line=dict(width=2, color='darkblue')
1369
+ )
1370
  ))
1371
+
1372
+ # 添加榮枯線
1373
  fig.add_hline(y=50, line_dash="dash", line_color="black", annotation_text="榮枯線(50)")
1374
+
1375
+ # 添加背景色區域
1376
+ # 根據台股慣例修改顏色
1377
  fig.add_hrect(
1378
+ y0=50, y1=60,
1379
+ fillcolor="lightcoral", opacity=0.2,
1380
  annotation_text="擴張區間", annotation_position="top left"
1381
  )
1382
  fig.add_hrect(
1383
+ y0=40, y1=50,
1384
+ fillcolor="lightgreen", opacity=0.2,
1385
  annotation_text="緊縮區間", annotation_position="bottom left"
1386
  )
1387
+
1388
  fig.update_layout(
1389
  title="台灣PMI指數走勢",
1390
  xaxis_title='日期',
 
1392
  height=300,
1393
  yaxis=dict(range=[35, 60])
1394
  )
1395
+
1396
  return fig
1397
 
1398
+
1399
+ # ==============================================================================
1400
+ # ===== 修改後的多檔股票比較回呼函式 =====
1401
+ # ==============================================================================
1402
  @app.callback(
1403
  [dash.dependencies.Output('comparison-chart', 'figure'),
1404
  dash.dependencies.Output('comparison-table', 'children')],
 
1406
  dash.dependencies.Input('comparison-period', 'value')]
1407
  )
1408
  def update_comparison_analysis(selected_stocks, period):
1409
+ # --- 新增:確保 0050.TW 始終存在 ---
1410
  fixed_stock = '0050.TW'
1411
+ # 如果列表為空或 None,則只顯示 0050
1412
  if not selected_stocks:
1413
  selected_stocks = [fixed_stock]
1414
+ # 如果 0050 不在列表中,則將其插入到最前面
1415
  elif fixed_stock not in selected_stocks:
1416
  selected_stocks.insert(0, fixed_stock)
1417
+ # --- 修改結束 ---
1418
+
1419
+ if not selected_stocks:
1420
+ return {}, html.Div("請選擇要比較的股票")
1421
 
1422
+ # 限制最多5檔
1423
  selected_stocks = selected_stocks[:5]
1424
 
1425
  fig = go.Figure()
 
1428
  for stock in selected_stocks:
1429
  data = get_stock_data(stock, period)
1430
  if not data.empty:
1431
+ # 安全地獲取股票名稱,如果找不到則使用代碼本身
1432
  stock_name = next((name for name, symbol in TAIWAN_STOCKS.items() if symbol == stock), stock)
1433
+
1434
+ # 正規化價格(以期初為基準100)
1435
  normalized_prices = (data['Close'] / data['Close'].iloc[0]) * 100
1436
+
1437
  fig.add_trace(go.Scatter(
1438
  x=data.index,
1439
  y=normalized_prices,
 
1441
  name=stock_name,
1442
  line=dict(width=2)
1443
  ))
1444
+
1445
+ # 計算績效數據
1446
  total_return = ((data['Close'].iloc[-1] / data['Close'].iloc[0]) - 1) * 100
1447
+ volatility = data['Close'].pct_change().std() * np.sqrt(252) * 100 # 年化波動率
1448
+
1449
  comparison_data.append({
1450
  'name': stock_name,
1451
  'return': total_return,
 
1461
  hovermode='x unified'
1462
  )
1463
 
1464
+ # 建立比較表格
1465
  if comparison_data:
1466
  table_rows = []
1467
  for item in sorted(comparison_data, key=lambda x: x['return'], reverse=True):
1468
+ # 根據台股慣例修改顏色
1469
  color = 'red' if item['return'] > 0 else 'green'
1470
  table_rows.append(
1471
  html.Tr([
 
1475
  html.Td(f"${item['current_price']:.2f}")
1476
  ])
1477
  )
1478
+
1479
  table = html.Table([
1480
+ html.Thead([
1481
+ html.Tr([
1482
+ html.Th("股票", style={'text-align': 'center'}),
1483
+ html.Th("報酬率", style={'text-align': 'center'}),
1484
+ html.Th("波動率", style={'text-align': 'center'}),
1485
+ html.Th("現價", style={'text-align': 'center'})
1486
+ ])
1487
+ ]),
1488
  html.Tbody(table_rows)
1489
+ ], style={
1490
+ 'width': '100%',
1491
+ 'border-collapse': 'collapse',
1492
+ 'font-size': '12px'
1493
+ })
1494
+
1495
  return fig, table
1496
 
1497
  return fig, html.Div("無可比較資料")
1498
 
1499
+ # 新增:市場情緒分析
1500
  @app.callback(
1501
  [dash.dependencies.Output('sentiment-gauge', 'children'),
1502
  dash.dependencies.Output('news-summary', 'children')],
1503
  [dash.dependencies.Input('stock-dropdown', 'value')]
1504
  )
1505
  def update_sentiment_analysis(selected_stock):
1506
+ # 模擬情緒指標(實際應用中可接入新聞API或情緒分析服務)
1507
+ sentiment_score = np.random.uniform(30, 80) # 模擬情緒分數 0-100
1508
+
1509
+ # 建立情緒指標圓形圖
1510
+ # 根據台股慣例修改顏色
1511
  gauge_fig = go.Figure(go.Indicator(
1512
  mode = "gauge+number+delta",
1513
  value = sentiment_score,
 
1529
  }
1530
  }
1531
  ))
1532
+
1533
  gauge_fig.update_layout(height=200, margin=dict(l=20, r=20, t=40, b=20))
1534
 
1535
+ # 模擬新聞摘要
1536
  stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
1537
+
1538
  news_items = [
1539
  f"📈 {stock_name}獲外資調升目標價,看好後續發展前景",
1540
  f"💼 法人預期{stock_name}下季營收將較上季成長5-10%",
 
1542
  f"⚡ 產業景氣回溫,{stock_name}受惠程度值得關注",
1543
  f"📊 技術面顯示{stock_name}突破關鍵壓力,短線偏多"
1544
  ]
1545
+
1546
  news_content = html.Div([
1547
  html.P(news, style={
1548
+ 'margin': '8px 0',
1549
+ 'padding': '8px',
1550
+ 'background': '#f8f9fa',
1551
+ 'border-radius': '5px',
1552
+ 'border-left': '3px solid #17a2b8',
1553
+ 'font-size': '13px'
1554
+ }) for news in news_items[:3] # 顯示前3條
1555
  ])
1556
 
1557
  return dcc.Graph(figure=gauge_fig), news_content
1558
 
1559
+ # 在 Colab 中執行的設定
1560
  if __name__ == '__main__':
1561
+ # 在執行前先測試檔案讀取
1562
  print("測試檔案讀取...")
1563
  business_data = get_business_climate_data()
1564
  pmi_data = get_pmi_data()
 
1568
  if not pmi_data.empty:
1569
  print(f"PMI資料預覽:\n{pmi_data.head()}")
1570
 
1571
+ # 在 Hugging Face Spaces 中執行
1572
  app.run(host="0.0.0.0", port=7860, debug=False)