| import streamlit as st |
| import yfinance as yf |
| import pandas as pd |
| import numpy as np |
| import plotly.graph_objects as go |
| from plotly.subplots import make_subplots |
| from datetime import datetime, timedelta |
| import warnings |
| warnings.filterwarnings('ignore') |
|
|
| |
| st.set_page_config( |
| page_title="台股技術分析系統", |
| page_icon="📈", |
| layout="wide" |
| ) |
|
|
| |
| DEFAULT_STOCKS = [ |
| "3515.TW", "2357.TW", "3260.TW", "6166.TW", "2395.TW", "2417.TW", |
| "3665.TW", "3048.TW", "2324.TW", "2308.TW", "3015.TW", "2376.TW", |
| "2317.TW", "2356.TW", "2465.TW", "2301.TW", "3533.TW", "2454.TW", |
| "3706.TW", "2377.TW", "4938.TW", "8299.TW", "2379.TW", "2330.TW", |
| "2303.TW", "3231.TW", "6579.TW", "2345.TW", "2353.TW", "5281.TW", |
| "6596.TW", "3324.TW", "3017.TW", "3163.TW", "2882.TW", "8210.TW", |
| "2362.TW", "2891.TW", "2884.TW", "5484.TW", "6189.TW", "6125.TW", |
| "2059.TW", "2449.TW", "6245.TW", "6922.TW", "8234.TW", "2359.TW", |
| "6584.TW", "3131.TW", "3037.TW", "6669.TW", "5474.TW" |
| ] |
|
|
| def calculate_indicators(df): |
| """計算各種技術指標""" |
| |
| df['MA5'] = df['Close'].rolling(window=5).mean() |
| df['MA10'] = df['Close'].rolling(window=10).mean() |
| df['MA20'] = df['Close'].rolling(window=20).mean() |
| df['MA60'] = df['Close'].rolling(window=60).mean() if len(df) >= 60 else np.nan |
| |
| |
| delta = df['Close'].diff() |
| gain = (delta.where(delta > 0, 0)).rolling(window=14).mean() |
| loss = (-delta.where(delta < 0, 0)).rolling(window=14).mean() |
| rs = gain / loss |
| df['RSI'] = 100 - (100 / (1 + rs)) |
| |
| |
| df['OBV'] = (np.sign(df['Close'].diff()) * df['Volume']).fillna(0).cumsum() |
| |
| |
| df['BB_Middle'] = df['Close'].rolling(window=20).mean() |
| bb_std = df['Close'].rolling(window=20).std() |
| df['BB_Upper'] = df['BB_Middle'] + (bb_std * 2) |
| df['BB_Lower'] = df['BB_Middle'] - (bb_std * 2) |
| |
| |
| df['%B'] = ((df['Close'] - df['BB_Lower']) / (df['BB_Upper'] - df['BB_Lower'])) * 100 |
| |
| |
| df['BBW'] = ((df['BB_Upper'] - df['BB_Lower']) / df['BB_Middle']) * 100 |
| |
| |
| low_min = df['Low'].rolling(window=9).min() |
| high_max = df['High'].rolling(window=9).max() |
| rsv = (df['Close'] - low_min) / (high_max - low_min) * 100 |
| df['K'] = rsv.ewm(com=2).mean() |
| df['D'] = df['K'].ewm(com=2).mean() |
| |
| |
| exp1 = df['Close'].ewm(span=12).mean() |
| exp2 = df['Close'].ewm(span=26).mean() |
| df['MACD'] = exp1 - exp2 |
| df['MACD_Signal'] = df['MACD'].ewm(span=9).mean() |
| df['MACD_Histogram'] = df['MACD'] - df['MACD_Signal'] |
| |
| |
| tp = (df['High'] + df['Low'] + df['Close']) / 3 |
| sma = tp.rolling(window=20).mean() |
| mad = tp.rolling(window=20).apply(lambda x: np.fabs(x - x.mean()).mean()) |
| df['CCI'] = (tp - sma) / (0.015 * mad) |
| |
| |
| df['Golden_Cross'] = np.where(df['MA5'] > df['MA20'], 1, 0) |
| df['Golden_Cross_Signal'] = df['Golden_Cross'].diff() |
| |
| return df |
|
|
| def get_stock_data(symbol, days=30): |
| """獲取股票數據""" |
| try: |
| end_date = datetime.now() |
| start_date = end_date - timedelta(days=days+30) |
| |
| stock = yf.Ticker(symbol) |
| df = stock.history(start=start_date, end=end_date) |
| |
| if df.empty: |
| return None, f"無法獲取 {symbol} 的數據" |
| |
| |
| df = calculate_indicators(df) |
| |
| |
| df = df.tail(days) |
| |
| return df, None |
| |
| except Exception as e: |
| return None, f"獲取 {symbol} 數據時發生錯誤: {str(e)}" |
|
|
| def create_stock_chart(df, symbol): |
| """創建股票圖表""" |
| fig = make_subplots( |
| rows=4, cols=1, |
| shared_xaxes=True, |
| vertical_spacing=0.05, |
| row_heights=[0.5, 0.2, 0.15, 0.15], |
| subplot_titles=(f'{symbol} 股價與技術指標', 'Volume & OBV', 'RSI', 'MACD') |
| ) |
| |
| |
| fig.add_trace(go.Candlestick( |
| x=df.index, |
| open=df['Open'], |
| high=df['High'], |
| low=df['Low'], |
| close=df['Close'], |
| name='價格' |
| ), row=1, col=1) |
| |
| |
| fig.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'], name='布林上軌', line=dict(color='gray', dash='dash')), row=1, col=1) |
| fig.add_trace(go.Scatter(x=df.index, y=df['BB_Lower'], name='布林下軌', line=dict(color='gray', dash='dash'), fill='tonexty'), row=1, col=1) |
| |
| |
| fig.add_trace(go.Scatter(x=df.index, y=df['MA5'], name='MA5', line=dict(color='blue')), row=1, col=1) |
| fig.add_trace(go.Scatter(x=df.index, y=df['MA20'], name='MA20', line=dict(color='red')), row=1, col=1) |
| |
| |
| fig.add_trace(go.Bar(x=df.index, y=df['Volume'], name='成交量', marker_color='lightblue'), row=2, col=1) |
| fig.add_trace(go.Scatter(x=df.index, y=df['OBV'], name='OBV', line=dict(color='orange'), yaxis='y2'), row=2, col=1) |
| |
| |
| fig.add_trace(go.Scatter(x=df.index, y=df['RSI'], name='RSI', line=dict(color='purple')), row=3, col=1) |
| fig.add_hline(y=70, line_dash="dash", line_color="red", row=3, col=1) |
| fig.add_hline(y=30, line_dash="dash", line_color="green", row=3, col=1) |
| |
| |
| fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], name='MACD', line=dict(color='blue')), row=4, col=1) |
| fig.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], name='Signal', line=dict(color='red')), row=4, col=1) |
| fig.add_trace(go.Bar(x=df.index, y=df['MACD_Histogram'], name='Histogram', marker_color='green'), row=4, col=1) |
| |
| fig.update_layout( |
| title=f'{symbol} 技術分析圖表', |
| xaxis_rangeslider_visible=False, |
| height=800, |
| showlegend=True |
| ) |
| |
| return fig |
|
|
| |
| st.title("📈 台股技術分析系統") |
| st.markdown("---") |
|
|
| |
| st.sidebar.header("設定參數") |
|
|
| |
| stock_input_method = st.sidebar.radio( |
| "選擇輸入方式:", |
| ["從清單選擇", "手動輸入股號"] |
| ) |
|
|
| if stock_input_method == "從清單選擇": |
| selected_stock = st.sidebar.selectbox( |
| "選擇股票:", |
| DEFAULT_STOCKS, |
| index=0 |
| ) |
| else: |
| selected_stock = st.sidebar.text_input( |
| "輸入股票代號 (例: 2330.TW):", |
| value="2330.TW" |
| ) |
|
|
| |
| days = st.sidebar.slider("選擇天數:", min_value=7, max_value=90, value=30) |
|
|
| |
| if st.sidebar.button("開始分析", type="primary"): |
| if selected_stock: |
| with st.spinner(f"正在獲取 {selected_stock} 的數據..."): |
| df, error = get_stock_data(selected_stock, days) |
| |
| if error: |
| st.error(error) |
| else: |
| |
| col1, col2, col3, col4 = st.columns(4) |
| |
| latest_price = df['Close'].iloc[-1] |
| price_change = df['Close'].iloc[-1] - df['Close'].iloc[-2] |
| price_change_pct = (price_change / df['Close'].iloc[-2]) * 100 |
| |
| with col1: |
| st.metric("最新價格", f"${latest_price:.2f}", f"{price_change:+.2f}") |
| |
| with col2: |
| st.metric("漲跌幅", f"{price_change_pct:+.2f}%") |
| |
| with col3: |
| st.metric("最高價", f"${df['High'].max():.2f}") |
| |
| with col4: |
| st.metric("最低價", f"${df['Low'].min():.2f}") |
| |
| |
| st.plotly_chart(create_stock_chart(df, selected_stock), use_container_width=True) |
| |
| |
| st.subheader("📊 技術指標摘要") |
| |
| col1, col2, col3 = st.columns(3) |
| |
| with col1: |
| st.write("**移動平均線**") |
| st.write(f"MA5: ${df['MA5'].iloc[-1]:.2f}") |
| st.write(f"MA20: ${df['MA20'].iloc[-1]:.2f}") |
| golden_cross = "✅ 黃金交叉" if df['MA5'].iloc[-1] > df['MA20'].iloc[-1] else "❌ 死亡交叉" |
| st.write(golden_cross) |
| |
| with col2: |
| st.write("**技術指標**") |
| st.write(f"RSI: {df['RSI'].iloc[-1]:.2f}") |
| st.write(f"K值: {df['K'].iloc[-1]:.2f}") |
| st.write(f"D值: {df['D'].iloc[-1]:.2f}") |
| |
| with col3: |
| st.write("**布林通道**") |
| st.write(f"%B: {df['%B'].iloc[-1]:.2f}%") |
| st.write(f"BBW: {df['BBW'].iloc[-1]:.2f}%") |
| st.write(f"CCI: {df['CCI'].iloc[-1]:.2f}") |
| |
| |
| st.subheader("📋 詳細數據") |
| |
| |
| display_columns = [ |
| 'Open', 'High', 'Low', 'Close', 'Volume', |
| 'MA5', 'MA20', 'RSI', 'OBV', 'BB_Upper', 'BB_Lower', |
| '%B', 'BBW', 'K', 'D', 'MACD', 'MACD_Signal', 'CCI' |
| ] |
| |
| |
| display_df = df[display_columns].copy() |
| for col in ['Open', 'High', 'Low', 'Close', 'MA5', 'MA20', 'BB_Upper', 'BB_Lower']: |
| if col in display_df.columns: |
| display_df[col] = display_df[col].round(2) |
| |
| for col in ['RSI', '%B', 'BBW', 'K', 'D', 'MACD', 'MACD_Signal', 'CCI']: |
| if col in display_df.columns: |
| display_df[col] = display_df[col].round(2) |
| |
| st.dataframe(display_df, use_container_width=True) |
| |
| |
| csv = df.to_csv() |
| st.download_button( |
| label="📥 下載完整數據 (CSV)", |
| data=csv, |
| file_name=f"{selected_stock}_{days}days_analysis.csv", |
| mime="text/csv" |
| ) |
| else: |
| st.warning("請輸入股票代號") |
|
|
| |
| st.sidebar.markdown("---") |
| st.sidebar.markdown("### 📖 使用說明") |
| st.sidebar.markdown(""" |
| - 選擇股票代號或手動輸入 |
| - 設定分析天數 (7-90天) |
| - 點擊「開始分析」按鈕 |
| - 查看技術指標和圖表分析 |
| """) |
|
|
| st.sidebar.markdown("### 📈 技術指標說明") |
| st.sidebar.markdown(""" |
| - **MA**: 移動平均線 |
| - **RSI**: 相對強弱指標 (14天) |
| - **OBV**: 成交量平衡指標 |
| - **布林通道**: 價格波動區間 |
| - **%B**: 布林通道位置 |
| - **BBW**: 布林通道寬度 |
| - **KD**: 隨機指標 |
| - **MACD**: 指數平滑移動平均線 |
| - **CCI**: 商品通道指標 |
| """) |
|
|
| |
| st.markdown("---") |
| st.markdown("*本系統僅供參考,投資有風險,請謹慎評估*") |