Triển khai chiến lược đầu tư cổ tức với Python

2024-03-07 — QuantTrade

Triển khai chiến lược đầu tư cổ tức với Python

Giới Thiệu

Đầu tư cổ tức là một chiến lược đầu tư phổ biến, tập trung vào việc xây dựng danh mục các cổ phiếu trả cổ tức đều đặn và hấp dẫn. Chiến lược này đặc biệt phù hợp với nhà đầu tư dài hạn, những người tìm kiếm dòng thu nhập ổn định và sự tăng trưởng vốn từ từ theo thời gian. Python, với hệ sinh thái phong phú các thư viện phân tích dữ liệu và tài chính, là công cụ lý tưởng để triển khai, backtest và tối ưu hóa chiến lược đầu tư cổ tức.

Trong bài viết này, chúng ta sẽ khám phá cách sử dụng Python để:

  1. Phân tích và lựa chọn cổ phiếu trả cổ tức tốt
  2. Xây dựng và tối ưu hóa danh mục đầu tư cổ tức
  3. Backtest chiến lược theo dữ liệu lịch sử
  4. Theo dõi và tái cân bằng danh mục theo thời gian
  5. Tự động hóa quy trình xây dựng và quản lý danh mục đầu tư cổ tức

Phần 1: Chuẩn Bị Dữ Liệu và Công Cụ

1.1 Cài Đặt Các Thư Viện Cần Thiết

# Cài đặt các thư viện
# pip install pandas numpy matplotlib seaborn yfinance pandas-datareader plotly scikit-learn

# Import các thư viện
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import yfinance as yf
import pandas_datareader.data as web
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from datetime import datetime, timedelta
from sklearn.preprocessing import StandardScaler

1.2 Thu Thập Dữ Liệu Cổ Phiếu và Cổ Tức

Sử dụng Yahoo Finance API để lấy dữ liệu

def get_stock_data(ticker, start_date, end_date=None):
    """
    Lấy dữ liệu giá và cổ tức của một cổ phiếu
    
    Tham số:
    ---------
    ticker : str
        Mã cổ phiếu
    start_date : str
        Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
    end_date : str, tùy chọn
        Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
    
    Trả về:
    --------
    tuple
        (df_price, df_dividends) - Dữ liệu giá và dữ liệu cổ tức
    """
    if end_date is None:
        end_date = datetime.now().strftime('%Y-%m-%d')
    
    # Lấy thông tin cổ phiếu từ yfinance
    stock = yf.Ticker(ticker)
    
    # Lấy dữ liệu giá
    df_price = stock.history(start=start_date, end=end_date)
    
    # Lấy dữ liệu cổ tức
    df_dividends = stock.dividends.to_frame()
    df_dividends.columns = ['Dividend']
    df_dividends = df_dividends.loc[start_date:end_date]
    
    return df_price, df_dividends

# Ví dụ sử dụng hàm
ticker = 'VNM.HM'  # Vinamilk
start_date = '2018-01-01'
price_data, dividend_data = get_stock_data(ticker, start_date)

print(f"Dữ liệu giá {ticker}:")
print(price_data.head())
print(f"\nDữ liệu cổ tức {ticker}:")
print(dividend_data)

Thu thập dữ liệu cho nhiều cổ phiếu

def get_multiple_stocks_data(tickers, start_date, end_date=None):
    """
    Lấy dữ liệu giá và cổ tức cho nhiều cổ phiếu
    
    Tham số:
    ---------
    tickers : list
        Danh sách mã cổ phiếu
    start_date : str
        Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
    end_date : str, tùy chọn
        Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
    
    Trả về:
    --------
    dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    """
    if end_date is None:
        end_date = datetime.now().strftime('%Y-%m-%d')
    
    data = {}
    
    for ticker in tickers:
        try:
            price_data, dividend_data = get_stock_data(ticker, start_date, end_date)
            data[ticker] = {
                'price': price_data,
                'dividends': dividend_data
            }
            print(f"Đã lấy dữ liệu cho {ticker}")
        except Exception as e:
            print(f"Lỗi khi lấy dữ liệu cho {ticker}: {e}")
    
    return data

# Ví dụ với một vài cổ phiếu trả cổ tức tốt tại Việt Nam
vn_dividend_tickers = ['VNM.HM', 'MSN.HM', 'FPT.HM', 'PNJ.HM', 'MWG.HM']
stocks_data = get_multiple_stocks_data(vn_dividend_tickers, '2018-01-01')

1.3 Tính Toán Các Chỉ Số Cổ Tức Quan Trọng

def calculate_dividend_metrics(ticker, price_data, dividend_data):
    """
    Tính toán các chỉ số liên quan đến cổ tức
    
    Tham số:
    ---------
    ticker : str
        Mã cổ phiếu
    price_data : DataFrame
        Dữ liệu giá
    dividend_data : DataFrame
        Dữ liệu cổ tức
    
    Trả về:
    --------
    dict
        Từ điển chứa các chỉ số cổ tức
    """
    # Nếu không có dữ liệu cổ tức
    if dividend_data.empty:
        return {
            'ticker': ticker,
            'current_dividend_yield': 0,
            'average_dividend_yield': 0,
            'annual_dividend': 0,
            'payout_frequency': 0,
            'dividend_growth_rate': 0,
            'years_of_dividend_growth': 0,
            'payout_ratio': 0
        }
    
    # Giá hiện tại
    current_price = price_data['Close'].iloc[-1]
    
    # Tổng cổ tức trong 12 tháng gần nhất
    one_year_ago = datetime.now() - timedelta(days=365)
    recent_dividends = dividend_data[dividend_data.index >= one_year_ago.strftime('%Y-%m-%d')]
    ttm_dividend = recent_dividends['Dividend'].sum()
    
    # Tính tỷ suất cổ tức hiện tại
    current_dividend_yield = (ttm_dividend / current_price) * 100 if ttm_dividend > 0 else 0
    
    # Tính tần suất chi trả cổ tức (số lần/năm)
    years = (dividend_data.index[-1] - dividend_data.index[0]).days / 365
    payout_frequency = len(dividend_data) / years if years > 0 else 0
    
    # Tính tỷ suất cổ tức trung bình trong lịch sử
    dividend_yields = []
    for date, row in dividend_data.iterrows():
        # Tìm giá đóng cửa gần nhất trước ngày chia cổ tức
        closest_price_idx = price_data.index.get_indexer([date], method='pad')[0]
        if closest_price_idx >= 0:
            price = price_data['Close'].iloc[closest_price_idx]
            dividend_yield = (row['Dividend'] / price) * 100
            dividend_yields.append(dividend_yield)
    
    average_dividend_yield = np.mean(dividend_yields) if dividend_yields else 0
    
    # Tính tốc độ tăng trưởng cổ tức
    if len(dividend_data) > 1:
        # Nhóm theo năm và tính tổng cổ tức hàng năm
        annual_dividends = dividend_data.groupby(dividend_data.index.year)['Dividend'].sum()
        
        if len(annual_dividends) > 1:
            # Tính tốc độ tăng trưởng hàng năm
            first_year_div = annual_dividends.iloc[0]
            last_year_div = annual_dividends.iloc[-1]
            years_passed = len(annual_dividends) - 1
            
            # Sử dụng CAGR (Compound Annual Growth Rate)
            dividend_growth_rate = ((last_year_div / first_year_div) ** (1 / years_passed) - 1) * 100 if first_year_div > 0 else 0
            years_of_consecutive_growth = 0
            
            # Đếm số năm tăng trưởng cổ tức liên tiếp
            for i in range(1, len(annual_dividends)):
                if annual_dividends.iloc[i] > annual_dividends.iloc[i-1]:
                    years_of_consecutive_growth += 1
                else:
                    break
        else:
            dividend_growth_rate = 0
            years_of_consecutive_growth = 0
    else:
        dividend_growth_rate = 0
        years_of_consecutive_growth = 0
    
    # Lấy thông tin tài chính để tính tỷ lệ chi trả
    try:
        stock = yf.Ticker(ticker)
        info = stock.info
        
        # Tính tỷ lệ chi trả (Payout ratio)
        if 'trailingEps' in info and info['trailingEps'] and info['trailingEps'] > 0:
            payout_ratio = (ttm_dividend / info['trailingEps']) * 100
        else:
            payout_ratio = 0
    except:
        payout_ratio = 0
    
    return {
        'ticker': ticker,
        'current_dividend_yield': current_dividend_yield,
        'average_dividend_yield': average_dividend_yield,
        'annual_dividend': ttm_dividend,
        'payout_frequency': payout_frequency,
        'dividend_growth_rate': dividend_growth_rate,
        'years_of_dividend_growth': years_of_consecutive_growth,
        'payout_ratio': payout_ratio
    }

# Ví dụ tính toán chỉ số cổ tức cho một cổ phiếu
metrics = calculate_dividend_metrics('VNM.HM', price_data, dividend_data)
print(metrics)

Phần 2: Phân Tích và Lựa Chọn Cổ Phiếu Trả Cổ Tức

2.1 Tạo Bảng So Sánh Các Cổ Phiếu Theo Tiêu Chí Cổ Tức

def create_dividend_comparison_table(stocks_data):
    """
    Tạo bảng so sánh các cổ phiếu theo tiêu chí cổ tức
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    
    Trả về:
    --------
    DataFrame
        Bảng so sánh các cổ phiếu
    """
    metrics_list = []
    
    for ticker, data in stocks_data.items():
        metrics = calculate_dividend_metrics(ticker, data['price'], data['dividends'])
        metrics_list.append(metrics)
    
    # Tạo DataFrame từ list các metrics
    comparison_df = pd.DataFrame(metrics_list)
    
    # Sắp xếp theo tỷ suất cổ tức giảm dần
    comparison_df = comparison_df.sort_values('current_dividend_yield', ascending=False)
    
    return comparison_df

# Tạo bảng so sánh
comparison_table = create_dividend_comparison_table(stocks_data)
print(comparison_table)

# Hiển thị dạng biểu đồ cột
plt.figure(figsize=(14, 8))
sns.barplot(x='ticker', y='current_dividend_yield', data=comparison_table)
plt.title('So sánh tỷ suất cổ tức hiện tại giữa các cổ phiếu')
plt.xlabel('Mã cổ phiếu')
plt.ylabel('Tỷ suất cổ tức (%)')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

2.2 Phân Tích Lịch Sử Chi Trả Cổ Tức

def analyze_dividend_history(ticker, price_data, dividend_data):
    """
    Phân tích lịch sử chi trả cổ tức của một cổ phiếu
    
    Tham số:
    ---------
    ticker : str
        Mã cổ phiếu
    price_data : DataFrame
        Dữ liệu giá
    dividend_data : DataFrame
        Dữ liệu cổ tức
    """
    if dividend_data.empty:
        print(f"{ticker} không có dữ liệu cổ tức trong khoảng thời gian đã chọn.")
        return
    
    # Tính tỷ suất cổ tức tại thời điểm chi trả
    dividend_yields = []
    for date, row in dividend_data.iterrows():
        # Tìm giá đóng cửa gần nhất trước ngày chia cổ tức
        closest_price_idx = price_data.index.get_indexer([date], method='pad')[0]
        if closest_price_idx >= 0:
            price = price_data['Close'].iloc[closest_price_idx]
            dividend_yield = (row['Dividend'] / price) * 100
            dividend_yields.append({
                'date': date,
                'dividend': row['Dividend'],
                'price': price,
                'yield': dividend_yield
            })
    
    # Tạo DataFrame từ danh sách
    dividend_history = pd.DataFrame(dividend_yields)
    
    # Nhóm theo năm để phân tích xu hướng
    annual_dividends = dividend_data.groupby(dividend_data.index.year)['Dividend'].sum()
    
    # Vẽ biểu đồ lịch sử cổ tức
    fig = make_subplots(rows=2, cols=1, 
                       shared_xaxes=True,
                       subplot_titles=('Cổ tức theo thời gian', 'Tỷ suất cổ tức tại thời điểm chi trả'))
    
    # Cổ tức theo thời gian
    fig.add_trace(
        go.Bar(x=dividend_data.index, y=dividend_data['Dividend'], name='Cổ tức'),
        row=1, col=1
    )
    
    # Tỷ suất cổ tức
    if not dividend_history.empty:
        fig.add_trace(
            go.Scatter(x=dividend_history['date'], y=dividend_history['yield'], 
                      mode='markers+lines', name='Tỷ suất cổ tức (%)'),
            row=2, col=1
        )
    
    fig.update_layout(
        title=f'Lịch sử chi trả cổ tức của {ticker}',
        height=600,
        width=1000
    )
    
    fig.show()
    
    # In thống kê
    print(f"Thống kê cổ tức của {ticker}:")
    print(f"Tổng số lần chi trả cổ tức: {len(dividend_data)}")
    print(f"Cổ tức trung bình: {dividend_data['Dividend'].mean():.2f}")
    print(f"Tỷ suất cổ tức trung bình tại thời điểm chi trả: {dividend_history['yield'].mean():.2f}%")
    
    # In xu hướng cổ tức hàng năm
    print("\nCổ tức hàng năm:")
    for year, dividend in annual_dividends.items():
        print(f"{year}: {dividend:.2f}")

# Phân tích lịch sử chi trả cổ tức cho một cổ phiếu
analyze_dividend_history('VNM.HM', price_data, dividend_data)

2.3 Xây Dựng Bộ Lọc Cổ Phiếu Trả Cổ Tức

def filter_dividend_stocks(comparison_df, min_yield=3.0, min_frequency=2, min_growth_rate=0, max_payout_ratio=80):
    """
    Lọc cổ phiếu theo các tiêu chí về cổ tức
    
    Tham số:
    ---------
    comparison_df : DataFrame
        Bảng so sánh các cổ phiếu
    min_yield : float
        Tỷ suất cổ tức tối thiểu (%)
    min_frequency : int
        Tần suất chi trả tối thiểu (số lần/năm)
    min_growth_rate : float
        Tốc độ tăng trưởng cổ tức tối thiểu (%)
    max_payout_ratio : float
        Tỷ lệ chi trả tối đa (%)
    
    Trả về:
    --------
    DataFrame
        Các cổ phiếu thỏa mãn điều kiện
    """
    filtered = comparison_df[
        (comparison_df['current_dividend_yield'] >= min_yield) &
        (comparison_df['payout_frequency'] >= min_frequency) &
        (comparison_df['dividend_growth_rate'] >= min_growth_rate) &
        ((comparison_df['payout_ratio'] <= max_payout_ratio) | (comparison_df['payout_ratio'] == 0))
    ]
    
    return filtered

# Lọc cổ phiếu theo tiêu chí
filtered_stocks = filter_dividend_stocks(comparison_table, min_yield=2.0, min_frequency=1)
print("Các cổ phiếu thỏa mãn tiêu chí:")
print(filtered_stocks)

Phần 3: Xây Dựng và Tối Ưu Hóa Danh Mục Đầu Tư Cổ Tức

3.1 Phân Tích Tương Quan Giữa Các Cổ Phiếu

def analyze_correlation(stocks_data, selected_tickers):
    """
    Phân tích tương quan giữa các cổ phiếu
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    selected_tickers : list
        Danh sách mã cổ phiếu được chọn
    
    Trả về:
    --------
    DataFrame
        Ma trận tương quan
    """
    # Tạo DataFrame chứa giá đóng cửa của các cổ phiếu
    close_prices = pd.DataFrame()
    
    for ticker in selected_tickers:
        if ticker in stocks_data:
            close_prices[ticker] = stocks_data[ticker]['price']['Close']
    
    # Tính toán lợi nhuận hàng ngày
    returns = close_prices.pct_change().dropna()
    
    # Tính ma trận tương quan
    correlation_matrix = returns.corr()
    
    # Vẽ heatmap tương quan
    plt.figure(figsize=(10, 8))
    sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', linewidths=0.5)
    plt.title('Ma trận tương quan giữa các cổ phiếu')
    plt.tight_layout()
    plt.show()
    
    return correlation_matrix

# Phân tích tương quan giữa các cổ phiếu đã lọc
selected_tickers = filtered_stocks['ticker'].tolist()
correlation_matrix = analyze_correlation(stocks_data, selected_tickers)

3.2 Tối Ưu Hóa Danh Mục Đầu Tư Theo Tiêu Chí Đa Dạng Hóa và Tối Đa Cổ Tức

from scipy.optimize import minimize

def optimize_dividend_portfolio(stocks_data, selected_tickers, risk_tolerance=0.5):
    """
    Tối ưu hóa danh mục đầu tư cổ tức
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    selected_tickers : list
        Danh sách mã cổ phiếu được chọn
    risk_tolerance : float
        Mức chấp nhận rủi ro (0-1), 0 = ưu tiên giảm rủi ro, 1 = ưu tiên tối đa cổ tức
    
    Trả về:
    --------
    dict
        Trọng số của các cổ phiếu trong danh mục tối ưu
    """
    # Lấy dữ liệu tỷ suất cổ tức và tỷ suất lợi nhuận
    dividend_yields = []
    returns_data = pd.DataFrame()
    
    for ticker in selected_tickers:
        if ticker in stocks_data:
            # Lấy tỷ suất cổ tức
            price_data = stocks_data[ticker]['price']
            dividend_data = stocks_data[ticker]['dividends']
            metrics = calculate_dividend_metrics(ticker, price_data, dividend_data)
            dividend_yields.append(metrics['current_dividend_yield'])
            
            # Lấy lợi nhuận hàng ngày
            returns_data[ticker] = price_data['Close'].pct_change()
    
    # Xử lý dữ liệu thiếu
    returns_data = returns_data.dropna()
    
    # Tính ma trận hiệp phương sai
    cov_matrix = returns_data.cov() * 252  # Chuyển về hiệp phương sai hàng năm
    
    # Số lượng cổ phiếu
    n = len(selected_tickers)
    
    # Tạo hàm mục tiêu để tối đa hóa tỷ suất cổ tức và giảm thiểu rủi ro
    def objective(weights):
        # Tỷ suất cổ tức của danh mục
        portfolio_yield = np.sum(weights * dividend_yields)
        
        # Rủi ro (độ lệch chuẩn) của danh mục
        portfolio_risk = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
        
        # Hàm mục tiêu kết hợp: tối đa hóa tỷ suất cổ tức và giảm thiểu rủi ro
        # Sử dụng risk_tolerance để điều chỉnh mức độ ưu tiên
        return -(risk_tolerance * portfolio_yield - (1 - risk_tolerance) * portfolio_risk)
    
    # Ràng buộc: tổng trọng số = 1 và trọng số không âm
    constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
    bounds = tuple((0, 1) for _ in range(n))
    
    # Trọng số ban đầu (đều nhau)
    initial_weights = np.array([1/n] * n)
    
    # Thực hiện tối ưu hóa
    result = minimize(objective, initial_weights, method='SLSQP', bounds=bounds, constraints=constraints)
    
    # Trả về trọng số tối ưu
    optimal_weights = result['x']
    
    # Tính toán các chỉ số của danh mục tối ưu
    portfolio_yield = np.sum(optimal_weights * dividend_yields)
    portfolio_risk = np.sqrt(np.dot(optimal_weights.T, np.dot(cov_matrix, optimal_weights)))
    
    print("Danh mục đầu tư tối ưu:")
    print(f"Tỷ suất cổ tức ước tính: {portfolio_yield:.2f}%")
    print(f"Độ lệch chuẩn hàng năm: {portfolio_risk:.2f}%")
    
    # Tạo từ điển kết quả
    portfolio = {ticker: weight for ticker, weight in zip(selected_tickers, optimal_weights)}
    
    # Vẽ biểu đồ phân bổ
    plt.figure(figsize=(12, 6))
    plt.pie(optimal_weights, labels=selected_tickers, autopct='%1.1f%%', startangle=90, shadow=True)
    plt.title('Phân bổ danh mục đầu tư cổ tức tối ưu')
    plt.axis('equal')
    plt.tight_layout()
    plt.show()
    
    return portfolio

# Tối ưu hóa danh mục đầu tư
optimal_portfolio = optimize_dividend_portfolio(stocks_data, selected_tickers, risk_tolerance=0.7)
print("Trọng số tối ưu:")
for ticker, weight in sorted(optimal_portfolio.items(), key=lambda x: x[1], reverse=True):
    print(f"{ticker}: {weight*100:.2f}%")

3.3 Đánh Giá Hiệu Suất Lịch Sử của Danh Mục Đầu Tư

def evaluate_portfolio_performance(stocks_data, portfolio, start_date):
    """
    Đánh giá hiệu suất lịch sử của danh mục đầu tư
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    portfolio : dict
        Trọng số của các cổ phiếu trong danh mục
    start_date : str
        Ngày bắt đầu đánh giá (định dạng: 'YYYY-MM-DD')
    
    Trả về:
    --------
    DataFrame
        Dữ liệu hiệu suất của danh mục theo thời gian
    """
    # Lấy danh sách cổ phiếu trong danh mục
    tickers = list(portfolio.keys())
    
    # Tạo DataFrame chứa giá đóng cửa và cổ tức của các cổ phiếu
    portfolio_data = pd.DataFrame()
    
    # Lấy dữ liệu giá
    for ticker in tickers:
        if ticker in stocks_data:
            portfolio_data[f'{ticker}_price'] = stocks_data[ticker]['price']['Close']
    
    # Tính toán giá trị danh mục (giả định bắt đầu với 1 tỷ đồng)
    initial_investment = 1_000_000_000  # 1 tỷ đồng
    
    # Tính số lượng cổ phiếu ban đầu
    start_date_pd = pd.to_datetime(start_date)
    
    # Tìm ngày gần nhất sau start_date có dữ liệu
    closest_date = portfolio_data.index[portfolio_data.index >= start_date_pd][0]
    
    # Tính số lượng cổ phiếu mua ban đầu
    stock_quantities = {}
    for ticker in tickers:
        price = portfolio_data[f'{ticker}_price'].loc[closest_date]
        allocation = portfolio[ticker] * initial_investment
        quantity = allocation / price
        stock_quantities[ticker] = quantity
    
    # Tạo chuỗi giá trị danh mục theo thời gian
    portfolio_values = pd.Series(index=portfolio_data.index[portfolio_data.index >= closest_date])
    
    # Tính giá trị danh mục không bao gồm cổ tức
    for date in portfolio_values.index:
        value = 0
        for ticker in tickers:
            if date in portfolio_data.index:
                price = portfolio_data[f'{ticker}_price'].loc[date]
                value += stock_quantities[ticker] * price
        portfolio_values[date] = value
    
    # Tính tổng giá trị cổ tức nhận được
    total_dividends = 0
    dividend_cashflow = pd.Series(0, index=portfolio_values.index)
    
    for ticker in tickers:
        if ticker in stocks_data and not stocks_data[ticker]['dividends'].empty:
            div_data = stocks_data[ticker]['dividends']
            # Chỉ xét các cổ tức sau ngày bắt đầu
            div_data = div_data[div_data.index >= closest_date]
            
            for date, row in div_data.iterrows():
                dividend_per_share = row['Dividend']
                dividend_amount = stock_quantities[ticker] * dividend_per_share
                total_dividends += dividend_amount
                
                # Thêm vào chuỗi dòng tiền cổ tức
                if date in dividend_cashflow.index:
                    dividend_cashflow[date] += dividend_amount
    
    # Tạo DataFrame kết quả
    result = pd.DataFrame({
        'Portfolio_Value': portfolio_values,
        'Dividend_Cashflow': dividend_cashflow
    })
    
    # Tính lợi nhuận lũy kế từ cổ tức
    result['Cumulative_Dividends'] = dividend_cashflow.cumsum()
    
    # Tính tổng giá trị (bao gồm cả cổ tức)
    result['Total_Value'] = result['Portfolio_Value'] + result['Cumulative_Dividends']
    
    # Tính lợi nhuận
    result['Return'] = result['Total_Value'] / initial_investment - 1
    
    # Vẽ biểu đồ hiệu suất
    fig = make_subplots(rows=2, cols=1, 
                       shared_xaxes=True,
                       subplot_titles=('Giá trị danh mục theo thời gian', 'Dòng tiền cổ tức'))
    
    # Giá trị danh mục
    fig.add_trace(
        go.Scatter(x=result.index, y=result['Portfolio_Value'], name='Giá trị danh mục'),
        row=1, col=1
    )
    
    fig.add_trace(
        go.Scatter(x=result.index, y=result['Total_Value'], name='Tổng giá trị (bao gồm cổ tức)'),
        row=1, col=1
    )
    
    # Dòng tiền cổ tức
    fig.add_trace(
        go.Bar(x=result.index, y=result['Dividend_Cashflow'], name='Cổ tức theo kỳ'),
        row=2, col=1
    )
    
    fig.add_trace(
        go.Scatter(x=result.index, y=result['Cumulative_Dividends'], name='Cổ tức lũy kế'),
        row=2, col=1
    )
    
    fig.update_layout(
        title='Hiệu suất danh mục đầu tư cổ tức',
        height=800,
        width=1000
    )
    
    fig.show()
    
    # Tính các chỉ số hiệu suất
    years = (result.index[-1] - result.index[0]).days / 365
    annualized_return = (result['Return'].iloc[-1] + 1) ** (1 / years) - 1 if years > 0 else 0
    dividend_yield = total_dividends / initial_investment / years * 100 if years > 0 else 0
    
    print("Hiệu suất danh mục đầu tư:")
    print(f"Tổng lợi nhuận: {result['Return'].iloc[-1]*100:.2f}%")
    print(f"Lợi nhuận hàng năm: {annualized_return*100:.2f}%")
    print(f"Tổng cổ tức nhận được: {total_dividends:,.0f} đồng")
    print(f"Tỷ suất cổ tức thực tế hàng năm: {dividend_yield:.2f}%")
    print(f"Tỷ lệ cổ tức/tổng lợi nhuận: {(total_dividends / (result['Total_Value'].iloc[-1] - initial_investment) * 100):.2f}% (nếu tổng lợi nhuận > 0)")
    
    return result

# Đánh giá hiệu suất danh mục
portfolio_performance = evaluate_portfolio_performance(stocks_data, optimal_portfolio, '2018-01-01')

Phần 4: Theo Dõi và Tái Cân Bằng Danh Mục

4.1 Theo Dõi Chỉ Số Cổ Tức Theo Thời Gian

def track_dividend_metrics_over_time(stocks_data, portfolio, window=90):
    """
    Theo dõi các chỉ số cổ tức của danh mục theo thời gian
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    portfolio : dict
        Trọng số của các cổ phiếu trong danh mục
    window : int
        Cửa sổ thời gian (số ngày) để tính tỷ suất cổ tức
    
    Trả về:
    --------
    DataFrame
        Dữ liệu các chỉ số cổ tức theo thời gian
    """
    # Lấy danh sách cổ phiếu trong danh mục
    tickers = list(portfolio.keys())
    
    # Tạo DataFrame để lưu trữ kết quả
    all_dates = set()
    for ticker in tickers:
        if ticker in stocks_data:
            all_dates.update(stocks_data[ticker]['price'].index)
    
    all_dates = sorted(all_dates)
    metrics_over_time = pd.DataFrame(index=all_dates)
    
    # Tính tỷ suất cổ tức của danh mục theo thời gian
    portfolio_yield = pd.Series(index=all_dates)
    
    for date in all_dates:
        # Xem xét cửa sổ thời gian trước ngày hiện tại
        start_window = date - pd.Timedelta(days=window)
        
        weighted_yield = 0
        total_weight = 0
        
        for ticker in tickers:
            if ticker in stocks_data:
                # Lấy dữ liệu trong cửa sổ thời gian
                price_data = stocks_data[ticker]['price']
                dividend_data = stocks_data[ticker]['dividends']
                
                if date in price_data.index:
                    # Lấy giá đóng cửa tại ngày hiện tại
                    current_price = price_data.loc[date, 'Close']
                    
                    # Tính tổng cổ tức trong cửa sổ thời gian
                    window_dividends = dividend_data[(dividend_data.index >= start_window) & (dividend_data.index <= date)]
                    total_dividend = window_dividends['Dividend'].sum() if not window_dividends.empty else 0
                    
                    # Tính tỷ suất cổ tức (đã điều chỉnh theo năm)
                    annualized_factor = 365 / window
                    dividend_yield = (total_dividend / current_price) * 100 * annualized_factor if total_dividend > 0 else 0
                    
                    # Tính tỷ suất cổ tức có trọng số
                    weighted_yield += dividend_yield * portfolio[ticker]
                    total_weight += portfolio[ticker]
        
        # Lưu tỷ suất cổ tức của danh mục
        if total_weight > 0:
            portfolio_yield[date] = weighted_yield / total_weight
        else:
            portfolio_yield[date] = 0
    
    # Thêm vào DataFrame kết quả
    metrics_over_time['Portfolio_Dividend_Yield'] = portfolio_yield
    
    # Vẽ biểu đồ tỷ suất cổ tức theo thời gian
    plt.figure(figsize=(14, 6))
    plt.plot(metrics_over_time.index, metrics_over_time['Portfolio_Dividend_Yield'])
    plt.title(f'Tỷ suất cổ tức của danh mục theo thời gian (cửa sổ {window} ngày)')
    plt.xlabel('Ngày')
    plt.ylabel('Tỷ suất cổ tức (%)')
    plt.grid(True)
    plt.tight_layout()
    plt.show()
    
    return metrics_over_time

# Theo dõi chỉ số cổ tức theo thời gian
dividend_metrics = track_dividend_metrics_over_time(stocks_data, optimal_portfolio)

4.2 Tái Cân Bằng Danh Mục Đầu Tư

def rebalance_portfolio(stocks_data, current_portfolio, rebalance_date, risk_tolerance=0.5):
    """
    Tái cân bằng danh mục đầu tư
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    current_portfolio : dict
        Trọng số hiện tại của các cổ phiếu trong danh mục
    rebalance_date : str
        Ngày tái cân bằng (định dạng: 'YYYY-MM-DD')
    risk_tolerance : float
        Mức chấp nhận rủi ro (0-1)
    
    Trả về:
    --------
    dict
        Trọng số mới của các cổ phiếu trong danh mục
    """
    # Lọc dữ liệu đến ngày tái cân bằng
    rebalance_date_pd = pd.to_datetime(rebalance_date)
    
    filtered_data = {}
    for ticker, data in stocks_data.items():
        if ticker in current_portfolio:
            price_data = data['price'][data['price'].index <= rebalance_date_pd].copy()
            dividend_data = data['dividends'][data['dividends'].index <= rebalance_date_pd].copy()
            
            if not price_data.empty:
                filtered_data[ticker] = {
                    'price': price_data,
                    'dividends': dividend_data
                }
    
    # Tái tối ưu hóa danh mục
    tickers = list(current_portfolio.keys())
    new_portfolio = optimize_dividend_portfolio(filtered_data, tickers, risk_tolerance)
    
    # So sánh danh mục cũ và mới
    comparison = pd.DataFrame({
        'Trọng số cũ': pd.Series(current_portfolio),
        'Trọng số mới': pd.Series(new_portfolio)
    })
    
    comparison['Thay đổi'] = comparison['Trọng số mới'] - comparison['Trọng số cũ']
    comparison['Thay đổi (%)'] = comparison['Thay đổi'] * 100
    
    print(f"So sánh trọng số danh mục trước và sau khi tái cân bằng (ngày {rebalance_date}):")
    print(comparison)
    
    # Vẽ biểu đồ so sánh
    plt.figure(figsize=(12, 6))
    
    x = np.arange(len(tickers))
    width = 0.35
    
    plt.bar(x - width/2, [current_portfolio[t] * 100 for t in tickers], width, label='Trọng số cũ')
    plt.bar(x + width/2, [new_portfolio[t] * 100 for t in tickers], width, label='Trọng số mới')
    
    plt.xlabel('Cổ phiếu')
    plt.ylabel('Trọng số (%)')
    plt.title('So sánh trọng số danh mục trước và sau khi tái cân bằng')
    plt.xticks(x, tickers)
    plt.legend()
    plt.grid(True, axis='y')
    plt.tight_layout()
    plt.show()
    
    return new_portfolio

# Tái cân bằng danh mục sau 1 năm
rebalanced_portfolio = rebalance_portfolio(stocks_data, optimal_portfolio, '2019-01-01')

4.3 Đánh Giá Hiệu Suất Trước và Sau Khi Tái Cân Bằng

def compare_portfolio_performance(stocks_data, original_portfolio, rebalanced_portfolio, start_date, rebalance_date, end_date=None):
    """
    So sánh hiệu suất danh mục trước và sau khi tái cân bằng
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    original_portfolio : dict
        Trọng số ban đầu của các cổ phiếu trong danh mục
    rebalanced_portfolio : dict
        Trọng số sau khi tái cân bằng
    start_date : str
        Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
    rebalance_date : str
        Ngày tái cân bằng (định dạng: 'YYYY-MM-DD')
    end_date : str, tùy chọn
        Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
    
    Trả về:
    --------
    tuple
        (original_performance, rebalanced_performance)
    """
    if end_date is None:
        end_date = datetime.now().strftime('%Y-%m-%d')
    
    # Đánh giá hiệu suất danh mục ban đầu (không tái cân bằng)
    original_performance = evaluate_portfolio_performance(stocks_data, original_portfolio, start_date)
    
    # Mô phỏng danh mục tái cân bằng
    # 1. Hiệu suất đến ngày tái cân bằng
    performance_to_rebalance = evaluate_portfolio_performance(stocks_data, original_portfolio, start_date)
    performance_to_rebalance = performance_to_rebalance[performance_to_rebalance.index <= pd.to_datetime(rebalance_date)]
    
    # 2. Hiệu suất sau khi tái cân bằng
    performance_after_rebalance = evaluate_portfolio_performance(stocks_data, rebalanced_portfolio, rebalance_date)
    
    # 3. Kết hợp hai khoảng thời gian
    # Giả định: Giá trị cuối cùng trước khi tái cân bằng = giá trị ban đầu sau khi tái cân bằng
    scaling_factor = performance_to_rebalance['Total_Value'].iloc[-1] / performance_after_rebalance['Total_Value'].iloc[0]
    
    rebalanced_performance = pd.concat([
        performance_to_rebalance,
        performance_after_rebalance[['Portfolio_Value', 'Dividend_Cashflow', 'Cumulative_Dividends', 'Total_Value']] * scaling_factor
    ])
    
    # Tính lại lợi nhuận
    rebalanced_performance['Return'] = rebalanced_performance['Total_Value'] / performance_to_rebalance['Total_Value'].iloc[0] - 1
    
    # Vẽ biểu đồ so sánh
    plt.figure(figsize=(14, 7))
    plt.plot(original_performance.index, original_performance['Return'] * 100, label='Danh mục không tái cân bằng')
    plt.plot(rebalanced_performance.index, rebalanced_performance['Return'] * 100, label='Danh mục có tái cân bằng')
    
    # Đánh dấu thời điểm tái cân bằng
    plt.axvline(x=pd.to_datetime(rebalance_date), color='r', linestyle='--', label='Thời điểm tái cân bằng')
    
    plt.title('So sánh hiệu suất danh mục trước và sau khi tái cân bằng')
    plt.xlabel('Ngày')
    plt.ylabel('Lợi nhuận (%)')
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()
    
    # So sánh chỉ số hiệu suất
    original_final_return = original_performance['Return'].iloc[-1]
    rebalanced_final_return = rebalanced_performance['Return'].iloc[-1]
    
    original_years = (original_performance.index[-1] - original_performance.index[0]).days / 365
    rebalanced_years = (rebalanced_performance.index[-1] - rebalanced_performance.index[0]).days / 365
    
    original_annualized = (original_final_return + 1) ** (1 / original_years) - 1 if original_years > 0 else 0
    rebalanced_annualized = (rebalanced_final_return + 1) ** (1 / rebalanced_years) - 1 if rebalanced_years > 0 else 0
    
    print("So sánh hiệu suất:")
    print(f"Tổng lợi nhuận (không tái cân bằng): {original_final_return*100:.2f}%")
    print(f"Tổng lợi nhuận (có tái cân bằng): {rebalanced_final_return*100:.2f}%")
    print(f"Lợi nhuận hàng năm (không tái cân bằng): {original_annualized*100:.2f}%")
    print(f"Lợi nhuận hàng năm (có tái cân bằng): {rebalanced_annualized*100:.2f}%")
    
    return original_performance, rebalanced_performance

# So sánh hiệu suất trước và sau khi tái cân bằng
original_perf, rebalanced_perf = compare_portfolio_performance(
    stocks_data, optimal_portfolio, rebalanced_portfolio, '2018-01-01', '2019-01-01', '2020-01-01'
)

Phần 5: Tự Động Hóa Quy Trình

5.1 Xây Dựng Lịch Trình Tự Động Cập Nhật Dữ Liệu

def create_data_update_schedule(tickers, update_interval=7, data_file='dividend_stocks_data.pkl'):
    """
    Tạo lịch trình tự động cập nhật dữ liệu
    
    Tham số:
    ---------
    tickers : list
        Danh sách mã cổ phiếu cần theo dõi
    update_interval : int
        Số ngày giữa các lần cập nhật
    data_file : str
        Tên file để lưu dữ liệu
    
    Trả về:
    --------
    dict
        Lịch trình cập nhật
    """
    # Tạo lịch trình
    schedule = {
        'tickers': tickers,
        'update_interval': update_interval,
        'last_update': datetime.now().strftime('%Y-%m-%d'),
        'next_update': (datetime.now() + timedelta(days=update_interval)).strftime('%Y-%m-%d'),
        'data_file': data_file
    }
    
    # Lưu lịch trình vào file
    import pickle
    with open('dividend_portfolio_schedule.pkl', 'wb') as f:
        pickle.dump(schedule, f)
    
    print(f"Đã tạo lịch trình cập nhật dữ liệu:")
    print(f"- Danh sách cổ phiếu: {tickers}")
    print(f"- Tần suất cập nhật: {update_interval} ngày")
    print(f"- Lần cập nhật tiếp theo: {schedule['next_update']}")
    
    return schedule

def update_stock_data(schedule):
    """
    Cập nhật dữ liệu cổ phiếu theo lịch trình
    
    Tham số:
    ---------
    schedule : dict
        Lịch trình cập nhật
    
    Trả về:
    --------
    dict
        Dữ liệu cổ phiếu đã cập nhật
    """
    import pickle
    from datetime import datetime
    
    today = datetime.now().strftime('%Y-%m-%d')
    next_update = schedule['next_update']
    
    # Kiểm tra xem có cần cập nhật không
    if pd.to_datetime(today) < pd.to_datetime(next_update):
        print(f"Chưa đến thời điểm cập nhật tiếp theo ({next_update})")
        
        # Thử tải dữ liệu từ file nếu có
        try:
            with open(schedule['data_file'], 'rb') as f:
                stocks_data = pickle.load(f)
            print(f"Đã tải dữ liệu từ file {schedule['data_file']}")
            return stocks_data
        except:
            print(f"Không tìm thấy file dữ liệu {schedule['data_file']}, sẽ tải dữ liệu mới")
    else:
        print(f"Đến thời điểm cập nhật dữ liệu ({today} >= {next_update})")
    
    # Tải dữ liệu mới
    start_date = '2018-01-01'  # Có thể điều chỉnh tùy theo nhu cầu
    stocks_data = get_multiple_stocks_data(schedule['tickers'], start_date)
    
    # Lưu dữ liệu vào file
    with open(schedule['data_file'], 'wb') as f:
        pickle.dump(stocks_data, f)
    
    # Cập nhật lịch trình
    schedule['last_update'] = today
    schedule['next_update'] = (datetime.now() + timedelta(days=schedule['update_interval'])).strftime('%Y-%m-%d')
    
    # Lưu lịch trình đã cập nhật
    with open('dividend_portfolio_schedule.pkl', 'wb') as f:
        pickle.dump(schedule, f)
    
    print(f"Đã cập nhật dữ liệu thành công")
    print(f"Lần cập nhật tiếp theo: {schedule['next_update']}")
    
    return stocks_data

# Tạo lịch trình tự động cập nhật
all_tickers = vn_dividend_tickers
schedule = create_data_update_schedule(all_tickers, update_interval=30)

# Cập nhật dữ liệu
updated_stocks_data = update_stock_data(schedule)

5.2 Tự Động Hóa Báo Cáo Hiệu Suất và Tái Cân Bằng

def generate_portfolio_report(stocks_data, portfolio, start_date, title="Báo Cáo Danh Mục Đầu Tư Cổ Tức"):
    """
    Tạo báo cáo hiệu suất danh mục đầu tư
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    portfolio : dict
        Trọng số của các cổ phiếu trong danh mục
    start_date : str
        Ngày bắt đầu đánh giá (định dạng: 'YYYY-MM-DD')
    title : str
        Tiêu đề báo cáo
    
    Trả về:
    --------
    str
        Đường dẫn đến file báo cáo HTML
    """
    from datetime import datetime
    import os
    
    # Tạo thời gian báo cáo
    report_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
    
    # Tính toán hiệu suất
    performance = evaluate_portfolio_performance(stocks_data, portfolio, start_date)
    
    # Tạo bảng phân bổ danh mục
    allocation_data = []
    for ticker, weight in portfolio.items():
        if ticker in stocks_data:
            current_price = stocks_data[ticker]['price']['Close'].iloc[-1]
            
            # Tính tỷ suất cổ tức
            price_data = stocks_data[ticker]['price']
            dividend_data = stocks_data[ticker]['dividends']
            metrics = calculate_dividend_metrics(ticker, price_data, dividend_data)
            
            allocation_data.append({
                'Cổ phiếu': ticker,
                'Trọng số': f"{weight*100:.2f}%",
                'Giá hiện tại': f"{current_price:,.0f}",
                'Tỷ suất cổ tức': f"{metrics['current_dividend_yield']:.2f}%",
                'Tăng trưởng cổ tức': f"{metrics['dividend_growth_rate']:.2f}%"
            })
    
    allocation_df = pd.DataFrame(allocation_data)
    
    # Tính các chỉ số hiệu suất
    years = (performance.index[-1] - performance.index[0]).days / 365
    annualized_return = (performance['Return'].iloc[-1] + 1) ** (1 / years) - 1 if years > 0 else 0
    total_dividends = performance['Cumulative_Dividends'].iloc[-1]
    initial_investment = performance['Portfolio_Value'].iloc[0]
    current_value = performance['Total_Value'].iloc[-1]
    dividend_yield = total_dividends / initial_investment / years * 100 if years > 0 else 0
    
    # Tạo nội dung HTML
    html_content = f"""
    <!DOCTYPE html>
    <html>
    <head>
        <title>{title}</title>
        <style>
            body {{ font-family: Arial, sans-serif; margin: 20px; }}
            .header {{ background-color: #4CAF50; color: white; padding: 10px; text-align: center; }}
            .section {{ margin-top: 20px; }}
            table {{ border-collapse: collapse; width: 100%; }}
            th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
            th {{ background-color: #f2f2f2; }}
            tr:nth-child(even) {{ background-color: #f9f9f9; }}
            .performance {{ display: flex; flex-wrap: wrap; }}
            .metric {{ flex: 1; min-width: 200px; border: 1px solid #ddd; padding: 10px; margin: 5px; text-align: center; }}
            .metric h3 {{ margin-top: 0; color: #333; }}
            .metric p {{ font-size: 24px; font-weight: bold; color: #4CAF50; }}
        </style>
    </head>
    <body>
        <div class="header">
            <h1>{title}</h1>
            <p>Ngày báo cáo: {report_time}</p>
        </div>
        
        <div class="section">
            <h2>Phân bổ danh mục</h2>
            <table>
                <tr>
                    {''.join([f"<th>{col}</th>" for col in allocation_df.columns])}
                </tr>
                {''.join([f"<tr>{''.join([f'<td>{value}</td>' for value in row.values])}</tr>" for _, row in allocation_df.iterrows()])}
            </table>
        </div>
        
        <div class="section">
            <h2>Hiệu suất danh mục</h2>
            <div class="performance">
                <div class="metric">
                    <h3>Tổng lợi nhuận</h3>
                    <p>{performance['Return'].iloc[-1]*100:.2f}%</p>
                </div>
                <div class="metric">
                    <h3>Lợi nhuận hàng năm</h3>
                    <p>{annualized_return*100:.2f}%</p>
                </div>
                <div class="metric">
                    <h3>Tổng cổ tức</h3>
                    <p>{total_dividends:,.0f} đồng</p>
                </div>
                <div class="metric">
                    <h3>Tỷ suất cổ tức thực tế</h3>
                    <p>{dividend_yield:.2f}%/năm</p>
                </div>
            </div>
        </div>
        
        <div class="section">
            <h2>Giá trị danh mục</h2>
            <div class="performance">
                <div class="metric">
                    <h3>Vốn ban đầu</h3>
                    <p>{initial_investment:,.0f} đồng</p>
                </div>
                <div class="metric">
                    <h3>Giá trị hiện tại</h3>
                    <p>{current_value:,.0f} đồng</p>
                </div>
                <div class="metric">
                    <h3>Tăng trưởng giá trị</h3>
                    <p>{(current_value - initial_investment):,.0f} đồng</p>
                </div>
                <div class="metric">
                    <h3>% Cổ tức / Tổng lợi nhuận</h3>
                    <p>{(total_dividends / (current_value - initial_investment) * 100) if current_value > initial_investment else 0:.2f}%</p>
                </div>
            </div>
        </div>
        
        <div class="section">
            <h2>Khuyến nghị</h2>
            <p>Dựa trên phân tích hiện tại, danh mục đầu tư cổ tức của bạn:</p>
            <ul>
                <li>{'<span style="color: green;">Đang hoạt động tốt với tỷ suất cổ tức cao hơn thị trường</span>' if dividend_yield > 3 else '<span style="color: orange;">Có tỷ suất cổ tức thấp hơn mức mục tiêu 3%</span>'}</li>
                <li>{'<span style="color: green;">Đang tạo ra lợi nhuận tích cực</span>' if performance['Return'].iloc[-1] > 0 else '<span style="color: red;">Đang gặp khó khăn với lợi nhuận âm</span>'}</li>
                <li>{'<span style="color: green;">Đóng góp cổ tức chiếm tỷ trọng lớn trong tổng lợi nhuận</span>' if total_dividends > 0 and current_value > initial_investment and total_dividends / (current_value - initial_investment) > 0.3 else '<span style="color: orange;">Đóng góp từ cổ tức còn hạn chế trong tổng lợi nhuận</span>'}</li>
            </ul>
            <p><strong>Hành động khuyến nghị:</strong></p>
            <ul>
                <li>{'<span style="color: green;">Duy trì danh mục hiện tại</span>' if performance['Return'].iloc[-1] > 0 and dividend_yield > 3 else '<span style="color: orange;">Xem xét tái cân bằng danh mục để tối ưu hóa tỷ suất cổ tức</span>' if dividend_yield < 3 else '<span style="color: orange;">Xem xét bổ sung các cổ phiếu có tiềm năng tăng trưởng cổ tức tốt hơn</span>'}</li>
                <li>Theo dõi chặt chẽ lịch trả cổ tức sắp tới và các thông báo về thay đổi chính sách cổ tức</li>
                <li>Xem xét tái đầu tư cổ tức nhận được để tận dụng sức mạnh của lãi kép</li>
            </ul>
        </div>
    </body>
    </html>
    """
    
    # Lưu nội dung HTML vào file
    report_file = f"dividend_portfolio_report_{datetime.now().strftime('%Y%m%d')}.html"
    with open(report_file, 'w', encoding='utf-8') as f:
        f.write(html_content)
    
    print(f"Đã tạo báo cáo: {report_file}")
    return report_file

def schedule_portfolio_maintenance(portfolio, rebalance_interval=180, report_interval=90):
    """
    Lên lịch bảo trì danh mục đầu tư
    
    Tham số:
    ---------
    portfolio : dict
        Trọng số của các cổ phiếu trong danh mục
    rebalance_interval : int
        Số ngày giữa các lần tái cân bằng
    report_interval : int
        Số ngày giữa các báo cáo
    
    Trả về:
    --------
    dict
        Lịch trình bảo trì
    """
    today = datetime.now()
    
    maintenance_schedule = {
        'portfolio': portfolio,
        'last_rebalance': today.strftime('%Y-%m-%d'),
        'next_rebalance': (today + timedelta(days=rebalance_interval)).strftime('%Y-%m-%d'),
        'last_report': today.strftime('%Y-%m-%d'),
        'next_report': (today + timedelta(days=report_interval)).strftime('%Y-%m-%d'),
        'rebalance_interval': rebalance_interval,
        'report_interval': report_interval
    }
    
    # Lưu lịch trình
    import pickle
    with open('portfolio_maintenance_schedule.pkl', 'wb') as f:
        pickle.dump(maintenance_schedule, f)
    
    print(f"Đã lên lịch bảo trì danh mục đầu tư:")
    print(f"- Tái cân bằng kế tiếp: {maintenance_schedule['next_rebalance']} (mỗi {rebalance_interval} ngày)")
    print(f"- Báo cáo kế tiếp: {maintenance_schedule['next_report']} (mỗi {report_interval} ngày)")
    
    return maintenance_schedule

def check_and_perform_maintenance(schedule, stocks_data, start_date):
    """
    Kiểm tra và thực hiện bảo trì danh mục theo lịch trình
    
    Tham số:
    ---------
    schedule : dict
        Lịch trình bảo trì
    stocks_data : dict
        Dữ liệu cổ phiếu
    start_date : str
        Ngày bắt đầu đánh giá
    
    Trả về:
    --------
    dict
        Lịch trình đã cập nhật
    """
    import pickle
    from datetime import datetime
    
    today = datetime.now().strftime('%Y-%m-%d')
    portfolio = schedule['portfolio']
    
    # Kiểm tra báo cáo
    if pd.to_datetime(today) >= pd.to_datetime(schedule['next_report']):
        print(f"Đến thời điểm tạo báo cáo ({today} >= {schedule['next_report']})")
        
        # Tạo báo cáo
        report_file = generate_portfolio_report(stocks_data, portfolio, start_date)
        
        # Cập nhật lịch trình báo cáo
        schedule['last_report'] = today
        schedule['next_report'] = (datetime.now() + timedelta(days=schedule['report_interval'])).strftime('%Y-%m-%d')
        print(f"Báo cáo kế tiếp: {schedule['next_report']}")
    
    # Kiểm tra tái cân bằng
    if pd.to_datetime(today) >= pd.to_datetime(schedule['next_rebalance']):
        print(f"Đến thời điểm tái cân bằng danh mục ({today} >= {schedule['next_rebalance']})")
        
        # Tái cân bằng danh mục
        new_portfolio = rebalance_portfolio(stocks_data, portfolio, today)
        
        # Cập nhật danh mục
        schedule['portfolio'] = new_portfolio
        
        # Cập nhật lịch trình tái cân bằng
        schedule['last_rebalance'] = today
        schedule['next_rebalance'] = (datetime.now() + timedelta(days=schedule['rebalance_interval'])).strftime('%Y-%m-%d')
        print(f"Tái cân bằng kế tiếp: {schedule['next_rebalance']}")
    
    # Lưu lịch trình đã cập nhật
    with open('portfolio_maintenance_schedule.pkl', 'wb') as f:
        pickle.dump(schedule, f)
    
    return schedule

# Tạo lịch trình bảo trì
maintenance_schedule = schedule_portfolio_maintenance(optimal_portfolio, rebalance_interval=180, report_interval=90)

# Kiểm tra và thực hiện bảo trì
updated_schedule = check_and_perform_maintenance(maintenance_schedule, updated_stocks_data, '2018-01-01')

5.3 Tạo Script Chạy Định Kỳ

def create_automation_script():
    """
    Tạo script tự động hóa toàn bộ quy trình
    """
    script_content = """
import os
import pickle
import pandas as pd
from datetime import datetime, timedelta

# Import các hàm đã định nghĩa
# (Giả sử đã lưu tất cả các hàm vào file dividend_portfolio_functions.py)
from dividend_portfolio_functions import (
    get_multiple_stocks_data, update_stock_data, check_and_perform_maintenance
)

def run_automation():
    # Kiểm tra và tải lịch trình
    try:
        with open('dividend_portfolio_schedule.pkl', 'rb') as f:
            data_schedule = pickle.load(f)
        
        with open('portfolio_maintenance_schedule.pkl', 'rb') as f:
            maintenance_schedule = pickle.load(f)
    except FileNotFoundError:
        print("Không tìm thấy file lịch trình. Hãy thiết lập lịch trình trước.")
        return
    
    # Cập nhật dữ liệu
    stocks_data = update_stock_data(data_schedule)
    
    # Thực hiện bảo trì danh mục
    start_date = '2018-01-01'  # Hoặc bất kỳ ngày nào phù hợp
    updated_schedule = check_and_perform_maintenance(maintenance_schedule, stocks_data, start_date)
    
    print("Tự động hóa hoàn tất!")

if __name__ == "__main__":
    print(f"Bắt đầu tự động hóa quy trình lúc {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    run_automation()
    """
    
    # Lưu script
    with open('run_dividend_portfolio_automation.py', 'w') as f:
        f.write(script_content)
    
    # Tạo file chứa các hàm
    functions_content = """
# Export tất cả các hàm đã định nghĩa
# (Sao chép tất cả các định nghĩa hàm vào đây)
"""
    
    with open('dividend_portfolio_functions.py', 'w') as f:
        f.write(functions_content)
    
    print("Đã tạo các file tự động hóa:")
    print("1. run_dividend_portfolio_automation.py - Script chính để chạy tự động")
    print("2. dividend_portfolio_functions.py - File chứa các hàm (cần bổ sung các định nghĩa hàm)")
    
    # Hướng dẫn thiết lập cron job
    print("\nĐể chạy script định kỳ, bạn có thể thiết lập cron job (Linux/Mac) hoặc Task Scheduler (Windows).")
    print("Ví dụ crontab để chạy mỗi ngày lúc 8 giờ sáng:")
    print("0 8 * * * /usr/bin/python3 /đường/dẫn/đến/run_dividend_portfolio_automation.py >> /đường/dẫn/đến/dividend_automation.log 2>&1")

# Tạo script tự động hóa
create_automation_script()

Phần 6: Phân Tích Nâng Cao

6.1 Phân Tích Độ Nhạy của Tham Số Tối Ưu

def sensitivity_analysis(stocks_data, selected_tickers, parameter_ranges):
    """
    Phân tích độ nhạy của các tham số tối ưu
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    selected_tickers : list
        Danh sách mã cổ phiếu được chọn
    parameter_ranges : dict
        Phạm vi các tham số cần phân tích (ví dụ: {'risk_tolerance': [0.1, 0.3, 0.5, 0.7, 0.9]})
    
    Trả về:
    --------
    DataFrame
        Kết quả phân tích
    """
    import itertools
    
    # Khởi tạo list để lưu kết quả
    results = []
    
    # Tạo tất cả tổ hợp tham số
    param_names = list(parameter_ranges.keys())
    param_values = list(parameter_ranges.values())
    param_combinations = list(itertools.product(*param_values))
    
    for combo in param_combinations:
        # Tạo từ điển tham số
        params = {name: value for name, value in zip(param_names, combo)}
        
        # Thực hiện tối ưu hóa với tham số hiện tại
        # Ví dụ với risk_tolerance
        if 'risk_tolerance' in params:
            portfolio = optimize_dividend_portfolio(
                stocks_data, selected_tickers, risk_tolerance=params['risk_tolerance']
            )
            
            # Đánh giá hiệu suất
            performance = evaluate_portfolio_performance(stocks_data, portfolio, '2018-01-01')
            
            # Tính các chỉ số hiệu suất
            total_return = performance['Return'].iloc[-1] * 100
            years = (performance.index[-1] - performance.index[0]).days / 365
            annualized_return = ((total_return / 100 + 1) ** (1 / years) - 1) * 100 if years > 0 else 0
            
            # Tính tỷ suất cổ tức danh mục
            weighted_yield = 0
            for ticker, weight in portfolio.items():
                if ticker in stocks_data:
                    price_data = stocks_data[ticker]['price']
                    dividend_data = stocks_data[ticker]['dividends']
                    metrics = calculate_dividend_metrics(ticker, price_data, dividend_data)
                    weighted_yield += metrics['current_dividend_yield'] * weight
            
            # Lưu kết quả
            result = {
                'risk_tolerance': params['risk_tolerance'],
                'dividend_yield': weighted_yield,
                'total_return': total_return,
                'annualized_return': annualized_return
            }
            
            # Thêm trọng số của từng cổ phiếu
            for ticker in selected_tickers:
                result[f'{ticker}_weight'] = portfolio.get(ticker, 0) * 100
            
            results.append(result)
    
    # Tạo DataFrame từ kết quả
    sensitivity_df = pd.DataFrame(results)
    
    # Vẽ biểu đồ kết quả
    plt.figure(figsize=(15, 10))
    
    # 1. Biểu đồ quan hệ giữa risk_tolerance và tỷ suất cổ tức
    plt.subplot(2, 2, 1)
    plt.plot(sensitivity_df['risk_tolerance'], sensitivity_df['dividend_yield'], marker='o')
    plt.title('Risk Tolerance vs Dividend Yield')
    plt.xlabel('Risk Tolerance')
    plt.ylabel('Dividend Yield (%)')
    plt.grid(True)
    
    # 2. Biểu đồ quan hệ giữa risk_tolerance và lợi nhuận hàng năm
    plt.subplot(2, 2, 2)
    plt.plot(sensitivity_df['risk_tolerance'], sensitivity_df['annualized_return'], marker='o')
    plt.title('Risk Tolerance vs Annualized Return')
    plt.xlabel('Risk Tolerance')
    plt.ylabel('Annualized Return (%)')
    plt.grid(True)
    
    # 3. Biểu đồ stacked cho phân bổ danh mục
    plt.subplot(2, 2, 3)
    weight_columns = [col for col in sensitivity_df.columns if '_weight' in col]
    sensitivity_df[weight_columns].plot(kind='bar', stacked=True, ax=plt.gca())
    plt.title('Portfolio Allocation by Risk Tolerance')
    plt.xlabel('Sample Index')
    plt.ylabel('Allocation (%)')
    plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=3)
    
    # 4. Biểu đồ lợi nhuận vs tỷ suất cổ tức
    plt.subplot(2, 2, 4)
    plt.scatter(sensitivity_df['dividend_yield'], sensitivity_df['annualized_return'])
    
    # Thêm chú thích cho từng điểm
    for i, row in sensitivity_df.iterrows():
        plt.annotate(f"RT={row['risk_tolerance']}", 
                    (row['dividend_yield'], row['annualized_return']),
                    textcoords="offset points", 
                    xytext=(0,10), 
                    ha='center')
    
    plt.title('Dividend Yield vs Annualized Return')
    plt.xlabel('Dividend Yield (%)')
    plt.ylabel('Annualized Return (%)')
    plt.grid(True)
    
    plt.tight_layout()
    plt.show()
    
    return sensitivity_df

# Phân tích độ nhạy
parameter_ranges = {
    'risk_tolerance': [0.1, 0.3, 0.5, 0.7, 0.9]
}
sensitivity_results = sensitivity_analysis(stocks_data, selected_tickers, parameter_ranges)

6.2 So Sánh Với Chiến Lược Đầu Tư Khác

def compare_with_other_strategies(stocks_data, dividend_portfolio, start_date, end_date=None):
    """
    So sánh chiến lược đầu tư cổ tức với các chiến lược khác
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    dividend_portfolio : dict
        Trọng số của các cổ phiếu trong danh mục cổ tức
    start_date : str
        Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
    end_date : str, tùy chọn
        Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
    
    Trả về:
    --------
    DataFrame
        Kết quả so sánh các chiến lược
    """
    if end_date is None:
        end_date = datetime.now().strftime('%Y-%m-%d')
    
    # Đánh giá hiệu suất danh mục cổ tức
    dividend_performance = evaluate_portfolio_performance(stocks_data, dividend_portfolio, start_date)
    
    # Tạo danh mục theo chiến lược tăng trưởng (chọn các cổ phiếu có tăng trưởng giá cao nhất)
    growth_tickers = []
    growth_rates = {}
    
    for ticker in stocks_data:
        price_data = stocks_data[ticker]['price']
        if not price_data.empty:
            start_price = price_data[price_data.index >= pd.to_datetime(start_date)]['Close'].iloc[0]
            end_price = price_data[price_data.index <= pd.to_datetime(end_date)]['Close'].iloc[-1]
            growth_rate = (end_price / start_price - 1) * 100
            growth_rates[ticker] = growth_rate
            growth_tickers.append(ticker)
    
    # Sắp xếp theo tốc độ tăng trưởng giảm dần
    growth_tickers = sorted(growth_tickers, key=lambda x: growth_rates.get(x, 0), reverse=True)
    
    # Chọn top 5 cổ phiếu tăng trưởng mạnh nhất
    top_growth_tickers = growth_tickers[:5]
    
    # Tạo danh mục tăng trưởng (trọng số bằng nhau)
    growth_portfolio = {ticker: 1/len(top_growth_tickers) for ticker in top_growth_tickers}
    
    # Đánh giá hiệu suất danh mục tăng trưởng
    growth_performance = evaluate_portfolio_performance(stocks_data, growth_portfolio, start_date)
    
    # Tạo danh mục theo chiến lược cân bằng (kết hợp giữa tăng trưởng và cổ tức)
    # Chọn cả cổ phiếu trả cổ tức và cổ phiếu tăng trưởng
    balanced_tickers = list(set(list(dividend_portfolio.keys()) + top_growth_tickers))
    balanced_portfolio = {ticker: 1/len(balanced_tickers) for ticker in balanced_tickers}
    
    # Đánh giá hiệu suất danh mục cân bằng
    balanced_performance = evaluate_portfolio_performance(stocks_data, balanced_portfolio, start_date)
    
    # Vẽ biểu đồ so sánh
    plt.figure(figsize=(12, 6))
    
    plt.plot(dividend_performance.index, dividend_performance['Return'] * 100, label='Chiến lược cổ tức')
    plt.plot(growth_performance.index, growth_performance['Return'] * 100, label='Chiến lược tăng trưởng')
    plt.plot(balanced_performance.index, balanced_performance['Return'] * 100, label='Chiến lược cân bằng')
    
    plt.title('So sánh hiệu suất các chiến lược đầu tư')
    plt.xlabel('Ngày')
    plt.ylabel('Lợi nhuận (%)')
    plt.legend()
    plt.grid(True)
    plt.tight_layout()
    plt.show()
    
    # Tính các chỉ số hiệu suất
    years = (dividend_performance.index[-1] - dividend_performance.index[0]).days / 365
    
    strategies = ['Cổ tức', 'Tăng trưởng', 'Cân bằng']
    performances = [dividend_performance, growth_performance, balanced_performance]
    
    comparison_data = []
    
    for strategy, perf in zip(strategies, performances):
        total_return = perf['Return'].iloc[-1] * 100
        annual_return = ((total_return / 100 + 1) ** (1 / years) - 1) * 100 if years > 0 else 0
        
        # Tính độ biến động (volatility)
        daily_returns = perf['Total_Value'].pct_change().dropna()
        volatility = daily_returns.std() * np.sqrt(252) * 100  # Annualized
        
        # Tính Sharpe Ratio (giả định lãi suất không rủi ro = 0)
        sharpe_ratio = annual_return / volatility if volatility > 0 else 0
        
        # Tính Maximum Drawdown
        cumulative = perf['Total_Value']
        running_max = cumulative.cummax()
        drawdown = (cumulative - running_max) / running_max
        max_drawdown = drawdown.min() * 100
        
        comparison_data.append({
            'Strategy': strategy,
            'Total Return (%)': total_return,
            'Annual Return (%)': annual_return,
            'Volatility (%)': volatility,
            'Sharpe Ratio': sharpe_ratio,
            'Max Drawdown (%)': max_drawdown
        })
    
    comparison_df = pd.DataFrame(comparison_data)
    
    return comparison_df

# So sánh với các chiến lược khác
comparison_results = compare_with_other_strategies(stocks_data, optimal_portfolio, '2018-01-01')
print(comparison_results)

6.3 Phân Tích Tác Động của Giai Đoạn Thị Trường

def analyze_market_phase_impact(stocks_data, portfolio, market_ticker='^VNINDEX'):
    """
    Phân tích tác động của các giai đoạn thị trường khác nhau
    
    Tham số:
    ---------
    stocks_data : dict
        Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
    portfolio : dict
        Trọng số của các cổ phiếu trong danh mục
    market_ticker : str
        Mã chỉ số thị trường để phân tích giai đoạn
    
    Trả về:
    --------
    DataFrame
        Kết quả phân tích theo giai đoạn thị trường
    """
    # Tải dữ liệu chỉ số thị trường
    try:
        market_data = yf.download(market_ticker, start='2018-01-01')
        
        # Tính SMA 200 ngày để xác định xu hướng
        market_data['SMA_200'] = market_data['Close'].rolling(window=200).mean()
        
        # Tính biến động 20 ngày
        market_data['Volatility'] = market_data['Close'].pct_change().rolling(window=20).std() * np.sqrt(252)
        
        # Xác định các giai đoạn thị trường
        market_data['Market_Phase'] = 'Sideway'  # Mặc định
        
        # Thị trường tăng: giá trên SMA200 và tăng hơn 20% trong 1 năm
        market_data['Annual_Return'] = market_data['Close'].pct_change(periods=252)
        bull_market = (market_data['Close'] > market_data['SMA_200']) & (market_data['Annual_Return'] > 0.2)
        market_data.loc[bull_market, 'Market_Phase'] = 'Bull'
        
        # Thị trường giảm: giá dưới SMA200 và giảm hơn 20% trong 1 năm
        bear_market = (market_data['Close'] < market_data['SMA_200']) & (market_data['Annual_Return'] < -0.2)
        market_data.loc[bear_market, 'Market_Phase'] = 'Bear'
        
        # Thị trường biến động cao: biến động > 25%
        high_volatility = market_data['Volatility'] > 0.25
        market_data.loc[high_volatility, 'Market_Phase'] = 'High Volatility'
        
        # Đánh giá hiệu suất danh mục
        performance = evaluate_portfolio_performance(stocks_data, portfolio, '2018-01-01')
        
        # Kết hợp dữ liệu hiệu suất với giai đoạn thị trường
        combined_data = pd.DataFrame()
        combined_data['Portfolio_Return'] = performance['Return']
        combined_data['Market_Phase'] = market_data['Market_Phase']
        
        # Phân tích hiệu suất theo từng giai đoạn
        phase_analysis = []
        
        for phase in ['Bull', 'Bear', 'Sideway', 'High Volatility']:
            phase_data = combined_data[combined_data['Market_Phase'] == phase]
            
            if not phase_data.empty:
                # Tính lợi nhuận trung bình hàng ngày trong giai đoạn
                phase_daily_return = phase_data['Portfolio_Return'].pct_change().mean() * 100
                
                # Tính tổng số ngày trong giai đoạn
                phase_days = len(phase_data)
                
                # Tính phần trăm thời gian trong giai đoạn
                total_days = len(combined_data)
                phase_percentage = phase_days / total_days * 100 if total_days > 0 else 0
                
                phase_analysis.append({
                    'Market_Phase': phase,
                    'Days': phase_days,
                    'Percentage_of_Time': phase_percentage,
                    'Avg_Daily_Return': phase_daily_return
                })
        
        phase_df = pd.DataFrame(phase_analysis)
        
        # Vẽ biểu đồ phân tích
        plt.figure(figsize=(15, 10))
        
        # 1. Hiệu suất theo giai đoạn thị trường
        plt.subplot(2, 2, 1)
        sns.barplot(x='Market_Phase', y='Avg_Daily_Return', data=phase_df)
        plt.title('Lợi nhuận trung bình hàng ngày theo giai đoạn thị trường')
        plt.xlabel('Giai đoạn thị trường')
        plt.ylabel('Lợi nhuận hàng ngày (%)')
        
        # 2. Phân bổ thời gian theo giai đoạn
        plt.subplot(2, 2, 2)
        plt.pie(phase_df['Percentage_of_Time'], labels=phase_df['Market_Phase'], autopct='%1.1f%%')
        plt.title('Phân bổ thời gian theo giai đoạn thị trường')
        
        # 3. Hiệu suất theo thời gian với màu sắc theo giai đoạn
        plt.subplot(2, 1, 2)
        
        # Vẽ hiệu suất danh mục
        plt.plot(performance.index, performance['Return'] * 100, label='Hiệu suất danh mục')
        
        # Tô màu nền theo giai đoạn thị trường
        unique_phases = combined_data['Market_Phase'].unique()
        colors = {'Bull': 'lightgreen', 'Bear': 'lightcoral', 'Sideway': 'lightgray', 'High Volatility': 'lightyellow'}
        
        for phase in unique_phases:
            phase_periods = combined_data[combined_data['Market_Phase'] == phase]
            if not phase_periods.empty:
                for i in range(len(phase_periods) - 1):
                    start_date = phase_periods.index[i]
                    end_date = phase_periods.index[i+1]
                    plt.axvspan(start_date, end_date, alpha=0.3, color=colors.get(phase, 'white'))
        
        plt.title('Hiệu suất danh mục theo giai đoạn thị trường')
        plt.xlabel('Ngày')
        plt.ylabel('Lợi nhuận (%)')
        plt.legend()
        plt.grid(True)
        
        plt.tight_layout()
        plt.show()
        
        return phase_df
    
    except Exception as e:
        print(f"Lỗi khi phân tích tác động của giai đoạn thị trường: {e}")
        return pd.DataFrame()

# Phân tích tác động của giai đoạn thị trường
market_phase_analysis = analyze_market_phase_impact(stocks_data, optimal_portfolio)
print(market_phase_analysis)

Kết Luận

Bài viết đã trình bày cách triển khai một chiến lược đầu tư cổ tức toàn diện bằng Python, từ việc phân tích và lựa chọn cổ phiếu trả cổ tức, xây dựng danh mục đầu tư tối ưu, theo dõi và tái cân bằng danh mục, đến việc tự động hóa toàn bộ quy trình. Các kỹ thuật phân tích nâng cao cũng được giới thiệu để giúp nhà đầu tư hiểu rõ hơn về các yếu tố ảnh hưởng đến hiệu suất của chiến lược đầu tư cổ tức.

Đầu tư cổ tức là một chiến lược phù hợp với mục tiêu tài chính dài hạn, đặc biệt là việc xây dựng dòng thu nhập thụ động. Với sự hỗ trợ của các công cụ phân tích định lượng và tự động hóa từ Python, nhà đầu tư có thể xây dựng và quản lý danh mục đầu tư cổ tức một cách hiệu quả và tiết kiệm thời gian.

Tuy nhiên, cần lưu ý rằng không có chiến lược đầu tư nào là hoàn hảo cho mọi thị trường và mọi giai đoạn. Việc kết hợp giữa phân tích kỹ thuật, phân tích cơ bản, và liên tục điều chỉnh chiến lược theo điều kiện thị trường là chìa khóa để đạt được thành công trong đầu tư cổ tức dài hạn.

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