Xây dựng bảng điều khiển theo dõi thị trường với Dash và Plotly

2024-03-12 — QuantTrade

Xây dựng bảng điều khiển theo dõi thị trường với Dash và Plotly

Giới thiệu

Trong thời đại số hóa, việc theo dõi và phân tích thị trường tài chính một cách hiệu quả đã trở thành nhu cầu thiết yếu đối với các nhà đầu tư và giao dịch viên. Một bảng điều khiển (dashboard) thị trường tương tác không chỉ giúp trực quan hóa dữ liệu mà còn cho phép người dùng tương tác và phân tích thông tin một cách nhanh chóng và hiệu quả.

Dash và Plotly là hai công cụ mạnh mẽ trong hệ sinh thái Python, đặc biệt phù hợp để xây dựng các ứng dụng web trực quan hóa dữ liệu tương tác mà không cần kiến thức sâu về phát triển web. Bài viết này sẽ hướng dẫn bạn cách xây dựng một bảng điều khiển theo dõi thị trường tài chính hoàn chỉnh sử dụng Dash và Plotly, từ thiết kế ban đầu đến triển khai ứng dụng thực tế.

I. Tổng quan về Dash và Plotly

Dash là gì?

Dash là một framework Python mã nguồn mở được phát triển bởi Plotly, cho phép xây dựng các ứng dụng web phân tích dữ liệu tương tác mà không cần kiến thức JavaScript. Dash được xây dựng trên nền tảng Flask, Plotly.js và React.js, kết hợp sức mạnh của các công nghệ này để tạo ra một công cụ mạnh mẽ nhưng đơn giản để sử dụng.

Plotly là gì?

Plotly là thư viện trực quan hóa dữ liệu cung cấp các biểu đồ tương tác chất lượng cao. Plotly.py là phiên bản Python của Plotly, cho phép tạo ra các biểu đồ đẹp mắt và tương tác trong Python.

Lợi ích của việc sử dụng Dash và Plotly

  1. Tương tác mạnh mẽ: Người dùng có thể tương tác với dữ liệu thông qua các thành phần như dropdown, slider, checkbox.
  2. Đa dạng biểu đồ: Plotly hỗ trợ hơn 40 loại biểu đồ khác nhau, từ đơn giản đến phức tạp.
  3. Phát triển nhanh chóng: Xây dựng ứng dụng web chỉ với Python, không cần HTML, CSS, JavaScript.
  4. Khả năng mở rộng: Dễ dàng nâng cấp từ nguyên mẫu (prototype) thành ứng dụng cấp sản phẩm.
  5. Tích hợp tốt: Hoạt động liền mạch với các thư viện phân tích dữ liệu Python khác như Pandas, NumPy, scikit-learn.

II. Cài đặt môi trường và các công cụ cần thiết

Cài đặt thư viện

pip install dash dash-bootstrap-components plotly pandas yfinance ta

Các thư viện chính và chức năng:

  • dash: Framework chính để xây dựng ứng dụng web
  • dash-bootstrap-components: Thành phần Bootstrap cho Dash
  • plotly: Thư viện trực quan hóa
  • pandas: Xử lý và phân tích dữ liệu
  • yfinance: Lấy dữ liệu thị trường từ Yahoo Finance
  • ta: Tính toán các chỉ báo phân tích kỹ thuật

III. Thu thập và chuẩn bị dữ liệu

1. Thu thập dữ liệu thị trường

import yfinance as yf
import pandas as pd

def get_stock_data(ticker, period="1y", interval="1d"):
    """
    Lấy dữ liệu cổ phiếu từ Yahoo Finance
    
    Parameters:
    -----------
    ticker : str
        Mã cổ phiếu
    period : str
        Khoảng thời gian (1d, 5d, 1mo, 3mo, 6mo, 1y, 2y, 5y, 10y, ytd, max)
    interval : str
        Khoảng thời gian giữa các điểm dữ liệu (1m, 2m, 5m, 15m, 30m, 60m, 90m, 1h, 1d, 5d, 1wk, 1mo, 3mo)
    
    Returns:
    --------
    pd.DataFrame
        DataFrame chứa dữ liệu OHLCV
    """
    data = yf.download(ticker, period=period, interval=interval)
    return data

# Ví dụ: Lấy dữ liệu của Apple
aapl_data = get_stock_data("AAPL", period="1y")

2. Tính toán chỉ báo kỹ thuật

import ta

def add_technical_indicators(df):
    """
    Thêm các chỉ báo kỹ thuật vào DataFrame
    
    Parameters:
    -----------
    df : pd.DataFrame
        DataFrame chứa dữ liệu OHLCV
    
    Returns:
    --------
    pd.DataFrame
        DataFrame với các chỉ báo kỹ thuật đã được thêm vào
    """
    # Tạo bản sao để không thay đổi dữ liệu gốc
    df_with_indicators = df.copy()
    
    # Thêm chỉ báo xu hướng
    df_with_indicators['SMA20'] = ta.trend.sma_indicator(df['Close'], window=20)
    df_with_indicators['SMA50'] = ta.trend.sma_indicator(df['Close'], window=50)
    df_with_indicators['SMA200'] = ta.trend.sma_indicator(df['Close'], window=200)
    df_with_indicators['EMA20'] = ta.trend.ema_indicator(df['Close'], window=20)
    
    # Thêm chỉ báo khối lượng
    df_with_indicators['VWAP'] = ta.volume.volume_weighted_average_price(
        df['High'], df['Low'], df['Close'], df['Volume']
    )
    
    # Thêm chỉ báo dao động
    df_with_indicators['RSI'] = ta.momentum.rsi(df['Close'], window=14)
    df_with_indicators['MACD'] = ta.trend.macd(df['Close'])
    df_with_indicators['MACD_Signal'] = ta.trend.macd_signal(df['Close'])
    df_with_indicators['MACD_Diff'] = ta.trend.macd_diff(df['Close'])
    
    # Thêm Bollinger Bands
    bollinger = ta.volatility.BollingerBands(df['Close'], window=20, window_dev=2)
    df_with_indicators['BB_Upper'] = bollinger.bollinger_hband()
    df_with_indicators['BB_Lower'] = bollinger.bollinger_lband()
    df_with_indicators['BB_MA'] = bollinger.bollinger_mavg()
    df_with_indicators['BB_Width'] = bollinger.bollinger_wband()
    
    return df_with_indicators

# Thêm chỉ báo vào dữ liệu Apple
aapl_with_indicators = add_technical_indicators(aapl_data)

3. Lấy dữ liệu từ nhiều mã cổ phiếu

def get_multiple_stocks(tickers, period="1y"):
    """
    Lấy dữ liệu từ nhiều mã cổ phiếu
    
    Parameters:
    -----------
    tickers : list
        Danh sách các mã cổ phiếu
    period : str
        Khoảng thời gian
    
    Returns:
    --------
    dict
        Dictionary chứa DataFrame của từng mã
    """
    data = {}
    for ticker in tickers:
        data[ticker] = get_stock_data(ticker, period=period)
    
    return data

# Ví dụ: Lấy dữ liệu của các công ty công nghệ lớn
tech_tickers = ["AAPL", "MSFT", "GOOGL", "AMZN", "META"]
tech_stocks = get_multiple_stocks(tech_tickers)

IV. Thiết kế cấu trúc bảng điều khiển

Một bảng điều khiển thị trường hiệu quả cần bao gồm các thành phần sau:

1. Layout tổng thể

import dash
from dash import html, dcc
import dash_bootstrap_components as dbc

# Khởi tạo ứng dụng Dash
app = dash.Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP])

# Tạo layout cho ứng dụng
app.layout = dbc.Container([
    # Header
    dbc.Row([
        dbc.Col(html.H1("Bảng điều khiển theo dõi thị trường", className="text-center p-4"), width=12)
    ]),
    
    # Bộ lọc và điều khiển
    dbc.Row([
        dbc.Col([
            html.Label("Chọn mã chứng khoán:"),
            dcc.Dropdown(
                id="ticker-dropdown",
                options=[{"label": ticker, "value": ticker} for ticker in tech_tickers],
                value="AAPL"
            )
        ], width=3),
        
        dbc.Col([
            html.Label("Chọn khoảng thời gian:"),
            dcc.Dropdown(
                id="timeframe-dropdown",
                options=[
                    {"label": "1 tháng", "value": "1mo"},
                    {"label": "3 tháng", "value": "3mo"},
                    {"label": "6 tháng", "value": "6mo"},
                    {"label": "1 năm", "value": "1y"},
                    {"label": "2 năm", "value": "2y"},
                    {"label": "5 năm", "value": "5y"},
                ],
                value="1y"
            )
        ], width=3),
        
        dbc.Col([
            html.Label("Chọn loại biểu đồ:"),
            dcc.Dropdown(
                id="chart-type-dropdown",
                options=[
                    {"label": "Candlestick", "value": "candlestick"},
                    {"label": "Line", "value": "line"},
                    {"label": "OHLC", "value": "ohlc"}
                ],
                value="candlestick"
            )
        ], width=3),
        
        dbc.Col([
            html.Label("Chọn chỉ báo kỹ thuật:"),
            dcc.Dropdown(
                id="indicator-dropdown",
                options=[
                    {"label": "Bollinger Bands", "value": "bollinger"},
                    {"label": "Moving Averages", "value": "ma"},
                    {"label": "RSI", "value": "rsi"},
                    {"label": "MACD", "value": "macd"},
                    {"label": "Tất cả", "value": "all"},
                    {"label": "Không", "value": "none"},
                ],
                value="ma",
                multi=True
            )
        ], width=3),
    ], className="mb-4"),
    
    # Biểu đồ giá và khối lượng chính
    dbc.Row([
        dbc.Col([
            html.H4("Biểu đồ giá", className="text-center"),
            dcc.Graph(id="main-chart")
        ], width=12)
    ], className="mb-4"),
    
    # Bảng điều khiển chỉ báo kỹ thuật
    dbc.Row([
        dbc.Col([
            html.H4("Chỉ báo kỹ thuật", className="text-center"),
            dcc.Graph(id="technical-indicators")
        ], width=12)
    ], className="mb-4"),
    
    # Bảng tổng quan thị trường và so sánh ngành
    dbc.Row([
        dbc.Col([
            html.H4("Tổng quan thị trường", className="text-center"),
            dcc.Graph(id="market-overview")
        ], width=6),
        
        dbc.Col([
            html.H4("So sánh ngành", className="text-center"),
            dcc.Graph(id="sector-comparison")
        ], width=6)
    ], className="mb-4"),
    
    # Bảng thông tin chi tiết và tin tức
    dbc.Row([
        dbc.Col([
            html.H4("Thông tin chi tiết", className="text-center"),
            html.Div(id="stock-info")
        ], width=6),
        
        dbc.Col([
            html.H4("Tin tức thị trường", className="text-center"),
            html.Div(id="market-news")
        ], width=6)
    ])
], fluid=True)

2. Tạo callback cho biểu đồ chính

from dash.dependencies import Input, Output
import plotly.graph_objects as go
from plotly.subplots import make_subplots

@app.callback(
    Output("main-chart", "figure"),
    [
        Input("ticker-dropdown", "value"),
        Input("timeframe-dropdown", "value"),
        Input("chart-type-dropdown", "value"),
        Input("indicator-dropdown", "value")
    ]
)
def update_main_chart(ticker, timeframe, chart_type, indicators):
    # Lấy dữ liệu mới
    df = get_stock_data(ticker, period=timeframe)
    df = add_technical_indicators(df)
    
    # Tạo subplot với biểu đồ chính và biểu đồ khối lượng
    fig = make_subplots(rows=2, cols=1, shared_xaxes=True, 
                        vertical_spacing=0.1, row_heights=[0.7, 0.3],
                        subplot_titles=(f"{ticker} Stock Price", "Volume"))
    
    # Tạo biểu đồ theo loại đã chọn
    if chart_type == "candlestick":
        fig.add_trace(
            go.Candlestick(
                x=df.index, open=df['Open'], high=df['High'],
                low=df['Low'], close=df['Close'],
                name="OHLC"
            ),
            row=1, col=1
        )
    elif chart_type == "line":
        fig.add_trace(
            go.Scatter(x=df.index, y=df['Close'], mode='lines', name="Close Price"),
            row=1, col=1
        )
    elif chart_type == "ohlc":
        fig.add_trace(
            go.Ohlc(
                x=df.index, open=df['Open'], high=df['High'],
                low=df['Low'], close=df['Close'],
                name="OHLC"
            ),
            row=1, col=1
        )
    
    # Thêm các chỉ báo kỹ thuật đã chọn
    if 'ma' in indicators or 'all' in indicators:
        fig.add_trace(
            go.Scatter(x=df.index, y=df['SMA20'], mode='lines', name="SMA 20", line=dict(width=1, color='blue')),
            row=1, col=1
        )
        fig.add_trace(
            go.Scatter(x=df.index, y=df['SMA50'], mode='lines', name="SMA 50", line=dict(width=1, color='orange')),
            row=1, col=1
        )
        fig.add_trace(
            go.Scatter(x=df.index, y=df['SMA200'], mode='lines', name="SMA 200", line=dict(width=1, color='red')),
            row=1, col=1
        )
    
    if 'bollinger' in indicators or 'all' in indicators:
        fig.add_trace(
            go.Scatter(x=df.index, y=df['BB_Upper'], mode='lines', name="BB Upper", line=dict(width=1, color='rgba(0,128,0,0.3)')),
            row=1, col=1
        )
        fig.add_trace(
            go.Scatter(x=df.index, y=df['BB_Lower'], mode='lines', name="BB Lower", line=dict(width=1, color='rgba(0,128,0,0.3)'),
                      fill='tonexty', fillcolor='rgba(0,128,0,0.1)'),
            row=1, col=1
        )
    
    # Thêm biểu đồ khối lượng
    fig.add_trace(
        go.Bar(x=df.index, y=df['Volume'], name="Volume", marker_color='rgba(0,0,128,0.5)'),
        row=2, col=1
    )
    
    # Cập nhật layout
    fig.update_layout(
        title=f"{ticker} Stock Analysis",
        xaxis_title="Ngày",
        yaxis_title="Giá ($)",
        height=800,
        hovermode='x unified',
        xaxis_rangeslider_visible=False
    )
    
    return fig

3. Tạo callback cho biểu đồ chỉ báo kỹ thuật

@app.callback(
    Output("technical-indicators", "figure"),
    [
        Input("ticker-dropdown", "value"),
        Input("timeframe-dropdown", "value"),
        Input("indicator-dropdown", "value")
    ]
)
def update_technical_indicators(ticker, timeframe, indicators):
    # Lấy dữ liệu mới
    df = get_stock_data(ticker, period=timeframe)
    df = add_technical_indicators(df)
    
    # Xác định số lượng subplot cần thiết
    num_subplots = sum([
        'rsi' in indicators or 'all' in indicators,
        'macd' in indicators or 'all' in indicators
    ])
    
    if num_subplots == 0:
        # Nếu không có chỉ báo nào được chọn, trả về biểu đồ trống
        fig = go.Figure()
        fig.update_layout(
            title="Không có chỉ báo kỹ thuật nào được chọn",
            height=300
        )
        return fig
    
    # Tạo subplots cho các chỉ báo
    fig = make_subplots(rows=num_subplots, cols=1, shared_xaxes=True,
                       vertical_spacing=0.1, subplot_titles=[])
    
    # Biến lưu trữ vị trí subplot hiện tại
    current_row = 1
    
    # Thêm RSI
    if 'rsi' in indicators or 'all' in indicators:
        fig.add_trace(
            go.Scatter(x=df.index, y=df['RSI'], mode='lines', name="RSI", line=dict(color='purple')),
            row=current_row, col=1
        )
        fig.add_hline(y=70, line_dash="dot", line_color="red", row=current_row, col=1)
        fig.add_hline(y=30, line_dash="dot", line_color="green", row=current_row, col=1)
        fig.update_yaxes(title_text="RSI", row=current_row, col=1)
        current_row += 1
    
    # Thêm MACD
    if 'macd' in indicators or 'all' in indicators:
        fig.add_trace(
            go.Scatter(x=df.index, y=df['MACD'], mode='lines', name="MACD", line=dict(color='blue')),
            row=current_row, col=1
        )
        fig.add_trace(
            go.Scatter(x=df.index, y=df['MACD_Signal'], mode='lines', name="Signal", line=dict(color='red')),
            row=current_row, col=1
        )
        fig.add_trace(
            go.Bar(x=df.index, y=df['MACD_Diff'], name="Histogram", 
                  marker_color=np.where(df['MACD_Diff'] >= 0, 'rgba(0,128,0,0.5)', 'rgba(128,0,0,0.5)')),
            row=current_row, col=1
        )
        fig.update_yaxes(title_text="MACD", row=current_row, col=1)
    
    # Cập nhật layout
    fig.update_layout(
        title=f"Chỉ báo kỹ thuật - {ticker}",
        height=300 * num_subplots,
        hovermode='x unified',
        showlegend=True
    )
    
    return fig

4. Tạo callback cho tổng quan thị trường

@app.callback(
    Output("market-overview", "figure"),
    [Input("timeframe-dropdown", "value")]
)
def update_market_overview(timeframe):
    # Lấy dữ liệu chỉ số thị trường
    indices = ["^GSPC", "^DJI", "^IXIC", "^RUT"]  # S&P 500, Dow Jones, Nasdaq, Russell 2000
    index_names = ["S&P 500", "Dow Jones", "Nasdaq", "Russell 2000"]
    
    data = get_multiple_stocks(indices, period=timeframe)
    
    # Tính toán % thay đổi so với đầu kỳ
    fig = go.Figure()
    
    for ticker, name in zip(indices, index_names):
        df = data[ticker]
        normalized = df['Close'] / df['Close'].iloc[0] * 100 - 100  # Phần trăm thay đổi
        fig.add_trace(
            go.Scatter(x=df.index, y=normalized, mode='lines', name=name)
        )
    
    # Cập nhật layout
    fig.update_layout(
        title="Hiệu suất chỉ số thị trường (% thay đổi)",
        xaxis_title="Ngày",
        yaxis_title="% Thay đổi",
        hovermode='x unified'
    )
    
    return fig

5. Tạo callback cho so sánh ngành

@app.callback(
    Output("sector-comparison", "figure"),
    [Input("timeframe-dropdown", "value")]
)
def update_sector_comparison(timeframe):
    # Lấy dữ liệu ETF ngành
    sector_etfs = ["XLK", "XLF", "XLE", "XLV", "XLY", "XLP", "XLI", "XLB", "XLU", "XLRE"]
    sector_names = ["Công nghệ", "Tài chính", "Năng lượng", "Y tế", "Tiêu dùng", 
                  "Hàng thiết yếu", "Công nghiệp", "Vật liệu", "Tiện ích", "Bất động sản"]
    
    data = get_multiple_stocks(sector_etfs, period=timeframe)
    
    # Tính hiệu suất cuối kỳ của từng ngành
    performance = []
    for ticker, name in zip(sector_etfs, sector_names):
        df = data[ticker]
        perf = (df['Close'].iloc[-1] / df['Close'].iloc[0] - 1) * 100  # Phần trăm thay đổi
        performance.append({"Sector": name, "Performance": perf})
    
    performance_df = pd.DataFrame(performance)
    performance_df = performance_df.sort_values("Performance", ascending=False)
    
    # Tạo biểu đồ cột
    fig = go.Figure()
    fig.add_trace(
        go.Bar(
            x=performance_df["Sector"],
            y=performance_df["Performance"],
            marker_color=np.where(performance_df["Performance"] >= 0, 'green', 'red')
        )
    )
    
    # Cập nhật layout
    fig.update_layout(
        title=f"Hiệu suất các ngành (% thay đổi trong {timeframe})",
        xaxis_title="Ngành",
        yaxis_title="% Thay đổi",
        hovermode='closest'
    )
    
    return fig

6. Tạo callback cho thông tin chi tiết

@app.callback(
    Output("stock-info", "children"),
    [Input("ticker-dropdown", "value")]
)
def update_stock_info(ticker):
    # Lấy thông tin cơ bản về cổ phiếu
    stock = yf.Ticker(ticker)
    info = stock.info
    
    # Các thông tin cần hiển thị
    try:
        company_name = info.get('longName', 'N/A')
        sector = info.get('sector', 'N/A')
        industry = info.get('industry', 'N/A')
        market_cap = info.get('marketCap', 'N/A')
        if market_cap != 'N/A':
            market_cap = f"${market_cap/1000000000:.2f}B"
        pe_ratio = info.get('trailingPE', 'N/A')
        dividend_yield = info.get('dividendYield', 'N/A')
        if dividend_yield != 'N/A':
            dividend_yield = f"{dividend_yield*100:.2f}%"
        fiftytwo_week_high = info.get('fiftyTwoWeekHigh', 'N/A')
        fiftytwo_week_low = info.get('fiftyTwoWeekLow', 'N/A')
        
        # Tạo bảng thông tin
        info_table = dbc.Table([
            html.Thead([
                html.Tr([html.Th("Thông tin", colSpan=2)])
            ]),
            html.Tbody([
                html.Tr([html.Td("Tên công ty"), html.Td(company_name)]),
                html.Tr([html.Td("Ngành"), html.Td(sector)]),
                html.Tr([html.Td("Lĩnh vực"), html.Td(industry)]),
                html.Tr([html.Td("Vốn hóa"), html.Td(market_cap)]),
                html.Tr([html.Td("P/E"), html.Td(pe_ratio)]),
                html.Tr([html.Td("Tỷ lệ cổ tức"), html.Td(dividend_yield)]),
                html.Tr([html.Td("52-tuần cao"), html.Td(fiftytwo_week_high)]),
                html.Tr([html.Td("52-tuần thấp"), html.Td(fiftytwo_week_low)]),
            ])
        ], bordered=True, hover=True, striped=True, responsive=True)
        
        return info_table
    except:
        return html.Div("Không thể lấy thông tin cho mã chứng khoán này")

7. Tạo callback cho tin tức thị trường

# Giả lập dữ liệu tin tức (trong thực tế bạn có thể sử dụng API tin tức)
def get_mock_news(ticker):
    """
    Lấy tin tức giả lập về mã chứng khoán
    """
    current_date = pd.Timestamp.now()
    
    mock_news = [
        {
            "title": f"Phân tích kỹ thuật: {ticker} có thể sẽ kiểm tra lại vùng hỗ trợ",
            "date": (current_date - pd.Timedelta(days=1)).strftime("%Y-%m-%d"),
            "source": "MarketWatch",
            "url": "#"
        },
        {
            "title": f"{ticker} công bố kết quả kinh doanh quý vượt dự báo",
            "date": (current_date - pd.Timedelta(days=3)).strftime("%Y-%m-%d"),
            "source": "Bloomberg",
            "url": "#"
        },
        {
            "title": f"Nhà phân tích nâng dự báo giá mục tiêu cho {ticker}",
            "date": (current_date - pd.Timedelta(days=5)).strftime("%Y-%m-%d"),
            "source": "CNBC",
            "url": "#"
        },
        {
            "title": f"{ticker} thông báo kế hoạch mở rộng thị trường mới",
            "date": (current_date - pd.Timedelta(days=7)).strftime("%Y-%m-%d"),
            "source": "Reuters",
            "url": "#"
        },
        {
            "title": f"Báo cáo ngành: Triển vọng cho các công ty như {ticker}",
            "date": (current_date - pd.Timedelta(days=10)).strftime("%Y-%m-%d"),
            "source": "Financial Times",
            "url": "#"
        }
    ]
    
    return mock_news

@app.callback(
    Output("market-news", "children"),
    [Input("ticker-dropdown", "value")]
)
def update_market_news(ticker):
    # Lấy tin tức (trong thực tế bạn sẽ sử dụng API tin tức thực)
    news = get_mock_news(ticker)
    
    # Tạo danh sách tin tức
    news_items = []
    for item in news:
        news_card = dbc.Card(
            dbc.CardBody([
                html.H5(item["title"], className="card-title"),
                html.P(f"{item['date']} - {item['source']}", className="card-text text-muted"),
                dbc.Button("Đọc thêm", color="primary", size="sm", href=item["url"])
            ]),
            className="mb-3"
        )
        news_items.append(news_card)
    
    return html.Div(news_items)

V. Thêm tính năng nâng cao

1. Chế độ giao dịch thời gian thực

import time
from datetime import datetime, timedelta
from threading import Thread

# Khởi tạo dữ liệu toàn cục và biến theo dõi
global_data = {}
is_streaming = False

def stream_market_data(ticker, interval=60):
    """
    Cập nhật dữ liệu thị trường trong thời gian thực
    """
    global global_data, is_streaming
    is_streaming = True
    
    while is_streaming:
        # Kiểm tra xem thị trường có mở cửa không
        now = datetime.now()
        market_open = now.replace(hour=9, minute=30, second=0, microsecond=0)
        market_close = now.replace(hour=16, minute=0, second=0, microsecond=0)
        
        # Chỉ cập nhật khi thị trường mở cửa (giờ New York)
        if market_open <= now <= market_close and now.weekday() < 5:
            # Lấy dữ liệu mới nhất
            end_time = now
            start_time = end_time - timedelta(days=1)
            try:
                data = yf.download(ticker, start=start_time, end=end_time, interval="1m")
                if not data.empty:
                    global_data[ticker] = data
                    print(f"Đã cập nhật dữ liệu cho {ticker} lúc {now}")
            except Exception as e:
                print(f"Lỗi khi cập nhật dữ liệu: {e}")
        
        # Chờ đến lần cập nhật tiếp theo
        time.sleep(interval)

# Thêm nút điều khiển chế độ thời gian thực
real_time_controls = dbc.Row([
    dbc.Col([
        dbc.Button("Bắt đầu theo dõi thời gian thực", id="start-streaming-button", color="success", className="mr-2"),
        dbc.Button("Dừng theo dõi", id="stop-streaming-button", color="danger", className="mr-2"),
        html.Span(id="streaming-status")
    ], width=12)
], className="mb-4")

# Thêm vào layout
app.layout.children.insert(3, real_time_controls)

# Thêm callbacks
@app.callback(
    Output("streaming-status", "children"),
    [
        Input("start-streaming-button", "n_clicks"),
        Input("stop-streaming-button", "n_clicks")
    ],
    prevent_initial_call=True
)
def toggle_streaming(start_clicks, stop_clicks):
    global is_streaming
    ctx = dash.callback_context
    
    if not ctx.triggered:
        return "Không theo dõi thời gian thực"
    
    button_id = ctx.triggered[0]['prop_id'].split('.')[0]
    
    if button_id == "start-streaming-button":
        if not is_streaming:
            ticker = "AAPL"  # Mã mặc định hoặc lấy từ dropdown
            thread = Thread(target=stream_market_data, args=(ticker,))
            thread.daemon = True
            thread.start()
        return "Đang theo dõi thời gian thực..."
    elif button_id == "stop-streaming-button":
        is_streaming = False
        return "Đã dừng theo dõi"

2. Cảnh báo kỹ thuật

def generate_technical_alerts(df, ticker):
    """
    Tạo cảnh báo dựa trên các chỉ báo kỹ thuật
    """
    alerts = []
    latest = df.iloc[-1]
    prev = df.iloc[-2]
    
    # Kiểm tra MA Cross
    if prev['SMA20'] <= prev['SMA50'] and latest['SMA20'] > latest['SMA50']:
        alerts.append({
            "type": "Golden Cross",
            "message": f"Golden Cross cho {ticker} - MA20 đã cắt lên trên MA50",
            "severity": "positive"
        })
    
    if prev['SMA20'] >= prev['SMA50'] and latest['SMA20'] < latest['SMA50']:
        alerts.append({
            "type": "Death Cross",
            "message": f"Death Cross cho {ticker} - MA20 đã cắt xuống dưới MA50",
            "severity": "negative"
        })
    
    # Kiểm tra RSI
    if latest['RSI'] < 30:
        alerts.append({
            "type": "RSI Oversold",
            "message": f"RSI cho {ticker} trong vùng quá bán ({latest['RSI']:.2f})",
            "severity": "positive"
        })
        
    if latest['RSI'] > 70:
        alerts.append({
            "type": "RSI Overbought",
            "message": f"RSI cho {ticker} trong vùng quá mua ({latest['RSI']:.2f})",
            "severity": "negative"
        })
    
    # Kiểm tra MACD
    if prev['MACD'] <= prev['MACD_Signal'] and latest['MACD'] > latest['MACD_Signal']:
        alerts.append({
            "type": "MACD Cross",
            "message": f"MACD cho {ticker} đã cắt lên trên đường Signal",
            "severity": "positive"
        })
        
    if prev['MACD'] >= prev['MACD_Signal'] and latest['MACD'] < latest['MACD_Signal']:
        alerts.append({
            "type": "MACD Cross",
            "message": f"MACD cho {ticker} đã cắt xuống dưới đường Signal",
            "severity": "negative"
        })
    
    # Kiểm tra Bollinger Bands
    if latest['Close'] < latest['BB_Lower']:
        alerts.append({
            "type": "Bollinger Bands",
            "message": f"Giá {ticker} đang ở dưới dải dưới Bollinger Bands",
            "severity": "positive"
        })
        
    if latest['Close'] > latest['BB_Upper']:
        alerts.append({
            "type": "Bollinger Bands",
            "message": f"Giá {ticker} đang ở trên dải trên Bollinger Bands",
            "severity": "negative"
        })
    
    return alerts

# Thêm phần cảnh báo vào layout
alerts_section = dbc.Row([
    dbc.Col([
        html.H4("Cảnh báo kỹ thuật", className="text-center"),
        html.Div(id="technical-alerts")
    ], width=12)
], className="mb-4")

app.layout.children.insert(5, alerts_section)

# Thêm callback
@app.callback(
    Output("technical-alerts", "children"),
    [
        Input("ticker-dropdown", "value"),
        Input("timeframe-dropdown", "value"),
    ]
)
def update_alerts(ticker, timeframe):
    # Lấy dữ liệu
    df = get_stock_data(ticker, period=timeframe)
    df = add_technical_indicators(df)
    
    # Tạo cảnh báo
    alerts = generate_technical_alerts(df, ticker)
    
    if not alerts:
        return html.Div("Không có cảnh báo kỹ thuật nào", className="text-center")
    
    # Tạo danh sách cảnh báo
    alert_items = []
    for alert in alerts:
        color = "success" if alert["severity"] == "positive" else "danger"
        alert_card = dbc.Alert(
            [
                html.H5(alert["type"], className="alert-heading"),
                html.P(alert["message"])
            ],
            color=color,
            className="mb-2"
        )
        alert_items.append(alert_card)
    
    return html.Div(alert_items)

3. Chức năng sàng lọc cổ phiếu

# Thêm tab sàng lọc cổ phiếu
screener_tab = dbc.Tab([
    dbc.Row([
        dbc.Col([
            html.H4("Thiết lập tiêu chí sàng lọc", className="mb-4"),
            
            # Tiêu chí về giá
            html.H5("Giá"),
            dbc.Row([
                dbc.Col(
                    dbc.Input(id="min-price", type="number", placeholder="Giá tối thiểu ($)")
                ),
                dbc.Col(
                    dbc.Input(id="max-price", type="number", placeholder="Giá tối đa ($)")
                )
            ], className="mb-3"),
            
            # Tiêu chí về vốn hóa
            html.H5("Vốn hóa thị trường"),
            dcc.Dropdown(
                id="market-cap-filter",
                options=[
                    {"label": "Mega (>$200B)", "value": "mega"},
                    {"label": "Large ($10B-$200B)", "value": "large"},
                    {"label": "Mid ($2B-$10B)", "value": "mid"},
                    {"label": "Small ($300M-$2B)", "value": "small"},
                    {"label": "Micro (<$300M)", "value": "micro"}
                ],
                multi=True
            ),
            
            # Tiêu chí về ngành
            html.H5("Ngành", className="mt-3"),
            dcc.Dropdown(
                id="sector-filter",
                options=[
                    {"label": "Công nghệ", "value": "Technology"},
                    {"label": "Tài chính", "value": "Financial"},
                    {"label": "Y tế", "value": "Healthcare"},
                    {"label": "Tiêu dùng", "value": "Consumer Cyclical"},
                    {"label": "Bất động sản", "value": "Real Estate"},
                    {"label": "Năng lượng", "value": "Energy"},
                    {"label": "Vật liệu", "value": "Basic Materials"}
                ],
                multi=True
            ),
            
            # Tiêu chí về chỉ báo kỹ thuật
            html.H5("Chỉ báo kỹ thuật", className="mt-3"),
            dcc.Checklist(
                id="technical-filter",
                options=[
                    {"label": "Giá trên MA200", "value": "price_above_ma200"},
                    {"label": "Giá dưới MA200", "value": "price_below_ma200"},
                    {"label": "Golden Cross (MA50 > MA200)", "value": "golden_cross"},
                    {"label": "Death Cross (MA50 < MA200)", "value": "death_cross"},
                    {"label": "RSI < 30 (Quá bán)", "value": "rsi_oversold"},
                    {"label": "RSI > 70 (Quá mua)", "value": "rsi_overbought"}
                ],
                value=[]
            ),
            
            # Nút sàng lọc
            dbc.Button("Sàng lọc", id="run-screener", color="primary", className="mt-4")
        ], width=4),
        
        dbc.Col([
            html.H4("Kết quả sàng lọc", className="mb-4"),
            html.Div(id="screener-results")
        ], width=8)
    ])
], label="Sàng lọc cổ phiếu")

# Thêm tab vào layout
app.layout = dbc.Container([
    # Header
    dbc.Row([
        dbc.Col(html.H1("Bảng điều khiển theo dõi thị trường", className="text-center p-4"), width=12)
    ]),
    
    # Tabs
    dbc.Tabs([
        dbc.Tab([
            # Nội dung tab Dashboard hiện tại
            # (Đặt phần thân của layout hiện tại vào đây)
        ], label="Dashboard"),
        
        screener_tab
    ])
])

# Callback cho sàng lọc cổ phiếu
@app.callback(
    Output("screener-results", "children"),
    [Input("run-screener", "n_clicks")],
    [
        State("min-price", "value"),
        State("max-price", "value"),
        State("market-cap-filter", "value"),
        State("sector-filter", "value"),
        State("technical-filter", "value")
    ],
    prevent_initial_call=True
)
def run_stock_screener(n_clicks, min_price, max_price, market_caps, sectors, technical):
    if n_clicks is None:
        return html.Div("Chọn các tiêu chí và nhấn 'Sàng lọc' để xem kết quả")
    
    # Danh sách các cổ phiếu để sàng lọc (trong thực tế sẽ rộng hơn nhiều)
    all_stocks = ["AAPL", "MSFT", "GOOGL", "AMZN", "META", "TSLA", "JPM", "V", "JNJ", "PG", 
                 "HD", "MA", "UNH", "BAC", "NVDA", "DIS", "ADBE", "CRM", "NFLX", "INTC"]
    
    # Giả lập kết quả sàng lọc (trong thực tế sẽ lấy dữ liệu thực và áp dụng các bộ lọc)
    filtered_stocks = []
    for ticker in all_stocks:
        # Trong thực tế, đây là nơi áp dụng các bộ lọc dựa trên tiêu chí
        # Ở đây chúng ta giả lập kết quả
        stock = yf.Ticker(ticker)
        try:
            info = stock.info
            current_price = info.get('currentPrice', 0)
            market_cap = info.get('marketCap', 0)
            sector = info.get('sector', 'Unknown')
            
            # Áp dụng bộ lọc giá
            if min_price and current_price < min_price:
                continue
            if max_price and current_price > max_price:
                continue
                
            # Áp dụng bộ lọc vốn hóa
            if market_caps:
                market_cap_cat = ""
                if market_cap > 200e9:
                    market_cap_cat = "mega"
                elif market_cap > 10e9:
                    market_cap_cat = "large"
                elif market_cap > 2e9:
                    market_cap_cat = "mid"
                elif market_cap > 300e6:
                    market_cap_cat = "small"
                else:
                    market_cap_cat = "micro"
                
                if market_cap_cat not in market_caps:
                    continue
            
            # Áp dụng bộ lọc ngành
            if sectors and sector not in sectors:
                continue
            
            # Áp dụng bộ lọc kỹ thuật (giả lập)
            if 'price_above_ma200' in technical:
                # Trong thực tế, kiểm tra giá có cao hơn MA200 không
                pass
            
            # Nếu vượt qua tất cả các bộ lọc, thêm vào danh sách kết quả
            filtered_stocks.append({
                "ticker": ticker,
                "name": info.get('longName', 'N/A'),
                "price": current_price,
                "change": info.get('regularMarketChangePercent', 0) * 100,
                "market_cap": market_cap / 1e9,  # Convert to billions
                "sector": sector
            })
        except:
            continue
    
    # Tạo bảng kết quả
    if not filtered_stocks:
        return html.Div("Không tìm thấy cổ phiếu nào phù hợp với tiêu chí", className="text-center mt-4")
    
    # Tạo DataFrame từ kết quả
    results_df = pd.DataFrame(filtered_stocks)
    
    # Tạo bảng hiển thị
    table_header = [
        html.Thead(html.Tr([
            html.Th("Mã"), html.Th("Tên công ty"), html.Th("Giá ($)"), 
            html.Th("Thay đổi (%)"), html.Th("Vốn hóa ($B)"), html.Th("Ngành")
        ]))
    ]
    
    table_rows = []
    for i, row in results_df.iterrows():
        change_color = "text-success" if row["change"] >= 0 else "text-danger"
        change_text = f"{row['change']:.2f}%" if not pd.isna(row["change"]) else "N/A"
        
        table_rows.append(html.Tr([
            html.Td(row["ticker"]),
            html.Td(row["name"]),
            html.Td(f"${row['price']:.2f}" if not pd.isna(row["price"]) else "N/A"),
            html.Td(change_text, className=change_color),
            html.Td(f"${row['market_cap']:.2f}B" if not pd.isna(row["market_cap"]) else "N/A"),
            html.Td(row["sector"])
        ]))
    
    table_body = [html.Tbody(table_rows)]
    
    return dbc.Table(table_header + table_body, bordered=True, hover=True, striped=True, responsive=True)

4. Thiết kế chức năng so sánh nhiều mã cổ phiếu

# Tạo tab so sánh cổ phiếu
comparison_tab = dbc.Tab([
    dbc.Row([
        dbc.Col([
            html.H4("Chọn cổ phiếu để so sánh", className="mb-3"),
            dcc.Dropdown(
                id="comparison-tickers",
                options=[{"label": ticker, "value": ticker} for ticker in tech_tickers + ["NFLX", "TSLA", "IBM", "INTC", "AMD"]],
                value=["AAPL", "MSFT", "GOOGL"],
                multi=True
            ),
            
            html.H5("Thông số so sánh", className="mt-4 mb-3"),
            dcc.Checklist(
                id="comparison-metrics",
                options=[
                    {"label": "Giá tương đối", "value": "relative_price"},
                    {"label": "Khối lượng giao dịch", "value": "volume"},
                    {"label": "Biến động", "value": "volatility"},
                    {"label": "RSI", "value": "rsi"},
                ],
                value=["relative_price"]
            ),
            
            html.H5("Khoảng thời gian", className="mt-4 mb-3"),
            dcc.Dropdown(
                id="comparison-timeframe",
                options=[
                    {"label": "1 tháng", "value": "1mo"},
                    {"label": "3 tháng", "value": "3mo"},
                    {"label": "6 tháng", "value": "6mo"},
                    {"label": "1 năm", "value": "1y"},
                    {"label": "2 năm", "value": "2y"},
                    {"label": "5 năm", "value": "5y"},
                ],
                value="1y"
            ),
            
            dbc.Button("So sánh", id="run-comparison", color="primary", className="mt-4")
        ], width=3),
        
        dbc.Col([
            html.Div(id="comparison-charts")
        ], width=9)
    ])
], label="So sánh cổ phiếu")

# Thêm tab vào tabs
# (Cập nhật lại phần layout để thêm tab này vào)

# Callback cho phần so sánh
@app.callback(
    Output("comparison-charts", "children"),
    [Input("run-comparison", "n_clicks")],
    [
        State("comparison-tickers", "value"),
        State("comparison-metrics", "value"),
        State("comparison-timeframe", "value")
    ],
    prevent_initial_call=True
)
def update_comparison_charts(n_clicks, tickers, metrics, timeframe):
    if n_clicks is None or not tickers:
        return html.Div("Chọn ít nhất một mã cổ phiếu và nhấn 'So sánh' để xem kết quả")
    
    # Lấy dữ liệu cho các mã đã chọn
    data = get_multiple_stocks(tickers, period=timeframe)
    
    # Tạo danh sách biểu đồ
    charts = []
    
    # Biểu đồ giá tương đối
    if "relative_price" in metrics:
        fig = go.Figure()
        
        for ticker in tickers:
            df = data[ticker]
            normalized = df['Close'] / df['Close'].iloc[0] * 100
            fig.add_trace(
                go.Scatter(x=df.index, y=normalized, mode='lines', name=ticker)
            )
        
        fig.update_layout(
            title="So sánh giá tương đối (điểm cơ sở = 100)",
            xaxis_title="Ngày",
            yaxis_title="Giá tương đối",
            hovermode='x unified'
        )
        
        charts.append(dcc.Graph(figure=fig))
    
    # Biểu đồ khối lượng giao dịch
    if "volume" in metrics:
        fig = go.Figure()
        
        for ticker in tickers:
            df = data[ticker]
            # Chuẩn hóa khối lượng để dễ so sánh
            volume_avg = df['Volume'].mean()
            normalized_volume = (df['Volume'] / volume_avg) * 100
            
            fig.add_trace(
                go.Scatter(x=df.index, y=normalized_volume, mode='lines', name=f"{ticker} (Avg={volume_avg/1e6:.1f}M)")
            )
        
        fig.update_layout(
            title="So sánh khối lượng giao dịch tương đối (% của trung bình)",
            xaxis_title="Ngày",
            yaxis_title="Khối lượng tương đối (%)",
            hovermode='x unified'
        )
        
        charts.append(dcc.Graph(figure=fig))
    
    # Biểu đồ biến động
    if "volatility" in metrics:
        fig = go.Figure()
        
        for ticker in tickers:
            df = data[ticker]
            # Tính biến động 20 ngày (độ lệch chuẩn của lợi nhuận)
            df['Returns'] = df['Close'].pct_change()
            volatility = df['Returns'].rolling(window=20).std() * np.sqrt(252) * 100  # Annualized volatility
            
            fig.add_trace(
                go.Scatter(x=df.index, y=volatility, mode='lines', name=ticker)
            )
        
        fig.update_layout(
            title="So sánh biến động giá (% biến động hàng năm, cửa sổ 20 ngày)",
            xaxis_title="Ngày",
            yaxis_title="Biến động (%)",
            hovermode='x unified'
        )
        
        charts.append(dcc.Graph(figure=fig))
    
    # Biểu đồ RSI
    if "rsi" in metrics:
        fig = go.Figure()
        
        for ticker in tickers:
            df = data[ticker]
            # Tính RSI
            df_with_indicators = add_technical_indicators(df)
            
            fig.add_trace(
                go.Scatter(x=df.index, y=df_with_indicators['RSI'], mode='lines', name=ticker)
            )
        
        # Thêm vùng quá mua/quá bán
        fig.add_hrect(y0=70, y1=100, line_width=0, fillcolor="red", opacity=0.2)
        fig.add_hrect(y0=0, y1=30, line_width=0, fillcolor="green", opacity=0.2)
        
        fig.update_layout(
            title="So sánh chỉ báo RSI",
            xaxis_title="Ngày",
            yaxis_title="RSI (14)",
            hovermode='x unified',
            yaxis=dict(range=[0, 100])
        )
        
        charts.append(dcc.Graph(figure=fig))
    
    # Bảng so sánh các chỉ số cơ bản
    # Lấy thông tin cơ bản về các cổ phiếu để so sánh
    stock_info = []
    for ticker in tickers:
        try:
            stock = yf.Ticker(ticker)
            info = stock.info
            
            stock_info.append({
                "Ticker": ticker,
                "Tên": info.get('shortName', 'N/A'),
                "Giá hiện tại": info.get('currentPrice', 'N/A'),
                "Thay đổi (%)": info.get('regularMarketChangePercent', 'N/A') * 100 if info.get('regularMarketChangePercent') else 'N/A',
                "P/E": info.get('trailingPE', 'N/A'),
                "EPS": info.get('trailingEps', 'N/A'),
                "Vốn hóa ($B)": info.get('marketCap', 'N/A') / 1e9 if info.get('marketCap') else 'N/A',
                "Tỷ lệ cổ tức (%)": info.get('dividendYield', 'N/A') * 100 if info.get('dividendYield') else 'N/A'
            })
        except:
            stock_info.append({
                "Ticker": ticker,
                "Tên": "N/A",
                "Giá hiện tại": "N/A",
                "Thay đổi (%)": "N/A",
                "P/E": "N/A",
                "EPS": "N/A",
                "Vốn hóa ($B)": "N/A",
                "Tỷ lệ cổ tức (%)": "N/A"
            })
    
    # Tạo DataFrame từ thông tin đã thu thập
    info_df = pd.DataFrame(stock_info)
    
    # Tạo bảng so sánh
    table_header = [
        html.Thead(html.Tr([html.Th(col) for col in info_df.columns]))
    ]
    
    table_rows = []
    for i, row in info_df.iterrows():
        cells = []
        for col in info_df.columns:
            value = row[col]
            # Định dạng giá trị
            if isinstance(value, (float, int)) and col not in ["Ticker"]:
                if col in ["Thay đổi (%)", "Tỷ lệ cổ tức (%)"]:
                    formatted = f"{value:.2f}%" if value != 'N/A' else 'N/A'
                    color_class = "text-success" if value > 0 else "text-danger" if value < 0 else ""
                    cells.append(html.Td(formatted, className=color_class))
                elif col == "Vốn hóa ($B)":
                    formatted = f"${value:.2f}B" if value != 'N/A' else 'N/A'
                    cells.append(html.Td(formatted))
                elif col in ["Giá hiện tại", "EPS"]:
                    formatted = f"${value:.2f}" if value != 'N/A' else 'N/A'
                    cells.append(html.Td(formatted))
                else:
                    formatted = f"{value:.2f}" if value != 'N/A' else 'N/A'
                    cells.append(html.Td(formatted))
            else:
                cells.append(html.Td(value))
        
        table_rows.append(html.Tr(cells))
    
    table_body = [html.Tbody(table_rows)]
    comparison_table = dbc.Table(
        table_header + table_body,
        bordered=True,
        hover=True,
        striped=True,
        responsive=True,
        className="mt-4"
    )
    
    return html.Div(charts + [html.H4("So sánh chỉ số cơ bản", className="mt-4 mb-3"), comparison_table])

VI. Triển khai và tối ưu hóa ứng dụng

1. Chạy ứng dụng

if __name__ == '__main__':
    app.run_server(debug=True, host='0.0.0.0', port=5000)

2. Tối ưu hóa hiệu suất

Một bảng điều khiển thị trường cần xử lý lượng lớn dữ liệu và yêu cầu phản hồi nhanh. Dưới đây là một số kỹ thuật tối ưu hóa:

# 1. Lưu bộ nhớ đệm dữ liệu
import diskcache as dc
from flask_caching import Cache

cache = dc.Cache('./cache')
server = app.server
flask_cache = Cache(server, config={
    'CACHE_TYPE': 'filesystem',
    'CACHE_DIR': 'flask_cache',
    'CACHE_DEFAULT_TIMEOUT': 300
})

@flask_cache.memoize(timeout=300)  # Cache trong 5 phút
def cached_get_stock_data(ticker, period, interval):
    return get_stock_data(ticker, period, interval)

# Sử dụng hàm đã lưu đệm thay vì gọi API trực tiếp
@app.callback(...)
def update_main_chart(ticker, timeframe, chart_type, indicators):
    df = cached_get_stock_data(ticker, period=timeframe, interval='1d')
    # ...tiếp tục callback

3. Xử lý lỗi và cải thiện trải nghiệm người dùng

# Tạo callback để hiển thị thông báo lỗi
@app.callback(
    Output("error-message", "children"),
    Output("error-message", "is_open"),
    [Input("main-chart", "figure")],
    prevent_initial_call=True
)
def handle_errors(figure):
    if 'data' not in figure or not figure['data']:
        return "Lỗi khi tải dữ liệu. Vui lòng thử lại sau.", True
    return "", False

# Thêm thành phần thông báo lỗi vào layout
error_message = dbc.Modal(
    [
        dbc.ModalHeader("Lỗi"),
        dbc.ModalBody(id="error-message"),
        dbc.ModalFooter(dbc.Button("Đóng", id="close-error", className="ml-auto"))
    ],
    id="error-modal",
    is_open=False
)

app.layout.children.append(error_message)

# Callback để đóng thông báo lỗi
@app.callback(
    Output("error-modal", "is_open"),
    [Input("close-error", "n_clicks")],
    [State("error-modal", "is_open")],
    prevent_initial_call=True
)
def close_error_modal(n_clicks, is_open):
    if n_clicks:
        return False
    return is_open

4. Bảo mật và xác thực người dùng (cho ứng dụng có yêu cầu đăng nhập)

# Tạo layout đăng nhập
login_layout = dbc.Container([
    dbc.Row([
        dbc.Col([
            html.H2("Đăng nhập", className="text-center mb-4"),
            dbc.Card([
                dbc.CardBody([
                    dbc.Input(id="username-input", placeholder="Tên đăng nhập", type="text", className="mb-3"),
                    dbc.Input(id="password-input", placeholder="Mật khẩu", type="password", className="mb-3"),
                    dbc.Button("Đăng nhập", id="login-button", color="primary", className="w-100"),
                    html.Div(id="login-error", className="text-danger mt-3")
                ])
            ])
        ], width={"size": 6, "offset": 3})
    ], className="vh-100 d-flex align-items-center")
])

# Hàm kiểm tra đăng nhập (thực tế sẽ sử dụng cơ sở dữ liệu và mã hóa)
def check_credentials(username, password):
    valid_credentials = {
        "user1": "password1",
        "user2": "password2",
        "admin": "admin123"
    }
    return username in valid_credentials and valid_credentials[username] == password

# Tạo callback đăng nhập
@app.callback(
    Output("url", "pathname"),
    Output("login-error", "children"),
    [Input("login-button", "n_clicks")],
    [State("username-input", "value"), State("password-input", "value")],
    prevent_initial_call=True
)
def process_login(n_clicks, username, password):
    if n_clicks is None:
        return no_update, ""
    
    if not username or not password:
        return no_update, "Vui lòng nhập đầy đủ thông tin đăng nhập"
    
    if check_credentials(username, password):
        return "/dashboard", ""
    else:
        return no_update, "Tên đăng nhập hoặc mật khẩu không chính xác"

# Cập nhật layout chính của ứng dụng để hỗ trợ định tuyến
app.layout = html.Div([
    dcc.Location(id="url", refresh=False),
    html.Div(id="page-content")
])

@app.callback(
    Output("page-content", "children"),
    [Input("url", "pathname")]
)
def display_page(pathname):
    if pathname == "/login" or pathname == "/":
        return login_layout
    elif pathname == "/dashboard":
        return main_layout  # Layout chính của ứng dụng
    else:
        return "404 - Trang không tồn tại"

VII. Mở rộng và cải tiến bảng điều khiển

1. Thêm tính năng dự đoán giá sử dụng Machine Learning

from sklearn.preprocessing import MinMaxScaler
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, LSTM, Dropout

def predict_price_movement(ticker, prediction_days=30):
    """
    Dự đoán giá cổ phiếu trong tương lai sử dụng mô hình ML đơn giản
    """
    # Lấy dữ liệu
    df = get_stock_data(ticker, period="2y")
    
    # Chuẩn bị dữ liệu
    data = df['Close'].values.reshape(-1, 1)
    scaler = MinMaxScaler()
    scaled_data = scaler.fit_transform(data)
    
    # Tạo chuỗi dữ liệu (X: 60 ngày trước, y: ngày tiếp theo)
    X, y = [], []
    for i in range(60, len(scaled_data)):
        X.append(scaled_data[i-60:i, 0])
        y.append(scaled_data[i, 0])
    X, y = np.array(X), np.array(y)
    
    # Chia tập huấn luyện và kiểm thử
    split = int(0.8 * len(X))
    X_train, X_test = X[:split], X[split:]
    y_train, y_test = y[:split], y[split:]
    
    # Định hình dữ liệu cho LSTM
    X_train = np.reshape(X_train, (X_train.shape[0], X_train.shape[1], 1))
    X_test = np.reshape(X_test, (X_test.shape[0], X_test.shape[1], 1))
    
    # Xây dựng mô hình LSTM
    model = Sequential()
    model.add(LSTM(units=50, return_sequences=True, input_shape=(X_train.shape[1], 1)))
    model.add(Dropout(0.2))
    model.add(LSTM(units=50, return_sequences=False))
    model.add(Dropout(0.2))
    model.add(Dense(units=1))
    
    model.compile(optimizer='adam', loss='mean_squared_error')
    model.fit(X_train, y_train, epochs=25, batch_size=32, verbose=0)
    
    # Dự đoán
    test_prediction = model.predict(X_test)
    test_prediction = scaler.inverse_transform(test_prediction)
    real_values = scaler.inverse_transform(y_test.reshape(-1, 1))
    
    # Đánh giá mô hình
    mse = np.mean((test_prediction - real_values)**2)
    rmse = np.sqrt(mse)
    
    # Dự đoán cho tương lai
    future_data = scaled_data[-60:].reshape(1, 60, 1)
    future_predictions = []
    
    for _ in range(prediction_days):
        prediction = model.predict(future_data)[0][0]
        future_predictions.append(prediction)
        future_data = np.append(future_data[:,1:,:], [[prediction]], axis=1)
    
    # Chuyển đổi về giá thực
    future_predictions = scaler.inverse_transform(np.array(future_predictions).reshape(-1, 1)).flatten()
    
    # Tạo dữ liệu ngày trong tương lai
    last_date = df.index[-1]
    future_dates = pd.date_range(start=last_date + pd.Timedelta(days=1), periods=prediction_days, freq='B')
    
    return {
        'model_performance': {
            'rmse': rmse,
            'mse': mse
        },
        'test_prediction': test_prediction.flatten(),
        'real_values': real_values.flatten(),
        'future_dates': future_dates,
        'future_predictions': future_predictions,
        'last_price': df['Close'].iloc[-1]
    }

# Thêm tab dự đoán
prediction_tab = dbc.Tab([
    dbc.Row([
        dbc.Col([
            html.H4("Thiết lập dự đoán", className="mb-4"),
            
            dcc.Dropdown(
                id="prediction-ticker",
                options=[{"label": ticker, "value": ticker} for ticker in tech_tickers],
                value="AAPL",
                placeholder="Chọn mã chứng khoán"
            ),
            
            html.H5("Khoảng thời gian dự đoán", className="mt-3"),
            dcc.Slider(
                id="prediction-days",
                min=5,
                max=60,
                step=5,
                value=30,
                marks={i: f"{i}" for i in range(5, 61, 5)}
            ),
            
            dbc.Button("Bắt đầu dự đoán", id="run-prediction", color="primary", className="mt-4")
        ], width=3),
        
        dbc.Col([
            dcc.Loading(
                id="prediction-loading",
                type="circle",
                children=[html.Div(id="prediction-results")]
            )
        ], width=9)
    ])
], label="Dự đoán giá")

# Thêm tab vào tabs
# (Cập nhật phần layout để thêm tab này)

# Callback cho dự đoán
@app.callback(
    Output("prediction-results", "children"),
    [Input("run-prediction", "n_clicks")],
    [State("prediction-ticker", "value"), State("prediction-days", "value")],
    prevent_initial_call=True
)
def display_prediction(n_clicks, ticker, days):
    if n_clicks is None:
        return html.Div("Chọn cài đặt và nhấn 'Bắt đầu dự đoán'")
    
    try:
        # Thực hiện dự đoán
        prediction = predict_price_movement(ticker, prediction_days=days)
        
        # Tạo biểu đồ kết quả
        results = []
        
        # Biểu đồ dự đoán
        fig = go.Figure()
        
        # Lấy dữ liệu quá khứ
        df = get_stock_data(ticker, period="2y")
        
        # Vẽ giá quá khứ
        fig.add_trace(
            go.Scatter(x=df.index, y=df['Close'], mode='lines', name="Giá quá khứ", line=dict(color='blue'))
        )
        
        # Vẽ dự đoán tương lai
        fig.add_trace(
            go.Scatter(
                x=prediction['future_dates'],
                y=prediction['future_predictions'],
                mode='lines',
                name="Dự đoán",
                line=dict(color='red', dash='dash')
            )
        )
        
        # Thêm vùng tin cậy (đơn giản: ±10%)
        upper_bound = prediction['future_predictions'] * 1.1
        lower_bound = prediction['future_predictions'] * 0.9
        
        fig.add_trace(
            go.Scatter(
                x=prediction['future_dates'],
                y=upper_bound,
                mode='lines',
                line=dict(width=0),
                showlegend=False
            )
        )
        
        fig.add_trace(
            go.Scatter(
                x=prediction['future_dates'],
                y=lower_bound,
                mode='lines',
                line=dict(width=0),
                fill='tonexty',
                fillcolor='rgba(255, 0, 0, 0.1)',
                name="Khoảng tin cậy"
            )
        )
        
        fig.update_layout(
            title=f"Dự đoán giá {ticker} - {days} ngày tiếp theo",
            xaxis_title="Ngày",
            yaxis_title="Giá ($)",
            hovermode='x unified'
        )
        
        results.append(dcc.Graph(figure=fig))
        
        # Thêm thông tin về hiệu suất mô hình
        performance_info = [
            html.H5("Hiệu suất mô hình", className="mt-4"),
            html.P(f"Root Mean Squared Error (RMSE): ${prediction['model_performance']['rmse']:.2f}"),
            html.P(f"Giá hiện tại: ${prediction['last_price']:.2f}"),
            html.P(f"Giá dự đoán sau {days} ngày: ${prediction['future_predictions'][-1]:.2f}"),
            html.P(f"Thay đổi dự kiến: {((prediction['future_predictions'][-1] / prediction['last_price']) - 1) * 100:.2f}%")
        ]
        
        results.append(html.Div(performance_info))
        
        # Thêm cảnh báo
        alert = dbc.Alert(
            "Lưu ý: Dự đoán này chỉ mang tính chất tham khảo và có thể không chính xác. "
            "Không nên sử dụng làm cơ sở duy nhất cho quyết định đầu tư.",
            color="warning",
            className="mt-4"
        )
        
        results.append(alert)
        
        return html.Div(results)
    except Exception as e:
        return html.Div([
            html.H5("Lỗi khi thực hiện dự đoán", className="text-danger"),
            html.P(str(e))
        ])

2. Thêm tính năng thông báo qua email hoặc tin nhắn

# Thêm tab thông báo và cảnh báo
alerts_tab = dbc.Tab([
    dbc.Row([
        dbc.Col([
            html.H4("Thiết lập cảnh báo", className="mb-4"),
            
            html.H5("Chọn mã chứng khoán"),
            dcc.Dropdown(
                id="alert-ticker",
                options=[{"label": ticker, "value": ticker} for ticker in tech_tickers],
                placeholder="Chọn mã chứng khoán",
                value="AAPL"
            ),
            
            html.H5("Loại cảnh báo", className="mt-3"),
            dcc.RadioItems(
                id="alert-type",
                options=[
                    {"label": "Cảnh báo giá", "value": "price"},
                    {"label": "Cảnh báo % thay đổi", "value": "percent"},
                    {"label": "Cảnh báo chỉ báo kỹ thuật", "value": "technical"}
                ],
                value="price"
            ),
            
            # Điều kiện dựa vào loại cảnh báo
            html.Div(id="alert-condition-container", className="mt-3"),
            
            html.H5("Phương thức nhận thông báo", className="mt-3"),
            dcc.Checklist(
                id="alert-method",
                options=[
                    {"label": "Email", "value": "email"},
                    {"label": "Thông báo trên ứng dụng", "value": "app"}
                ],
                value=["app"]
            ),
            
            # Tùy chọn email nếu được chọn
            html.Div(id="email-options-container"),
            
            dbc.Button("Tạo cảnh báo", id="create-alert", color="primary", className="mt-4")
        ], width=4),
        
        dbc.Col([
            html.H4("Cảnh báo của bạn", className="mb-4"),
            html.Div(id="active-alerts")
        ], width=8)
    ])
], label="Thông báo & Cảnh báo")

# Callback để thay đổi điều kiện cảnh báo dựa vào loại cảnh báo
@app.callback(
    Output("alert-condition-container", "children"),
    [Input("alert-type", "value"), Input("alert-ticker", "value")]
)
def update_alert_condition(alert_type, ticker):
    # Lấy dữ liệu hiện tại để hiển thị điểm tham chiếu
    try:
        current_data = yf.Ticker(ticker).history(period="1d")
        current_price = current_data['Close'].iloc[-1]
    except:
        current_price = 100  # Giá mặc định nếu không lấy được
    
    if alert_type == "price":
        return html.Div([
            html.H5(f"Giá hiện tại: ${current_price:.2f}"),
            dbc.Row([
                dbc.Col([
                    html.Label("Điều kiện"),
                    dcc.Dropdown(
                        id="price-condition",
                        options=[
                            {"label": "Cao hơn hoặc bằng", "value": ">="},
                            {"label": "Thấp hơn hoặc bằng", "value": "<="}
                        ],
                        value=">="
                    )
                ], width=6),
                dbc.Col([
                    html.Label("Giá mục tiêu ($)"),
                    dbc.Input(
                        id="price-target",
                        type="number",
                        value=round(current_price * 1.05, 2)  # Mặc định +5%
                    )
                ], width=6)
            ])
        ])
    
    elif alert_type == "percent":
        return html.Div([
            dbc.Row([
                dbc.Col([
                    html.Label("Điều kiện"),
                    dcc.Dropdown(
                        id="percent-condition",
                        options=[
                            {"label": "Tăng hơn", "value": "increase"},
                            {"label": "Giảm hơn", "value": "decrease"}
                        ],
                        value="increase"
                    )
                ], width=6),
                dbc.Col([
                    html.Label("Phần trăm (%)"),
                    dbc.Input(
                        id="percent-target",
                        type="number",
                        value=5  # Mặc định 5%
                    )
                ], width=6)
            ]),
            dbc.Row([
                dbc.Col([
                    html.Label("Trong khoảng thời gian"),
                    dcc.Dropdown(
                        id="percent-timeframe",
                        options=[
                            {"label": "Trong ngày", "value": "day"},
                            {"label": "Trong tuần", "value": "week"},
                            {"label": "Trong tháng", "value": "month"}
                        ],
                        value="day"
                    )
                ], width=12)
            ], className="mt-2")
        ])
    
    elif alert_type == "technical":
        return html.Div([
            html.Label("Chọn chỉ báo kỹ thuật"),
            dcc.Dropdown(
                id="technical-indicator",
                options=[
                    {"label": "Golden Cross (MA20 cắt lên MA50)", "value": "golden_cross"},
                    {"label": "Death Cross (MA20 cắt xuống MA50)", "value": "death_cross"},
                    {"label": "RSI vào vùng quá bán (<30)", "value": "rsi_oversold"},
                    {"label": "RSI vào vùng quá mua (>70)", "value": "rsi_overbought"},
                    {"label": "MACD cắt lên Signal", "value": "macd_bullish_cross"},
                    {"label": "MACD cắt xuống Signal", "value": "macd_bearish_cross"},
                    {"label": "Giá phá vỡ dải trên Bollinger", "value": "bb_upper_break"},
                    {"label": "Giá phá vỡ dải dưới Bollinger", "value": "bb_lower_break"}
                ],
                value="golden_cross"
            )
        ])

# Callback để hiển thị tùy chọn email
@app.callback(
    Output("email-options-container", "children"),
    [Input("alert-method", "value")]
)
def update_email_options(methods):
    if "email" in methods:
        return html.Div([
            html.Label("Địa chỉ email", className="mt-3"),
            dbc.Input(id="alert-email", type="email", placeholder="Nhập địa chỉ email của bạn")
        ])
    return html.Div()

# Tạo danh sách cảnh báo (trong thực tế sẽ lưu vào cơ sở dữ liệu)
alert_list = []

# Callback để tạo cảnh báo mới
@app.callback(
    Output("active-alerts", "children"),
    [Input("create-alert", "n_clicks")],
    [
        State("alert-ticker", "value"),
        State("alert-type", "value"),
        State("alert-method", "value"),
        State("price-condition", "value") if "price-condition" in app.callback_context.inputs else None,
        State("price-target", "value") if "price-target" in app.callback_context.inputs else None,
        State("percent-condition", "value") if "percent-condition" in app.callback_context.inputs else None,
        State("percent-target", "value") if "percent-target" in app.callback_context.inputs else None,
        State("percent-timeframe", "value") if "percent-timeframe" in app.callback_context.inputs else None,
        State("technical-indicator", "value") if "technical-indicator" in app.callback_context.inputs else None,
        State("alert-email", "value") if "alert-email" in app.callback_context.inputs else None
    ],
    prevent_initial_call=True
)
def create_alert(n_clicks, ticker, alert_type, methods, price_cond=None, price_target=None,
                pct_cond=None, pct_target=None, pct_timeframe=None, tech_indicator=None, email=None):
    if n_clicks is None:
        return html.Div("Chưa có cảnh báo nào được thiết lập")
    
    # Lấy giá hiện tại
    try:
        current_data = yf.Ticker(ticker).history(period="1d")
        current_price = current_data['Close'].iloc[-1]
    except:
        current_price = 0
    
    # Tạo cảnh báo mới
    new_alert = {
        "id": len(alert_list) + 1,
        "ticker": ticker,
        "type": alert_type,
        "methods": methods,
        "created_at": datetime.now().strftime("%Y-%m-%d %H:%M"),
        "status": "Active"
    }
    
    # Thêm thông tin chi tiết dựa trên loại cảnh báo
    if alert_type == "price":
        new_alert["condition"] = price_cond
        new_alert["target"] = price_target
        new_alert["description"] = f"{ticker} {price_cond} ${price_target}"
    
    elif alert_type == "percent":
        new_alert["condition"] = pct_cond
        new_alert["target"] = pct_target
        new_alert["timeframe"] = pct_timeframe
        direction = "tăng" if pct_cond == "increase" else "giảm"
        timeframe_text = {"day": "trong ngày", "week": "trong tuần", "month": "trong tháng"}[pct_timeframe]
        new_alert["description"] = f"{ticker} {direction} {pct_target}% {timeframe_text}"
    
    elif alert_type == "technical":
        new_alert["indicator"] = tech_indicator
        indicator_descriptions = {
            "golden_cross": "Golden Cross (MA20 cắt lên MA50)",
            "death_cross": "Death Cross (MA20 cắt xuống MA50)",
            "rsi_oversold": "RSI vào vùng quá bán (<30)",
            "rsi_overbought": "RSI vào vùng quá mua (>70)",
            "macd_bullish_cross": "MACD cắt lên Signal",
            "macd_bearish_cross": "MACD cắt xuống Signal",
            "bb_upper_break": "Giá phá vỡ dải trên Bollinger",
            "bb_lower_break": "Giá phá vỡ dải dưới Bollinger"
        }
        new_alert["description"] = f"{ticker} - {indicator_descriptions[tech_indicator]}"
    
    # Thêm email nếu có
    if "email" in methods and email:
        new_alert["email"] = email
    
    # Thêm cảnh báo vào danh sách
    alert_list.append(new_alert)
    
    # Hiển thị danh sách cảnh báo
    return display_alerts()

# Hiển thị danh sách cảnh báo
def display_alerts():
    if not alert_list:
        return html.Div("Chưa có cảnh báo nào được thiết lập")
    
    alert_cards = []
    for alert in alert_list:
        # Màu sắc dựa trên loại cảnh báo
        if alert["type"] == "price":
            color = "info"
        elif alert["type"] == "percent":
            color = "warning"
        else:  # technical
            color = "success"
        
        # Tạo card cho cảnh báo
        card = dbc.Card([
            dbc.CardHeader(
                html.H5(f"Cảnh báo #{alert['id']} - {alert['ticker']}", className="mb-0")
            ),
            dbc.CardBody([
                html.P(alert["description"]),
                html.P(f"Trạng thái: {alert['status']}", className="text-muted"),
                html.P(f"Tạo lúc: {alert['created_at']}", className="text-muted small"),
                html.P(f"Phương thức: {', '.join(alert['methods'])}", className="text-muted small"),
                dbc.Button("Xóa", id=f"delete-alert-{alert['id']}", color="danger", size="sm",
                          className="mt-2")
            ])
        ], color=color, outline=True, className="mb-3")
        
        alert_cards.append(card)
    
    return html.Div(alert_cards)

3. Xuất báo cáo và phân tích

# Thêm tab phân tích và báo cáo
reports_tab = dbc.Tab([
    dbc.Row([
        dbc.Col([
            html.H4("Tạo báo cáo phân tích", className="mb-4"),
            
            html.H5("Chọn mã chứng khoán"),
            dcc.Dropdown(
                id="report-ticker",
                options=[{"label": ticker, "value": ticker} for ticker in tech_tickers],
                placeholder="Chọn mã chứng khoán",
                value="AAPL"
            ),
            
            html.H5("Khoảng thời gian", className="mt-3"),
            dcc.Dropdown(
                id="report-timeframe",
                options=[
                    {"label": "1 tháng", "value": "1mo"},
                    {"label": "3 tháng", "value": "3mo"},
                    {"label": "6 tháng", "value": "6mo"},
                    {"label": "1 năm", "value": "1y"},
                    {"label": "2 năm", "value": "2y"},
                    {"label": "5 năm", "value": "5y"}
                ],
                value="1y"
            ),
            
            html.H5("Loại phân tích", className="mt-3"),
            dcc.Checklist(
                id="report-analysis-types",
                options=[
                    {"label": "Phân tích giá", "value": "price"},
                    {"label": "Phân tích kỹ thuật", "value": "technical"},
                    {"label": "Phân tích so sánh", "value": "comparison"},
                    {"label": "Phân tích ngành", "value": "sector"}
                ],
                value=["price", "technical"]
            ),
            
            dbc.Button("Tạo báo cáo", id="generate-report", color="primary", className="mt-4")
        ], width=3),
        
        dbc.Col([
            dcc.Loading(
                id="report-loading",
                type="circle",
                children=[html.Div(id="report-content")]
            )
        ], width=9)
    ])
], label="Báo cáo & Phân tích")

# Thêm tab vào tabs
# (Cập nhật phần layout để thêm tab này)

# Callback để tạo báo cáo
@app.callback(
    Output("report-content", "children"),
    [Input("generate-report", "n_clicks")],
    [
        State("report-ticker", "value"),
        State("report-timeframe", "value"),
        State("report-analysis-types", "value")
    ],
    prevent_initial_call=True
)
def generate_report(n_clicks, ticker, timeframe, analysis_types):
    if n_clicks is None:
        return html.Div("Chọn các tùy chọn và nhấn 'Tạo báo cáo'")
    
    report_sections = []
    
    # Thông tin chung về cổ phiếu
    try:
        stock = yf.Ticker(ticker)
        info = stock.info
        company_name = info.get('longName', ticker)
        
        # Header
        report_sections.append(html.Div([
            html.H3(f"Báo cáo phân tích: {company_name} ({ticker})"),
            html.P(f"Ngày tạo: {datetime.now().strftime('%Y-%m-%d')}"),
            html.P(f"Khoảng thời gian: {timeframe}"),
            html.Hr()
        ]))
        
        # Thông tin cơ bản
        basic_info = html.Div([
            html.H4("Thông tin cơ bản"),
            dbc.Row([
                dbc.Col([
                    html.P(f"Ngành: {info.get('sector', 'N/A')}"),
                    html.P(f"Lĩnh vực: {info.get('industry', 'N/A')}"),
                    html.P(f"Quốc gia: {info.get('country', 'N/A')}"),
                ], width=6),
                dbc.Col([
                    html.P(f"Vốn hóa: ${info.get('marketCap', 0)/1e9:.2f}B"),
                    html.P(f"P/E: {info.get('trailingPE', 'N/A')}"),
                    html.P(f"Tỷ lệ cổ tức: {info.get('dividendYield', 0)*100:.2f}%"),
                ], width=6)
            ]),
            html.Hr()
        ])
        report_sections.append(basic_info)
    except:
        # Header đơn giản nếu không lấy được thông tin
        report_sections.append(html.Div([
            html.H3(f"Báo cáo phân tích: {ticker}"),
            html.P(f"Ngày tạo: {datetime.now().strftime('%Y-%m-%d')}"),
            html.P(f"Khoảng thời gian: {timeframe}"),
            html.Hr()
        ]))
    
    # Phân tích giá
    if "price" in analysis_types:
        try:
            df = get_stock_data(ticker, period=timeframe)
            
            # Tính toán % thay đổi
            price_change = (df['Close'].iloc[-1] / df['Close'].iloc[0] - 1) * 100
            color = "text-success" if price_change >= 0 else "text-danger"
            
            # Tạo biểu đồ giá
            fig = go.Figure(
                go.Candlestick(
                    x=df.index,
                    open=df['Open'],
                    high=df['High'],
                    low=df['Low'],
                    close=df['Close'],
                    name="OHLC"
                )
            )
            
            fig.update_layout(
                title=f"Biểu đồ giá {ticker}",
                xaxis_title="Ngày",
                yaxis_title="Giá ($)",
                height=500,
                xaxis_rangeslider_visible=False
            )
            
            price_analysis = html.Div([
                html.H4("Phân tích giá"),
                dbc.Row([
                    dbc.Col([
                        html.P(f"Giá hiện tại: ${df['Close'].iloc[-1]:.2f}"),
                        html.P([
                            "Thay đổi: ",
                            html.Span(f"{price_change:.2f}%", className=color)
                        ]),
                        html.P(f"Cao nhất: ${df['High'].max():.2f}"),
                        html.P(f"Thấp nhất: ${df['Low'].min():.2f}")
                    ], width=4),
                    dbc.Col([
                        dcc.Graph(figure=fig)
                    ], width=8)
                ]),
                
                # Phân tích xu hướng
                html.H5("Xu hướng giá", className="mt-3"),
                html.P(f"Xu hướng hiện tại: {'Tăng' if price_change > 0 else 'Giảm'}"),
                
                # Mức hỗ trợ và kháng cự (đơn giản)
                html.H5("Mức hỗ trợ và kháng cự", className="mt-3"),
                html.P(f"Kháng cự: ${df['High'].nlargest(3).mean():.2f}"),
                html.P(f"Hỗ trợ: ${df['Low'].nsmallest(3).mean():.2f}"),
                
                html.Hr()
            ])
            report_sections.append(price_analysis)
        except:
            report_sections.append(html.Div([
                html.H4("Phân tích giá"),
                html.P("Không thể tải dữ liệu giá cho mã này"),
                html.Hr()
            ]))
    
    # Phân tích kỹ thuật
    if "technical" in analysis_types:
        try:
            df = get_stock_data(ticker, period=timeframe)
            df = add_technical_indicators(df)
            
            # Tạo biểu đồ chỉ báo kỹ thuật
            fig = make_subplots(rows=3, cols=1, shared_xaxes=True,
                               vertical_spacing=0.1, subplot_titles=('Giá và MA', 'RSI', 'MACD'),
                               row_heights=[0.5, 0.25, 0.25])
            
            # Thêm giá và MA
            fig.add_trace(
                go.Candlestick(
                    x=df.index,
                    open=df['Open'],
                    high=df['High'],
                    low=df['Low'],
                    close=df['Close'],
                    name="OHLC"
                ), row=1, col=1
            )
            
            fig.add_trace(go.Scatter(x=df.index, y=df['SMA20'], mode='lines', name='SMA20'),
                        row=1, col=1)
            fig.add_trace(go.Scatter(x=df.index, y=df['SMA50'], mode='lines', name='SMA50'),
                        row=1, col=1)
            
            # Thêm RSI
            fig.add_trace(go.Scatter(x=df.index, y=df['RSI'], mode='lines', name='RSI'),
                        row=2, col=1)
            fig.add_hline(y=70, line_dash="dash", line_color="red", row=2, col=1)
            fig.add_hline(y=30, line_dash="dash", line_color="green", row=2, col=1)
            
            # Thêm MACD
            fig.add_trace(go.Scatter(x=df.index, y=df['MACD'], mode='lines', name='MACD'),
                        row=3, col=1)
            fig.add_trace(go.Scatter(x=df.index, y=df['MACD_Signal'], mode='lines', name='Signal'),
                        row=3, col=1)
            
            # Cập nhật layout
            fig.update_layout(
                title="Phân tích kỹ thuật",
                height=800,
                xaxis_rangeslider_visible=False,
                showlegend=True,
                legend=dict(orientation="h", y=1.02)
            )
            
            # Tìm các tín hiệu kỹ thuật gần đây
            recent_df = df.tail(20)
            signals = []
            
            # Kiểm tra MA Cross
            for i in range(1, len(recent_df)):
                if (recent_df['SMA20'].iloc[i-1] <= recent_df['SMA50'].iloc[i-1] and
                    recent_df['SMA20'].iloc[i] > recent_df['SMA50'].iloc[i]):
                    signals.append({"type": "Golden Cross", "date": recent_df.index[i].strftime("%Y-%m-%d")})
                
                if (recent_df['SMA20'].iloc[i-1] >= recent_df['SMA50'].iloc[i-1] and
                    recent_df['SMA20'].iloc[i] < recent_df['SMA50'].iloc[i]):
                    signals.append({"type": "Death Cross", "date": recent_df.index[i].strftime("%Y-%m-%d")})
            
            # Kiểm tra RSI
            if recent_df['RSI'].iloc[-1] < 30:
                signals.append({"type": "RSI Quá bán", "date": recent_df.index[-1].strftime("%Y-%m-%d")})
            
            if recent_df['RSI'].iloc[-1] > 70:
                signals.append({"type": "RSI Quá mua", "date": recent_df.index[-1].strftime("%Y-%m-%d")})
            
            # Kiểm tra MACD
            for i in range(1, len(recent_df)):
                if (recent_df['MACD'].iloc[i-1] <= recent_df['MACD_Signal'].iloc[i-1] and
                    recent_df['MACD'].iloc[i] > recent_df['MACD_Signal'].iloc[i]):
                    signals.append({"type": "MACD Bullish Cross", "date": recent_df.index[i].strftime("%Y-%m-%d")})
                
                if (recent_df['MACD'].iloc[i-1] >= recent_df['MACD_Signal'].iloc[i-1] and
                    recent_df['MACD'].iloc[i] < recent_df['MACD_Signal'].iloc[i]):
                    signals.append({"type": "MACD Bearish Cross", "date": recent_df.index[i].strftime("%Y-%m-%d")})
            
            # Tạo bảng tín hiệu
            if signals:
                signal_table = dbc.Table(
                    [
                        html.Thead(html.Tr([html.Th("Tín hiệu"), html.Th("Ngày")])),
                        html.Tbody([html.Tr([html.Td(s["type"]), html.Td(s["date"])]) for s in signals])
                    ],
                    bordered=True, hover=True, striped=True, size="sm"
                )
            else:
                signal_table = html.P("Không tìm thấy tín hiệu kỹ thuật trong 20 phiên gần đây")
            
            technical_analysis = html.Div([
                html.H4("Phân tích kỹ thuật"),
                dcc.Graph(figure=fig),
                html.H5("Các tín hiệu kỹ thuật gần đây", className="mt-3"),
                signal_table,
                
                # Kết luận kỹ thuật
                html.H5("Kết luận kỹ thuật", className="mt-3"),
                html.P([
                    "Xu hướng trung hạn: ",
                    html.Span(
                        "Tăng" if df['SMA20'].iloc[-1] > df['SMA50'].iloc[-1] else "Giảm",
                        className="text-success" if df['SMA20'].iloc[-1] > df['SMA50'].iloc[-1] else "text-danger"
                    )
                ]),
                html.P([
                    "Tín hiệu RSI: ",
                    html.Span(
                        "Quá mua" if df['RSI'].iloc[-1] > 70 else "Quá bán" if df['RSI'].iloc[-1] < 30 else "Trung tính",
                        className="text-danger" if df['RSI'].iloc[-1] > 70 else "text-success" if df['RSI'].iloc[-1] < 30 else ""
                    )
                ]),
                html.P([
                    "Tín hiệu MACD: ",
                    html.Span(
                        "Tích cực" if df['MACD'].iloc[-1] > df['MACD_Signal'].iloc[-1] else "Tiêu cực",
                        className="text-success" if df['MACD'].iloc[-1] > df['MACD_Signal'].iloc[-1] else "text-danger"
                    )
                ]),
                
                html.Hr()
            ])
            report_sections.append(technical_analysis)
        except:
            report_sections.append(html.Div([
                html.H4("Phân tích kỹ thuật"),
                html.P("Không thể tải dữ liệu kỹ thuật cho mã này"),
                html.Hr()
            ]))
    
    # Phân tích so sánh
    if "comparison" in analysis_types:
        try:
            # Lấy dữ liệu của mã cần phân tích và S&P 500
            stock_data = get_stock_data(ticker, period=timeframe)
            market_data = get_stock_data("^GSPC", period=timeframe)  # S&P 500
            
            # Tính toán % thay đổi so với đầu kỳ
            stock_change = (stock_data['Close'] / stock_data['Close'].iloc[0]) * 100 - 100
            market_change = (market_data['Close'] / market_data['Close'].iloc[0]) * 100 - 100
            
            # Tạo biểu đồ so sánh
            fig = go.Figure()
            fig.add_trace(go.Scatter(x=stock_data.index, y=stock_change, mode='lines', name=ticker))
            fig.add_trace(go.Scatter(x=market_data.index, y=market_change, mode='lines', name="S&P 500"))
            
            fig.update_layout(
                title=f"So sánh hiệu suất: {ticker} vs S&P 500",
                xaxis_title="Ngày",
                yaxis_title="% Thay đổi",
                height=500,
                hovermode='x unified'
            )
            
            # Tính Alpha (hiệu suất vượt trội so với thị trường)
            alpha = stock_change.iloc[-1] - market_change.iloc[-1]
            alpha_color = "text-success" if alpha > 0 else "text-danger"
            
            # Tính Beta (độ nhạy cảm với thị trường)
            stock_returns = stock_data['Close'].pct_change().dropna()
            market_returns = market_data['Close'].pct_change().dropna()
            
            # Đảm bảo cùng độ dài
            min_len = min(len(stock_returns), len(market_returns))
            stock_returns = stock_returns[-min_len:]
            market_returns = market_returns[-min_len:]
            
            beta = np.cov(stock_returns, market_returns)[0, 1] / np.var(market_returns)
            
            comparison_analysis = html.Div([
                html.H4("Phân tích so sánh"),
                dcc.Graph(figure=fig),
                dbc.Row([
                    dbc.Col([
                        html.P([
                            f"{ticker} vs S&P 500: ",
                            html.Span(
                                f"{alpha:.2f}%",
                                className=alpha_color
                            )
                        ]),
                        html.P(f"Beta: {beta:.2f}")
                    ], width=6),
                    dbc.Col([
                        html.P(f"Hiệu suất {ticker}: {stock_change.iloc[-1]:.2f}%"),
                        html.P(f"Hiệu suất S&P 500: {market_change.iloc[-1]:.2f}%")
                    ], width=6)
                ]),
                html.Hr()
            ])
            report_sections.append(comparison_analysis)
        except:
            report_sections.append(html.Div([
                html.H4("Phân tích so sánh"),
                html.P("Không thể tải dữ liệu so sánh cho mã này"),
                html.Hr()
            ]))
    
    # Phân tích ngành
    if "sector" in analysis_types:
        try:
            # Lấy thông tin ngành từ thông tin cổ phiếu
            sector = info.get('sector', '')
            
            if sector:
                # Danh sách các ETF theo ngành
                sector_etfs = {
                    "Technology": "XLK",
                    "Financial": "XLF",
                    "Healthcare": "XLV", 
                    "Consumer Cyclical": "XLY",
                    "Consumer Defensive": "XLP",
                    "Energy": "XLE",
                    "Utilities": "XLU",
                    "Real Estate": "XLRE",
                    "Industrials": "XLI",
                    "Basic Materials": "XLB",
                    "Communication Services": "XLC"
                }
                
                # Lấy ETF tương ứng với ngành
                etf_ticker = sector_etfs.get(sector, None)
                
                if etf_ticker:
                    # Lấy dữ liệu ETF ngành
                    etf_data = get_stock_data(etf_ticker, period=timeframe)
                    
                    # Tính % thay đổi
                    etf_change = (etf_data['Close'].iloc[-1] / etf_data['Close'].iloc[0] - 1) * 100
                    
                    # Tạo biểu đồ so sánh với ngành
                    fig = go.Figure()
                    fig.add_trace(go.Scatter(x=stock_data.index, y=stock_change, mode='lines', name=ticker))
                    fig.add_trace(go.Scatter(x=etf_data.index, y=(etf_data['Close'] / etf_data['Close'].iloc[0]) * 100 - 100, mode='lines', name=f"{sector} (ETF: {etf_ticker})"))
                    
                    fig.update_layout(
                        title=f"So sánh với ngành {sector}",
                        xaxis_title="Ngày",
                        yaxis_title="% Thay đổi",
                        height=500,
                        hovermode='x unified'
                    )
                    
                    # Tính hiệu suất tương đối so với ngành
                    relative_performance = stock_change.iloc[-1] - etf_change
                    perf_color = "text-success" if relative_performance > 0 else "text-danger"
                    
                    sector_analysis = html.Div([
                        html.H4("Phân tích ngành"),
                        dcc.Graph(figure=fig),
                        dbc.Row([
                            dbc.Col([
                                html.P([
                                    f"Hiệu suất so với ngành: ",
                                    html.Span(
                                        f"{relative_performance:.2f}%",
                                        className=perf_color
                                    )
                                ]),
                                html.P(f"Vị thế trong ngành: {'Vượt trội' if relative_performance > 0 else 'Kém hơn'}")
                            ], width=6),
                            dbc.Col([
                                html.P(f"Hiệu suất {ticker}: {stock_change.iloc[-1]:.2f}%"),
                                html.P(f"Hiệu suất ngành {sector}: {etf_change:.2f}%")
                            ], width=6)
                        ]),
                        html.Hr()
                    ])
                    report_sections.append(sector_analysis)
                else:
                    report_sections.append(html.Div([
                        html.H4("Phân tích ngành"),
                        html.P(f"Không tìm thấy ETF cho ngành {sector}"),
                        html.Hr()
                    ]))
            else:
                report_sections.append(html.Div([
                    html.H4("Phân tích ngành"),
                    html.P("Không tìm thấy thông tin ngành cho mã này"),
                    html.Hr()
                ]))
        except:
            report_sections.append(html.Div([
                html.H4("Phân tích ngành"),
                html.P("Không thể tải dữ liệu ngành cho mã này"),
                html.Hr()
            ]))
    
    # Nút xuất báo cáo PDF
    export_button = dbc.Button("Xuất báo cáo PDF", id="export-pdf", color="success", className="mt-3")
    report_sections.append(export_button)
    
    return html.Div(report_sections)

VIII. Kết luận

Xây dựng một bảng điều khiển theo dõi thị trường với Dash và Plotly là một cách hiệu quả để tạo ra một công cụ phân tích thị trường mạnh mẽ mà không cần kiến thức chuyên sâu về phát triển web. Với hệ thống như vậy, các nhà đầu tư và giao dịch viên có thể:

  1. Theo dõi thị trường thời gian thực với các biểu đồ tương tác và chỉ báo kỹ thuật
  2. Phân tích kỹ thuật thông qua nhiều chỉ báo và công cụ
  3. So sánh hiệu suất của các mã cổ phiếu khác nhau
  4. Thiết lập cảnh báo để không bỏ lỡ các cơ hội giao dịch
  5. Tạo báo cáo phân tích toàn diện

Dash và Plotly cung cấp một nền tảng linh hoạt và mạnh mẽ để xây dựng các ứng dụng phân tích dữ liệu tương tác, đặc biệt phù hợp cho phân tích tài chính. Khả năng kết hợp mã Python thuần túy với các thành phần web tương tác giúp đơn giản hóa quá trình phát triển, cho phép tập trung vào logic phân tích thay vì chi tiết triển khai giao diện người dùng.

Mặc dù bài viết này đã trình bày một bảng điều khiển khá toàn diện, bạn vẫn có thể mở rộng và tùy chỉnh thêm dựa trên nhu cầu cụ thể của mình, như thêm các chiến lược giao dịch tự động, tích hợp với API của sàn giao dịch thực, hay áp dụng các mô hình học máy phức tạp hơn cho việc dự đoán thị trường.

← Xem tất cả bài viết · Trang chủ