Update app.py
Browse files
app.py
CHANGED
|
@@ -176,76 +176,25 @@ def calculate_technical_indicators(df):
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
try:
|
| 184 |
-
# 檢查檔案是否存在
|
| 185 |
-
if not os.path.exists('business_climate.csv'):
|
| 186 |
-
print("business_climate.csv 檔案不存在")
|
| 187 |
-
return pd.DataFrame()
|
| 188 |
-
|
| 189 |
-
# 讀取CSV檔案,假設列名為 Date 和 Index
|
| 190 |
-
df = pd.read_csv('business_climate.csv')
|
| 191 |
-
|
| 192 |
-
# 檢查列名並調整
|
| 193 |
-
if 'Date' not in df.columns:
|
| 194 |
-
# 如果第一列是日期,重新命名
|
| 195 |
-
df.columns = ['Date', 'Index'] if len(df.columns) == 2 else df.columns
|
| 196 |
-
|
| 197 |
-
# 轉換日期格式 (處理 YYYY-MM 格式)
|
| 198 |
-
if 'Date' in df.columns:
|
| 199 |
-
try:
|
| 200 |
-
# 如果是 YYYY-MM 格式,轉換為日期
|
| 201 |
-
df['Date'] = pd.to_datetime(df['Date'] + '-01', format='%Y-%m-%d', errors='coerce')
|
| 202 |
-
except:
|
| 203 |
-
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
|
| 204 |
-
|
| 205 |
-
# 移除日期轉換失敗的行
|
| 206 |
-
df = df.dropna(subset=['Date'])
|
| 207 |
-
|
| 208 |
-
print(f"成功讀取景氣燈號資料:{len(df)} 筆記錄")
|
| 209 |
-
return df
|
| 210 |
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
# 檢查檔案是否存在
|
| 219 |
-
if not os.path.exists('taiwan_pmi.csv'):
|
| 220 |
-
print("taiwan_pmi.csv 檔案不存在")
|
| 221 |
-
return pd.DataFrame()
|
| 222 |
-
|
| 223 |
-
# 讀取CSV檔案
|
| 224 |
-
df = pd.read_csv('taiwan_pmi.csv')
|
| 225 |
-
|
| 226 |
-
# 檢查列名並調整 (處理 DATE/INDEX 或其他可能的列名)
|
| 227 |
-
if 'DATE' in df.columns:
|
| 228 |
-
df = df.rename(columns={'DATE': 'Date', 'INDEX': 'Index'})
|
| 229 |
-
elif len(df.columns) == 2:
|
| 230 |
-
df.columns = ['Date', 'Index']
|
| 231 |
-
|
| 232 |
-
# 轉換日期格式
|
| 233 |
-
if 'Date' in df.columns:
|
| 234 |
-
try:
|
| 235 |
-
# 如果是 YYYY-MM 格式,轉換為日期
|
| 236 |
-
df['Date'] = pd.to_datetime(df['Date'] + '-01', format='%Y-%m-%d', errors='coerce')
|
| 237 |
-
except:
|
| 238 |
-
df['Date'] = pd.to_datetime(df['Date'], errors='coerce')
|
| 239 |
-
|
| 240 |
-
# 移除日期轉換失敗的行
|
| 241 |
-
df = df.dropna(subset=['Date'])
|
| 242 |
-
|
| 243 |
-
print(f"成功讀取 PMI 資料:{len(df)} 筆記錄")
|
| 244 |
-
return df
|
| 245 |
|
| 246 |
-
|
| 247 |
-
print(f"無法獲取 PMI 資料: {str(e)}")
|
| 248 |
-
return pd.DataFrame()
|
| 249 |
|
| 250 |
def calculate_volume_profile(df, num_bins=50):
|
| 251 |
"""
|
|
@@ -401,7 +350,8 @@ 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 |
],
|
| 406 |
value='RSI',
|
| 407 |
style={'width': '100%'}
|
|
@@ -1034,6 +984,34 @@ def update_advanced_technical_chart(indicator, selected_stock, period):
|
|
| 1034 |
)
|
| 1035 |
fig.update_yaxes(range=[-100, 0], row=2, col=1)
|
| 1036 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1037 |
return fig
|
| 1038 |
|
| 1039 |
# 更新成交量圖表
|
|
@@ -1210,6 +1188,10 @@ def update_analysis_text(selected_stock, period):
|
|
| 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 |
# 根據台股慣例修改顏色
|
|
@@ -1264,6 +1246,15 @@ def update_analysis_text(selected_stock, period):
|
|
| 1264 |
),
|
| 1265 |
"。"
|
| 1266 |
]),
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1267 |
html.P([
|
| 1268 |
html.Strong("成交量分析:"),
|
| 1269 |
f"近期成交量{'放大' if recent_volume > volume_avg * 1.2 else '萎縮' if recent_volume < volume_avg * 0.8 else '平穩'},",
|
|
|
|
| 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 |
+
# DMI (Directional Movement Index)
|
| 180 |
+
# 計算正向運動 (+DM) 和負向運動 (-DM)
|
| 181 |
+
df['up_move'] = df['High'].diff()
|
| 182 |
+
df['down_move'] = df['Low'].diff()
|
| 183 |
+
df['+DM'] = np.where((df['up_move'] > df['down_move']) & (df['up_move'] > 0), df['up_move'], 0)
|
| 184 |
+
df['-DM'] = np.where((df['down_move'] > df['up_move']) & (df['down_move'] > 0), df['down_move'], 0)
|
| 185 |
|
| 186 |
+
# 計算真實範圍 (TR)
|
| 187 |
+
df['TR'] = np.max([df['High'] - df['Low'], abs(df['High'] - df['Close'].shift(1)), abs(df['Low'] - df['Close'].shift(1))], axis=0)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 188 |
|
| 189 |
+
# 計算平滑後的 +DM, -DM, TR (通常使用 14 天)
|
| 190 |
+
df['+DI'] = df['+DM'].ewm(alpha=1/14, adjust=False).mean() / df['TR'].ewm(alpha=1/14, adjust=False).mean() * 100
|
| 191 |
+
df['-DI'] = df['-DM'].ewm(alpha=1/14, adjust=False).mean() / df['TR'].ewm(alpha=1/14, adjust=False).mean() * 100
|
| 192 |
|
| 193 |
+
# 計算 ADX
|
| 194 |
+
df['DX'] = abs(df['+DI'] - df['-DI']) / (df['+DI'] + df['-DI']) * 100
|
| 195 |
+
df['ADX'] = df['DX'].ewm(alpha=1/14, adjust=False).mean()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 196 |
|
| 197 |
+
return df
|
|
|
|
|
|
|
| 198 |
|
| 199 |
def calculate_volume_profile(df, num_bins=50):
|
| 200 |
"""
|
|
|
|
| 350 |
{'label': 'MACD 指數平滑異同移動平均線', 'value': 'MACD'},
|
| 351 |
{'label': '布林通道 Bollinger Bands', 'value': 'BB'},
|
| 352 |
{'label': 'KD 隨機指標', 'value': 'KD'},
|
| 353 |
+
{'label': '威廉指標 %R', 'value': 'WR'},
|
| 354 |
+
{'label': 'DMI 動向指標', 'value': 'DMI'}
|
| 355 |
],
|
| 356 |
value='RSI',
|
| 357 |
style={'width': '100%'}
|
|
|
|
| 984 |
)
|
| 985 |
fig.update_yaxes(range=[-100, 0], row=2, col=1)
|
| 986 |
|
| 987 |
+
elif indicator == 'DMI':
|
| 988 |
+
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
|
| 989 |
+
vertical_spacing=0.1,
|
| 990 |
+
row_heights=[0.6, 0.4],
|
| 991 |
+
subplot_titles=('價格走勢', 'DMI 指標'))
|
| 992 |
+
|
| 993 |
+
# 上方:價格線
|
| 994 |
+
fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name='收盤價',
|
| 995 |
+
line=dict(color='black', width=1)), row=1, col=1)
|
| 996 |
+
|
| 997 |
+
# 下方:DMI 線
|
| 998 |
+
fig.add_trace(go.Scatter(x=data.index, y=data['+DI'], mode='lines', name='+DI',
|
| 999 |
+
line=dict(color='red', width=2)), row=2, col=1)
|
| 1000 |
+
fig.add_trace(go.Scatter(x=data.index, y=data['-DI'], mode='lines', name='-DI',
|
| 1001 |
+
line=dict(color='green', width=2)), row=2, col=1)
|
| 1002 |
+
fig.add_trace(go.Scatter(x=data.index, y=data['ADX'], mode='lines', name='ADX',
|
| 1003 |
+
line=dict(color='blue', width=2, dash='dot')), row=2, col=1)
|
| 1004 |
+
|
| 1005 |
+
# DMI 參考線
|
| 1006 |
+
fig.add_hline(y=20, line_dash="dash", line_color="gray", annotation_text="ADX強弱線(20)", row=2, col=1)
|
| 1007 |
+
|
| 1008 |
+
fig.update_layout(
|
| 1009 |
+
title=f'{stock_name} - DMI 動向指標 (14日)',
|
| 1010 |
+
height=500,
|
| 1011 |
+
showlegend=True
|
| 1012 |
+
)
|
| 1013 |
+
fig.update_yaxes(range=[0, 100], row=2, col=1)
|
| 1014 |
+
|
| 1015 |
return fig
|
| 1016 |
|
| 1017 |
# 更新成交量圖表
|
|
|
|
| 1188 |
bb_position = data['BB_Position'].iloc[-1] if not pd.isna(data['BB_Position'].iloc[-1]) else 0.5
|
| 1189 |
k_current = data['K'].iloc[-1] if not pd.isna(data['K'].iloc[-1]) else 50
|
| 1190 |
d_current = data['D'].iloc[-1] if not pd.isna(data['D'].iloc[-1]) else 50
|
| 1191 |
+
pdi_current = data['+DI'].iloc[-1] if not pd.isna(data['+DI'].iloc[-1]) else 0
|
| 1192 |
+
ndi_current = data['-DI'].iloc[-1] if not pd.isna(data['-DI'].iloc[-1]) else 0
|
| 1193 |
+
adx_current = data['ADX'].iloc[-1] if not pd.isna(data['ADX'].iloc[-1]) else 0
|
| 1194 |
+
|
| 1195 |
|
| 1196 |
# 技術面分析
|
| 1197 |
# 根據台股慣例修改顏色
|
|
|
|
| 1246 |
),
|
| 1247 |
"。"
|
| 1248 |
]),
|
| 1249 |
+
html.P([
|
| 1250 |
+
html.Strong("DMI指標:"),
|
| 1251 |
+
f"目前+DI ({pdi_current:.1f}) 與 -DI ({ndi_current:.1f}),",
|
| 1252 |
+
html.Span(
|
| 1253 |
+
"呈現多頭趨勢" if pdi_current > ndi_current else "呈現空頭趨勢",
|
| 1254 |
+
style={'color': 'red' if pdi_current > ndi_current else 'green', 'font-weight': 'bold'}
|
| 1255 |
+
),
|
| 1256 |
+
f"。ADX值為 {adx_current:.1f},顯示市場趨勢{'強勁' if adx_current > 25 else '不明顯' if adx_current < 20 else '有趨勢'}"
|
| 1257 |
+
]),
|
| 1258 |
html.P([
|
| 1259 |
html.Strong("成交量分析:"),
|
| 1260 |
f"近期成交量{'放大' if recent_volume > volume_avg * 1.2 else '萎縮' if recent_volume < volume_avg * 0.8 else '平穩'},",
|