Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| from vnstock3 import Vnstock, Listing, Quote, Company, Finance, Trading, Screener | |
| 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 | |
| st.set_page_config(page_title="Vietnam Stock Market Insight", layout="wide") | |
| st.title("📈 Vietnam Stock Market Insight") | |
| # Cho phép người dùng chọn nguồn dữ liệu | |
| data_source = st.sidebar.selectbox( | |
| "Chọn nguồn dữ liệu:", | |
| ["VCI", "TCBS", "SSI", "DNSE", "VPS"], | |
| index=0 # Mặc định là VCI | |
| ) | |
| symbol = st.text_input("Nhập mã cổ phiếu (ví dụ: VNM, FPT, VCB):", value="VNM") | |
| col1, col2 = st.columns(2) | |
| with col1: | |
| start_date = st.date_input("Ngày bắt đầu", pd.to_datetime("2025-01-01")) | |
| with col2: | |
| end_date = st.date_input("Ngày kết thúc", pd.to_datetime("2025-12-31")) | |
| tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([ | |
| "📉 Phân tích kỹ thuật", | |
| "📊 Phân tích cơ bản", | |
| "🏢 Thông tin doanh nghiệp", | |
| "💹 Bảng giá giao dịch", | |
| "🔍 Bộ lọc cổ phiếu", | |
| "💰 Dữ liệu thị trường" | |
| ]) | |
| # TAB 1 - Phân tích kỹ thuật | |
| with tab1: | |
| if st.button("Phân tích kỹ thuật", key="tech_analysis"): | |
| with st.spinner("Đang tải dữ liệu..."): | |
| try: | |
| # Sử dụng cấu trúc API mới | |
| stock = Vnstock().stock(symbol=symbol, source=data_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ã cổ phiếu {symbol}. Vui lòng kiểm tra lại mã cổ phiếu.") | |
| else: | |
| # Vẽ biểu đồ nến | |
| 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'] | |
| )]) | |
| fig_candle.update_layout( | |
| title=f'Biểu đồ nến {symbol}', | |
| xaxis_title='Ngày', | |
| yaxis_title='Giá', | |
| xaxis_rangeslider_visible=False | |
| ) | |
| st.plotly_chart(fig_candle, use_container_width=True) | |
| # Tiếp tục với các phân tích khác | |
| df['date'] = pd.to_datetime(df['time']) | |
| df.set_index('date', 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['BB_Middle'] = df['close'].rolling(window=20).mean() | |
| df['BB_Std'] = df['close'].rolling(window=20).std() | |
| 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) | |
| loss = -delta.where(delta < 0, 0) | |
| avg_gain = gain.rolling(window=14).mean() | |
| avg_loss = loss.rolling(window=14).mean() | |
| rs = avg_gain / avg_loss | |
| df['RSI'] = 100 - (100 / (1 + rs)) | |
| st.subheader("📊 Dữ liệu giá cổ phiếu") | |
| st.dataframe(df[['open', 'high', 'low', 'close', 'volume']].dropna().tail(10)) | |
| st.subheader("📈 Bollinger Bands") | |
| fig_bb = go.Figure() | |
| fig_bb.add_trace(go.Scatter(x=df.index, y=df['close'], name='Close')) | |
| fig_bb.add_trace(go.Scatter(x=df.index, y=df['BB_Upper'], name='Upper Band')) | |
| fig_bb.add_trace(go.Scatter(x=df.index, y=df['BB_Lower'], name='Lower Band')) | |
| st.plotly_chart(fig_bb, use_container_width=True) | |
| st.subheader("📉 RSI") | |
| fig_rsi = go.Figure() | |
| fig_rsi.add_trace(go.Scatter(x=df.index, y=df['RSI'], name='RSI')) | |
| fig_rsi.update_layout(yaxis=dict(range=[0, 100])) | |
| st.plotly_chart(fig_rsi, use_container_width=True) | |
| st.subheader("📉 MACD") | |
| fig_macd = go.Figure() | |
| fig_macd.add_trace(go.Scatter(x=df.index, y=df['MACD'], name='MACD')) | |
| fig_macd.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], name='Signal')) | |
| st.plotly_chart(fig_macd, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi kỹ thuật: {e}") | |
| st.info(f"Thử thay đổi nguồn dữ liệu khác ngoài {data_source} hoặc kiểm tra kết nối mạng.") | |
| # Thống kê giá lịch sử | |
| if st.button("Thống kê giá lịch sử", key="price_history"): | |
| with st.spinner("Đang tải dữ liệu giá lịch sử..."): | |
| try: | |
| stock = Vnstock().stock(symbol=symbol, source=data_source) | |
| df_history = stock.quote.history( | |
| start=start_date.strftime("%Y-%m-%d"), | |
| end=end_date.strftime("%Y-%m-%d"), | |
| interval='1D' | |
| ) | |
| df_history['date'] = pd.to_datetime(df_history['time']) | |
| df_history.set_index('date', inplace=True) | |
| st.subheader("📊 Dữ liệu giá cổ phiếu lịch sử") | |
| st.dataframe(df_history[['open', 'high', 'low', 'close', 'volume']].tail(10)) | |
| # Thống kê cơ bản | |
| st.subheader("📊 Thống kê cơ bản") | |
| stats = pd.DataFrame({ | |
| 'Giá cao nhất': [df_history['high'].max()], | |
| 'Giá thấp nhất': [df_history['low'].min()], | |
| 'Giá đóng cửa trung bình': [df_history['close'].mean()], | |
| 'Khối lượng trung bình': [df_history['volume'].mean()], | |
| 'Khối lượng lớn nhất': [df_history['volume'].max()], | |
| 'Biến động giá (%)': [(df_history['high'].max() - df_history['low'].min()) / df_history['low'].min() * 100] | |
| }) | |
| st.dataframe(stats.T) | |
| # Biểu đồ khối lượng | |
| st.subheader("📊 Biểu đồ khối lượng giao dịch") | |
| fig_vol = go.Figure() | |
| fig_vol.add_trace(go.Bar(x=df_history.index, y=df_history['volume'], name='Khối lượng')) | |
| st.plotly_chart(fig_vol, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi lấy dữ liệu giá lịch sử: {e}") | |
| st.info(f"Thử thay đổi nguồn dữ liệu khác ngoài {data_source} hoặc kiểm tra kết nối mạng.") | |
| # TAB 2 - Phân tích cơ bản | |
| with tab2: | |
| if st.button("Phân tích cơ bản", key="fundamental"): | |
| with st.spinner("Đang tải báo cáo tài chính..."): | |
| try: | |
| stock = Vnstock().stock(symbol=symbol, source=data_source) | |
| # Báo cáo kết quả kinh doanh | |
| df_income = stock.finance.income_statement(period='quarter', lang='vi') | |
| st.subheader("📊 Báo cáo kết quả kinh doanh quý") | |
| st.dataframe(df_income) | |
| # Bảng cân đối kế toán | |
| df_balance = stock.finance.balance_sheet(period='year', lang='vi', dropna=True) | |
| st.subheader("📊 Bảng cân đối kế toán năm") | |
| st.dataframe(df_balance) | |
| # Báo cáo lưu chuyển tiền tệ | |
| df_cashflow = stock.finance.cash_flow(period='year', dropna=True) | |
| st.subheader("📊 Báo cáo lưu chuyển tiền tệ") | |
| st.dataframe(df_cashflow) | |
| # Chỉ số tài chính | |
| df_ratio = stock.finance.ratio(period='year', lang='vi', dropna=True) | |
| st.subheader("📊 Chỉ số tài chính") | |
| st.dataframe(df_ratio) | |
| # Biểu diễn trực quan | |
| if not df_income.empty: | |
| st.subheader("📈 Biểu đồ doanh thu và lợi nhuận") | |
| # Hiển thị tên các cột để debug | |
| revenue_keywords = ['doanh thu', 'thu nhập', 'revenue', 'income'] | |
| profit_keywords = ['lợi nhuận', 'lãi', 'profit', 'earnings', 'lnst'] | |
| revenue_cols = [] | |
| for keyword in revenue_keywords: | |
| revenue_cols.extend([col for col in df_income.columns if keyword in col.lower()]) | |
| revenue_cols = list(set(revenue_cols)) # Loại bỏ trùng lặp | |
| profit_cols = [] | |
| for keyword in profit_keywords: | |
| profit_cols.extend([col for col in df_income.columns if keyword in col.lower()]) | |
| profit_cols = list(set(profit_cols)) # Loại bỏ trùng lặp | |
| if revenue_cols and profit_cols: | |
| # Tạo button để force refresh biểu đồ | |
| if st.button("Hiển thị biểu đồ", key="show_finance_chart"): | |
| fig_finance = go.Figure() | |
| fig_finance.add_trace(go.Bar(x=df_income.index, y=df_income[revenue_cols[0]], name='Doanh thu')) | |
| fig_finance.add_trace(go.Bar(x=df_income.index, y=df_income[profit_cols[0]], name='Lợi nhuận')) | |
| fig_finance.update_layout( | |
| title='Biểu đồ doanh thu và lợi nhuận', | |
| xaxis_title='Kỳ báo cáo', | |
| yaxis_title='Giá trị (VND)', | |
| barmode='group' | |
| ) | |
| st.plotly_chart(fig_finance, use_container_width=True) | |
| else: | |
| st.warning("Không tìm thấy cột doanh thu hoặc lợi nhuận trong dữ liệu. Vui lòng kiểm tra lại nguồn dữ liệu.") | |
| else: | |
| st.warning("Không có dữ liệu báo cáo tài chính.") | |
| except Exception as e: | |
| st.error(f"❌ Lỗi dữ liệu tài chính: {e}") | |
| st.info(f"Thử thay đổi nguồn dữ liệu khác ngoài {data_source} hoặc kiểm tra kết nối mạng.") | |
| # TAB 3 - Thông tin doanh nghiệp đầy đủ | |
| with tab3: | |
| if st.button("Xem thông tin doanh nghiệp", key="company_info"): | |
| with st.spinner("Đang tải thông tin doanh nghiệp..."): | |
| try: | |
| # Sử dụng cấu trúc API mới | |
| stock = Vnstock().stock(symbol=symbol, source='TCBS') # Thử nguồn TCBS thay vì VCI | |
| company = stock.company | |
| finance = stock.finance | |
| # Thông tin tổng quan | |
| st.subheader(f"🏢 Tổng quan công ty: {symbol}") | |
| st.dataframe(company.overview()) | |
| # Thông tin niêm yết | |
| listing = Listing() | |
| symbol_info = listing.all_symbols() | |
| company_listing = symbol_info[symbol_info['ticker'] == symbol] if 'ticker' in symbol_info.columns else None | |
| if company_listing is not None and not company_listing.empty: | |
| st.subheader("📋 Thông tin niêm yết") | |
| st.dataframe(company_listing) | |
| # Bảng cân đối kế toán | |
| st.subheader("💰 Bảng cân đối kế toán") | |
| st.dataframe(finance.balance_sheet()) | |
| # Báo cáo lưu chuyển tiền tệ | |
| st.subheader("💸 Báo cáo lưu chuyển tiền tệ") | |
| st.dataframe(finance.cash_flow()) | |
| # Cổ đông lớn | |
| st.subheader("👥 Cổ đông lớn") | |
| st.dataframe(company.shareholders()) | |
| # Ban lãnh đạo - sử dụng filter_by='working' | |
| st.subheader("🧑💼 Ban lãnh đạo (đang làm việc)") | |
| try: | |
| officers_data = company.officers(filter_by='working') | |
| st.dataframe(officers_data) | |
| # Hiển thị tổng số cổ phiếu nắm giữ nếu có cột officer_own_percent | |
| if 'officer_own_percent' in officers_data.columns: | |
| total_shares = officers_data['officer_own_percent'].sum() | |
| st.info(f"Tổng tỷ lệ sở hữu của ban lãnh đạo đang làm việc: {total_shares:.4f}%") | |
| elif 'quantity' in officers_data.columns: | |
| total_shares = officers_data['quantity'].sum() | |
| st.info(f"Tổng số cổ phiếu nắm giữ bởi ban lãnh đạo đang làm việc: {total_shares:,}") | |
| except Exception as e: | |
| # Nếu filter_by không được hỗ trợ, lấy tất cả và lọc bằng pandas | |
| officers_data = company.officers() | |
| if 'status' in officers_data.columns: | |
| working_officers = officers_data[officers_data['status'] == 'working'] | |
| st.dataframe(working_officers) | |
| else: | |
| st.dataframe(officers_data) | |
| st.warning("Không thể lọc ban lãnh đạo đang làm việc do thiếu thông tin trạng thái.") | |
| # Lịch sử cổ tức (Tính năng mới) | |
| st.subheader("💰 Lịch sử chia cổ tức") | |
| try: | |
| # Sử dụng nguồn TCBS để lấy dữ liệu cổ tức | |
| stock_tcbs = Vnstock().stock(symbol=symbol, source='TCBS') | |
| company_tcbs = stock_tcbs.company | |
| dividend_history = company_tcbs.dividends() | |
| if not dividend_history.empty: | |
| st.dataframe(dividend_history) | |
| # Vẽ biểu đồ cổ tức nếu có dữ liệu | |
| if 'cashDividendPercentage' in dividend_history.columns: | |
| fig_dividend = go.Figure() | |
| fig_dividend.add_trace(go.Bar( | |
| x=dividend_history['exerciseDate'] if 'exerciseDate' in dividend_history.columns else dividend_history.index, | |
| y=dividend_history['cashDividendPercentage'], | |
| name='Tỷ lệ cổ tức tiền mặt (%)' | |
| )) | |
| fig_dividend.update_layout( | |
| title='Lịch sử chia cổ tức', | |
| xaxis_title='Ngày thực hiện', | |
| yaxis_title='Tỷ lệ (%)' | |
| ) | |
| st.plotly_chart(fig_dividend, use_container_width=True) | |
| else: | |
| st.info(f"Không có dữ liệu cổ tức cho mã {symbol}") | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi lấy thông tin cổ tức: {e}") | |
| st.info("Tính năng này có thể không được hỗ trợ bởi nguồn dữ liệu hiện tại. Thử thay đổi nguồn dữ liệu sang TCBS.") | |
| # Công ty con (Tính năng mới) | |
| st.subheader("🏭 Công ty con") | |
| try: | |
| subsidiaries_data = company.subsidiaries() | |
| st.dataframe(subsidiaries_data) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi lấy thông tin công ty con: {e}") | |
| st.info("Tính năng này có thể không được hỗ trợ bởi nguồn dữ liệu hiện tại.") | |
| # Tin tức công ty (Tính năng mới) | |
| st.subheader("📰 Tin tức công ty") | |
| try: | |
| news_data = company.news().head(5) # Lấy 5 tin tức mới nhất | |
| st.dataframe(news_data) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi lấy tin tức công ty: {e}") | |
| st.info("Tính năng này có thể không được hỗ trợ bởi nguồn dữ liệu hiện tại.") | |
| # Tài liệu công bố | |
| st.subheader("📎 Tài liệu công bố") | |
| st.dataframe(company.documents()) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi lấy thông tin doanh nghiệp: {e}") | |
| st.info("Thử thay đổi nguồn dữ liệu hoặc kiểm tra kết nối mạng.") | |
| # TAB 4 - Bảng giá giao dịch | |
| with tab4: | |
| if st.button("Xem bảng giá giao dịch", key="price_board"): | |
| with st.spinner("Đang tải bảng giá giao dịch..."): | |
| try: | |
| # Cho phép người dùng nhập nhiều mã cổ phiếu | |
| symbols_input = st.text_input("Nhập các mã cổ phiếu (phân cách bằng dấu phẩy):", value="VNM,VCB,FPT,HPG") | |
| symbols_list = [s.strip() for s in symbols_input.split(',')] | |
| trading = Trading(source=data_source) | |
| price_board = trading.price_board(symbols_list) | |
| st.subheader("💹 Bảng giá giao dịch") | |
| st.dataframe(price_board) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi lấy bảng giá giao dịch: {e}") | |
| st.info(f"Thử thay đổi nguồn dữ liệu khác ngoài {data_source} hoặc kiểm tra kết nối mạng.") | |
| # TAB 5 - Bộ lọc cổ phiếu | |
| with tab5: | |
| if st.button("Sử dụng bộ lọc cổ phiếu", key="stock_screener"): | |
| with st.spinner("Đang lọc cổ phiếu..."): | |
| try: | |
| # Cho phép người dùng chọn sàn giao dịch | |
| exchanges = st.multiselect( | |
| "Chọn sàn giao dịch:", | |
| ["HOSE", "HNX", "UPCOM"], | |
| default=["HOSE", "HNX", "UPCOM"] | |
| ) | |
| exchange_param = ",".join(exchanges) | |
| # Các tham số lọc khác | |
| limit = st.number_input("Số lượng cổ phiếu tối đa:", min_value=10, max_value=2000, value=100) | |
| # Thực hiện lọc | |
| stock = Vnstock().stock(symbol=symbol, source=data_source) | |
| screened_stocks = stock.screener.stock(params={"exchangeName": exchange_param}, limit=limit) | |
| st.subheader("🔍 Kết quả lọc cổ phiếu") | |
| st.dataframe(screened_stocks) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi sử dụng bộ lọc cổ phiếu: {e}") | |
| st.info(f"Thử thay đổi nguồn dữ liệu khác ngoài {data_source} hoặc kiểm tra kết nối mạng.") | |
| # TAB 6 - Dữ liệu thị trường | |
| with tab6: | |
| market_data_type = st.selectbox( | |
| "Chọn loại dữ liệu thị trường:", | |
| ["Quỹ mở", "Tỷ giá ngoại tệ", "Giá vàng", "Thị trường quốc tế"] | |
| ) | |
| if st.button("Xem dữ liệu thị trường", key="market_data"): | |
| with st.spinner("Đang tải dữ liệu thị trường..."): | |
| try: | |
| if market_data_type == "Quỹ mở": | |
| fund = Fund() | |
| fund_listing = fund.listing() | |
| st.subheader("💰 Danh sách quỹ mở") | |
| st.dataframe(fund_listing) | |
| elif market_data_type == "Tỷ giá ngoại tệ": | |
| exchange_date = st.date_input("Chọn ngày xem tỷ giá:", pd.to_datetime("2025-04-17"), key="exchange_date") | |
| exchange_rate = vcb_exchange_rate(date=exchange_date.strftime("%Y-%m-%d")) | |
| st.subheader("💱 Tỷ giá ngoại tệ VCB") | |
| st.dataframe(exchange_rate) | |
| elif market_data_type == "Giá vàng": | |
| gold_price = sjc_gold_price() | |
| st.subheader("🥇 Giá vàng SJC") | |
| st.dataframe(gold_price) | |
| elif market_data_type == "Thị trường quốc tế": | |
| # Cho phép người dùng chọn cặp tiền tệ | |
| fx_symbol = st.selectbox( | |
| "Chọn cặp tiền tệ:", | |
| ["JPYVND", "USDVND", "EURVND", "GBPVND"] | |
| ) | |
| fx = Vnstock().fx(symbol=fx_symbol, source='MSN') | |
| fx_data = fx.quote.history( | |
| start=start_date.strftime("%Y-%m-%d"), | |
| end=end_date.strftime("%Y-%m-%d"), | |
| interval='1D' | |
| ) | |
| st.subheader(f"💱 Dữ liệu tỷ giá {fx_symbol}") | |
| st.dataframe(fx_data) | |
| # Biểu đồ tỷ giá | |
| st.subheader(f"📈 Biểu đồ tỷ giá {fx_symbol}") | |
| fig_fx = go.Figure() | |
| fig_fx.add_trace(go.Scatter(x=fx_data['time'], y=fx_data['close'], name='Tỷ giá')) | |
| st.plotly_chart(fig_fx, use_container_width=True) | |
| except Exception as e: | |
| st.error(f"❌ Lỗi khi lấy dữ liệu thị trường: {e}") | |
| st.info("Thử kiểm tra kết nối mạng hoặc tính khả dụng của dịch vụ.") | |
| # Thêm thông tin về các nguồn dữ liệu | |
| st.sidebar.markdown("## Thông tin nguồn dữ liệu") | |
| st.sidebar.markdown("- **VCI**: Vietcap Securities") | |
| st.sidebar.markdown("- **TCBS**: Techcombank Securities") | |
| st.sidebar.markdown("- **SSI**: SSI Securities") | |
| st.sidebar.markdown("- **DNSE**: Dragon Capital Securities") | |
| st.sidebar.markdown("- **VPS**: VPS Securities") | |
| # Thêm thông tin về ứng dụng | |
| st.sidebar.markdown("## Về ứng dụng") | |
| st.sidebar.markdown("Ứng dụng này sử dụng thư viện vnstock3 để truy xuất và phân tích dữ liệu chứng khoán Việt Nam.") | |
| # Thêm phần xuất dữ liệu | |
| st.sidebar.markdown("## Xuất dữ liệu") | |
| st.sidebar.markdown("Tất cả dữ liệu từ vnstock3 được trả về dưới dạng Pandas DataFrame hoặc Series.") | |
| st.sidebar.markdown("Bạn có thể xuất dữ liệu ra Excel hoặc CSV bằng cách sử dụng các phương thức sau:") | |
| st.sidebar.code("# Xuất dữ liệu ra Excel\ndf.to_excel('data.xlsx', index=False)\n# Xuất dữ liệu ra CSV\ndf.to_csv('data.csv', index=False)") | |