Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -219,24 +219,30 @@ def get_exchange_rate():
|
|
| 219 |
return 31.5
|
| 220 |
|
| 221 |
def simple_statistical_predict(data, predict_days=5):
|
| 222 |
-
"""
|
| 223 |
if len(data) < 60:
|
| 224 |
return None
|
| 225 |
|
| 226 |
prices = data['Close'].values
|
|
|
|
|
|
|
| 227 |
ma_short = np.mean(prices[-5:])
|
| 228 |
ma_medium = np.mean(prices[-20:])
|
| 229 |
ma_long = np.mean(prices[-60:])
|
| 230 |
recent_trend = np.polyfit(range(20), prices[-20:], 1)[0]
|
| 231 |
volatility = np.std(prices[-20:]) / np.mean(prices[-20:])
|
|
|
|
| 232 |
base_change = recent_trend * predict_days
|
| 233 |
trend_factor = 1.0 + (0.02 if ma_short > ma_medium > ma_long else -0.02 if ma_short < ma_medium < ma_long else 0)
|
| 234 |
noise_factor = np.random.normal(1, volatility * 0.1)
|
| 235 |
-
predicted_price =
|
| 236 |
-
|
|
|
|
|
|
|
|
|
|
| 237 |
return {
|
| 238 |
'predicted_price': predicted_price,
|
| 239 |
-
'change_pct': change_pct,
|
| 240 |
'confidence': max(0.6, 1 - volatility * 2)
|
| 241 |
}
|
| 242 |
|
|
@@ -289,9 +295,10 @@ def calculate_new_features(df):
|
|
| 289 |
def advanced_xgboost_predict(predict_days=5):
|
| 290 |
"""
|
| 291 |
【進階模型】使用 XGBoost 模型進行預測 - 新特徵版本
|
|
|
|
| 292 |
"""
|
| 293 |
try:
|
| 294 |
-
print(f"開始使用 XGBoost 模型進行 {predict_days}
|
| 295 |
|
| 296 |
# 初始化 XGBoost 模型
|
| 297 |
xgb_model = XGBoostModel()
|
|
@@ -328,7 +335,7 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 328 |
# 取得昨日收盤價
|
| 329 |
yesterday_close = latest_data['Close']
|
| 330 |
|
| 331 |
-
#
|
| 332 |
new_feature_columns = [
|
| 333 |
'return_t-1',
|
| 334 |
'return_t-5',
|
|
@@ -383,7 +390,7 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 383 |
|
| 384 |
feature_names.append(feature)
|
| 385 |
|
| 386 |
-
#
|
| 387 |
# 7. dji_return_t-1
|
| 388 |
features_list.append(dji_return)
|
| 389 |
feature_names.append('dji_return_t-1')
|
|
@@ -421,7 +428,7 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 421 |
|
| 422 |
# 詳細的資料驗證日誌
|
| 423 |
print("=" * 60)
|
| 424 |
-
print("XGBoost 模型輸入特徵檢查報告 (
|
| 425 |
print("=" * 60)
|
| 426 |
|
| 427 |
print(f"總特徵數量: {len(features_list)} 個")
|
|
@@ -431,7 +438,7 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 431 |
print("\n特徵狀態詳情:")
|
| 432 |
for i, (name, value) in enumerate(zip(feature_names, features_list)):
|
| 433 |
status = feature_status.get(name, {})
|
| 434 |
-
status_symbol = "✓正常" if status.get('is_real', False) else "
|
| 435 |
print(f" [{i+1:2d}] {name:18s}: {value:12.6f} ({status_symbol})")
|
| 436 |
|
| 437 |
# 統計完整性
|
|
@@ -456,12 +463,12 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 456 |
# 進行預測
|
| 457 |
predictions = xgb_model.predict('xgboost_model', input_df)
|
| 458 |
|
| 459 |
-
#
|
| 460 |
pred_mapping = {
|
| 461 |
-
1: '
|
| 462 |
-
5: '
|
| 463 |
-
10: '
|
| 464 |
-
20: '
|
| 465 |
}
|
| 466 |
|
| 467 |
# 找到最接近的預測天數
|
|
@@ -469,21 +476,24 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 469 |
closest_day = min(available_days, key=lambda x: abs(x - predict_days))
|
| 470 |
pred_key = pred_mapping[closest_day]
|
| 471 |
|
| 472 |
-
|
|
|
|
|
|
|
|
|
|
| 473 |
current_price = latest_data['Close']
|
| 474 |
-
|
| 475 |
|
| 476 |
print(f"XGBoost 預測完成:")
|
| 477 |
print(f"- 預測天數: {predict_days} (使用 {closest_day} 天模型)")
|
| 478 |
print(f"- 當前價格: {current_price:.2f}")
|
| 479 |
-
print(f"-
|
| 480 |
-
print(f"-
|
| 481 |
print(f"- 使用特徵數: {len(features_list)} 個")
|
| 482 |
print(f"- 特徵完整性: {completeness:.1f}%")
|
| 483 |
|
| 484 |
return {
|
| 485 |
-
'predicted_price': predicted_price,
|
| 486 |
-
'change_pct':
|
| 487 |
'confidence': max(0.6, min(0.85, completeness / 100)) # 根據特徵完整性調整信心度
|
| 488 |
}
|
| 489 |
|
|
@@ -789,7 +799,7 @@ app.layout = html.Div([
|
|
| 789 |
], style={'margin-top': '30px','padding': '20px','background': 'white','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)'}),
|
| 790 |
])
|
| 791 |
|
| 792 |
-
#
|
| 793 |
@app.callback(
|
| 794 |
[dash.dependencies.Output('taiex-prediction-results', 'children'),
|
| 795 |
dash.dependencies.Output('taiex-prediction-chart', 'figure')],
|
|
@@ -804,32 +814,42 @@ def update_taiex_prediction(predict_days):
|
|
| 804 |
|
| 805 |
if final_prediction is None: return html.Div("資料不足,無法進行預測"), {}
|
| 806 |
current_price, last_date = data['Close'].iloc[-1], data.index[-1]
|
|
|
|
|
|
|
| 807 |
predicted_price, change_pct, confidence = final_prediction['predicted_price'], final_prediction['change_pct'], final_prediction['confidence']
|
| 808 |
|
|
|
|
| 809 |
prediction_paths = {1: [1], 5: [1, 5], 10: [1, 5, 10], 20: [1, 10, 20], 60: [1, 10, 20, 60]}
|
| 810 |
intervals_to_predict = prediction_paths.get(predict_days, [predict_days])
|
| 811 |
prediction_dates, prediction_prices = [last_date], [current_price]
|
| 812 |
|
| 813 |
for days in intervals_to_predict:
|
| 814 |
-
# === 修改點:迴圈內也使用統一的預測控制器 ===
|
| 815 |
interim_prediction = get_prediction(data, days)
|
| 816 |
if interim_prediction:
|
| 817 |
prediction_dates.append(last_date + timedelta(days=days))
|
| 818 |
prediction_prices.append(interim_prediction['predicted_price'])
|
| 819 |
|
| 820 |
-
#
|
| 821 |
color, arrow = ('red', '📈') if change_pct >= 0 else ('green', '📉')
|
| 822 |
result_card = html.Div([
|
| 823 |
html.H4(f"{predict_days}日後預測結果", style={'margin': '0 0 15px 0', 'color': 'white'}),
|
| 824 |
-
html.Div([
|
| 825 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
html.P(f"信心度: {confidence:.1%}", style={'margin': '5px 0', 'font-size': '14px'})
|
| 827 |
], style={'background': 'rgba(255,255,255,0.1)','padding': '20px','border-radius': '10px','border': '1px solid rgba(255,255,255,0.2)'})
|
|
|
|
|
|
|
| 828 |
fig = go.Figure()
|
| 829 |
recent_data = data.tail(30)
|
| 830 |
fig.add_trace(go.Scatter(x=recent_data.index, y=recent_data['Close'], mode='lines', name='歷史價格', line=dict(color='#FFA726', width=2)))
|
| 831 |
fig.add_trace(go.Scatter(x=prediction_dates, y=prediction_prices, mode='lines+markers', name=f'{predict_days}日預測路徑', line=dict(color=color, width=3, dash='dash'), marker=dict(size=8)))
|
| 832 |
-
fig.update_layout(title=f'台指期 {predict_days}日預測走勢', xaxis_title='日期', yaxis_title='指數點位', height=350, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='white'))
|
|
|
|
| 833 |
return result_card, fig
|
| 834 |
|
| 835 |
@app.callback(
|
|
|
|
| 219 |
return 31.5
|
| 220 |
|
| 221 |
def simple_statistical_predict(data, predict_days=5):
|
| 222 |
+
"""【備用模型】簡化的統計預測模型 - 更新為輸出漲幅百分比格式。"""
|
| 223 |
if len(data) < 60:
|
| 224 |
return None
|
| 225 |
|
| 226 |
prices = data['Close'].values
|
| 227 |
+
current_price = prices[-1]
|
| 228 |
+
|
| 229 |
ma_short = np.mean(prices[-5:])
|
| 230 |
ma_medium = np.mean(prices[-20:])
|
| 231 |
ma_long = np.mean(prices[-60:])
|
| 232 |
recent_trend = np.polyfit(range(20), prices[-20:], 1)[0]
|
| 233 |
volatility = np.std(prices[-20:]) / np.mean(prices[-20:])
|
| 234 |
+
|
| 235 |
base_change = recent_trend * predict_days
|
| 236 |
trend_factor = 1.0 + (0.02 if ma_short > ma_medium > ma_long else -0.02 if ma_short < ma_medium < ma_long else 0)
|
| 237 |
noise_factor = np.random.normal(1, volatility * 0.1)
|
| 238 |
+
predicted_price = current_price * trend_factor + base_change + (current_price * noise_factor * 0.01)
|
| 239 |
+
|
| 240 |
+
# 【重要更新】計算漲幅百分比
|
| 241 |
+
change_pct = ((predicted_price - current_price) / current_price) * 100
|
| 242 |
+
|
| 243 |
return {
|
| 244 |
'predicted_price': predicted_price,
|
| 245 |
+
'change_pct': change_pct, # 現在這個值是真正的漲幅百分比
|
| 246 |
'confidence': max(0.6, 1 - volatility * 2)
|
| 247 |
}
|
| 248 |
|
|
|
|
| 295 |
def advanced_xgboost_predict(predict_days=5):
|
| 296 |
"""
|
| 297 |
【進階模型】使用 XGBoost 模型進行預測 - 新特徵版本
|
| 298 |
+
【重要更新】現在輸出漲幅百分比而非絕對價格
|
| 299 |
"""
|
| 300 |
try:
|
| 301 |
+
print(f"開始使用 XGBoost 模型進行 {predict_days} 天預測(漲幅百分比版本)...")
|
| 302 |
|
| 303 |
# 初始化 XGBoost 模型
|
| 304 |
xgb_model = XGBoostModel()
|
|
|
|
| 335 |
# 取得昨日收盤價
|
| 336 |
yesterday_close = latest_data['Close']
|
| 337 |
|
| 338 |
+
# 特徵列表,確保與模型訓練時完全一致
|
| 339 |
new_feature_columns = [
|
| 340 |
'return_t-1',
|
| 341 |
'return_t-5',
|
|
|
|
| 390 |
|
| 391 |
feature_names.append(feature)
|
| 392 |
|
| 393 |
+
# 按照模型訓練的順序添加剩餘特徵
|
| 394 |
# 7. dji_return_t-1
|
| 395 |
features_list.append(dji_return)
|
| 396 |
feature_names.append('dji_return_t-1')
|
|
|
|
| 428 |
|
| 429 |
# 詳細的資料驗證日誌
|
| 430 |
print("=" * 60)
|
| 431 |
+
print("XGBoost 模型輸入特徵檢查報告 (漲幅百分比版本)")
|
| 432 |
print("=" * 60)
|
| 433 |
|
| 434 |
print(f"總特徵數量: {len(features_list)} 個")
|
|
|
|
| 438 |
print("\n特徵狀態詳情:")
|
| 439 |
for i, (name, value) in enumerate(zip(feature_names, features_list)):
|
| 440 |
status = feature_status.get(name, {})
|
| 441 |
+
status_symbol = "✓正常" if status.get('is_real', False) else "⚠ 預設值"
|
| 442 |
print(f" [{i+1:2d}] {name:18s}: {value:12.6f} ({status_symbol})")
|
| 443 |
|
| 444 |
# 統計完整性
|
|
|
|
| 463 |
# 進行預測
|
| 464 |
predictions = xgb_model.predict('xgboost_model', input_df)
|
| 465 |
|
| 466 |
+
# 【重要更新】處理新的漲幅百分比輸出格式
|
| 467 |
pred_mapping = {
|
| 468 |
+
1: 'Change_pct_t1_pred', # 1天後漲幅%
|
| 469 |
+
5: 'Change_pct_t5_pred', # 5天後漲幅%
|
| 470 |
+
10: 'Change_pct_t10_pred', # 10天後漲幅%
|
| 471 |
+
20: 'Change_pct_t20_pred' # 20天後漲幅%
|
| 472 |
}
|
| 473 |
|
| 474 |
# 找到最接近的預測天數
|
|
|
|
| 476 |
closest_day = min(available_days, key=lambda x: abs(x - predict_days))
|
| 477 |
pred_key = pred_mapping[closest_day]
|
| 478 |
|
| 479 |
+
# 【關鍵修改】現在直接取得漲幅百分比
|
| 480 |
+
predicted_change_pct = predictions[pred_key]
|
| 481 |
+
|
| 482 |
+
# 【新增】為了兼容性,計算預測價格(僅供參考)
|
| 483 |
current_price = latest_data['Close']
|
| 484 |
+
predicted_price = current_price * (1 + predicted_change_pct / 100)
|
| 485 |
|
| 486 |
print(f"XGBoost 預測完成:")
|
| 487 |
print(f"- 預測天數: {predict_days} (使用 {closest_day} 天模型)")
|
| 488 |
print(f"- 當前價格: {current_price:.2f}")
|
| 489 |
+
print(f"- 預測漲幅: {predicted_change_pct:+.2f}%")
|
| 490 |
+
print(f"- 預測價格: {predicted_price:.2f} (參考)")
|
| 491 |
print(f"- 使用特徵數: {len(features_list)} 個")
|
| 492 |
print(f"- 特徵完整性: {completeness:.1f}%")
|
| 493 |
|
| 494 |
return {
|
| 495 |
+
'predicted_price': predicted_price, # 為了兼容現有代碼
|
| 496 |
+
'change_pct': predicted_change_pct, # 【新增】直接的漲幅百分比
|
| 497 |
'confidence': max(0.6, min(0.85, completeness / 100)) # 根據特徵完整性調整信心度
|
| 498 |
}
|
| 499 |
|
|
|
|
| 799 |
], style={'margin-top': '30px','padding': '20px','background': 'white','border-radius': '10px','box-shadow': '0 2px 10px rgba(0,0,0,0.1)'}),
|
| 800 |
])
|
| 801 |
|
| 802 |
+
# 修改台指期預測的回調函數
|
| 803 |
@app.callback(
|
| 804 |
[dash.dependencies.Output('taiex-prediction-results', 'children'),
|
| 805 |
dash.dependencies.Output('taiex-prediction-chart', 'figure')],
|
|
|
|
| 814 |
|
| 815 |
if final_prediction is None: return html.Div("資料不足,無法進行預測"), {}
|
| 816 |
current_price, last_date = data['Close'].iloc[-1], data.index[-1]
|
| 817 |
+
|
| 818 |
+
# 【重要更新】現在 change_pct 已經是正確的漲幅百分比
|
| 819 |
predicted_price, change_pct, confidence = final_prediction['predicted_price'], final_prediction['change_pct'], final_prediction['confidence']
|
| 820 |
|
| 821 |
+
# 生成預測路徑(為了圖表顯示)
|
| 822 |
prediction_paths = {1: [1], 5: [1, 5], 10: [1, 5, 10], 20: [1, 10, 20], 60: [1, 10, 20, 60]}
|
| 823 |
intervals_to_predict = prediction_paths.get(predict_days, [predict_days])
|
| 824 |
prediction_dates, prediction_prices = [last_date], [current_price]
|
| 825 |
|
| 826 |
for days in intervals_to_predict:
|
|
|
|
| 827 |
interim_prediction = get_prediction(data, days)
|
| 828 |
if interim_prediction:
|
| 829 |
prediction_dates.append(last_date + timedelta(days=days))
|
| 830 |
prediction_prices.append(interim_prediction['predicted_price'])
|
| 831 |
|
| 832 |
+
# 後續繪圖邏輯不變,但現在 change_pct 是真正的漲幅百分比
|
| 833 |
color, arrow = ('red', '📈') if change_pct >= 0 else ('green', '📉')
|
| 834 |
result_card = html.Div([
|
| 835 |
html.H4(f"{predict_days}日後預測結果", style={'margin': '0 0 15px 0', 'color': 'white'}),
|
| 836 |
+
html.Div([
|
| 837 |
+
html.Span(f"{arrow} ", style={'font-size': '24px'}),
|
| 838 |
+
html.Span(f"{change_pct:+.2f}%", style={'font-size': '28px','font-weight': 'bold','color': color})
|
| 839 |
+
], style={'margin': '10px 0'}),
|
| 840 |
+
html.P(f"目前價格: {current_price:.2f}", style={'margin': '5px 0'}),
|
| 841 |
+
html.P(f"預測價格: {predicted_price:.2f}", style={'margin': '5px 0'}),
|
| 842 |
+
html.P(f"預測漲幅: {change_pct:+.2f}%", style={'margin': '5px 0', 'font-weight': 'bold'}), # 【新增】
|
| 843 |
html.P(f"信心度: {confidence:.1%}", style={'margin': '5px 0', 'font-size': '14px'})
|
| 844 |
], style={'background': 'rgba(255,255,255,0.1)','padding': '20px','border-radius': '10px','border': '1px solid rgba(255,255,255,0.2)'})
|
| 845 |
+
|
| 846 |
+
# 繪圖部分保持不變
|
| 847 |
fig = go.Figure()
|
| 848 |
recent_data = data.tail(30)
|
| 849 |
fig.add_trace(go.Scatter(x=recent_data.index, y=recent_data['Close'], mode='lines', name='歷史價格', line=dict(color='#FFA726', width=2)))
|
| 850 |
fig.add_trace(go.Scatter(x=prediction_dates, y=prediction_prices, mode='lines+markers', name=f'{predict_days}日預測路徑', line=dict(color=color, width=3, dash='dash'), marker=dict(size=8)))
|
| 851 |
+
fig.update_layout(title=f'台指期 {predict_days}日預測走勢 (預測漲幅: {change_pct:+.2f}%)', xaxis_title='日期', yaxis_title='指數點位', height=350, plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='white'))
|
| 852 |
+
|
| 853 |
return result_card, fig
|
| 854 |
|
| 855 |
@app.callback(
|