stocksvnv2 / app.py
NguyNhu's picture
Update app.py
d9c3ec9 verified
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)")