Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -13,72 +13,24 @@ from plotly.subplots import make_subplots
|
|
| 13 |
import re
|
| 14 |
from bs4 import BeautifulSoup
|
| 15 |
import requests
|
| 16 |
-
import time # 引用 time 模組以處理時間戳
|
| 17 |
|
| 18 |
# 引用您組員的預測器程式
|
| 19 |
from Bert_predict import BertPredictor
|
| 20 |
|
| 21 |
-
# ========================= CACHE 設定 START =========================
|
| 22 |
-
# 分析結果的快取字典
|
| 23 |
-
ANALYSIS_CACHE = {}
|
| 24 |
-
# 快取有效時間(秒),例如:4 小時 = 4 * 60 * 60 = 14400 秒
|
| 25 |
-
CACHE_DURATION_SECONDS = 8 * 60 * 60
|
| 26 |
-
# ========================== CACHE 設定 END ==========================
|
| 27 |
-
|
| 28 |
-
|
| 29 |
# 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
|
| 30 |
TAIWAN_STOCKS = {
|
| 31 |
-
'元大台灣50': '0050.TW',
|
| 32 |
'台積電': '2330.TW',
|
| 33 |
'聯發科': '2454.TW',
|
| 34 |
'鴻海': '2317.TW',
|
| 35 |
-
'
|
| 36 |
-
'
|
| 37 |
'富邦金': '2881.TW',
|
| 38 |
-
'中信金': '2891.TW',
|
| 39 |
'國泰金': '2882.TW',
|
| 40 |
-
'
|
| 41 |
-
'中華電': '2412.TW',
|
| 42 |
-
'玉山金': '2884.TW',
|
| 43 |
-
'兆豐金': '2886.TW',
|
| 44 |
-
'日月光投控': '3711.TW',
|
| 45 |
-
'華碩': '2357.TW',
|
| 46 |
'統一': '1216.TW',
|
| 47 |
-
'
|
| 48 |
-
'智邦': '2345.TW',
|
| 49 |
-
'緯創': '3231.TW',
|
| 50 |
-
'聯詠': '3034.TW',
|
| 51 |
-
'第一金': '2892.TW',
|
| 52 |
-
'瑞昱': '2379.TW',
|
| 53 |
-
'緯穎': '6669.TWO',
|
| 54 |
-
'永豐金': '2890.TW',
|
| 55 |
-
'合庫金': '5880.TW',
|
| 56 |
-
'華南金': '2880.TW',
|
| 57 |
-
'台光電': '2383.TW',
|
| 58 |
-
'世芯-KY': '3661.TWO',
|
| 59 |
-
'奇鋐': '3017.TW',
|
| 60 |
-
'凱基金': '2883.TW',
|
| 61 |
-
'大立光': '3008.TW',
|
| 62 |
'長榮': '2603.TW',
|
| 63 |
-
'光寶科': '2301.TW',
|
| 64 |
-
'中鋼': '2002.TW',
|
| 65 |
-
'中租-KY': '5871.TW',
|
| 66 |
-
'國巨': '2327.TW',
|
| 67 |
-
'台新金': '2887.TW',
|
| 68 |
-
'上海商銀': '5876.TW',
|
| 69 |
-
'台泥': '1101.TW',
|
| 70 |
-
'台灣大': '3045.TW',
|
| 71 |
-
'和碩': '4938.TW',
|
| 72 |
-
'遠傳': '4904.TW',
|
| 73 |
-
'和泰車': '2207.TW',
|
| 74 |
-
'研華': '2395.TW',
|
| 75 |
-
'台塑': '1301.TW',
|
| 76 |
-
'統一超': '2912.TW',
|
| 77 |
-
'藥華藥': '6446.TWO',
|
| 78 |
-
'南亞': '1303.TW',
|
| 79 |
-
'陽明': '2609.TW',
|
| 80 |
-
'萬海': '2615.TW',
|
| 81 |
-
'台塑化': '6505.TW',
|
| 82 |
'慧洋-KY': '2637.TW',
|
| 83 |
'上銀': '2049.TW',
|
| 84 |
'台泥': '1101.TW',
|
|
@@ -92,57 +44,18 @@ TAIWAN_STOCKS = {
|
|
| 92 |
|
| 93 |
# 產業分類
|
| 94 |
INDUSTRY_MAPPING = {
|
| 95 |
-
'0050.TW': 'ETF',
|
| 96 |
'2330.TW': '半導體',
|
| 97 |
'2454.TW': '半導體',
|
| 98 |
'2317.TW': '電子組件',
|
| 99 |
-
'
|
| 100 |
-
'
|
| 101 |
'2881.TW': '金融',
|
| 102 |
-
'2891.TW': '金融',
|
| 103 |
'2882.TW': '金融',
|
| 104 |
-
'
|
| 105 |
-
'2412.TW': '電信',
|
| 106 |
-
'2884.TW': '金融',
|
| 107 |
-
'2886.TW': '金融',
|
| 108 |
-
'3711.TW': '半導體',
|
| 109 |
-
'2357.TW': '電子',
|
| 110 |
'1216.TW': '食品',
|
| 111 |
-
'
|
| 112 |
-
'2345.TW': '網通設備',
|
| 113 |
-
'3231.TW': '電子',
|
| 114 |
-
'3034.TW': '半導體',
|
| 115 |
-
'2892.TW': '金融',
|
| 116 |
-
'2379.TW': '半導體',
|
| 117 |
-
'6669.TWO': '電子',
|
| 118 |
-
'2890.TW': '金融',
|
| 119 |
-
'5880.TW': '金融',
|
| 120 |
-
'2880.TW': '金融',
|
| 121 |
-
'2383.TW': '電子',
|
| 122 |
-
'3661.TWO': '半導體',
|
| 123 |
-
'3017.TW': '電子',
|
| 124 |
-
'2883.TW': '金融',
|
| 125 |
-
'3008.TW': '光學',
|
| 126 |
'2603.TW': '航運',
|
| 127 |
-
'2301.TW': '電子',
|
| 128 |
-
'2002.TW': '鋼鐵',
|
| 129 |
-
'5871.TW': '金融',
|
| 130 |
-
'2327.TW': '電子被動元件',
|
| 131 |
-
'2887.TW': '金融',
|
| 132 |
-
'5876.TW': '金融',
|
| 133 |
-
'1101.TW': '營建',
|
| 134 |
-
'3045.TW': '電信',
|
| 135 |
-
'4938.TW': '電子',
|
| 136 |
-
'4904.TW': '電信',
|
| 137 |
-
'2207.TW': '汽車',
|
| 138 |
-
'2395.TW': '電腦周邊',
|
| 139 |
-
'1301.TW': '塑膠',
|
| 140 |
-
'2912.TW': '百貨',
|
| 141 |
-
'6446.TWO': '生技',
|
| 142 |
-
'1303.TW': '塑膠',
|
| 143 |
-
'2609.TW': '航運',
|
| 144 |
-
'2615.TW': '航運',
|
| 145 |
-
'6505.TW': '塑膠',
|
| 146 |
'2637.TW': '散裝航運',
|
| 147 |
'2049.TW': '工具機',
|
| 148 |
'1101.TW': '營建',
|
|
@@ -274,6 +187,7 @@ def get_pmi_data():
|
|
| 274 |
print(f"無法獲取 PMI 資料: {str(e)}")
|
| 275 |
return pd.DataFrame()
|
| 276 |
|
|
|
|
| 277 |
def generate_gemini_analysis(stock_name, stock_symbol, period, data):
|
| 278 |
"""
|
| 279 |
使用 Gemini API 生成基本面和市場展望分析。
|
|
@@ -286,6 +200,7 @@ def generate_gemini_analysis(stock_name, stock_symbol, period, data):
|
|
| 286 |
genai.configure(api_key=api_key)
|
| 287 |
model = genai.GenerativeModel('gemini-1.5-flash')
|
| 288 |
|
|
|
|
| 289 |
price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
|
| 290 |
rsi_current = data['RSI'].iloc[-1]
|
| 291 |
macd_current = data['MACD'].iloc[-1]
|
|
@@ -327,13 +242,13 @@ def generate_gemini_analysis(stock_name, stock_symbol, period, data):
|
|
| 327 |
market_outlook = parts[1].strip()
|
| 328 |
return dcc.Markdown(fundamental_analysis), dcc.Markdown(market_outlook)
|
| 329 |
else:
|
| 330 |
-
|
| 331 |
-
return dcc.Markdown("無法解析 Gemini 回應,請稍後再試。"), dcc.Markdown(response.text)
|
| 332 |
|
| 333 |
except Exception as e:
|
| 334 |
error_message = f"呼叫 Gemini API 時發生錯誤: {str(e)}"
|
| 335 |
print(error_message)
|
| 336 |
-
return
|
|
|
|
| 337 |
|
| 338 |
# 建立 Dash 應用程式
|
| 339 |
app = dash.Dash(__name__, suppress_callback_exceptions=True)
|
|
@@ -397,7 +312,7 @@ app.layout = html.Div([
|
|
| 397 |
html.Label("時間範圍:"),
|
| 398 |
dcc.Dropdown(id='period-dropdown',
|
| 399 |
options=[{'label': '1個月', 'value': '1mo'},{'label': '3個月', 'value': '3mo'},{'label': '6個月', 'value': '6mo'},{'label': '1年', 'value': '1y'},{'label': '2年', 'value': '2y'}],
|
| 400 |
-
value='1mo', style={'margin-bottom': '10px'})
|
| 401 |
], style={'width': '30%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'}),
|
| 402 |
html.Div([
|
| 403 |
html.Label("圖表類型:"),
|
|
@@ -676,7 +591,7 @@ def update_business_climate_chart(selected_stock):
|
|
| 676 |
fig.update_layout(title="台灣景氣燈號走勢", xaxis_title='日期', yaxis_title='燈號分數', height=300, yaxis=dict(range=[0, 40]))
|
| 677 |
return fig
|
| 678 |
|
| 679 |
-
# ========================= MODIFIED SECTION START
|
| 680 |
@app.callback(
|
| 681 |
[dash.dependencies.Output('technical-analysis-text', 'children'),
|
| 682 |
dash.dependencies.Output('fundamental-analysis-text', 'children'),
|
|
@@ -685,29 +600,14 @@ def update_business_climate_chart(selected_stock):
|
|
| 685 |
dash.dependencies.Input('period-dropdown', 'value')]
|
| 686 |
)
|
| 687 |
def update_analysis_text(selected_stock, period):
|
| 688 |
-
# 建立快取的唯一鍵值
|
| 689 |
-
cache_key = f"{selected_stock}-{period}"
|
| 690 |
-
current_time = time.time()
|
| 691 |
-
|
| 692 |
-
# 1. 檢查快取
|
| 693 |
-
if cache_key in ANALYSIS_CACHE:
|
| 694 |
-
cached_data = ANALYSIS_CACHE[cache_key]
|
| 695 |
-
if current_time - cached_data['timestamp'] < CACHE_DURATION_SECONDS:
|
| 696 |
-
print(f"從快取載入分析: {cache_key}")
|
| 697 |
-
# 直接回傳快取的內容
|
| 698 |
-
return cached_data['technical'], cached_data['fundamental'], cached_data['outlook']
|
| 699 |
-
|
| 700 |
-
print(f"重新生成分析: {cache_key}")
|
| 701 |
-
# --- 如果快取沒有,才繼續執行以下程式 ---
|
| 702 |
-
|
| 703 |
data = get_stock_data(selected_stock, period)
|
| 704 |
stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
|
| 705 |
-
if data.empty or len(data) < 20:
|
| 706 |
return "資料不足,無法分析", "資料不足,無法分析", "資料不足,無法分析"
|
| 707 |
|
| 708 |
data = calculate_technical_indicators(data)
|
| 709 |
|
| 710 |
-
#
|
| 711 |
price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
|
| 712 |
rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
|
| 713 |
macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
|
|
@@ -719,16 +619,17 @@ def update_analysis_text(selected_stock, period):
|
|
| 719 |
html.P([html.Strong("MACD 指標:"), f"MACD 快線 ({macd_current:.3f}) 目前", html.Span("高於" if macd_current > macd_signal_current else "低於", style={'color': 'red' if macd_current > macd_signal_current else 'green', 'font-weight': 'bold'}), f" Signal 慢線 ({macd_signal_current:.3f}),", f"顯示市場動能偏向{'多頭' if macd_current > macd_signal_current else '空頭'}。"]),
|
| 720 |
])
|
| 721 |
|
| 722 |
-
#
|
| 723 |
-
|
|
|
|
|
|
|
|
|
|
| 724 |
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
'timestamp': current_time
|
| 731 |
-
}
|
| 732 |
|
| 733 |
return technical_text, fundamental_text, market_outlook_text
|
| 734 |
# ========================== MODIFIED SECTION END ==========================
|
|
|
|
| 13 |
import re
|
| 14 |
from bs4 import BeautifulSoup
|
| 15 |
import requests
|
|
|
|
| 16 |
|
| 17 |
# 引用您組員的預測器程式
|
| 18 |
from Bert_predict import BertPredictor
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# 台股代號對應表 (移除台指期,因為它現在是獨立區塊)
|
| 21 |
TAIWAN_STOCKS = {
|
| 22 |
+
'元大台灣50': '0050.TW',
|
| 23 |
'台積電': '2330.TW',
|
| 24 |
'聯發科': '2454.TW',
|
| 25 |
'鴻海': '2317.TW',
|
| 26 |
+
'台塑': '1301.TW',
|
| 27 |
+
'中華電': '2412.TW',
|
| 28 |
'富邦金': '2881.TW',
|
|
|
|
| 29 |
'國泰金': '2882.TW',
|
| 30 |
+
'台達電': '2308.TW',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
'統一': '1216.TW',
|
| 32 |
+
'日月光': '3711.TW',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
'長榮': '2603.TW',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
'慧洋-KY': '2637.TW',
|
| 35 |
'上銀': '2049.TW',
|
| 36 |
'台泥': '1101.TW',
|
|
|
|
| 44 |
|
| 45 |
# 產業分類
|
| 46 |
INDUSTRY_MAPPING = {
|
| 47 |
+
'0050.TW': 'ETF',
|
| 48 |
'2330.TW': '半導體',
|
| 49 |
'2454.TW': '半導體',
|
| 50 |
'2317.TW': '電子組件',
|
| 51 |
+
'1301.TW': '塑膠',
|
| 52 |
+
'2412.TW': '電信',
|
| 53 |
'2881.TW': '金融',
|
|
|
|
| 54 |
'2882.TW': '金融',
|
| 55 |
+
'2308.TW': '電子',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
'1216.TW': '食品',
|
| 57 |
+
'3711.TW': '半導體',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
'2603.TW': '航運',
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
'2637.TW': '散裝航運',
|
| 60 |
'2049.TW': '工具機',
|
| 61 |
'1101.TW': '營建',
|
|
|
|
| 187 |
print(f"無法獲取 PMI 資料: {str(e)}")
|
| 188 |
return pd.DataFrame()
|
| 189 |
|
| 190 |
+
# ========================= GEMINI 整合 START =========================
|
| 191 |
def generate_gemini_analysis(stock_name, stock_symbol, period, data):
|
| 192 |
"""
|
| 193 |
使用 Gemini API 生成基本面和市場展望分析。
|
|
|
|
| 200 |
genai.configure(api_key=api_key)
|
| 201 |
model = genai.GenerativeModel('gemini-1.5-flash')
|
| 202 |
|
| 203 |
+
# 準備傳送給模型的數據
|
| 204 |
price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
|
| 205 |
rsi_current = data['RSI'].iloc[-1]
|
| 206 |
macd_current = data['MACD'].iloc[-1]
|
|
|
|
| 242 |
market_outlook = parts[1].strip()
|
| 243 |
return dcc.Markdown(fundamental_analysis), dcc.Markdown(market_outlook)
|
| 244 |
else:
|
| 245 |
+
return "無法解析 Gemini 回應", response.text
|
|
|
|
| 246 |
|
| 247 |
except Exception as e:
|
| 248 |
error_message = f"呼叫 Gemini API 時發生錯誤: {str(e)}"
|
| 249 |
print(error_message)
|
| 250 |
+
return error_message, "請檢查後台日誌或 API 金鑰設定"
|
| 251 |
+
# ========================== GEMINI 整合 END ==========================
|
| 252 |
|
| 253 |
# 建立 Dash 應用程式
|
| 254 |
app = dash.Dash(__name__, suppress_callback_exceptions=True)
|
|
|
|
| 312 |
html.Label("時間範圍:"),
|
| 313 |
dcc.Dropdown(id='period-dropdown',
|
| 314 |
options=[{'label': '1個月', 'value': '1mo'},{'label': '3個月', 'value': '3mo'},{'label': '6個月', 'value': '6mo'},{'label': '1年', 'value': '1y'},{'label': '2年', 'value': '2y'}],
|
| 315 |
+
value='1mo', style={'margin-bottom': '10px'}) # 預設改為 1mo
|
| 316 |
], style={'width': '30%', 'display': 'inline-block', 'margin-left': '5%', 'vertical-align': 'top'}),
|
| 317 |
html.Div([
|
| 318 |
html.Label("圖表類型:"),
|
|
|
|
| 591 |
fig.update_layout(title="台灣景氣燈號走勢", xaxis_title='日期', yaxis_title='燈號分數', height=300, yaxis=dict(range=[0, 40]))
|
| 592 |
return fig
|
| 593 |
|
| 594 |
+
# ========================= MODIFIED SECTION START =========================
|
| 595 |
@app.callback(
|
| 596 |
[dash.dependencies.Output('technical-analysis-text', 'children'),
|
| 597 |
dash.dependencies.Output('fundamental-analysis-text', 'children'),
|
|
|
|
| 600 |
dash.dependencies.Input('period-dropdown', 'value')]
|
| 601 |
)
|
| 602 |
def update_analysis_text(selected_stock, period):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 603 |
data = get_stock_data(selected_stock, period)
|
| 604 |
stock_name = [name for name, symbol in TAIWAN_STOCKS.items() if symbol == selected_stock][0]
|
| 605 |
+
if data.empty or len(data) < 20: # 確保有足夠資料計算指標
|
| 606 |
return "資料不足,無法分析", "資料不足,無法分析", "資料不足,無法分析"
|
| 607 |
|
| 608 |
data = calculate_technical_indicators(data)
|
| 609 |
|
| 610 |
+
# 1. 技術面分析 (保留客觀數據呈現)
|
| 611 |
price_change = ((data['Close'].iloc[-1] - data['Close'].iloc[0]) / data['Close'].iloc[0]) * 100
|
| 612 |
rsi_current = data['RSI'].iloc[-1] if not pd.isna(data['RSI'].iloc[-1]) else 50
|
| 613 |
macd_current = data['MACD'].iloc[-1] if not pd.isna(data['MACD'].iloc[-1]) else 0
|
|
|
|
| 619 |
html.P([html.Strong("MACD 指標:"), f"MACD 快線 ({macd_current:.3f}) 目前", html.Span("高於" if macd_current > macd_signal_current else "低於", style={'color': 'red' if macd_current > macd_signal_current else 'green', 'font-weight': 'bold'}), f" Signal 慢線 ({macd_signal_current:.3f}),", f"顯示市場動能偏向{'多頭' if macd_current > macd_signal_current else '空頭'}。"]),
|
| 620 |
])
|
| 621 |
|
| 622 |
+
# 2. 基本面與展望分析 (呼叫 Gemini)
|
| 623 |
+
# 顯示“正在生成…”提示,改善使用者體驗
|
| 624 |
+
loading_text = html.Div([
|
| 625 |
+
dcc.Loading(id="loading-analysis", type="dots", children=[html.Div(id="loading-output")])
|
| 626 |
+
])
|
| 627 |
|
| 628 |
+
try:
|
| 629 |
+
fundamental_text, market_outlook_text = generate_gemini_analysis(stock_name, selected_stock, period, data)
|
| 630 |
+
except Exception as e:
|
| 631 |
+
fundamental_text = f"生成分析時發生錯誤: {e}"
|
| 632 |
+
market_outlook_text = "請檢查 API 金鑰或網路連線。"
|
|
|
|
|
|
|
| 633 |
|
| 634 |
return technical_text, fundamental_text, market_outlook_text
|
| 635 |
# ========================== MODIFIED SECTION END ==========================
|