Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -2,44 +2,61 @@ import streamlit as st
|
|
| 2 |
import yfinance as yf
|
| 3 |
import plotly.graph_objects as go
|
| 4 |
from plotly.subplots import make_subplots
|
|
|
|
| 5 |
import pandas as pd
|
| 6 |
-
from
|
| 7 |
|
|
|
|
| 8 |
st.set_page_config(
|
| 9 |
-
page_title="台灣股票比較分析
|
| 10 |
page_icon="📈",
|
| 11 |
layout="wide"
|
| 12 |
)
|
| 13 |
|
| 14 |
-
#
|
| 15 |
-
st.
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
stock_data = {}
|
| 30 |
-
|
| 31 |
with st.spinner("正在獲取股票數據..."):
|
| 32 |
for stock_id in stock_ids:
|
| 33 |
try:
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
if '.' not in stock_id:
|
| 37 |
-
stock_id = f"{stock_id}.TW"
|
| 38 |
-
|
| 39 |
-
# 獲取股票數據
|
| 40 |
stock = yf.Ticker(stock_id)
|
| 41 |
hist = stock.history(period=period)
|
| 42 |
-
|
| 43 |
if not hist.empty:
|
| 44 |
stock_data[stock_id] = hist
|
| 45 |
st.success(f"成功獲取 {stock_id} 的數據")
|
|
@@ -47,187 +64,81 @@ def get_stock_data(stock_ids, period="1y"):
|
|
| 47 |
st.error(f"未找到 {stock_id} 的數據")
|
| 48 |
except Exception as e:
|
| 49 |
st.error(f"獲取 {stock_id} 數據時出錯: {str(e)}")
|
| 50 |
-
|
| 51 |
return stock_data
|
| 52 |
|
| 53 |
def normalize_data(df):
|
| 54 |
-
"""
|
| 55 |
-
將數據正規化,以便比較不同價格範圍的股票
|
| 56 |
-
|
| 57 |
-
參數:
|
| 58 |
-
df (DataFrame): 包含收盤價的 DataFrame
|
| 59 |
-
|
| 60 |
-
返回:
|
| 61 |
-
DataFrame: 正規化後的數據
|
| 62 |
-
"""
|
| 63 |
return df / df.iloc[0] * 100
|
| 64 |
|
| 65 |
def plot_stock_comparison(stock_data):
|
| 66 |
-
"""
|
| 67 |
-
使用 Plotly 繪製股票比較圖表
|
| 68 |
-
|
| 69 |
-
參數:
|
| 70 |
-
stock_data (dict): 股票數據字典
|
| 71 |
-
"""
|
| 72 |
if not stock_data:
|
| 73 |
st.warning("沒有可用的股票數據來繪製圖表")
|
| 74 |
return
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
vertical_spacing=0.1,
|
| 80 |
-
subplot_titles=("股價走勢比較", "正規化股價比較 (基準=100)"),
|
| 81 |
-
row_heights=[0.6, 0.4])
|
| 82 |
-
|
| 83 |
colors = ['blue', 'red', 'green', 'purple', 'orange']
|
| 84 |
color_idx = 0
|
| 85 |
-
|
| 86 |
-
# 用於正規化的數據
|
| 87 |
-
normalized_data = {}
|
| 88 |
-
|
| 89 |
-
# 添加每個股票的數據到圖表
|
| 90 |
for stock_id, data in stock_data.items():
|
| 91 |
-
# 確保數據不為空
|
| 92 |
if data.empty:
|
| 93 |
continue
|
| 94 |
-
|
| 95 |
-
# 股票名稱顯示
|
| 96 |
-
display_name = stock_id
|
| 97 |
-
|
| 98 |
-
# 獲取顏色
|
| 99 |
color = colors[color_idx % len(colors)]
|
| 100 |
color_idx += 1
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
fig.add_trace(
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
mode='lines',
|
| 108 |
-
name=f"{display_name}",
|
| 109 |
-
line=dict(color=color)
|
| 110 |
-
),
|
| 111 |
-
row=1, col=1
|
| 112 |
-
)
|
| 113 |
-
|
| 114 |
-
# 正規化數據
|
| 115 |
-
normalized = normalize_data(data['Close'])
|
| 116 |
-
normalized_data[stock_id] = normalized
|
| 117 |
-
|
| 118 |
-
# 添加正規化折線圖
|
| 119 |
-
fig.add_trace(
|
| 120 |
-
go.Scatter(
|
| 121 |
-
x=data.index,
|
| 122 |
-
y=normalized,
|
| 123 |
-
mode='lines',
|
| 124 |
-
name=f"{display_name} (正規化)",
|
| 125 |
-
line=dict(color=color, dash='dot')
|
| 126 |
-
),
|
| 127 |
-
row=2, col=1
|
| 128 |
-
)
|
| 129 |
-
|
| 130 |
-
# 更新布局
|
| 131 |
-
fig.update_layout(
|
| 132 |
-
title="股票價格比較",
|
| 133 |
-
height=800,
|
| 134 |
-
legend=dict(
|
| 135 |
-
orientation="h",
|
| 136 |
-
yanchor="bottom",
|
| 137 |
-
y=1.02,
|
| 138 |
-
xanchor="right",
|
| 139 |
-
x=1
|
| 140 |
-
),
|
| 141 |
-
template="plotly_white"
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
# 更新Y軸標題
|
| 145 |
fig.update_yaxes(title_text="價格 (TWD)", row=1, col=1)
|
| 146 |
fig.update_yaxes(title_text="正規化價格 (基準=100)", row=2, col=1)
|
| 147 |
-
|
| 148 |
-
# 顯示圖表
|
| 149 |
st.plotly_chart(fig, use_container_width=True)
|
| 150 |
|
| 151 |
-
# 側邊欄:股票選擇和參數設定
|
| 152 |
-
with st.sidebar:
|
| 153 |
-
st.header("設定")
|
| 154 |
-
|
| 155 |
-
# 股票選擇
|
| 156 |
-
st.subheader("選擇股票")
|
| 157 |
-
|
| 158 |
-
default_stocks = ["2330", "2454"]
|
| 159 |
-
stock_input = st.text_input(
|
| 160 |
-
"輸入股票代碼 (以逗號分隔)",
|
| 161 |
-
value="2330,2454",
|
| 162 |
-
help="例如: 2330,2454,2317"
|
| 163 |
-
)
|
| 164 |
-
|
| 165 |
-
# 解析股票代碼
|
| 166 |
-
stock_ids = [s.strip() for s in stock_input.split(',') if s.strip()]
|
| 167 |
-
|
| 168 |
-
# 時間範圍選擇
|
| 169 |
-
st.subheader("時間範圍")
|
| 170 |
-
period = st.selectbox(
|
| 171 |
-
"選擇時間範圍",
|
| 172 |
-
options=["1m", "3m", "6m", "1y", "2y", "5y"],
|
| 173 |
-
index=3, # 預設 1年
|
| 174 |
-
format_func=lambda x: {
|
| 175 |
-
"1m": "1個月",
|
| 176 |
-
"3m": "3個月",
|
| 177 |
-
"6m": "6個月",
|
| 178 |
-
"1y": "1年",
|
| 179 |
-
"2y": "2年",
|
| 180 |
-
"5y": "5年"
|
| 181 |
-
}.get(x, x)
|
| 182 |
-
)
|
| 183 |
-
|
| 184 |
-
analyze_button = st.button("分析", type="primary", use_container_width=True)
|
| 185 |
-
|
| 186 |
-
# 主要內容區域
|
| 187 |
if analyze_button or st.session_state.get('has_analyzed', False):
|
| 188 |
st.session_state['has_analyzed'] = True
|
| 189 |
-
|
| 190 |
-
# 檢查是否有輸入股票代碼
|
| 191 |
if not stock_ids:
|
| 192 |
st.error("請至少輸入一個股票代碼")
|
| 193 |
elif len(stock_ids) > 5:
|
| 194 |
st.warning("最多只能比較5個股票,已取前5個")
|
| 195 |
stock_ids = stock_ids[:5]
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
# 繪製比較圖表
|
| 206 |
plot_stock_comparison(stock_data)
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
st.subheader("最近交易數據")
|
| 210 |
-
|
| 211 |
for stock_id, data in stock_data.items():
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
else:
|
| 219 |
-
|
| 220 |
-
st.info("👈 請在左側設定要比較的股票和時間範圍,然後點擊「分析」按鈕開始分析")
|
| 221 |
-
|
| 222 |
-
# 添加一些使用指南
|
| 223 |
st.markdown("""
|
| 224 |
-
### 使用
|
| 225 |
-
1. 在側邊欄輸入要比較的股票代碼(
|
| 226 |
-
2. 選擇
|
| 227 |
-
3. 點擊「分析」
|
| 228 |
-
|
| 229 |
-
###
|
| 230 |
-
- 同時比較多
|
| 231 |
-
-
|
| 232 |
-
-
|
| 233 |
-
|
|
|
|
|
|
| 2 |
import yfinance as yf
|
| 3 |
import plotly.graph_objects as go
|
| 4 |
from plotly.subplots import make_subplots
|
| 5 |
+
from datetime import datetime
|
| 6 |
import pandas as pd
|
| 7 |
+
from streamlit_extras.metric_cards import style_metric_cards
|
| 8 |
|
| 9 |
+
# 設定頁面
|
| 10 |
st.set_page_config(
|
| 11 |
+
page_title="台灣股票比較分析儀表板",
|
| 12 |
page_icon="📈",
|
| 13 |
layout="wide"
|
| 14 |
)
|
| 15 |
|
| 16 |
+
# 樣式設定
|
| 17 |
+
st.markdown("""
|
| 18 |
+
<style>
|
| 19 |
+
.block-container {
|
| 20 |
+
padding-top: 2rem;
|
| 21 |
+
padding-bottom: 2rem;
|
| 22 |
+
}
|
| 23 |
+
.main-title {
|
| 24 |
+
font-size: 2.5rem;
|
| 25 |
+
font-weight: bold;
|
| 26 |
+
color: #2c3e50;
|
| 27 |
+
}
|
| 28 |
+
.sub-header {
|
| 29 |
+
font-size: 1.3rem;
|
| 30 |
+
color: #7f8c8d;
|
| 31 |
+
margin-bottom: 1.5rem;
|
| 32 |
+
}
|
| 33 |
+
</style>
|
| 34 |
+
""", unsafe_allow_html=True)
|
| 35 |
+
|
| 36 |
+
st.markdown('<div class="main-title">📊 台灣股票比較分析儀表板</div>', unsafe_allow_html=True)
|
| 37 |
+
st.markdown('<div class="sub-header">視覺化多檔股票表現,洞察市場趨勢</div>', unsafe_allow_html=True)
|
| 38 |
+
|
| 39 |
+
# 側邊欄設定
|
| 40 |
+
with st.sidebar:
|
| 41 |
+
st.header("設定選項")
|
| 42 |
+
stock_input = st.text_input("輸入股票代碼 (以逗號分隔)", value="2330,2454", help="例如: 2330,2454,2317")
|
| 43 |
+
stock_ids = [s.strip() for s in stock_input.split(',') if s.strip()]
|
| 44 |
+
|
| 45 |
+
period = st.selectbox("選擇時間範圍", options=["1m", "3m", "6m", "1y", "2y", "5y"], index=3,
|
| 46 |
+
format_func=lambda x: {"1m": "1個月", "3m": "3個月", "6m": "6個月", "1y": "1年", "2y": "2年", "5y": "5年"}.get(x, x))
|
| 47 |
+
|
| 48 |
+
analyze_button = st.button("分析", type="primary", use_container_width=True)
|
| 49 |
+
st.markdown("🕒 資料更新時間: " + datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
| 50 |
+
|
| 51 |
+
def get_stock_data(stock_ids, period):
|
| 52 |
stock_data = {}
|
|
|
|
| 53 |
with st.spinner("正在獲取股票數據..."):
|
| 54 |
for stock_id in stock_ids:
|
| 55 |
try:
|
| 56 |
+
if stock_id.isdigit() and '.' not in stock_id:
|
| 57 |
+
stock_id = f"{stock_id}.TW"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
stock = yf.Ticker(stock_id)
|
| 59 |
hist = stock.history(period=period)
|
|
|
|
| 60 |
if not hist.empty:
|
| 61 |
stock_data[stock_id] = hist
|
| 62 |
st.success(f"成功獲取 {stock_id} 的數據")
|
|
|
|
| 64 |
st.error(f"未找到 {stock_id} 的數據")
|
| 65 |
except Exception as e:
|
| 66 |
st.error(f"獲取 {stock_id} 數據時出錯: {str(e)}")
|
|
|
|
| 67 |
return stock_data
|
| 68 |
|
| 69 |
def normalize_data(df):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
return df / df.iloc[0] * 100
|
| 71 |
|
| 72 |
def plot_stock_comparison(stock_data):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
if not stock_data:
|
| 74 |
st.warning("沒有可用的股票數據來繪製圖表")
|
| 75 |
return
|
| 76 |
+
|
| 77 |
+
fig = make_subplots(rows=2, cols=1, shared_xaxes=True, vertical_spacing=0.1,
|
| 78 |
+
subplot_titles=("股價走勢比較", "正規化股價比較 (基準=100)"), row_heights=[0.6, 0.4])
|
| 79 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
colors = ['blue', 'red', 'green', 'purple', 'orange']
|
| 81 |
color_idx = 0
|
| 82 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
for stock_id, data in stock_data.items():
|
|
|
|
| 84 |
if data.empty:
|
| 85 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
color = colors[color_idx % len(colors)]
|
| 87 |
color_idx += 1
|
| 88 |
+
|
| 89 |
+
fig.add_trace(go.Scatter(x=data.index, y=data['Close'], mode='lines', name=stock_id, line=dict(color=color)), row=1, col=1)
|
| 90 |
+
fig.add_trace(go.Bar(x=data.index, y=data['Volume'], name=f"{stock_id} 成交量", marker_color=color, opacity=0.3), row=1, col=1)
|
| 91 |
+
fig.add_trace(go.Scatter(x=data.index, y=normalize_data(data['Close']), mode='lines', name=f"{stock_id} (正規化)", line=dict(color=color, dash='dot')), row=2, col=1)
|
| 92 |
+
|
| 93 |
+
fig.update_layout(title="股票價格比較圖表", height=800, legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1), template="plotly_white")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
fig.update_yaxes(title_text="價格 (TWD)", row=1, col=1)
|
| 95 |
fig.update_yaxes(title_text="正規化價格 (基準=100)", row=2, col=1)
|
|
|
|
|
|
|
| 96 |
st.plotly_chart(fig, use_container_width=True)
|
| 97 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 98 |
if analyze_button or st.session_state.get('has_analyzed', False):
|
| 99 |
st.session_state['has_analyzed'] = True
|
| 100 |
+
|
|
|
|
| 101 |
if not stock_ids:
|
| 102 |
st.error("請至少輸入一個股票代碼")
|
| 103 |
elif len(stock_ids) > 5:
|
| 104 |
st.warning("最多只能比較5個股票,已取前5個")
|
| 105 |
stock_ids = stock_ids[:5]
|
| 106 |
+
|
| 107 |
+
st.markdown(f"### 分析 {', '.join(stock_ids)} 的股價表現")
|
| 108 |
+
st.markdown(f"時間範圍: {period}")
|
| 109 |
+
stock_data = get_stock_data(stock_ids, period)
|
| 110 |
+
|
| 111 |
+
if stock_data:
|
| 112 |
+
tab1, tab2 = st.tabs(["📈 價格趨勢圖", "📊 數據分析表"])
|
| 113 |
+
|
| 114 |
+
with tab1:
|
|
|
|
| 115 |
plot_stock_comparison(stock_data)
|
| 116 |
+
|
| 117 |
+
with tab2:
|
|
|
|
|
|
|
| 118 |
for stock_id, data in stock_data.items():
|
| 119 |
+
st.markdown(f"#### {stock_id} 最近 5 日交易資料")
|
| 120 |
+
st.dataframe(data.tail(5))
|
| 121 |
+
latest_close = data['Close'].iloc[-1]
|
| 122 |
+
first_close = data['Close'].iloc[0]
|
| 123 |
+
pct_change = ((latest_close - first_close) / first_close) * 100
|
| 124 |
+
col1, col2, col3 = st.columns(3)
|
| 125 |
+
col1.metric("最新價格", f"{latest_close:.2f} TWD")
|
| 126 |
+
col2.metric("起始價格", f"{first_close:.2f} TWD")
|
| 127 |
+
col3.metric("報酬率", f"{pct_change:.2f}%", delta=f"{pct_change:.2f}%")
|
| 128 |
+
style_metric_cards()
|
| 129 |
+
else:
|
| 130 |
+
st.error("無法獲取股票數據,請檢查股票代碼是否正確")
|
| 131 |
else:
|
| 132 |
+
st.info("👈 請在左側輸入股票代碼與時間範圍,然後點擊「分析」開始")
|
|
|
|
|
|
|
|
|
|
| 133 |
st.markdown("""
|
| 134 |
+
### 使用說明
|
| 135 |
+
1. 在側邊欄輸入要比較的股票代碼(用逗號分隔)
|
| 136 |
+
2. 選擇分析的時間範圍
|
| 137 |
+
3. 點擊「分析」查看結果
|
| 138 |
+
|
| 139 |
+
### 功能亮點
|
| 140 |
+
- 同時比較最多 5 支台灣股票走勢
|
| 141 |
+
- 股價與成交量視覺化
|
| 142 |
+
- 正規化股價比較(基準=100)
|
| 143 |
+
- 每支股票關鍵統計指標(報酬率、起始與最新價格)
|
| 144 |
+
""")
|