Spaces:
Sleeping
Sleeping
| # -*- coding: utf-8 -*- | |
| import streamlit as st | |
| from vnstock3 import Vnstock, Listing, Quote, Company, Finance, Trading | |
| from vnstock3.explorer.fmarket.fund import Fund | |
| from vnstock3.explorer.misc import vcb_exchange_rate, sjc_gold_price | |
| import pandas as pd | |
| import plotly.graph_objs as go | |
| import plotly.express as px | |
| from datetime import datetime, timedelta | |
| st.set_page_config(page_title="Vietnam Stock Market Insight", layout="wide") | |
| st.title("📈 Vietnam Stock Market Insight") | |
| st.sidebar.header("Cài đặt") | |
| user_selected_source = st.sidebar.selectbox( | |
| "Chọn nguồn dữ liệu Mặc định (cho PTKT, Bảng giá, Biểu đồ NC):", | |
| ["TCBS", "SSI", "VCI", "VPS", "DNSE"], | |
| index=0, | |
| help="Nguồn này áp dụng cho Phân tích Kỹ thuật, Bảng giá và Biểu đồ nâng cao. Các mục khác sẽ ưu tiên nguồn TCBS." | |
| ) | |
| symbol = st.sidebar.text_input("Nhập mã cổ phiếu (VD: FPT, VNM):", value="FPT").upper() | |
| st.sidebar.subheader("Khoảng thời gian") | |
| default_start_date = pd.to_datetime("2024-01-01") | |
| default_end_date = pd.Timestamp.now().date() | |
| col1, col2 = st.sidebar.columns(2) | |
| with col1: | |
| start_date = st.date_input("Ngày bắt đầu", default_start_date, key="start_date", max_value=default_end_date) | |
| with col2: | |
| end_date = st.date_input("Ngày kết thúc", default_end_date, key="end_date", max_value=default_end_date) | |
| if start_date > end_date: | |
| st.sidebar.error("Lỗi: Ngày bắt đầu không được sau ngày kết thúc.") | |
| st.stop() | |
| tab_titles = [ | |
| "📉 PT Kỹ thuật", | |
| "📊 PT Cơ bản", | |
| "🏢 TT Doanh nghiệp", | |
| "💹 Bảng giá", | |
| "📈 Chỉ số TT", | |
| "📊 Biểu đồ NC", | |
| "🥇 Kim loại quý", | |
| "📝 HĐ Tương lai", | |
| "💼 Quỹ đầu tư", | |
| "💱 Forex & Crypto" | |
| ] | |
| tabs = st.tabs(tab_titles) | |
| # TAB 1 - Phân tích kỹ thuật | |
| with tabs[0]: | |
| st.header(f"Phân tích kỹ thuật cho {symbol}") | |
| st.info(f"Sử dụng nguồn dữ liệu: **{user_selected_source}** (do bạn chọn ở sidebar). Nếu lỗi, hãy thử đổi nguồn.") | |
| if st.button("Tải dữ liệu kỹ thuật", key="tech_analysis_btn"): | |
| with st.spinner(f"Đang tải dữ liệu từ {user_selected_source}..."): | |
| try: | |
| stock = Vnstock().stock(symbol=symbol, source=user_selected_source) | |
| df = stock.quote.history(start=start_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"), interval='1D') | |
| if df.empty: | |
| st.error(f"❌ Không có dữ liệu cho mã {symbol} từ nguồn {user_selected_source} trong khoảng thời gian đã chọn.") | |
| else: | |
| df['time'] = pd.to_datetime(df['time']) | |
| st.success(f"Đã tải {len(df)} bản ghi giá cho {symbol} từ {user_selected_source}.") | |
| st.subheader(f"Biểu đồ nến {symbol}") | |
| fig_candle = go.Figure(data=[go.Candlestick(x=df['time'], open=df['open'], high=df['high'], low=df['low'], close=df['close'], name=symbol)]) | |
| fig_candle.update_layout(xaxis_title='Ngày', yaxis_title='Giá', xaxis_rangeslider_visible=False) | |
| st.plotly_chart(fig_candle, use_container_width=True) | |
| df.set_index('time', inplace=True); df.sort_index(inplace=True) | |
| df['EMA12'] = df['close'].ewm(span=12, adjust=False).mean() | |
| df['EMA26'] = df['close'].ewm(span=26, adjust=False).mean() | |
| df['MACD'] = df['EMA12'] - df['EMA26'] | |
| df['MACD_Signal'] = df['MACD'].ewm(span=9, adjust=False).mean() | |
| df['MACD_Hist'] = df['MACD'] - df['MACD_Signal'] | |
| df['BB_Middle'] = df['close'].rolling(window=20).mean() | |
| df['BB_Std'] = df['close'].rolling(window=20).std().fillna(0) | |
| df['BB_Upper'] = df['BB_Middle'] + (2 * df['BB_Std']) | |
| df['BB_Lower'] = df['BB_Middle'] - (2 * df['BB_Std']) | |
| delta = df['close'].diff() | |
| gain = delta.where(delta > 0, 0).fillna(0) | |
| loss = -delta.where(delta < 0, 0).fillna(0) | |
| avg_gain = gain.ewm(com=14 - 1, min_periods=14).mean() | |
| avg_loss = loss.ewm(com=14 - 1, min_periods=14).mean() | |
| rs = avg_gain / avg_loss.replace(0, 0.000001) | |
| df['RSI'] = 100 - (100 / (1 + rs)) | |
| display_cols_tech = ['open', 'high', 'low', 'close', 'volume', 'RSI', 'MACD', 'BB_Upper', 'BB_Lower'] | |
| st.dataframe(df[display_cols_tech].dropna(subset=['close']).tail(10).round(2)) | |
| st.subheader("Chỉ báo kỹ thuật") | |
| fig_bb = go.Figure() | |
| fig_bb.add_trace(go.Scatter(x=df.index, y=df['close'], name='Close', line=dict(color='blue'))) | |
| fig_bb.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'], name='Upper', line=dict(color='rgba(255,0,0,0.5)', dash='dash'))) | |
| fig_bb.add_trace(go.Scatter(x=df.index, y=df['BB_Middle'], name='Middle', line=dict(color='rgba(255,165,0,0.5)', dash='dash'))) | |
| fig_bb.add_trace(go.Scatter(x=df.index, y=df['BB_Lower'], name='Lower', line=dict(color='rgba(0,128,0,0.5)', dash='dash'))) | |
| fig_bb.update_layout(title='Bollinger Bands', yaxis_title='Giá') | |
| st.plotly_chart(fig_bb, use_container_width=True) | |
| fig_rsi = go.Figure() | |
| fig_rsi.add_trace(go.Scatter(x=df.index, y=df['RSI'], name='RSI', line=dict(color='purple'))) | |
| fig_rsi.add_hline(y=70, line=dict(color='red', dash='dash'), name='Quá mua') | |
| fig_rsi.add_hline(y=30, line=dict(color='green', dash='dash'), name='Quá bán') | |
| fig_rsi.update_layout(title='RSI', yaxis_title='RSI', yaxis=dict(range=[0, 100]), showlegend=True) | |
| st.plotly_chart(fig_rsi, use_container_width=True) | |
| fig_macd = go.Figure() | |
| fig_macd.add_trace(go.Scatter(x=df.index, y=df['MACD'], name='MACD', line=dict(color='blue'))) | |
| fig_macd.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], name='Signal', line=dict(color='orange'))) | |
| colors_macd = ['green' if val >= 0 else 'red' for val in df['MACD_Hist']] | |
| fig_macd.add_trace(go.Bar(x=df.index, y=df['MACD_Hist'], name='Histogram', marker_color=colors_macd, opacity=0.6)) | |
| fig_macd.update_layout(title='MACD', yaxis_title='Giá trị', bargap=0.01) | |
| st.plotly_chart(fig_macd, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi tải PTKT từ {user_selected_source}: {e}") | |
| st.exception(e) | |
| st.divider() | |
| if st.button("Thống kê giá lịch sử", key="price_history_btn"): | |
| with st.spinner(f"Đang tải lịch sử từ {user_selected_source}..."): | |
| try: | |
| df_history_data = None | |
| if 'df' in locals() and not df.empty and isinstance(df.index, pd.DatetimeIndex): | |
| if df.index.min().date() <= start_date and df.index.max().date() >= end_date: | |
| df_history_data = df.copy() | |
| st.info(f"Dùng dữ liệu đã tải từ {user_selected_source}.") | |
| if df_history_data is None: | |
| stock_hist = Vnstock().stock(symbol=symbol, source=user_selected_source) | |
| df_history_raw = stock_hist.quote.history(start=start_date.strftime("%Y-%m-%d"), end=end_date.strftime("%Y-%m-%d"), interval='1D') | |
| if df_history_raw.empty: | |
| st.error(f"❌ Không có lịch sử cho {symbol} từ {user_selected_source}.") | |
| st.stop() | |
| df_history_raw['time'] = pd.to_datetime(df_history_raw['time']) | |
| df_history_data = df_history_raw.set_index('time') | |
| st.subheader("Dữ liệu giá lịch sử (10 ngày cuối)") | |
| st.dataframe(df_history_data[['open', 'high', 'low', 'close', 'volume']].tail(10).round(2)) | |
| min_date_hist = df_history_data.index.min().strftime('%Y-%m-%d') | |
| max_date_hist = df_history_data.index.max().strftime('%Y-%m-%d') | |
| st.write(f"Khoảng thời gian: {min_date_hist} đến {max_date_hist}") | |
| low_min, high_max = df_history_data['low'].min(), df_history_data['high'].max() | |
| vol_mean, vol_max = df_history_data['volume'].mean(), df_history_data['volume'].max() | |
| stats = pd.DataFrame({ | |
| 'Chỉ số': ['Giá cao nhất', 'Giá thấp nhất', 'Giá đóng cửa TB', 'Khối lượng TB', 'Khối lượng LN', 'Biến động giá (%)'], | |
| 'Giá trị': [f"{high_max:,.0f}", f"{low_min:,.0f}", f"{df_history_data['close'].mean():,.0f}", f"{vol_mean:,.0f}", f"{vol_max:,.0f}", f"{(high_max - low_min) / low_min * 100:.2f}%" if low_min != 0 else "N/A"] | |
| }).set_index('Chỉ số') | |
| st.dataframe(stats) | |
| st.subheader("Biểu đồ khối lượng giao dịch") | |
| fig_vol = go.Figure(go.Bar(x=df_history_data.index, y=df_history_data['volume'], name='Khối lượng', marker_color='lightblue')) | |
| fig_vol.update_layout(yaxis_title='Khối lượng') | |
| st.plotly_chart(fig_vol, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi lấy lịch sử từ {user_selected_source}: {e}") | |
| st.exception(e) | |
| # ... (Các tab còn lại giữ nguyên theo diff đã fix, đảm bảo mỗi khối try đều có except) | |
| # Do giới hạn ký tự, bạn hãy dùng mẫu này cho các tab còn lại: | |
| # - Tất cả các khối try đều phải có except. | |
| # - Nếu có nhiều try-lồng-nhau, mỗi try đều phải có except. | |
| # - Nếu có try chỉ để kiểm tra một dòng, cũng phải có except. | |
| # Cuối file, không có lệnh bị thừa hoặc thiếu except. | |
| # --- Footer --- | |
| st.markdown("---") | |