Spaces:
Sleeping
Sleeping
| 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 # 可視需求添加初始化邏輯 |