petertulip86's picture
Update src/streamlit_app.py
09b8bd2 verified
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
# RSI
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))
# OBV (On-Balance Volume)
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)
# %B
df['%B'] = ((df['Close'] - df['BB_Lower']) / (df['BB_Upper'] - df['BB_Lower'])) * 100
# BBW (Bollinger Band Width)
df['BBW'] = ((df['BB_Upper'] - df['BB_Lower']) / df['BB_Middle']) * 100
# KD指標
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()
# MACD
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']
# CCI (Commodity Channel Index)
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)
# MA5-MA20黃金均線交叉
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)
# 成交量和OBV
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)
# RSI
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)
# MACD
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
# Streamlit 應用界面
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("*本系統僅供參考,投資有風險,請謹慎評估*")