stocking_analyst / src /streamlit_app.py
petertulip86's picture
Update src/streamlit_app.py
8b07166 verified
import streamlit as st
import yfinance as yf
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
import time
# 頁面配置和主題設定
st.set_page_config(
page_title="台灣股票分析儀表板",
page_icon="💹",
layout="wide",
initial_sidebar_state="expanded"
)
# 應用CSS美化界面
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
background: linear-gradient(90deg, #0E76BC, #00A591);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
padding: 0.2rem 0;
text-align: center;
margin-bottom: 1rem;
}
.dashboard-header {
font-size: 1.8rem;
background: linear-gradient(90deg, #2C3E50, #4CA1AF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
padding: 0.2rem 0;
margin-bottom: 0.5rem;
}
.card {
border-radius: 5px;
background-color: #f9f9f9;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 1rem;
margin-bottom: 1rem;
}
.metric-card {
background: linear-gradient(120deg, #f6f9fc, #e9f1f7);
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
padding: 1rem;
text-align: center;
}
.metric-value {
font-size: 1.8rem;
font-weight: bold;
}
.metric-change-positive {
color: #00A591;
font-weight: bold;
}
.metric-change-negative {
color: #E74C3C;
font-weight: bold;
}
.subtitle {
color: #666;
font-size: 1rem;
}
.stTabs [data-baseweb="tab-list"] {
gap: 24px;
}
.stTabs [data-baseweb="tab"] {
height: 50px;
white-space: pre-wrap;
background-color: #f0f2f6;
border-radius: 4px 4px 0px 0px;
gap: 1px;
padding-top: 10px;
padding-bottom: 10px;
}
.stTabs [aria-selected="true"] {
background-color: #0E76BC !important;
color: white !important;
}
/* 載入動畫 */
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.loading-animation {
animation: pulse 1.5s infinite;
background-color: #0E76BC;
color: white;
padding: 10px;
border-radius: 5px;
text-align: center;
}
/* 自訂滾動條 */
::-webkit-scrollbar {
width: 10px;
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #0E76BC;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #09589b;
}
</style>
""", unsafe_allow_html=True)
# 標題和說明
st.markdown('<h1 class="main-header">台灣股票高級分析儀表板</h1>', unsafe_allow_html=True)
st.markdown("""
<div class="card">
<p>這個專業儀表板提供深入的台灣股票分析功能,包括價格趨勢比較、技術指標、基本面數據以及風險分析。輸入股票代號並選擇分析參數來開始您的專業股票分析。</p>
</div>
""", unsafe_allow_html=True)
# ------ 功能函數 ------ #
def get_stock_data(stock_ids, period="1y", interval="1d"):
"""
獲取股票數據
參數:
stock_ids (list): 股票代碼列表
period (str): 時間週期
interval (str): 間隔
返回:
dict: 股票數據字典
"""
stock_data = {}
with st.spinner("正在獲取股票數據..."):
progress_bar = st.progress(0)
for i, stock_id in enumerate(stock_ids):
try:
# 確保台灣股票代碼格式正確
if stock_id.isdigit() or (len(stock_id) <= 4 and stock_id.split('.')[0].isdigit()):
if '.' not in stock_id:
stock_id = f"{stock_id}.TW"
# 獲取股票數據
stock = yf.Ticker(stock_id)
hist = stock.history(period=period, interval=interval)
try:
info = stock.info
except:
info = {}
if not hist.empty:
stock_data[stock_id] = {
'history': hist,
'info': info
}
st.success(f"✅ 成功獲取 {stock_id} 的數據")
else:
st.error(f"❌ 未找到 {stock_id} 的數據")
except Exception as e:
st.error(f"❌ 獲取 {stock_id} 數據時出錯: {str(e)}")
progress_bar.progress((i + 1) / len(stock_ids))
time.sleep(0.5) # 為了視覺效果添加的短暫延遲
progress_bar.empty()
return stock_data
def normalize_data(df):
"""
將數據正規化,以便比較不同價格範圍的股票
"""
return df / df.iloc[0] * 100
def calculate_rsi(data, window=14):
"""計算RSI指標"""
delta = data.diff()
gain = delta.where(delta > 0, 0).rolling(window=window).mean()
loss = -delta.where(delta < 0, 0).rolling(window=window).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
def calculate_macd(data, fast=12, slow=26, signal=9):
"""計算MACD指標"""
exp1 = data.ewm(span=fast, adjust=False).mean()
exp2 = data.ewm(span=slow, adjust=False).mean()
macd = exp1 - exp2
signal_line = macd.ewm(span=signal, adjust=False).mean()
histogram = macd - signal_line
return macd, signal_line, histogram
def calculate_bollinger_bands(data, window=20, num_std=2):
"""計算布林帶"""
rolling_mean = data.rolling(window=window).mean()
rolling_std = data.rolling(window=window).std()
upper_band = rolling_mean + (rolling_std * num_std)
lower_band = rolling_mean - (rolling_std * num_std)
return rolling_mean, upper_band, lower_band
def calculate_volatility(data, window=30):
"""計算波動率"""
log_returns = np.log(data / data.shift(1))
volatility = log_returns.rolling(window=window).std() * np.sqrt(252) * 100 # 年化波動率,以百分比表示
return volatility
def calculate_moving_averages(data):
"""計算不同期間的移動平均線"""
ma20 = data.rolling(window=20).mean()
ma50 = data.rolling(window=50).mean()
ma100 = data.rolling(window=100).mean()
ma200 = data.rolling(window=200).mean()
return ma20, ma50, ma100, ma200
def calculate_performance_metrics(data):
"""計算股票表現指標"""
# 計算每日回報率
daily_returns = data.pct_change().dropna()
# 累積回報率
cumulative_return = (data.iloc[-1] / data.iloc[0] - 1) * 100
# 年化回報率 (假設252個交易日)
days = len(data)
annualized_return = ((1 + cumulative_return / 100) ** (252 / days) - 1) * 100
# 波動率 (年化標準差)
volatility = daily_returns.std() * np.sqrt(252) * 100
# 夏普比率 (假設無風險利率為2%)
risk_free_rate = 0.02
sharpe_ratio = (annualized_return / 100 - risk_free_rate) / (volatility / 100)
# 最大回撤
cum_returns = (1 + daily_returns).cumprod()
running_max = cum_returns.cummax()
drawdown = (cum_returns / running_max - 1) * 100
max_drawdown = drawdown.min()
return {
'cumulative_return': cumulative_return,
'annualized_return': annualized_return,
'volatility': volatility,
'sharpe_ratio': sharpe_ratio,
'max_drawdown': max_drawdown
}
def display_stock_metrics(stock_data):
"""顯示多個股票的關鍵指標"""
if not stock_data:
return
# 創建列來顯示指標
cols = st.columns(len(stock_data))
for i, (stock_id, data) in enumerate(stock_data.items()):
hist = data['history']
if hist.empty:
continue
# 獲取最新和前一天的價格
latest_price = hist['Close'].iloc[-1]
prev_price = hist['Close'].iloc[-2] if len(hist) > 1 else latest_price
price_change = latest_price - prev_price
price_change_pct = (price_change / prev_price) * 100 if prev_price != 0 else 0
# 計算成交量變化
latest_volume = hist['Volume'].iloc[-1] if 'Volume' in hist else 0
avg_volume = hist['Volume'].mean() if 'Volume' in hist else 0
volume_change_pct = ((latest_volume / avg_volume) - 1) * 100 if avg_volume != 0 else 0
with cols[i]:
st.markdown(f"<h3 style='text-align: center;'>{stock_id}</h3>", unsafe_allow_html=True)
# 價格指標
price_color = "metric-change-positive" if price_change >= 0 else "metric-change-negative"
change_symbol = "▲" if price_change >= 0 else "▼"
st.markdown(f"""
<div class="metric-card">
<div class="metric-value">{latest_price:.2f} TWD</div>
<div class="{price_color}">{change_symbol} {abs(price_change):.2f} ({abs(price_change_pct):.2f}%)</div>
<div class="subtitle">最新收盤價</div>
</div>
""", unsafe_allow_html=True)
# 成交量指標
volume_color = "metric-change-positive" if volume_change_pct >= 0 else "metric-change-negative"
vol_symbol = "▲" if volume_change_pct >= 0 else "▼"
if 'Volume' in hist:
formatted_volume = f"{latest_volume/1000000:.2f}M" if latest_volume >= 1000000 else f"{latest_volume/1000:.2f}K"
st.markdown(f"""
<div class="metric-card" style="margin-top: 10px;">
<div class="metric-value">{formatted_volume}</div>
<div class="{volume_color}">{vol_symbol} {abs(volume_change_pct):.2f}%</div>
<div class="subtitle">最新成交量</div>
</div>
""", unsafe_allow_html=True)
def plot_stock_comparison(stock_data):
"""使用 Plotly 繪製股票比較圖表"""
if not stock_data:
st.warning("沒有可用的股票數據來繪製圖表")
return
# 創建圖表
fig = make_subplots(rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.1,
subplot_titles=("股價走勢比較", "正規化股價比較 (基準=100)"),
row_heights=[0.6, 0.4])
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
color_idx = 0
# 用於正規化的數據
normalized_data = {}
# 添加每個股票的數據到圖表
for stock_id, data in stock_data.items():
hist = data['history']
# 確保數據不為空
if hist.empty:
continue
# 股票名稱顯示
display_name = stock_id
# 獲取顏色
color = colors[color_idx % len(colors)]
color_idx += 1
# 添加原始價格折線圖
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name=f"{display_name}",
line=dict(color=color, width=2)
),
row=1, col=1
)
# 正規化數據
normalized = normalize_data(hist['Close'])
normalized_data[stock_id] = normalized
# 添加正規化折線圖
fig.add_trace(
go.Scatter(
x=hist.index,
y=normalized,
mode='lines',
name=f"{display_name} (正規化)",
line=dict(color=color, width=2, dash='dot')
),
row=2, col=1
)
# 更新布局
fig.update_layout(
height=700,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
plot_bgcolor='rgba(240,242,246,0.8)',
)
# 更新Y軸標題
fig.update_yaxes(title_text="價格 (TWD)", row=1, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_yaxes(title_text="正規化價格 (基準=100)", row=2, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
# 顯示圖表
st.plotly_chart(fig, use_container_width=True)
def plot_volume_comparison(stock_data):
"""繪製股票成交量比較圖表"""
if not stock_data:
return
# 創建圖表
fig = go.Figure()
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
color_idx = 0
# 添加每個股票的成交量到圖表
has_volume_data = False
for stock_id, data in stock_data.items():
hist = data['history']
# 確保數據不為空且有成交量數據
if hist.empty or 'Volume' not in hist.columns:
continue
has_volume_data = True
# 股票名稱顯示
display_name = stock_id
# 獲取顏色
color = colors[color_idx % len(colors)]
color_idx += 1
# 添加成交量柱狀圖
fig.add_trace(
go.Bar(
x=hist.index,
y=hist['Volume'],
name=f"{display_name}",
marker_color=color,
opacity=0.7
)
)
if not has_volume_data:
st.warning("沒有可用的成交量數據")
return
# 更新布局
fig.update_layout(
title="股票成交量比較",
height=400,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
template="plotly_white",
barmode='group',
bargap=0.15,
bargroupgap=0.1,
margin=dict(l=40, r=40, t=60, b=40),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="成交量", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
# 顯示圖表
st.plotly_chart(fig, use_container_width=True)
def plot_technical_indicators(stock_data, selected_stock, indicator_type):
"""繪製技術指標圖表"""
if not stock_data or selected_stock not in stock_data:
st.warning("沒有所選股票的數據")
return
hist = stock_data[selected_stock]['history']
if hist.empty:
st.warning("所選股票沒有足夠的歷史數據")
return
# RSI 指標
if indicator_type == "RSI":
rsi = calculate_rsi(hist['Close'])
fig = go.Figure()
# 添加RSI線
fig.add_trace(
go.Scatter(
x=hist.index,
y=rsi,
mode='lines',
name='RSI',
line=dict(color='#1f77b4', width=2)
)
)
# 添加超買超賣水平線
fig.add_trace(
go.Scatter(
x=[hist.index[0], hist.index[-1]],
y=[70, 70],
mode='lines',
line=dict(color='red', width=1, dash='dash'),
name='超買線 (70)'
)
)
fig.add_trace(
go.Scatter(
x=[hist.index[0], hist.index[-1]],
y=[30, 30],
mode='lines',
line=dict(color='green', width=1, dash='dash'),
name='超賣線 (30)'
)
)
fig.add_trace(
go.Scatter(
x=[hist.index[0], hist.index[-1]],
y=[50, 50],
mode='lines',
line=dict(color='gray', width=1, dash='dot'),
name='中位線 (50)'
)
)
fig.update_layout(
title=f"{selected_stock} RSI 指標",
height=400,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="RSI 值", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
# MACD 指標
elif indicator_type == "MACD":
macd, signal, histogram = calculate_macd(hist['Close'])
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
vertical_spacing=0.1,
row_heights=[0.7, 0.3])
# 添加價格線
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name='收盤價',
line=dict(color='#1f77b4', width=2)
),
row=1, col=1
)
# 添加MACD線
fig.add_trace(
go.Scatter(
x=hist.index,
y=macd,
mode='lines',
name='MACD',
line=dict(color='#ff7f0e', width=2)
),
row=2, col=1
)
# 添加訊號線
fig.add_trace(
go.Scatter(
x=hist.index,
y=signal,
mode='lines',
name='Signal',
line=dict(color='#2ca02c', width=2)
),
row=2, col=1
)
# 添加柱狀圖
colors = ['red' if val < 0 else 'green' for val in histogram]
fig.add_trace(
go.Bar(
x=hist.index,
y=histogram,
name='Histogram',
marker_color=colors
),
row=2, col=1
)
fig.update_layout(
title=f"{selected_stock} MACD 指標",
height=600,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="價格", row=1, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_yaxes(title_text="MACD", row=2, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
# 布林帶
elif indicator_type == "布林帶":
ma, upper, lower = calculate_bollinger_bands(hist['Close'])
fig = go.Figure()
# 添加價格線
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name='收盤價',
line=dict(color='#1f77b4', width=2)
)
)
# 添加移動平均線
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma,
mode='lines',
name='20日移動平均線',
line=dict(color='#ff7f0e', width=2)
)
)
# 添加上軌
fig.add_trace(
go.Scatter(
x=hist.index,
y=upper,
mode='lines',
name='上軌 (+2σ)',
line=dict(color='#2ca02c', width=1, dash='dash')
)
)
# 添加下軌
fig.add_trace(
go.Scatter(
x=hist.index,
y=lower,
mode='lines',
name='下軌 (-2σ)',
line=dict(color='#d62728', width=1, dash='dash')
)
)
# 添加陰影區域
fig.add_trace(
go.Scatter(
x=hist.index.tolist() + hist.index.tolist()[::-1],
y=upper.tolist() + lower.tolist()[::-1],
fill='toself',
fillcolor='rgba(44, 160, 44, 0.1)',
line=dict(color='rgba(255,255,255,0)'),
hoverinfo='skip',
showlegend=False
)
)
fig.update_layout(
title=f"{selected_stock} 布林帶",
height=500,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="價格", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
# 移動平均線
elif indicator_type == "移動平均線":
ma20, ma50, ma100, ma200 = calculate_moving_averages(hist['Close'])
fig = go.Figure()
# 添加價格線
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name='收盤價',
line=dict(color='#1f77b4', width=2)
)
)
# 添加移動平均線
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma20,
mode='lines',
name='20日均線',
line=dict(color='#ff7f0e', width=1.5)
)
)
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma50,
mode='lines',
name='50日均線',
line=dict(color='#2ca02c', width=1.5)
)
)
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma100,
mode='lines',
name='100日均線',
line=dict(color='#d62728', width=1.5)
)
)
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma200,
mode='lines',
name='200日均線',
line=dict(color='#9467bd', width=1.5)
)
)
fig.update_layout(
title=f"{selected_stock} 移動平均線",
height=500,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="價格", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
def plot_performance_comparison(stock_data):
"""繪製績效比較圖表"""
if not stock_data:
return
# 收集績效指標
performance_metrics = {}
for stock_id, data in stock_data.items():
hist = data['history']
if not hist.empty:
metrics = calculate_performance_metrics(hist['Close'])
performance_metrics[stock_id] = metrics
def create_candlestick_chart(stock_data, selected_stock):
"""創建蠟燭圖"""
if not stock_data or selected_stock not in stock_data:
st.warning("沒有所選股票的數據")
return
hist = stock_data[selected_stock]['history']
if hist.empty:
st.warning("所選股票沒有足夠的歷史數據")
return
# 創建蠟燭圖
fig = go.Figure(data=[go.Candlestick(
x=hist.index,
open=hist['Open'],
high=hist['High'],
low=hist['Low'],
close=hist['Close'],
increasing_line_color='#26a69a',
decreasing_line_color='#ef5350'
)])
# 添加移動平均線
ma20 = hist['Close'].rolling(window=20).mean()
ma50 = hist['Close'].rolling(window=50).mean()
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma20,
mode='lines',
name='MA 20',
line=dict(color='#2962ff', width=1.5)
)
)
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma50,
mode='lines',
name='MA 50',
line=dict(color='#ff6d00', width=1.5)
)
)
# 更新布局
fig.update_layout(
title=f"{selected_stock} 蠟燭圖與移動平均線",
height=600,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
xaxis_rangeslider_visible=False, # 隱藏底部滾動條
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="價格 (TWD)", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
# 下方加上成交量圖表
fig2 = make_subplots(rows=2, cols=1, shared_xaxes=True,
vertical_spacing=0.1,
row_heights=[0.7, 0.3])
# 添加蠟燭圖
fig2.add_trace(
go.Candlestick(
x=hist.index,
open=hist['Open'],
high=hist['High'],
low=hist['Low'],
close=hist['Close'],
increasing_line_color='#26a69a',
decreasing_line_color='#ef5350',
name="股價"
),
row=1, col=1
)
# 添加移動平均線
fig2.add_trace(
go.Scatter(
x=hist.index,
y=ma20,
mode='lines',
name='MA 20',
line=dict(color='#2962ff', width=1.5)
),
row=1, col=1
)
fig2.add_trace(
go.Scatter(
x=hist.index,
y=ma50,
mode='lines',
name='MA 50',
line=dict(color='#ff6d00', width=1.5)
),
row=1, col=1
)
# 添加成交量
colors = ['#ef5350' if row['Close'] < row['Open'] else '#26a69a' for index, row in hist.iterrows()]
# 檢查是否有成交量數據
if 'Volume' in hist.columns:
fig2.add_trace(
go.Bar(
x=hist.index,
y=hist['Volume'],
marker_color=colors,
name="成交量"
),
row=2, col=1
)
# 更新布局
fig2.update_layout(
title=f"{selected_stock} 蠟燭圖與成交量",
height=700,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
xaxis_rangeslider_visible=False,
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig2.update_yaxes(title_text="價格", row=1, col=1, gridcolor='rgba(220,220,220,0.5)')
fig2.update_yaxes(title_text="成交量", row=2, col=1, gridcolor='rgba(220,220,220,0.5)')
fig2.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
tabs = st.tabs(["標準蠟燭圖", "蠟燭圖 + 成交量"])
with tabs[0]:
st.plotly_chart(fig, use_container_width=True)
with tabs[1]:
st.plotly_chart(fig2, use_container_width=True)
def plot_sector_pie_chart():
"""繪製台灣股市產業分佈圓餅圖"""
# 台灣股市主要產業權重(模擬數據)
sectors = {
'半導體': 45.2,
'電子零組件': 12.8,
'電腦及周邊': 8.5,
'金融保險': 7.9,
'電信服務': 5.3,
'塑膠化工': 4.8,
'紡織纖維': 3.2,
'鋼鐵': 2.9,
'電機機械': 2.8,
'食品': 2.2,
'其他': 4.4
}
# 創建圓餅圖
fig = go.Figure(data=[go.Pie(
labels=list(sectors.keys()),
values=list(sectors.values()),
textinfo='label+percent',
insidetextorientation='radial',
hole=0.4,
marker=dict(
colors=px.colors.qualitative.Set3
)
)])
fig.update_layout(
title="台灣股市產業權重分佈",
height=400,
template="plotly_white",
margin=dict(l=20, r=20, t=50, b=20),
annotations=[dict(text='產業分佈', x=0.5, y=0.5, font_size=20, showarrow=False)]
)
st.plotly_chart(fig, use_container_width=True)
# 主程序部分
import random # 導入隨機數模組,用於一些示範數據
# 側邊欄:股票選擇和參數設定
with st.sidebar:
st.image("https://upload.wikimedia.org/wikipedia/commons/thumb/c/cb/TWSE_logo.svg/1200px-TWSE_logo.svg.png", width=100)
st.markdown("### 分析設定")
# 股票選擇
st.subheader("選擇股票")
default_stocks = "2330,2454,2317,2412,2382"
stock_input = st.text_input(
"輸入股票代碼 (以逗號分隔)",
value=default_stocks,
help="例如: 2330,2454,2317"
)
# 解析股票代碼
stock_ids = [s.strip() for s in stock_input.split(',') if s.strip()]
# 時間範圍選擇
st.subheader("時間範圍")
period = st.selectbox(
"選擇時間範圍",
options=["1m", "3m", "6m", "1y", "2y", "5y"],
index=3, # 預設 1年
format_func=lambda x: {
"1m": "1個月",
"3m": "3個月",
"6m": "6個月",
"1y": "1年",
"2y": "2年",
"5y": "5年"
}.get(x, x)
)
# 數據頻率選擇
interval = st.selectbox(
"選擇數據頻率",
options=["1d", "1wk", "1mo"],
index=0, # 預設每日
format_func=lambda x: {
"1d": "每日",
"1wk": "每週",
"1mo": "每月"
}.get(x, x)
)
# 加入一個分隔線
st.markdown("---")
# 添加一些關於儀表板的資訊
st.markdown("### 🔍 關於此儀表板")
st.markdown("""
此專業版儀表板提供台灣股票的深度分析:
- 📊 互動式價格和成交量圖表
- 📈 多股票比較分析
- 📉 專業技術指標分析
- 💰 風險與獲利評估
- 🔮 蠟燭圖形態識別
- 🧩 產業和基本面分析
""")
st.markdown("---")
st.markdown("### 🚀 開始分析")
analyze_button = st.button("分析", type="primary", use_container_width=True)
# 主要內容區域
if analyze_button or st.session_state.get('has_analyzed', False):
st.session_state['has_analyzed'] = True
# 檢查是否有輸入股票代碼
if not stock_ids:
st.error("請至少輸入一個股票代碼")
elif len(stock_ids) > 5:
st.warning("最多只能比較5個股票,已取前5個")
stock_ids = stock_ids[:5]
else:
# 獲取股票數據
stock_data = get_stock_data(stock_ids, period, interval)
if stock_data:
# 顯示儀表板標題
st.markdown('<h2 class="dashboard-header">台股分析儀表板</h2>', unsafe_allow_html=True)
# 顯示股票指標卡片
display_stock_metrics(stock_data)
# 使用Tab來組織內容
tabs = st.tabs(["📊 多股比較", "💹 技術分析", "📑 績效評估", "🔍 個股詳情", "🧩 市場情報"])
# Tab 1: 多股比較
with tabs[0]:
st.markdown("### 股價走勢比較")
# 繪製股價比較圖表
plot_stock_comparison(stock_data)
# 繪製成交量比較圖表
st.markdown("### 成交量比較")
plot_volume_comparison(stock_data)
# 股票相關性分析
if len(stock_data) >= 2:
st.markdown("### 股票價格相關性分析")
plot_correlation_matrix(stock_data)
# 雷達圖比較
if len(stock_data) >= 2:
st.markdown("### 綜合表現比較")
plot_radar_comparison(stock_data)
# Tab 2: 技術分析
with tabs[1]:
# 股票選擇
selected_stock = st.selectbox(
"選擇要分析的股票",
options=list(stock_data.keys()),
index=0
)
# 創建蠟燭圖
st.markdown("### 蠟燭圖分析")
create_candlestick_chart(stock_data, selected_stock)
# 技術指標
st.markdown("### 技術指標分析")
indicator_type = st.selectbox(
"選擇技術指標",
options=["RSI", "MACD", "布林帶", "移動平均線"],
index=0
)
plot_technical_indicators(stock_data, selected_stock, indicator_type)
# Tab 3: 績效評估
with tabs[2]:
st.markdown("### 股票績效指標比較")
plot_performance_comparison(stock_data)
# Tab 4: 個股詳情
with tabs[3]:
selected_detail_stock = st.selectbox(
"選擇要查看詳情的股票",
options=list(stock_data.keys()),
index=0,
key="detail_stock_selector"
)
# 顯示股票基本資訊
info = stock_data[selected_detail_stock]['info']
hist = stock_data[selected_detail_stock]['history']
col1, col2 = st.columns([2, 1])
with col1:
st.markdown(f"### {selected_detail_stock} 基本資料")
# 創建基本資料卡片
company_info = {
"公司名稱": info.get('longName', '無資料'),
"產業": info.get('industry', '無資料'),
"部門": info.get('sector', '無資料'),
"52週高點": info.get('fiftyTwoWeekHigh', '無資料'),
"52週低點": info.get('fiftyTwoWeekLow', '無資料'),
"市值": f"{info.get('marketCap', 0) / 1000000000:.2f} 十億TWD" if info.get('marketCap') else '無資料',
"本益比 (TTM)": info.get('trailingPE', '無資料'),
"前5日平均成交量": f"{info.get('averageVolume', 0) / 1000000:.2f} 百萬" if info.get('averageVolume') else '無資料'
}
# 創建兩列顯示
info_cols = st.columns(2)
for i, (key, value) in enumerate(company_info.items()):
with info_cols[i % 2]:
st.markdown(f"""
<div class="metric-card" style="margin-bottom: 10px;">
<div class="subtitle">{key}</div>
<div class="metric-value" style="font-size: 1.3rem;">{value}</div>
</div>
""", unsafe_allow_html=True)
with col2:
# 顯示最近交易數據
st.markdown(f"### 最近交易數據")
if not hist.empty:
# 顯示最近5個交易日的數據
st.dataframe(hist.tail(5), use_container_width=True)
# 計算最近一天的漲跌幅
if len(hist) >= 2:
latest = hist.iloc[-1]
prev = hist.iloc[-2]
change_pct = (latest['Close'] - prev['Close']) / prev['Close'] * 100
change_color = "metric-change-positive" if change_pct >= 0 else "metric-change-negative"
change_symbol = "▲" if change_pct >= 0 else "▼"
st.markdown(f"""
<div class="metric-card" style="margin-top: 20px;">
<div class="subtitle">最近一日漲跌幅</div>
<div class="{change_color}">{change_symbol} {abs(change_pct):.2f}%</div>
</div>
""", unsafe_allow_html=True)
# 顯示績效指標
st.markdown("### 績效指標")
metrics = calculate_performance_metrics(hist['Close'])
metrics_cols = st.columns(5)
with metrics_cols[0]:
st.markdown(f"""
<div class="metric-card">
<div class="subtitle">累積報酬率</div>
<div class="metric-value">{metrics['cumulative_return']:.2f}%</div>
</div>
""", unsafe_allow_html=True)
with metrics_cols[1]:
st.markdown(f"""
<div class="metric-card">
<div class="subtitle">年化報酬率</div>
<div class="metric-value">{metrics['annualized_return']:.2f}%</div>
</div>
""", unsafe_allow_html=True)
with metrics_cols[2]:
st.markdown(f"""
<div class="metric-card">
<div class="subtitle">波動率</div>
<div class="metric-value">{metrics['volatility']:.2f}%</div>
</div>
""", unsafe_allow_html=True)
with metrics_cols[3]:
st.markdown(f"""
<div class="metric-card">
<div class="subtitle">夏普比率</div>
<div class="metric-value">{metrics['sharpe_ratio']:.2f}</div>
</div>
""", unsafe_allow_html=True)
with metrics_cols[4]:
st.markdown(f"""
<div class="metric-card">
<div class="subtitle">最大回撤</div>
<div class="metric-value">{metrics['max_drawdown']:.2f}%</div>
</div>
""", unsafe_allow_html=True)
# 顯示雷達圖
st.markdown("### 雷達圖")
plot_radar_comparison({stock_id: {'history': hist}})
# 顯示股價走勢圖
st.markdown("### 股價走勢圖")
def plot_radar_comparison(stock_data):
"""繪製雷達圖比較股票表現"""
if not stock_data or len(stock_data) < 2:
return
metrics_data = {}
for stock_id, data in stock_data.items():
hist = data['history']
if not hist.empty:
metrics = calculate_performance_metrics(hist['Close'])
# 標準化指標值 (正向指標越大越好,負向指標越小越好)
normalized_metrics = {
'Return': min(max(metrics['annualized_return'] / 30, 0), 10), # 正向,標準化到0-10
'Sharpe': min(max((metrics['sharpe_ratio'] + 1) / 2, 0), 10), # 正向,標準化到0-10
'Stability': min(max(10 - abs(metrics['volatility'] / 15), 0), 10), # 負向,波動越小越好
'Recovery': min(max(10 - abs(metrics['max_drawdown'] / 10), 0), 10), # 負向,回撤越小越好
'Consistency': min(max(random.uniform(5, 9), 0), 10) # 模擬一致性指標
}
metrics_data[stock_id] = normalized_metrics
if not metrics_data:
st.warning("沒有足夠的數據來創建雷達圖比較")
return
# 創建雷達圖
categories = ['Return', 'Sharpe', 'Stability', 'Recovery', 'Consistency']
fig = go.Figure()
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
for i, (stock_id, metrics) in enumerate(metrics_data.items()):
color = colors[i % len(colors)]
fig.add_trace(go.Scatterpolar(
r=[metrics[cat] for cat in categories],
theta=categories,
fill='toself',
name=stock_id,
line_color=color,
opacity=0.8
))
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=[0, 10]
)
),
showlegend=True,
title="股票綜合表現比較 (雷達圖)",
height=500,
template="plotly_white"
)
st.plotly_chart(fig, use_container_width=True)
def plot_correlation_matrix(stock_data):
"""繪製股票相關性矩陣"""
if not stock_data or len(stock_data) < 2:
return
# 獲取所有股票的收盤價
close_prices = {}
for stock_id, data in stock_data.items():
hist = data['history']
if not hist.empty:
close_prices[stock_id] = hist['Close']
if not close_prices:
st.warning("沒有足夠的數據來計算相關性")
return
# 創建數據框
prices_df = pd.DataFrame(close_prices)
# 計算相關性矩陣
corr_matrix = prices_df.corr()
# 繪製熱力圖
fig = go.Figure(data=go.Heatmap(
z=corr_matrix.values,
x=corr_matrix.columns,
y=corr_matrix.index,
colorscale='Blues',
zmin=-1, zmax=1,
text=corr_matrix.round(2).values,
texttemplate="%{text:.2f}",
hoverongaps=False,
hoverinfo='text',
colorbar=dict(title='相關係數')
))
fig.update_layout(
title="股票價格相關性矩陣",
height=500,
margin=dict(l=40, r=40, t=60, b=40)
)
st.plotly_chart(fig, use_container_width=True)
import streamlit as st
# 頁面配置和主題設定
# 應用CSS美化界面
st.markdown("""
<style>
.main-header {
font-size: 2.5rem;
background: linear-gradient(90deg, #0E76BC, #00A591);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
padding: 0.2rem 0;
text-align: center;
margin-bottom: 1rem;
}
.dashboard-header {
font-size: 1.8rem;
background: linear-gradient(90deg, #2C3E50, #4CA1AF);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
padding: 0.2rem 0;
margin-bottom: 0.5rem;
}
.card {
border-radius: 5px;
background-color: #f9f9f9;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 1rem;
margin-bottom: 1rem;
}
.metric-card {
background: linear-gradient(120deg, #f6f9fc, #e9f1f7);
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
padding: 1rem;
text-align: center;
}
.metric-value {
font-size: 1.8rem;
font-weight: bold;
}
.metric-change-positive {
color: #00A591;
font-weight: bold;
}
.metric-change-negative {
color: #E74C3C;
font-weight: bold;
}
.subtitle {
color: #666;
font-size: 1rem;
}
.stTabs [data-baseweb="tab-list"] {
gap: 24px;
}
.stTabs [data-baseweb="tab"] {
height: 50px;
white-space: pre-wrap;
background-color: #f0f2f6;
border-radius: 4px 4px 0px 0px;
gap: 1px;
padding-top: 10px;
padding-bottom: 10px;
}
.stTabs [aria-selected="true"] {
background-color: #0E76BC !important;
color: white !important;
}
/* 載入動畫 */
@keyframes pulse {
0% { opacity: 0.6; }
50% { opacity: 1; }
100% { opacity: 0.6; }
}
.loading-animation {
animation: pulse 1.5s infinite;
background-color: #0E76BC;
color: white;
padding: 10px;
border-radius: 5px;
text-align: center;
}
/* 自訂滾動條 */
::-webkit-scrollbar {
width: 10px;
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #0E76BC;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #09589b;
}
</style>
""", unsafe_allow_html=True)
# 標題和說明
st.markdown('<h1 class="main-header">台灣股票高級分析儀表板</h1>', unsafe_allow_html=True)
st.markdown("""
<div class="card">
<p>這個專業儀表板提供深入的台灣股票分析功能,包括價格趨勢比較、技術指標、基本面數據以及風險分析。輸入股票代號並選擇分析參數來開始您的專業股票分析。</p>
</div>
""", unsafe_allow_html=True)
# ------ 功能函數 ------ #
def get_stock_data(stock_ids, period="1y", interval="1d"):
'''
參數:
stock_ids (list): 股票代碼列表
period (str): 時間週期
interval (str): 間隔
返回:
dict: 股票數據字典
'''
stock_data = {}
with st.spinner("正在獲取股票數據..."):
progress_bar = st.progress(0)
for i, stock_id in enumerate(stock_ids):
try:
# 確保台灣股票代碼格式正確
if stock_id.isdigit() or (len(stock_id) <= 4 and stock_id.split('.')[0].isdigit()):
if '.' not in stock_id:
stock_id = f"{stock_id}.TW"
# 獲取股票數據
stock = yf.Ticker(stock_id)
hist = stock.history(period=period, interval=interval)
try:
info = stock.info
except:
info = {}
if not hist.empty:
stock_data[stock_id] = {
'history': hist,
'info': info
}
st.success(f"✅ 成功獲取 {stock_id} 的數據")
else:
st.error(f"❌ 未找到 {stock_id} 的數據")
except Exception as e:
st.error(f"❌ 獲取 {stock_id} 數據時出錯: {str(e)}")
progress_bar.progress((i + 1) / len(stock_ids))
time.sleep(0.5) # 為了視覺效果添加的短暫延遲
progress_bar.empty()
return stock_data
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
# 累積回報率
cumulative_return = (data.iloc[-1] / data.iloc[0] - 1) * 100
# 年化回報率 (假設252個交易日)
days = len(data)
annualized_return = ((1 + cumulative_return / 100) ** (252 / days) - 1) * 100
# 波動率 (年化標準差)
volatility = daily_returns.std() * np.sqrt(252) * 100
# 夏普比率 (假設無風險利率為2%)
risk_free_rate = 0.02
sharpe_ratio = (annualized_return / 100 - risk_free_rate) / (volatility / 100)
# 最大回撤
cum_returns = (1 + daily_returns).cumprod()
running_max = cum_returns.cummax()
drawdown = (cum_returns / running_max - 1) * 100
max_drawdown = drawdown.min()
return {
'cumulative_return': cumulative_return,
'annualized_return': annualized_return,
'volatility': volatility,
'sharpe_ratio': sharpe_ratio,
'max_drawdown': max_drawdown
}
# 創建列來顯示指標
cols = st.columns(len(stock_data))
for i, (stock_id, data) in enumerate(stock_data.items()):
hist = data['history']
if hist.empty:
continue
# 獲取最新和前一天的價格
latest_price = hist['Close'].iloc[-1]
prev_price = hist['Close'].iloc[-2] if len(hist) > 1 else latest_price
price_change = latest_price - prev_price
price_change_pct = (price_change / prev_price) * 100 if prev_price != 0 else 0
# 計算成交量變化
latest_volume = hist['Volume'].iloc[-1] if 'Volume' in hist else 0
avg_volume = hist['Volume'].mean() if 'Volume' in hist else 0
volume_change_pct = ((latest_volume / avg_volume) - 1) * 100 if avg_volume != 0 else 0
with cols[i]:
st.markdown(f"<h3 style='text-align: center;'>{stock_id}</h3>", unsafe_allow_html=True)
# 價格指標
price_color = "metric-change-positive" if price_change >= 0 else "metric-change-negative"
change_symbol = "▲" if price_change >= 0 else "▼"
st.markdown(f"""
<div class="metric-card">
<div class="metric-value">{latest_price:.2f} TWD</div>
<div class="{price_color}">{change_symbol} {abs(price_change):.2f} ({abs(price_change_pct):.2f}%)</div>
<div class="subtitle">最新收盤價</div>
</div>
""", unsafe_allow_html=True)
# 成交量指標
volume_color = "metric-change-positive" if volume_change_pct >= 0 else "metric-change-negative"
vol_symbol = "▲" if volume_change_pct >= 0 else "▼"
if 'Volume' in hist:
formatted_volume = f"{latest_volume/1000000:.2f}M" if latest_volume >= 1000000 else f"{latest_volume/1000:.2f}K"
st.markdown(f"""
<div class="metric-card" style="margin-top: 10px;">
<div class="metric-value">{formatted_volume}</div>
<div class="{volume_color}">{vol_symbol} {abs(volume_change_pct):.2f}%</div>
<div class="subtitle">最新成交量</div>
</div>
""", unsafe_allow_html=True)
# 創建圖表
fig = make_subplots(rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.1,
subplot_titles=("股價走勢比較", "正規化股價比較 (基準=100)"),
row_heights=[0.6, 0.4])
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
color_idx = 0
# 用於正規化的數據
normalized_data = {}
# 添加每個股票的數據到圖表
for stock_id, data in stock_data.items():
hist = data['history']
# 確保數據不為空
if hist.empty:
continue
# 股票名稱顯示
display_name = stock_id
# 獲取顏色
color = colors[color_idx % len(colors)]
color_idx += 1
# 添加原始價格折線圖
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name=f"{display_name}",
line=dict(color=color, width=2)
),
row=1, col=1
)
# 正規化數據
normalized = normalize_data(hist['Close'])
normalized_data[stock_id] = normalized
# 添加正規化折線圖
fig.add_trace(
go.Scatter(
x=hist.index,
y=normalized,
mode='lines',
name=f"{display_name} (正規化)",
line=dict(color=color, width=2, dash='dot')
),
row=2, col=1
)
# 更新布局
fig.update_layout(
height=700,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
plot_bgcolor='rgba(240,242,246,0.8)',
)
# 更新Y軸標題
fig.update_yaxes(title_text="價格 (TWD)", row=1, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_yaxes(title_text="正規化價格 (基準=100)", row=2, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
# 顯示圖表
st.plotly_chart(fig, use_container_width=True)
# 創建圖表
fig = go.Figure()
colors = ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd']
color_idx = 0
# 添加每個股票的成交量到圖表
has_volume_data = False
for stock_id, data in stock_data.items():
hist = data['history']
# 確保數據不為空且有成交量數據
if hist.empty or 'Volume' not in hist.columns:
continue
has_volume_data = True
# 股票名稱顯示
display_name = stock_id
# 獲取顏色
color = colors[color_idx % len(colors)]
color_idx += 1
# 添加成交量柱狀圖
fig.add_trace(
go.Bar(
x=hist.index,
y=hist['Volume'],
name=f"{display_name}",
marker_color=color,
opacity=0.7
)
)
if not has_volume_data:
st.warning("沒有可用的成交量數據")
return
# 更新布局
fig.update_layout(
title="股票成交量比較",
height=400,
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
template="plotly_white",
barmode='group',
bargap=0.15,
bargroupgap=0.1,
margin=dict(l=40, r=40, t=60, b=40),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="成交量", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
# 顯示圖表
st.plotly_chart(fig, use_container_width=True)
hist = stock_data[selected_stock]['history']
if hist.empty:
st.warning("所選股票沒有足夠的歷史數據")
return
# RSI 指標
if indicator_type == "RSI":
rsi = calculate_rsi(hist['Close'])
fig = go.Figure()
# 添加RSI線
fig.add_trace(
go.Scatter(
x=hist.index,
y=rsi,
mode='lines',
name='RSI',
line=dict(color='#1f77b4', width=2)
)
)
# 添加超買超賣水平線
fig.add_trace(
go.Scatter(
x=[hist.index[0], hist.index[-1]],
y=[70, 70],
mode='lines',
line=dict(color='red', width=1, dash='dash'),
name='超買線 (70)'
)
)
fig.add_trace(
go.Scatter(
x=[hist.index[0], hist.index[-1]],
y=[30, 30],
mode='lines',
line=dict(color='green', width=1, dash='dash'),
name='超賣線 (30)'
)
)
fig.add_trace(
go.Scatter(
x=[hist.index[0], hist.index[-1]],
y=[50, 50],
mode='lines',
line=dict(color='gray', width=1, dash='dot'),
name='中位線 (50)'
)
)
fig.update_layout(
title=f"{selected_stock} RSI 指標",
height=400,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="RSI 值", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
# MACD 指標
elif indicator_type == "MACD":
macd, signal, histogram = calculate_macd(hist['Close'])
fig = make_subplots(rows=2, cols=1, shared_xaxes=True,
vertical_spacing=0.1,
row_heights=[0.7, 0.3])
# 添加價格線
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name='收盤價',
line=dict(color='#1f77b4', width=2)
),
row=1, col=1
)
# 添加MACD線
fig.add_trace(
go.Scatter(
x=hist.index,
y=macd,
mode='lines',
name='MACD',
line=dict(color='#ff7f0e', width=2)
),
row=2, col=1
)
# 添加訊號線
fig.add_trace(
go.Scatter(
x=hist.index,
y=signal,
mode='lines',
name='Signal',
line=dict(color='#2ca02c', width=2)
),
row=2, col=1
)
# 添加柱狀圖
colors = ['red' if val < 0 else 'green' for val in histogram]
fig.add_trace(
go.Bar(
x=hist.index,
y=histogram,
name='Histogram',
marker_color=colors
),
row=2, col=1
)
fig.update_layout(
title=f"{selected_stock} MACD 指標",
height=600,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="價格", row=1, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_yaxes(title_text="MACD", row=2, col=1, gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
# 布林帶
elif indicator_type == "布林帶":
ma, upper, lower = calculate_bollinger_bands(hist['Close'])
fig = go.Figure()
# 添加價格線
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name='收盤價',
line=dict(color='#1f77b4', width=2)
)
)
# 添加移動平均線
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma,
mode='lines',
name='20日移動平均線',
line=dict(color='#ff7f0e', width=2)
)
)
# 添加上軌
fig.add_trace(
go.Scatter(
x=hist.index,
y=upper,
mode='lines',
name='上軌 (+2σ)',
line=dict(color='#2ca02c', width=1, dash='dash')
)
)
# 添加下軌
fig.add_trace(
go.Scatter(
x=hist.index,
y=lower,
mode='lines',
name='下軌 (-2σ)',
line=dict(color='#d62728', width=1, dash='dash')
)
)
# 添加陰影區域
fig.add_trace(
go.Scatter(
x=hist.index.tolist() + hist.index.tolist()[::-1],
y=upper.tolist() + lower.tolist()[::-1],
fill='toself',
fillcolor='rgba(44, 160, 44, 0.1)',
line=dict(color='rgba(255,255,255,0)'),
hoverinfo='skip',
showlegend=False
)
)
fig.update_layout(
title=f"{selected_stock} 布林帶",
height=500,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="價格", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
# 移動平均線
elif indicator_type == "移動平均線":
ma20, ma50, ma100, ma200 = calculate_moving_averages(hist['Close'])
fig = go.Figure()
# 添加價格線
fig.add_trace(
go.Scatter(
x=hist.index,
y=hist['Close'],
mode='lines',
name='收盤價',
line=dict(color='#1f77b4', width=2)
)
)
# 添加移動平均線
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma20,
mode='lines',
name='20日均線',
line=dict(color='#ff7f0e', width=1.5)
)
)
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma50,
mode='lines',
name='50日均線',
line=dict(color='#2ca02c', width=1.5)
)
)
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma100,
mode='lines',
name='100日均線',
line=dict(color='#d62728', width=1.5)
)
)
fig.add_trace(
go.Scatter(
x=hist.index,
y=ma200,
mode='lines',
name='200日均線',
line=dict(color='#9467bd', width=1.5)
)
)
fig.update_layout(
title=f"{selected_stock} 移動平均線",
height=500,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
hovermode="x unified",
legend=dict(
orientation="h",
yanchor="bottom",
y=1.02,
xanchor="right",
x=1
),
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig.update_yaxes(title_text="價格", gridcolor='rgba(220,220,220,0.5)')
fig.update_xaxes(gridcolor='rgba(220,220,220,0.5)')
st.plotly_chart(fig, use_container_width=True)
# 收集績效指標
performance_metrics = {}
for stock_id, data in stock_data.items():
hist = data['history']
if not hist.empty:
metrics = calculate_performance_metrics(hist['Close'])
performance_metrics[stock_id] = metrics
if not performance_metrics:
st.warning("沒有足夠的數據來計算績效指標")
return
# 創建圖表數據
metrics_df = pd.DataFrame(performance_metrics).T
# 創建多圖表比較
cols = st.columns(2)
# 1. 累積回報率比較
with cols[0]:
fig_return = px.bar(
metrics_df,
y=metrics_df.index,
x='cumulative_return',
orientation='h',
title='累積報酬率比較 (%)',
color='cumulative_return',
color_continuous_scale=px.colors.sequential.Blues,
text='cumulative_return'
)
fig_return.update_layout(
height=350,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
xaxis_title="累積報酬率 (%)",
yaxis_title="",
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig_return.update_traces(
texttemplate='%{x:.2f}%',
textposition='outside'
)
st.plotly_chart(fig_return, use_container_width=True)
# 2. 波動率比較
with cols[1]:
fig_vol = px.bar(
metrics_df,
y=metrics_df.index,
x='volatility',
orientation='h',
title='波動率比較 (%)',
color='volatility',
color_continuous_scale=px.colors.sequential.Reds,
text='volatility'
)
fig_vol.update_layout(
height=350,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
xaxis_title="波動率 (%)",
yaxis_title="",
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig_vol.update_traces(
texttemplate='%{x:.2f}%',
textposition='outside'
)
st.plotly_chart(fig_vol, use_container_width=True)
cols = st.columns(2)
# 3. 夏普比率比較
with cols[0]:
sharpe_colors = ['#d62728' if s < 0 else '#2ca02c' for s in metrics_df['sharpe_ratio']]
fig_sharpe = px.bar(
metrics_df,
y=metrics_df.index,
x='sharpe_ratio',
orientation='h',
title='夏普比率比較',
text='sharpe_ratio'
)
fig_sharpe.update_traces(
marker_color=sharpe_colors,
texttemplate='%{x:.2f}',
textposition='outside'
)
fig_sharpe.update_layout(
height=350,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
xaxis_title="夏普比率",
yaxis_title="",
plot_bgcolor='rgba(240,242,246,0.8)',
)
st.plotly_chart(fig_sharpe, use_container_width=True)
# 4. 最大回撤比較
with cols[1]:
fig_drawdown = px.bar(
metrics_df,
y=metrics_df.index,
x='max_drawdown',
orientation='h',
title='最大回撤比較 (%)',
color='max_drawdown',
color_continuous_scale=px.colors.sequential.Reds_r,
text='max_drawdown'
)
fig_drawdown.update_layout(
height=350,
template="plotly_white",
margin=dict(l=40, r=40, t=60, b=40),
xaxis_title="最大回撤 (%)",
yaxis_title="",
plot_bgcolor='rgba(240,242,246,0.8)',
)
fig_drawdown.update_traces(
texttemplate='%{x:.2f}%',
textposition='outside'
)
st.plotly_chart(fig_drawdown, use_container_width=True)
# 5. 績效指標比較表格
st.markdown("### 績效指標詳細比較")
# 準備顯示的數據
display_df = pd.DataFrame({
'累積報酬率 (%)': metrics_df['cumulative_return'].round(2),
'年化報酬率 (%)': metrics_df['annualized_return'].round(2),
'波動率 (%)': metrics_df['volatility'].round(2),
'夏普比率': metrics_df['sharpe_ratio'].round(2),
'最大回撤 (%)': metrics_df['max_drawdown'].round(2)
})
# 表格高亮設定
st.dataframe(
display_df,
use_container_width=True,
height=max(35 * (len(display_df) + 1), 200),
column_config={
'累積報酬率 (%)': st.column_config.NumberColumn(format="%.2f%%"),
'年化報酬率 (%)': st.column_config.NumberColumn(format="%.2f%%"),
'波動率 (%)': st.column_config.NumberColumn(format="%.2f%%"),
'夏普比率': st.column_config.NumberColumn(format="%.2f"),
'最大回撤 (%)': st.column_config.NumberColumn(format="%.2f%%")
}
)
if __name__ == "__main__":
# 執行主程式,Streamlit 自動執行 app.py
pass # 可視需求添加初始化邏輯