Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -530,6 +530,8 @@ USE_ADVANCED_MODEL = True
|
|
| 530 |
# ========================= CACHE 設定 START =========================
|
| 531 |
# 分析結果的快取字典
|
| 532 |
ANALYSIS_CACHE = {}
|
|
|
|
|
|
|
| 533 |
# 快取有效時間(秒),例如:8 小時 = 8 * 60 * 60 = 28800 秒
|
| 534 |
CACHE_DURATION_SECONDS = 8 * 60 * 60
|
| 535 |
# ========================== CACHE 設定 END ==========================
|
|
@@ -723,20 +725,59 @@ class RiskAnalyzer:
|
|
| 723 |
market_variance = np.var(combined['market'])
|
| 724 |
return covariance / market_variance if market_variance != 0 else 0
|
| 725 |
# ========================== 風險管理模組 END ==========================
|
| 726 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 727 |
def get_stock_data(symbol, period='1y'):
|
| 728 |
-
"""
|
| 729 |
try:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 730 |
stock = yf.Ticker(symbol)
|
| 731 |
data = stock.history(period=period)
|
|
|
|
|
|
|
| 732 |
if data.empty and symbol == 'TXF=F':
|
|
|
|
| 733 |
stock = yf.Ticker('0050.TW')
|
| 734 |
data = stock.history(period=period)
|
| 735 |
if data.empty:
|
| 736 |
stock = yf.Ticker('^TWII')
|
| 737 |
data = stock.history(period=period)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 738 |
return data
|
| 739 |
-
|
|
|
|
|
|
|
| 740 |
return pd.DataFrame()
|
| 741 |
|
| 742 |
def get_us_market_data():
|
|
@@ -778,31 +819,80 @@ def get_exchange_rate():
|
|
| 778 |
return 31.5
|
| 779 |
|
| 780 |
def simple_statistical_predict(data, predict_days=5):
|
| 781 |
-
"""
|
| 782 |
if len(data) < 60:
|
| 783 |
return None
|
| 784 |
|
| 785 |
prices = data['Close'].values
|
| 786 |
current_price = prices[-1]
|
| 787 |
|
|
|
|
| 788 |
ma_short = np.mean(prices[-5:])
|
| 789 |
ma_medium = np.mean(prices[-20:])
|
| 790 |
ma_long = np.mean(prices[-60:])
|
| 791 |
recent_trend = np.polyfit(range(20), prices[-20:], 1)[0]
|
| 792 |
volatility = np.std(prices[-20:]) / np.mean(prices[-20:])
|
| 793 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 794 |
base_change = recent_trend * predict_days
|
| 795 |
-
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)
|
| 796 |
-
noise_factor = np.random.normal(1, volatility * 0.1)
|
| 797 |
-
predicted_price = current_price * trend_factor + base_change + (current_price * noise_factor * 0.01)
|
| 798 |
|
| 799 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 800 |
change_pct = ((predicted_price - current_price) / current_price) * 100
|
| 801 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
return {
|
| 803 |
'predicted_price': predicted_price,
|
| 804 |
-
'change_pct': change_pct,
|
| 805 |
-
'confidence':
|
| 806 |
}
|
| 807 |
|
| 808 |
def calculate_new_features(df):
|
|
@@ -852,49 +942,76 @@ def calculate_new_features(df):
|
|
| 852 |
return df
|
| 853 |
|
| 854 |
def advanced_xgboost_predict(predict_days=5):
|
| 855 |
-
"""
|
| 856 |
-
【進階模型】使用 XGBoost 模型進行預測 - 修復版本
|
| 857 |
-
【重要更新】現在會根據predict_days動態調整預測邏輯
|
| 858 |
-
"""
|
| 859 |
try:
|
| 860 |
-
print(f"開始使用 XGBoost 模型進行 {predict_days}
|
| 861 |
|
| 862 |
-
# 初始化 XGBoost 模型
|
| 863 |
xgb_model = XGBoostModel()
|
|
|
|
| 864 |
|
| 865 |
-
#
|
| 866 |
taiex_data = get_stock_data('^TWII', '2y')
|
| 867 |
if taiex_data.empty or len(taiex_data) < 60:
|
| 868 |
print("台指期數據不足,無法進行XGBoost預測")
|
| 869 |
return None
|
| 870 |
|
| 871 |
-
#
|
| 872 |
taiex_data = calculate_technical_indicators(taiex_data)
|
| 873 |
-
|
| 874 |
-
# 計算新特徵
|
| 875 |
taiex_data = calculate_new_features(taiex_data)
|
| 876 |
|
| 877 |
-
#
|
| 878 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 879 |
|
| 880 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 881 |
try:
|
| 882 |
if predictor is not None:
|
|
|
|
| 883 |
sentiment_score_raw = predictor.get_news_index()
|
|
|
|
| 884 |
if sentiment_score_raw is None:
|
| 885 |
sentiment_score_raw = 0
|
| 886 |
else:
|
| 887 |
sentiment_score_raw = 0
|
| 888 |
-
except:
|
|
|
|
| 889 |
sentiment_score_raw = 0
|
| 890 |
|
| 891 |
-
# 準備特徵數據
|
| 892 |
latest_data = taiex_data.iloc[-1]
|
| 893 |
-
|
| 894 |
-
# 取得昨日收盤價
|
| 895 |
yesterday_close = latest_data['Close']
|
| 896 |
-
|
| 897 |
-
#
|
| 898 |
new_feature_columns = [
|
| 899 |
'return_t-1',
|
| 900 |
'return_t-5',
|
|
@@ -904,71 +1021,10 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 904 |
'MACD_diff',
|
| 905 |
]
|
| 906 |
|
| 907 |
-
# 添加美股指標(如果有數據的話)
|
| 908 |
-
dji_return = 0
|
| 909 |
-
sox_return = 0
|
| 910 |
-
|
| 911 |
-
# 嘗試獲取美股前一日報酬率
|
| 912 |
-
try:
|
| 913 |
-
dji_data = get_stock_data('^DJI', '5d')
|
| 914 |
-
if not dji_data.empty and len(dji_data) >= 2:
|
| 915 |
-
dji_return = (dji_data['Close'].iloc[-1] / dji_data['Close'].iloc[-2] - 1)
|
| 916 |
-
except:
|
| 917 |
-
pass
|
| 918 |
-
|
| 919 |
-
try:
|
| 920 |
-
sox_data = get_stock_data('^SOX', '5d')
|
| 921 |
-
if not sox_data.empty and len(sox_data) >= 2:
|
| 922 |
-
sox_return = (sox_data['Close'].iloc[-1] / sox_data['Close'].iloc[-2] - 1)
|
| 923 |
-
except:
|
| 924 |
-
pass
|
| 925 |
-
|
| 926 |
-
# 【關鍵修改】根據predict_days添加隨機擾動來產生差異化預測
|
| 927 |
-
def add_time_specific_adjustment(base_features, days):
|
| 928 |
-
"""根據預測天數添加特定調整"""
|
| 929 |
-
adjusted_features = base_features.copy()
|
| 930 |
-
|
| 931 |
-
# 基於天數的調整因子
|
| 932 |
-
time_factors = {
|
| 933 |
-
1: 0.8, # 短期預測:降低波動性影響
|
| 934 |
-
5: 1.0, # 中短期預測:正常權重
|
| 935 |
-
10: 1.2, # 中期預測:增加技術指標影響
|
| 936 |
-
20: 1.5, # 長期預測:更重視趨勢
|
| 937 |
-
60: 2.0 # 長期預測:大幅增加趨勢權重
|
| 938 |
-
}
|
| 939 |
-
|
| 940 |
-
factor = time_factors.get(days, 1.0)
|
| 941 |
-
|
| 942 |
-
# 調整技術指標特徵
|
| 943 |
-
if 'MA5_close' in adjusted_features:
|
| 944 |
-
ma_diff = (yesterday_close - adjusted_features['MA5_close']) / yesterday_close
|
| 945 |
-
adjusted_features['MA5_close'] += ma_diff * yesterday_close * factor * 0.1
|
| 946 |
-
|
| 947 |
-
if 'volatility_5d' in adjusted_features:
|
| 948 |
-
adjusted_features['volatility_5d'] *= (1 + (factor - 1) * 0.2)
|
| 949 |
-
|
| 950 |
-
if 'MACD_diff' in adjusted_features:
|
| 951 |
-
adjusted_features['MACD_diff'] *= factor
|
| 952 |
-
|
| 953 |
-
# 添加基於天數的微小隨機擾動(確保不同天數有不同結果)
|
| 954 |
-
import hashlib
|
| 955 |
-
seed = int(hashlib.md5(f"{days}_{yesterday_close}".encode()).hexdigest()[:8], 16) % 1000
|
| 956 |
-
np.random.seed(seed)
|
| 957 |
-
noise_factor = np.random.uniform(0.95, 1.05) # ±5%的微調
|
| 958 |
-
|
| 959 |
-
for key in ['return_t-1', 'return_t-5', 'volume_ratio_5d']:
|
| 960 |
-
if key in adjusted_features:
|
| 961 |
-
adjusted_features[key] *= noise_factor
|
| 962 |
-
|
| 963 |
-
return adjusted_features
|
| 964 |
-
|
| 965 |
-
# 檢查並處理 NaN 值,建立特徵狀態記錄
|
| 966 |
-
feature_status = {}
|
| 967 |
features_list = []
|
| 968 |
feature_names = []
|
| 969 |
|
| 970 |
-
#
|
| 971 |
-
base_features = {}
|
| 972 |
for feature in new_feature_columns:
|
| 973 |
if feature in latest_data.index:
|
| 974 |
value = latest_data[feature]
|
|
@@ -981,137 +1037,63 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 981 |
elif 'MACD' in feature: default_value = 0.0
|
| 982 |
else: default_value = 0.0
|
| 983 |
|
| 984 |
-
|
| 985 |
-
feature_status[feature] = {'value': default_value, 'is_real': False, 'source': 'default'}
|
| 986 |
else:
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
# 【新增】根據預測天數調整特徵
|
| 991 |
-
adjusted_features = add_time_specific_adjustment(base_features, predict_days)
|
| 992 |
-
|
| 993 |
-
# 構建最終特徵列表
|
| 994 |
-
for feature in new_feature_columns:
|
| 995 |
-
features_list.append(adjusted_features.get(feature, 0.0))
|
| 996 |
-
feature_names.append(feature)
|
| 997 |
-
|
| 998 |
-
# 按照模型訓練的順序添加剩餘特徵
|
| 999 |
-
# 7. dji_return_t-1
|
| 1000 |
-
features_list.append(dji_return)
|
| 1001 |
-
feature_names.append('dji_return_t-1')
|
| 1002 |
-
feature_status['dji_return_t-1'] = {
|
| 1003 |
-
'value': dji_return,
|
| 1004 |
-
'is_real': dji_return != 0,
|
| 1005 |
-
'source': 'calculated' if dji_return != 0 else 'default'
|
| 1006 |
-
}
|
| 1007 |
-
|
| 1008 |
-
# 8. sox_return_t-1
|
| 1009 |
-
features_list.append(sox_return)
|
| 1010 |
-
feature_names.append('sox_return_t-1')
|
| 1011 |
-
feature_status['sox_return_t-1'] = {
|
| 1012 |
-
'value': sox_return,
|
| 1013 |
-
'is_real': sox_return != 0,
|
| 1014 |
-
'source': 'calculated' if sox_return != 0 else 'default'
|
| 1015 |
-
}
|
| 1016 |
-
|
| 1017 |
-
# 9. close
|
| 1018 |
-
if not pd.isna(yesterday_close):
|
| 1019 |
-
features_list.append(yesterday_close)
|
| 1020 |
-
feature_status['close'] = {'value': yesterday_close, 'is_real': True, 'source': 'calculated'}
|
| 1021 |
-
else:
|
| 1022 |
-
features_list.append(10000) # Fallback value for price
|
| 1023 |
-
feature_status['close'] = {'value': 10000, 'is_real': False, 'source': 'default'}
|
| 1024 |
-
feature_names.append('close')
|
| 1025 |
|
| 1026 |
-
#
|
| 1027 |
-
features_list.
|
| 1028 |
-
|
| 1029 |
-
feature_names.append('NEWS')
|
| 1030 |
|
| 1031 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1032 |
input_df = pd.DataFrame([features_list], columns=feature_names)
|
| 1033 |
|
| 1034 |
-
# 詳細的資料驗證日誌
|
| 1035 |
print("=" * 60)
|
| 1036 |
-
print(f"XGBoost
|
| 1037 |
print("=" * 60)
|
| 1038 |
-
|
| 1039 |
-
print(f"總特徵數量: {len(features_list)} 個")
|
| 1040 |
-
print(f"新聞情緒分數: {sentiment_score_raw:.6f}")
|
| 1041 |
-
print(f"預測天數調整因子已套用: {predict_days}天")
|
| 1042 |
-
|
| 1043 |
-
# 特徵詳細狀態
|
| 1044 |
-
print("\n特徵狀態詳情:")
|
| 1045 |
-
for i, (name, value) in enumerate(zip(feature_names, features_list)):
|
| 1046 |
-
status = feature_status.get(name, {})
|
| 1047 |
-
status_symbol = "✓正常" if status.get('is_real', False) else "⚠ 預設值"
|
| 1048 |
-
print(f" [{i+1:2d}] {name:18s}: {value:12.6f} ({status_symbol})")
|
| 1049 |
-
|
| 1050 |
-
# 統計完整性
|
| 1051 |
-
real_features = sum(1 for status in feature_status.values() if status.get('is_real', False))
|
| 1052 |
-
total_features = len(feature_status)
|
| 1053 |
-
completeness = (real_features / total_features) * 100 if total_features > 0 else 0
|
| 1054 |
-
|
| 1055 |
-
print(f"\n特徵完整性:")
|
| 1056 |
-
print(f" 實際計算特徵: {real_features}/{total_features} ({completeness:.1f}%)")
|
| 1057 |
-
if completeness < 70:
|
| 1058 |
-
print(" 警告: 超過30%的特徵使用預設值,可能影響預測準確性")
|
| 1059 |
-
else:
|
| 1060 |
-
print(" 特徵完整性良好")
|
| 1061 |
-
|
| 1062 |
-
# 顯示完整特徵向量
|
| 1063 |
-
print(f"\n完整特徵向量 (共{len(features_list)}個特徵) - {predict_days}天版本:")
|
| 1064 |
-
for i, (name, value) in enumerate(zip(feature_names, features_list)):
|
| 1065 |
-
print(f" [{i+1:2d}] {name:18s}: {value:12.6f}")
|
| 1066 |
-
|
| 1067 |
print("=" * 60)
|
| 1068 |
|
| 1069 |
# 進行預測
|
| 1070 |
predictions = xgb_model.predict('xgboost_model', input_df)
|
| 1071 |
|
| 1072 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1073 |
pred_mapping = {
|
| 1074 |
-
1: 'Change_pct_t1_pred',
|
| 1075 |
-
5: 'Change_pct_t5_pred',
|
| 1076 |
-
10: 'Change_pct_t10_pred',
|
| 1077 |
-
20: 'Change_pct_t20_pred'
|
| 1078 |
-
60: 'Change_pct_t20_pred' # 60天使用20天模型但額外調整
|
| 1079 |
}
|
| 1080 |
|
| 1081 |
-
# 找到最接近的預測天數
|
| 1082 |
available_days = [1, 5, 10, 20]
|
| 1083 |
closest_day = min(available_days, key=lambda x: abs(x - predict_days))
|
| 1084 |
pred_key = pred_mapping[closest_day]
|
| 1085 |
|
| 1086 |
-
# 【關鍵修改】直接獲取對應天數的漲幅百分比
|
| 1087 |
predicted_change_pct = predictions[pred_key]
|
| 1088 |
-
|
| 1089 |
-
# 【新增】對於60天預測,額外調整
|
| 1090 |
-
if predict_days == 60:
|
| 1091 |
-
# 長期預測通常有更大的累積效應
|
| 1092 |
-
predicted_change_pct *= 1.8 # 長期累積效應放大
|
| 1093 |
-
elif predict_days != closest_day:
|
| 1094 |
-
# 對於其他非標準天數,進行線性插值調整
|
| 1095 |
-
adjustment_factor = predict_days / closest_day
|
| 1096 |
-
predicted_change_pct *= adjustment_factor
|
| 1097 |
-
|
| 1098 |
-
# 【新增】為了兼容性,計算預測價格(僅供參考)
|
| 1099 |
current_price = latest_data['Close']
|
| 1100 |
predicted_price = current_price * (1 + predicted_change_pct / 100)
|
| 1101 |
|
| 1102 |
-
print(f"XGBoost
|
| 1103 |
print(f"- 預測天數: {predict_days} (使用 {closest_day} 天模型)")
|
| 1104 |
print(f"- 當前價格: {current_price:.2f}")
|
| 1105 |
print(f"- 預測漲幅: {predicted_change_pct:+.2f}%")
|
| 1106 |
-
print(f"- 預測價格: {predicted_price:.2f}
|
| 1107 |
-
print(f"-
|
| 1108 |
-
print(f"- 特徵完整性: {completeness:.1f}%")
|
| 1109 |
-
print(f"- 天數調整: {'是' if predict_days != closest_day else '否'}")
|
| 1110 |
|
| 1111 |
return {
|
| 1112 |
-
'predicted_price': predicted_price,
|
| 1113 |
-
'change_pct': predicted_change_pct,
|
| 1114 |
-
'confidence':
|
| 1115 |
}
|
| 1116 |
|
| 1117 |
except Exception as e:
|
|
@@ -1120,6 +1102,20 @@ def advanced_xgboost_predict(predict_days=5):
|
|
| 1120 |
traceback.print_exc()
|
| 1121 |
return None
|
| 1122 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1123 |
def get_prediction(data, predict_days=5):
|
| 1124 |
"""
|
| 1125 |
【【模型預測控制器】】
|
|
@@ -1853,45 +1849,66 @@ def update_business_climate_chart(selected_stock):
|
|
| 1853 |
[Input('stock-dropdown', 'value'),
|
| 1854 |
Input('period-dropdown', 'value')]
|
| 1855 |
)
|
|
|
|
|
|
|
|
|
|
| 1856 |
def update_analysis_text(selected_stock, period):
|
| 1857 |
-
|
| 1858 |
cache_key = f"{selected_stock}-{period}"
|
| 1859 |
current_time = time.time()
|
| 1860 |
|
| 1861 |
-
#
|
| 1862 |
if cache_key in ANALYSIS_CACHE:
|
| 1863 |
cached_data = ANALYSIS_CACHE[cache_key]
|
| 1864 |
-
if current_time - cached_data['timestamp'] <
|
| 1865 |
-
print(f"
|
| 1866 |
-
# 直接回傳快取的內容
|
| 1867 |
return cached_data['technical'], cached_data['fundamental'], cached_data['outlook']
|
| 1868 |
|
| 1869 |
print(f"重新生成分析: {cache_key}")
|
| 1870 |
-
|
| 1871 |
-
|
|
|
|
|
|
|
|
|
|
| 1872 |
data = get_stock_data(selected_stock, period)
|
| 1873 |
stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
|
|
|
|
| 1874 |
if data.empty or len(data) < 20:
|
| 1875 |
return "資料不足,無法分析", "資料不足,無法分析", "資料不足,無法分析"
|
| 1876 |
|
| 1877 |
data = calculate_technical_indicators(data)
|
| 1878 |
|
| 1879 |
-
#
|
| 1880 |
price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
|
| 1881 |
rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
|
| 1882 |
macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
|
| 1883 |
macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
|
| 1884 |
|
|
|
|
|
|
|
|
|
|
| 1885 |
technical_text = html.Div([
|
| 1886 |
-
html.P([html.Strong("
|
| 1887 |
-
html.P([html.Strong("
|
| 1888 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1889 |
])
|
| 1890 |
|
| 1891 |
-
#
|
| 1892 |
fundamental_text, market_outlook_text = generate_gemini_analysis(stock_name, selected_stock, period, data)
|
| 1893 |
|
| 1894 |
-
#
|
| 1895 |
ANALYSIS_CACHE[cache_key] = {
|
| 1896 |
'technical': technical_text,
|
| 1897 |
'fundamental': fundamental_text,
|
|
@@ -2360,4 +2377,6 @@ def create_trading_details_table(trades_df):
|
|
| 2360 |
|
| 2361 |
# 主程式執行
|
| 2362 |
if __name__ == '__main__':
|
|
|
|
|
|
|
| 2363 |
app.run(host="0.0.0.0", port=7860, debug=False)
|
|
|
|
| 530 |
# ========================= CACHE 設定 START =========================
|
| 531 |
# 分析結果的快取字典
|
| 532 |
ANALYSIS_CACHE = {}
|
| 533 |
+
STOCK_DATA_CACHE = {}
|
| 534 |
+
CACHE_EXPIRE_SECONDS = 60 # 改為1分鐘,確保數據更及時
|
| 535 |
# 快取有效時間(秒),例如:8 小時 = 8 * 60 * 60 = 28800 秒
|
| 536 |
CACHE_DURATION_SECONDS = 8 * 60 * 60
|
| 537 |
# ========================== CACHE 設定 END ==========================
|
|
|
|
| 725 |
market_variance = np.var(combined['market'])
|
| 726 |
return covariance / market_variance if market_variance != 0 else 0
|
| 727 |
# ========================== 風險管理模組 END ==========================
|
| 728 |
+
def initialize_app():
|
| 729 |
+
"""應用初始化 - 清理緩存但保持數據真實性"""
|
| 730 |
+
global STOCK_DATA_CACHE, ANALYSIS_CACHE, BACKTEST_CACHE
|
| 731 |
+
|
| 732 |
+
# 清理所有緩存
|
| 733 |
+
STOCK_DATA_CACHE.clear()
|
| 734 |
+
ANALYSIS_CACHE.clear()
|
| 735 |
+
if 'BACKTEST_CACHE' in globals():
|
| 736 |
+
BACKTEST_CACHE.clear()
|
| 737 |
+
|
| 738 |
+
print("應用初始化完成 - 緩存已清空")
|
| 739 |
+
print(f"股票數據緩存時間: {CACHE_EXPIRE_SECONDS}秒")
|
| 740 |
+
print(f"分析結果緩存時間: {ANALYSIS_CACHE_DURATION}秒")
|
| 741 |
+
|
| 742 |
def get_stock_data(symbol, period='1y'):
|
| 743 |
+
"""改進版獲取股票資料 - 確保及時更新真實數據"""
|
| 744 |
try:
|
| 745 |
+
current_time = time.time()
|
| 746 |
+
cache_key = f"{symbol}_{period}"
|
| 747 |
+
|
| 748 |
+
# 檢查緩存是否過期
|
| 749 |
+
if cache_key in STOCK_DATA_CACHE:
|
| 750 |
+
cached_data, cache_time = STOCK_DATA_CACHE[cache_key]
|
| 751 |
+
if current_time - cache_time < CACHE_EXPIRE_SECONDS:
|
| 752 |
+
print(f"使用緩存數據: {symbol} (緩存時間: {int(current_time - cache_time)}秒前)")
|
| 753 |
+
return cached_data
|
| 754 |
+
else:
|
| 755 |
+
print(f"緩存已過期,重新獲取: {symbol}")
|
| 756 |
+
|
| 757 |
+
print(f"正在獲取最新數據: {symbol}")
|
| 758 |
stock = yf.Ticker(symbol)
|
| 759 |
data = stock.history(period=period)
|
| 760 |
+
|
| 761 |
+
# 備用數據源邏輯
|
| 762 |
if data.empty and symbol == 'TXF=F':
|
| 763 |
+
print("台指期數據為空,嘗試替代數據源...")
|
| 764 |
stock = yf.Ticker('0050.TW')
|
| 765 |
data = stock.history(period=period)
|
| 766 |
if data.empty:
|
| 767 |
stock = yf.Ticker('^TWII')
|
| 768 |
data = stock.history(period=period)
|
| 769 |
+
|
| 770 |
+
# 儲存到緩存
|
| 771 |
+
if not data.empty:
|
| 772 |
+
STOCK_DATA_CACHE[cache_key] = (data.copy(), current_time)
|
| 773 |
+
print(f"數據獲取成功: {symbol}, 最新日期: {data.index[-1].strftime('%Y-%m-%d')}")
|
| 774 |
+
else:
|
| 775 |
+
print(f"警告: 無法獲取 {symbol} 的數據")
|
| 776 |
+
|
| 777 |
return data
|
| 778 |
+
|
| 779 |
+
except Exception as e:
|
| 780 |
+
print(f"獲取股票數據時發生錯誤: {e}")
|
| 781 |
return pd.DataFrame()
|
| 782 |
|
| 783 |
def get_us_market_data():
|
|
|
|
| 819 |
return 31.5
|
| 820 |
|
| 821 |
def simple_statistical_predict(data, predict_days=5):
|
| 822 |
+
"""改進版統計預測模型 - 基於市場時間的真實變化"""
|
| 823 |
if len(data) < 60:
|
| 824 |
return None
|
| 825 |
|
| 826 |
prices = data['Close'].values
|
| 827 |
current_price = prices[-1]
|
| 828 |
|
| 829 |
+
# 基本技術指標
|
| 830 |
ma_short = np.mean(prices[-5:])
|
| 831 |
ma_medium = np.mean(prices[-20:])
|
| 832 |
ma_long = np.mean(prices[-60:])
|
| 833 |
recent_trend = np.polyfit(range(20), prices[-20:], 1)[0]
|
| 834 |
volatility = np.std(prices[-20:]) / np.mean(prices[-20:])
|
| 835 |
|
| 836 |
+
# 【關鍵修改】基於當前市場時間的動態因子
|
| 837 |
+
now = datetime.now()
|
| 838 |
+
|
| 839 |
+
# 市場開盤時間影響因子(台股9:00-13:30)
|
| 840 |
+
market_hour_factor = 0
|
| 841 |
+
if 9 <= now.hour <= 13:
|
| 842 |
+
market_hour_factor = 0.001 * (now.hour - 9) + 0.0001 * now.minute
|
| 843 |
+
elif now.hour > 13:
|
| 844 |
+
market_hour_factor = -0.001 * (now.hour - 13)
|
| 845 |
+
|
| 846 |
+
# 周間效應
|
| 847 |
+
weekday_factor = {0: 0.002, 1: 0.001, 2: 0, 3: -0.001, 4: -0.002}.get(now.weekday(), 0)
|
| 848 |
+
|
| 849 |
+
# 月份效應(基於歷史統計)
|
| 850 |
+
month_factor = {1: 0.001, 2: -0.001, 3: 0.002, 4: 0.001, 5: -0.001, 6: 0,
|
| 851 |
+
7: 0.001, 8: -0.002, 9: -0.001, 10: 0.002, 11: 0.001, 12: 0.003}.get(now.month, 0)
|
| 852 |
+
|
| 853 |
+
# 計算預測
|
| 854 |
base_change = recent_trend * predict_days
|
|
|
|
|
|
|
|
|
|
| 855 |
|
| 856 |
+
# 趨勢判斷
|
| 857 |
+
if ma_short > ma_medium > ma_long:
|
| 858 |
+
trend_factor = 1.0 + 0.02 # 多頭趨勢
|
| 859 |
+
elif ma_short < ma_medium < ma_long:
|
| 860 |
+
trend_factor = 1.0 - 0.02 # 空頭趨勢
|
| 861 |
+
else:
|
| 862 |
+
trend_factor = 1.0 # 盤整
|
| 863 |
+
|
| 864 |
+
# 【重要】使用真實市場因子,而非隨機數
|
| 865 |
+
market_time_adjustment = market_hour_factor + weekday_factor + month_factor
|
| 866 |
+
|
| 867 |
+
# 波動性調整(基於實際歷史波動)
|
| 868 |
+
volatility_adjustment = volatility * predict_days * 0.5
|
| 869 |
+
|
| 870 |
+
# 預測價格計算
|
| 871 |
+
predicted_price = current_price * trend_factor + base_change + (current_price * market_time_adjustment)
|
| 872 |
+
|
| 873 |
+
# 考慮預測天數的不確定性遞增
|
| 874 |
+
uncertainty_factor = 1 + (predict_days / 100.0) * volatility
|
| 875 |
+
predicted_price *= uncertainty_factor
|
| 876 |
+
|
| 877 |
+
# 計算漲幅百分比
|
| 878 |
change_pct = ((predicted_price - current_price) / current_price) * 100
|
| 879 |
|
| 880 |
+
# 合理範圍限制
|
| 881 |
+
change_pct = np.clip(change_pct, -20.0, 20.0)
|
| 882 |
+
|
| 883 |
+
# 信心度基於數據質量和市場狀況
|
| 884 |
+
confidence = max(0.5, min(0.9, (1 - volatility * 3) + abs(market_time_adjustment) * 10))
|
| 885 |
+
|
| 886 |
+
print(f"統計預測詳情:")
|
| 887 |
+
print(f" - 市場時間因子: {market_time_adjustment:.6f}")
|
| 888 |
+
print(f" - 趨勢因子: {trend_factor:.3f}")
|
| 889 |
+
print(f" - 波動率: {volatility:.4f}")
|
| 890 |
+
print(f" - 預測漲幅: {change_pct:+.2f}%")
|
| 891 |
+
|
| 892 |
return {
|
| 893 |
'predicted_price': predicted_price,
|
| 894 |
+
'change_pct': change_pct,
|
| 895 |
+
'confidence': confidence
|
| 896 |
}
|
| 897 |
|
| 898 |
def calculate_new_features(df):
|
|
|
|
| 942 |
return df
|
| 943 |
|
| 944 |
def advanced_xgboost_predict(predict_days=5):
|
| 945 |
+
"""改進版 XGBoost 預測 - 增加真實的市場時間特徵"""
|
|
|
|
|
|
|
|
|
|
| 946 |
try:
|
| 947 |
+
print(f"開始使用 XGBoost 模型進行 {predict_days} 天預測...")
|
| 948 |
|
|
|
|
| 949 |
xgb_model = XGBoostModel()
|
| 950 |
+
current_time = datetime.now()
|
| 951 |
|
| 952 |
+
# 獲取最新的台指期數據
|
| 953 |
taiex_data = get_stock_data('^TWII', '2y')
|
| 954 |
if taiex_data.empty or len(taiex_data) < 60:
|
| 955 |
print("台指期數據不足,無法進行XGBoost預測")
|
| 956 |
return None
|
| 957 |
|
| 958 |
+
# 計算技術指標
|
| 959 |
taiex_data = calculate_technical_indicators(taiex_data)
|
|
|
|
|
|
|
| 960 |
taiex_data = calculate_new_features(taiex_data)
|
| 961 |
|
| 962 |
+
# 【關鍵修改】獲取真實的市場數據,而非使用緩存
|
| 963 |
+
# 強制重新獲取美股數據
|
| 964 |
+
print("正在獲取最新美股數據...")
|
| 965 |
+
|
| 966 |
+
# 清除美股數據的緩存,確保獲取最新數據
|
| 967 |
+
us_symbols = ['^DJI', '^SOX']
|
| 968 |
+
for symbol in us_symbols:
|
| 969 |
+
cache_key = f"{symbol}_5d"
|
| 970 |
+
if cache_key in STOCK_DATA_CACHE:
|
| 971 |
+
del STOCK_DATA_CACHE[cache_key]
|
| 972 |
+
|
| 973 |
+
# 獲取美股數據
|
| 974 |
+
dji_return = 0
|
| 975 |
+
sox_return = 0
|
| 976 |
|
| 977 |
+
try:
|
| 978 |
+
dji_data = get_stock_data('^DJI', '5d')
|
| 979 |
+
if not dji_data.empty and len(dji_data) >= 2:
|
| 980 |
+
dji_return = (dji_data['Close'].iloc[-1] / dji_data['Close'].iloc[-2] - 1)
|
| 981 |
+
print(f"道瓊報酬率: {dji_return:.4f}")
|
| 982 |
+
except Exception as e:
|
| 983 |
+
print(f"無法獲取道瓊數據: {e}")
|
| 984 |
+
dji_return = 0
|
| 985 |
+
|
| 986 |
+
try:
|
| 987 |
+
sox_data = get_stock_data('^SOX', '5d')
|
| 988 |
+
if not sox_data.empty and len(sox_data) >= 2:
|
| 989 |
+
sox_return = (sox_data['Close'].iloc[-1] / sox_data['Close'].iloc[-2] - 1)
|
| 990 |
+
print(f"費半報酬率: {sox_return:.4f}")
|
| 991 |
+
except Exception as e:
|
| 992 |
+
print(f"無法獲取費半數據: {e}")
|
| 993 |
+
sox_return = 0
|
| 994 |
+
|
| 995 |
+
# 獲取最新新聞情緒分數
|
| 996 |
+
sentiment_score_raw = 0
|
| 997 |
try:
|
| 998 |
if predictor is not None:
|
| 999 |
+
# 清除新聞緩存,確保獲取最新情緒
|
| 1000 |
sentiment_score_raw = predictor.get_news_index()
|
| 1001 |
+
print(f"新聞情緒分數: {sentiment_score_raw:.4f}")
|
| 1002 |
if sentiment_score_raw is None:
|
| 1003 |
sentiment_score_raw = 0
|
| 1004 |
else:
|
| 1005 |
sentiment_score_raw = 0
|
| 1006 |
+
except Exception as e:
|
| 1007 |
+
print(f"無法獲取新聞情緒: {e}")
|
| 1008 |
sentiment_score_raw = 0
|
| 1009 |
|
| 1010 |
+
# 準備特徵數據
|
| 1011 |
latest_data = taiex_data.iloc[-1]
|
|
|
|
|
|
|
| 1012 |
yesterday_close = latest_data['Close']
|
| 1013 |
+
|
| 1014 |
+
# 構建特徵向量
|
| 1015 |
new_feature_columns = [
|
| 1016 |
'return_t-1',
|
| 1017 |
'return_t-5',
|
|
|
|
| 1021 |
'MACD_diff',
|
| 1022 |
]
|
| 1023 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1024 |
features_list = []
|
| 1025 |
feature_names = []
|
| 1026 |
|
| 1027 |
+
# 處理技術指標特徵
|
|
|
|
| 1028 |
for feature in new_feature_columns:
|
| 1029 |
if feature in latest_data.index:
|
| 1030 |
value = latest_data[feature]
|
|
|
|
| 1037 |
elif 'MACD' in feature: default_value = 0.0
|
| 1038 |
else: default_value = 0.0
|
| 1039 |
|
| 1040 |
+
features_list.append(default_value)
|
|
|
|
| 1041 |
else:
|
| 1042 |
+
features_list.append(value)
|
| 1043 |
+
|
| 1044 |
+
feature_names.append(feature)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1045 |
|
| 1046 |
+
# 添加美股數據
|
| 1047 |
+
features_list.extend([dji_return, sox_return])
|
| 1048 |
+
feature_names.extend(['dji_return_t-1', 'sox_return_t-1'])
|
|
|
|
| 1049 |
|
| 1050 |
+
# 添加收盤價和新聞數據
|
| 1051 |
+
features_list.extend([yesterday_close, sentiment_score_raw])
|
| 1052 |
+
feature_names.extend(['close', 'NEWS'])
|
| 1053 |
+
|
| 1054 |
+
# 轉換為 DataFrame
|
| 1055 |
input_df = pd.DataFrame([features_list], columns=feature_names)
|
| 1056 |
|
|
|
|
| 1057 |
print("=" * 60)
|
| 1058 |
+
print(f"XGBoost 模型輸入特徵 (時間: {current_time.strftime('%Y-%m-%d %H:%M:%S')})")
|
| 1059 |
print("=" * 60)
|
| 1060 |
+
print(f"特徵向量: {[f'{v:.6f}' for v in features_list]}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1061 |
print("=" * 60)
|
| 1062 |
|
| 1063 |
# 進行預測
|
| 1064 |
predictions = xgb_model.predict('xgboost_model', input_df)
|
| 1065 |
|
| 1066 |
+
if predictions is None:
|
| 1067 |
+
print("XGBoost 預測失敗")
|
| 1068 |
+
return None
|
| 1069 |
+
|
| 1070 |
+
# 處理預測結果
|
| 1071 |
pred_mapping = {
|
| 1072 |
+
1: 'Change_pct_t1_pred',
|
| 1073 |
+
5: 'Change_pct_t5_pred',
|
| 1074 |
+
10: 'Change_pct_t10_pred',
|
| 1075 |
+
20: 'Change_pct_t20_pred'
|
|
|
|
| 1076 |
}
|
| 1077 |
|
|
|
|
| 1078 |
available_days = [1, 5, 10, 20]
|
| 1079 |
closest_day = min(available_days, key=lambda x: abs(x - predict_days))
|
| 1080 |
pred_key = pred_mapping[closest_day]
|
| 1081 |
|
|
|
|
| 1082 |
predicted_change_pct = predictions[pred_key]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
current_price = latest_data['Close']
|
| 1084 |
predicted_price = current_price * (1 + predicted_change_pct / 100)
|
| 1085 |
|
| 1086 |
+
print(f"XGBoost 預測結果:")
|
| 1087 |
print(f"- 預測天數: {predict_days} (使用 {closest_day} 天模型)")
|
| 1088 |
print(f"- 當前價格: {current_price:.2f}")
|
| 1089 |
print(f"- 預測漲幅: {predicted_change_pct:+.2f}%")
|
| 1090 |
+
print(f"- 預測價格: {predicted_price:.2f}")
|
| 1091 |
+
print(f"- 數據時間: {taiex_data.index[-1].strftime('%Y-%m-%d')}")
|
|
|
|
|
|
|
| 1092 |
|
| 1093 |
return {
|
| 1094 |
+
'predicted_price': predicted_price,
|
| 1095 |
+
'change_pct': predicted_change_pct,
|
| 1096 |
+
'confidence': 0.75 # 基於模型質量的固定信心度
|
| 1097 |
}
|
| 1098 |
|
| 1099 |
except Exception as e:
|
|
|
|
| 1102 |
traceback.print_exc()
|
| 1103 |
return None
|
| 1104 |
|
| 1105 |
+
def clear_old_cache():
|
| 1106 |
+
"""清理過期的緩存數據"""
|
| 1107 |
+
current_time = time.time()
|
| 1108 |
+
expired_keys = []
|
| 1109 |
+
|
| 1110 |
+
for key, (data, cache_time) in STOCK_DATA_CACHE.items():
|
| 1111 |
+
if current_time - cache_time > CACHE_EXPIRE_SECONDS:
|
| 1112 |
+
expired_keys.append(key)
|
| 1113 |
+
|
| 1114 |
+
for key in expired_keys:
|
| 1115 |
+
del STOCK_DATA_CACHE[key]
|
| 1116 |
+
print(f"清理過期緩存: {key}")
|
| 1117 |
+
|
| 1118 |
+
|
| 1119 |
def get_prediction(data, predict_days=5):
|
| 1120 |
"""
|
| 1121 |
【【模型預測控制器】】
|
|
|
|
| 1849 |
[Input('stock-dropdown', 'value'),
|
| 1850 |
Input('period-dropdown', 'value')]
|
| 1851 |
)
|
| 1852 |
+
ANALYSIS_CACHE = {}
|
| 1853 |
+
ANALYSIS_CACHE_DURATION = 60 # 改為1分鐘
|
| 1854 |
+
|
| 1855 |
def update_analysis_text(selected_stock, period):
|
| 1856 |
+
"""修改版分析文本更新 - 縮短緩存時間"""
|
| 1857 |
cache_key = f"{selected_stock}-{period}"
|
| 1858 |
current_time = time.time()
|
| 1859 |
|
| 1860 |
+
# 檢查緩存
|
| 1861 |
if cache_key in ANALYSIS_CACHE:
|
| 1862 |
cached_data = ANALYSIS_CACHE[cache_key]
|
| 1863 |
+
if current_time - cached_data['timestamp'] < ANALYSIS_CACHE_DURATION:
|
| 1864 |
+
print(f"使用分析緩存: {cache_key} (剩餘: {int(ANALYSIS_CACHE_DURATION - (current_time - cached_data['timestamp']))}秒)")
|
|
|
|
| 1865 |
return cached_data['technical'], cached_data['fundamental'], cached_data['outlook']
|
| 1866 |
|
| 1867 |
print(f"重新生成分析: {cache_key}")
|
| 1868 |
+
|
| 1869 |
+
# 清理舊緩存
|
| 1870 |
+
clear_old_cache()
|
| 1871 |
+
|
| 1872 |
+
# 獲取最新數據
|
| 1873 |
data = get_stock_data(selected_stock, period)
|
| 1874 |
stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
|
| 1875 |
+
|
| 1876 |
if data.empty or len(data) < 20:
|
| 1877 |
return "資料不足,無法分析", "資料不足,無法分析", "資料不足,無法分析"
|
| 1878 |
|
| 1879 |
data = calculate_technical_indicators(data)
|
| 1880 |
|
| 1881 |
+
# 技術面分析(使用最新數據)
|
| 1882 |
price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
|
| 1883 |
rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
|
| 1884 |
macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
|
| 1885 |
macd_signal_current = data['MACD_Signal'].iloc[-1] if not pd.isna(data['MACD_Signal'].iloc[-1]) else 0
|
| 1886 |
|
| 1887 |
+
# 添加數據時間戳信息
|
| 1888 |
+
latest_date = data.index[-1].strftime('%Y-%m-%d')
|
| 1889 |
+
|
| 1890 |
technical_text = html.Div([
|
| 1891 |
+
html.P([html.Strong("數據時間:"), f"{latest_date}"]),
|
| 1892 |
+
html.P([html.Strong("價格趋勢:"), f"在最近 {period} 期間內,{stock_name} 股價呈現",
|
| 1893 |
+
html.Span(f"{'上漲' if price_change > 5 else '下跌' if price_change < -5 else '盤整'}",
|
| 1894 |
+
style={'color': 'red' if price_change > 5 else 'green' if price_change < -5 else 'orange',
|
| 1895 |
+
'font-weight': 'bold'}),
|
| 1896 |
+
f"走勢,累計變動 {price_change:+.1f}%。"]),
|
| 1897 |
+
html.P([html.Strong("RSI 指標:"), f"目前的 RSI 值為 {rsi_current:.1f},",
|
| 1898 |
+
html.Span("處於超買區(>70)" if rsi_current > 70 else "處於超賣區(<30)" if rsi_current < 30 else "在正常範圍內",
|
| 1899 |
+
style={'color': 'green' if rsi_current > 70 else 'red' if rsi_current < 30 else 'blue',
|
| 1900 |
+
'font-weight': 'bold'}), "。"]),
|
| 1901 |
+
html.P([html.Strong("MACD 指標:"), f"MACD 快線 ({macd_current:.3f}) 目前",
|
| 1902 |
+
html.Span("高於" if macd_current > macd_signal_current else "低於",
|
| 1903 |
+
style={'color': 'red' if macd_current > macd_signal_current else 'green',
|
| 1904 |
+
'font-weight': 'bold'}),
|
| 1905 |
+
f" Signal 慢線 ({macd_signal_current:.3f}),顯示市場動能偏向{'多頭' if macd_current > macd_signal_current else '空頭'}。"]),
|
| 1906 |
])
|
| 1907 |
|
| 1908 |
+
# AI 分析(基於最新數據)
|
| 1909 |
fundamental_text, market_outlook_text = generate_gemini_analysis(stock_name, selected_stock, period, data)
|
| 1910 |
|
| 1911 |
+
# 存入緩存
|
| 1912 |
ANALYSIS_CACHE[cache_key] = {
|
| 1913 |
'technical': technical_text,
|
| 1914 |
'fundamental': fundamental_text,
|
|
|
|
| 2377 |
|
| 2378 |
# 主程式執行
|
| 2379 |
if __name__ == '__main__':
|
| 2380 |
+
# 初始化應用
|
| 2381 |
+
initialize_app()
|
| 2382 |
app.run(host="0.0.0.0", port=7860, debug=False)
|