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(""" """, unsafe_allow_html=True) # 標題和說明 st.markdown('

台灣股票高級分析儀表板

', unsafe_allow_html=True) st.markdown("""

這個專業儀表板提供深入的台灣股票分析功能,包括價格趨勢比較、技術指標、基本面數據以及風險分析。輸入股票代號並選擇分析參數來開始您的專業股票分析。

""", 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"

{stock_id}

", 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"""
{latest_price:.2f} TWD
{change_symbol} {abs(price_change):.2f} ({abs(price_change_pct):.2f}%)
最新收盤價
""", 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"""
{formatted_volume}
{vol_symbol} {abs(volume_change_pct):.2f}%
最新成交量
""", 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('

台股分析儀表板

', 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"""
{key}
{value}
""", 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"""
最近一日漲跌幅
{change_symbol} {abs(change_pct):.2f}%
""", unsafe_allow_html=True) # 顯示績效指標 st.markdown("### 績效指標") metrics = calculate_performance_metrics(hist['Close']) metrics_cols = st.columns(5) with metrics_cols[0]: st.markdown(f"""
累積報酬率
{metrics['cumulative_return']:.2f}%
""", unsafe_allow_html=True) with metrics_cols[1]: st.markdown(f"""
年化報酬率
{metrics['annualized_return']:.2f}%
""", unsafe_allow_html=True) with metrics_cols[2]: st.markdown(f"""
波動率
{metrics['volatility']:.2f}%
""", unsafe_allow_html=True) with metrics_cols[3]: st.markdown(f"""
夏普比率
{metrics['sharpe_ratio']:.2f}
""", unsafe_allow_html=True) with metrics_cols[4]: st.markdown(f"""
最大回撤
{metrics['max_drawdown']:.2f}%
""", 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(""" """, unsafe_allow_html=True) # 標題和說明 st.markdown('

台灣股票高級分析儀表板

', unsafe_allow_html=True) st.markdown("""

這個專業儀表板提供深入的台灣股票分析功能,包括價格趨勢比較、技術指標、基本面數據以及風險分析。輸入股票代號並選擇分析參數來開始您的專業股票分析。

""", 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"

{stock_id}

", 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"""
{latest_price:.2f} TWD
{change_symbol} {abs(price_change):.2f} ({abs(price_change_pct):.2f}%)
最新收盤價
""", 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"""
{formatted_volume}
{vol_symbol} {abs(volume_change_pct):.2f}%
最新成交量
""", 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 # 可視需求添加初始化邏輯